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:
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
In the Okta Admin Console, go to Applications > Applications, and open your app (e.g., My API Services App).
Under the General tab, locate the Client Credentials section.
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.
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
Navigate to your application in the Okta Admin Console.
Under the General tab, scroll to Client Credentials.
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.....sMEnter your Client ID: 0******IM6eeEnter your Okta token endpoint: https://youroktatenanturl......tokenPath to your private.key file (default: ./private.key): ./private.keyOutput 🔐 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.
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
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-keyEnter your Client ID:0******IM6eeEnter your Okta token endpoint:https://your-okta-domain.okta.com/oauth2/default/v1/tokenPath 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:
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.
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: