Understanding Client Authentication with Okta

In this article, you’ll learn how client authentication works with Okta for applications that need to request access tokens securely. The “client” here refers to a server-side or browser-based application or machine making a token requests.


We’ll walk through different authentication methods supported by Okta, including how to generate your own key pairs and create secure JWT-based client assertions.
You’ll also find practical code snippets and step-by-step instructions to help you implement client authentication using both Okta-generated keys and your own custom keys.

Introduction

When building secure applications that integrate with Okta, authenticating your app to the Okta Authorization Server is a critical step. Okta supports multiple ways for your application to prove its identity depending on the app type and trust level—primarily through client secrets or JWT assertions using public/private key pairs.

In the following examples, will use the client credentials flow for simplicity, it applies for other grants as well, the authentication it self has no interaction with the grant type.


The lab in this article uses a test environment. To follow along, you can use your own sandbox or refer to the prerequisites section to set up your lab environment.

All names, client_ids, and URLs shown in this guide are disabled and belong to a testing Okta org.

Prerequisites

Before you begin, make sure you have the following:

  • An Okta account
    You can sign up for a free trial or create a developer account.
  • API testing tools
    Use Postman or curl to test API requests.
  • Node.js installed
  • Packages: jose
    Required if you plan to run code examples or scripts.
  • Machine-to-Machine App Setup
    Refer to Appendix for steps to create a machine-to-machine (M2M) API application in Okta.

Note:
If you plan to create and customize your own authorization server (which is required for advanced flows like custom scopes or policies) in production tenant, you’ll need a subscription that includes API Access Management, for client crednitals grant
Machine-to-Machine Tokens 

Client secret authentication

This is the most straightforward authentication method. A client secret is a shared secret between your application and Okta—similar to a password. It is typically used in flows such as client_credentials or authorization_code to authenticate backend services or other confidential clients

Note: In Single Page Applications (SPAs), client secrets should not be used because the secret cannot be securely stored in the browser. Instead, public clients do not authenticate themselves with a secret. They identify themselves using the client_id, redirect URI, and use PKCE (Proof Key for Code Exchange) for secure authorization.

First step: Copy client_id and client_secret

  1. In the Okta Admin Console, go to Applications > Applications, and open your app (e.g., My API Services App).
  2. Under the General tab, locate the Client Credentials section.
  3. Copy the following:
    • Client ID – a public identifier for your application.
    • Client Secret – used to authenticate confidential clients (e.g., backends).

Step 2: Request an Access Token Using curl

Before making a token request, ensure your app is correctly set up:

Follow the Appendix to:

  • Create a Machine-to-Machine (M2M) app.
  • Assign appropriate scopes to the app.
  • Configure a token access policy in your Authorization Server.

🔎 You’ll find the token endpoint URL in your Authorization Server’s metadata, typically at:
https://{yourOktaDomain}/oauth2/{authServerId}/.well-known/openid-configuration
See the Appendix for details.

curl -X POST https://{yourOktaDomain}/oauth2/{authServerId}/v1/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id={CLIENT_ID} \
  -d client_secret={CLIENT_SECRET}

Replace the following values:

  • yourOktaDomain: Your Okta domain (e.g., dev-123456.okta.com)
  • authServerId: The ID of your custom authorization server (or default)
  • CLIENT_ID: The client ID from your app settings
  • CLIENT_SECRET: The client secret from your app settings

💡 If your authorization server requires a custom scope, you can add it like so:

-d scope=custom.scope 

Step 3: Verify the Access Token

After making a successful curl request, you should receive a JSON response like the following:

Result should look like this

{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJraWQiOiJgdteMOEhnUzFj...............9_XXNuxksRvnpH4u2A",
"scope": "Test"
}

This access token is a JWT (JSON Web Token), which encodes information about the request, including:

  • The expiration time
  • The issuer
  • The client ID
  • The scopes granted

🔍 How to Inspect the Token

You can decode and inspect the token by pasting it into jwt.io. This allows you to:

  • See the token’s payload
  • Confirm the scope
  • Check the issuer (iss) matches your Okta Authorization Server
  • Validate the audience (aud) and expiration (exp)

⚠️ Note: This tool does not verify the token’s signature unless you provide the public key, if you used okta the tool can automatically find the well known url and get the keys

This conclude the client secret authentication mechanism, a simple and straightforward authentication that is based on mutual secret shared between the client and Okta

Public/Private key authentication

This authentication method uses asymmetric encryption to ensure that only the holder of the private key (i.e., the client) can generate valid assertions. Okta uses the public key to verify the signature.

Concept:
It is like a digital signature:

  • Client build the JWT and sign it using private key
  • Okta verifies it using the associated public key stored in the application’s settings.

High level concept

  • Configure the app and prepare the private key on your client side.
  • The public key must be added to Okta — specifically within the application’s settings.
  • Generate the JWT assertion signed with the private key.
  • Send a token request to Okta including the client_assertion.
  • Okta will:
    • Verify the JWT signature using the public key.
    • Validate the JWT claims (issuer, audience, expiration, etc.).
    • If valid, issue an access token to the client.

Use Okta Generated Keys

Step 1: Configure Okta to Use Public/Private Key Authentication

  1. Navigate to your application in the Okta Admin Console.
  2. Under the General tab, scroll to Client Credentials.
  3. Change Client authentication to:
    Public key / Private key

At this step, you will have option to use the Okta generated key

Step 2: Generate and Download the Key Pair

  • Okta generates a public/private key pair.
  • The public key is stored in Okta and used to validate your JWT.
  • The private key is provided to you and must be saved securely.

The private key is available in two formats:

  • JSON
  • PEM

👉 In this example, we’ll use the PEM format.

Save the PEM file locally as:

private.key 

Now you should have the configuration ready.

Step 3: Generate the Client Assertion (JWT)

Let’s build the JWT client assertion, more information about the required attribute can be found here.

  • This step requires Node.js installed.
  • Place your private.key in the same folder as the code.

📄 Save the following JavaScript code as client-assertion.mjs:


import { SignJWT } from 'jose';
import fs from 'fs';
import { importPKCS8 } from 'jose';
import readlineSync from 'readline-sync';

const alg = 'RS256';
const kid=readlineSync.question('Enter the Kid from publickey: ');
const main = async () => {
  const clientId = readlineSync.question('Enter your Client ID: ');
  const oktaDomain = readlineSync.question('Enter your Okta token endpoint  (e.g., dev-123456.okta.com.../token): ');
  const privateKeyPath = readlineSync.question('Path to your private.key file (default: ./private.key): ') || './private.key';

  const aud = oktaDomain;
  console.log(aud);
  const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
  const key = await importPKCS8(privateKey, alg);

  const jwt = await new SignJWT({})
    .setProtectedHeader({ alg, kid })
    .setIssuer(clientId)
    .setSubject(clientId)
    .setAudience(aud)
    .setJti(Math.random().toString(36).substring(2))
    .setExpirationTime('5m')
    .sign(key);

  console.log('\n🔐 Client Assertion (JWT):\n');
  console.log(jwt);
};

main();

💻 Run the script:

node generate-client-assertion.mjs

Remember the values for KID and Client ID can be found from Okta applicaiton

🔁 Output

Enter the Kid from publickey: GSR.....sM
Enter your Client ID: 0******IM6ee
Enter your Okta token endpoint: https://youroktatenanturl......token
Path to your private.key file (default: ./private.key): ./private.key

Output 🔐 Client Assertion (JWT):

ey4JSffszI1N=..........................SP0T2wg

Step 4: Prepare the curl Call

Use the JWT assertion in a curl call to Okta’s /token endpoint, remember you can customize the scope you want to request.


curl -X POST https://youroktatenanturl.......token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id="Client_ID" \
  -d scope="Test" \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion="Assertion"

Example Token Response:


{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "JraWQiOiJr........gf",
  "scope": "Test"
}

Build your own JWT assertion and keys

In this section, we’ll generate EC (Elliptic Curve) key pairs, and prepare them for use in JWT client assertions. We’ll use ES256 (NIST P-256 curve), and upload the public key in JWK format to Okta, more information can be found here

Step 1: Generate Public/Private Key Pair

We’ll use Node.js and a few libraries to:

  • Generate EC key pairs
  • Save the private key locally (private.key)
  • Convert the public key to JWK (JSON Web Key) format for uploading to Okta

🔐 Signing Algorithm: ES256
This uses the NIST P-256 curve (prime256v1), offering strong security with smaller key sizes than RSA.

📄 Example code (Node.js — using jose):
📌 What this script does:

  • Generates a public/private key pair using P-256 curve.
  • Saves the JWK to ec-public.jwk.json.
  • Saves the keys in PEM format:
    • ec-private.key – private key
    • ec-public.key – public key
  • Converts the public key into JWK format (used for Okta).
  • Saves the JWK to ec-public.jwk.json, this one you will upload to your applicaiton in okta

Remember to save the script and name it, i names it generate-ec-keys1.mjs


import { generateKeyPair } from 'crypto';
import { exportJWK } from 'jose';
import fs from 'fs/promises';
import { promisify } from 'util';

const generateKeyPairAsync = promisify(generateKeyPair);

const { publicKey, privateKey } = await generateKeyPairAsync('ec', {
  namedCurve: 'P-256',
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem'
  }
});

// Save PEM files
await fs.writeFile('./ec-private.key', privateKey);
await fs.writeFile('./ec-public.key', publicKey);

// Create JWK and add metadata
import { createPublicKey } from 'crypto';
const pubObj = createPublicKey(publicKey);
const jwk = await exportJWK(pubObj);
jwk.alg = 'ES256';
jwk.use = 'sig';
jwk.kid = 'ec-signing-key'; // optional, you can use UUIDs

await fs.writeFile('./ec-public.jwk.json', JSON.stringify(jwk, null, 2));

console.log('✅ EC keys generated and saved:\\n- ec-private.key\\n- ec-public.key\\n- ec-public.jwk.json');

💻 Run the script :

node generate-ec-keys1.mjs 

🔁 Output Files

node generate-ec-keys1.mjs 
✅ EC keys generated and saved:
- ec-private.key
- ec-public.key
- ec-public.jwk.json

Step 2: Upload the Public Key to Okta

Now that you have generated your key pair, you need to upload the public key (JWK format) to your application in Okta.

📁 File to upload:
ec-public.jwk.json
Go to your application in Okta.

  • Under the General tab, scroll to Client Credentials.
  • Select Client Authentication → Public key / Private key.
  • Choose Save keys in Okta.
  • Click the “Add key” button.

The Okta platform only accepts keys in JSON Web Key (JWK) format when using the Public/Private key method.

That’s why in the previous step, the script converted the public key from PEM to JWK format — so it can be uploaded to Okta successfully.

Step 3: Create the Client Assertion

Now you should have two keys available:

  • The key generated by Okta (stored in the application settings).
  • The key you generated locally and uploaded to Okta

In this step, we’ll use the locally generated private key to create a JWT assertion signed with the ES256 algorithm.

Note:
The code shown here includes a slight modification to support the Elliptic Curve algorithm (ES256).

import { SignJWT, importPKCS8 } from 'jose';
import fs from 'fs';
import readlineSync from 'readline-sync';

const alg = 'ES256';

const kid=readlineSync.question('Enter the Kid from publickey: ');

const main = async () => {
  const clientId = readlineSync.question('Enter your Client ID: ');
  const oktaDomain = readlineSync.question('Enter your Okta token endpoint  (e.g., dev-123456.okta.com.../token): ');
  const privateKeyPath = readlineSync.question('Path to your private.key file (default: ./ec-private.key): ') || './ec-private.key';

  const aud = oktaDomain;
  const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
  const key = await importPKCS8(privateKey, alg);

  const jwt = await new SignJWT({})
    .setProtectedHeader({ alg , kid })
    .setIssuer(clientId)
    .setSubject(clientId)
    .setAudience(aud)
    .setJti(Math.random().toString(36).substring(2))
    .setExpirationTime('5m')
    .sign(key);

  console.log('\n🔐 Client Assertion (JWT):\n');
  console.log(jwt);
};

main();

Save the code and name it, i named generate-client-assertion-es.mjs

💻 Run the script :

 node generate-client-assertion-es.mjs 

🔁 Output

Enter the Kid from publickey: ec-signing-key
Enter your Client ID: 0******IM6ee
Enter your Okta token endpoint: https://your-okta-domain.okta.com/oauth2/default/v1/token
Path to your private.key file (default: ./ec-private.key): ./ec-private.key

🔐 Client Assertion (JWT):

eyJdfgedfer46sx4...X8IAyPA

Step 4: Prepare the curl Call

Use the JWT in a curl call to Okta’s /token endpoint:


curl -X POST https://yourdomain.okta.com//oauth2/default/v1/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id="Client_ID" \
  -d scope="Test" \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion="Assertion"

Example Token Response:


{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "yJraWQiOiJr........",
"scope": "Test"
}

This method uses your own generated keys and a different algorithm (ES256) to sign the JWT.
However, as of the time of writing this blog, Okta issues access tokens using the RS256 algorithm.
It’s important to note that the algorithm used for client authentication (e.g., ES256 in your JWT assertion) is separate from the algorithm used to sign the access token (e.g., RS256 by Okta). Mixing the two can cause confusion—be sure to distinguish between the two roles clearly.

Note: Instead of uploading static public keys to Okta, you can configure your application to fetch the public key dynamically from a URL. This is especially helpful if you’re rotating keys frequently.

Appendix

Create Machine-to-machine App

Step 1: Create the Application

  • Login to your Okta Admin Dashboard.
  • Navigate to Applications → Create App Integration.
Figure1

Step 2: Choose App Type

Select “API Services” as the sign-in method.

Name the applicaiton

Step 3: Configure the App

  • Go to general settings and clcik on edit.
  • In this example, disable DPoP (Demonstration of Proof-of-Possession).
  • Enter a name for your application.

⚠️ Important: After creating the app, you must allow it access through a proper authorization server policy.

Authorization Server & Application Access Policy

Step 1: Create or Use an Authorization Server

  • Navigate to Security → API in the Admin Console.
  • Either use the default authorization server or create a new one.

Step 2: Add Application Access Policy

  • Go into the authorization server settings.
  • Under the Access Policies tab, add a new policy.
  • Assign the newly created app to the policy.

✏️ Make sure to give the policy a meaningful name and description, also asssign it to your testing app

Add a rule, and just make it for any

Custom Default Scope

Add scope

Create a scope for testing and make it default so it will be returned in this example

Now the app should be ready to use

Metadata URL and Discovery Information

Discovery endpoints – org authorization servers

When integrating with Okta for machine-to-machine or OAuth 2.0-based flows, it’s essential to retrieve and use the correct metadata endpoints. These discovery endpoints help clients configure themselves automatically by retrieving necessary OAuth/OpenID configuration data.

The issuer url can be found in the default settings page

Click the URL and this should open the endpoints urls page.

In general for Discovery Endpoints – Org Authorization Servers
Okta provides two types of well-known metadata endpoints, check this link here.

ProtocolEndpoint URL
OpenID Connecthttps://{yourOktaOrg}/.well-known/openid-configuration
OAuth 2.0https://{yourOktaOrg}/.well-known/oauth-a
uthorization-server

Example: Token Endpoint

For Client Credentials Flow (machine-to-machine), you’ll need the token_endpoint from the discovery document. It’s typically found in the response like this:

{
"token_endpoint": "https://{yourOktaOrg}/oauth2/default/v1/token",
...
}

Now all data should be ready to start the flows and test them.

Resources

Authorization server discovery: https://developer.okta.com/docs/concepts/auth-servers/

Creating scopes: https://support.okta.com/help/s/article/Creating-a-Scope-for-an-Authorization-Server-in-Okta?language=en_US

Udemy course: https://www.udemy.com/course/oauth-2-simplified/?referralCode=B04F59AED67B8DA74FA7&couponCode=CP130525

Client Authentication: https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth/#jwt-with-private-key

Leave a Reply