Home

Documentation

Documentation

Token Structure

The XAA flow produces three tokens in sequence. Understanding what each one contains (and the rules governing them) is essential for debugging and correct implementation.

Plain Text

ID Token

Issued by: IDP (https://idp.xaa.dev)
Lifetime: ~10 minutes
Purpose: proves the user's identity to your application

JSON
ClaimDescription
issIDP URL
subUser's unique identifier (typically their email)
audMust equal your client_id; the IDP enforces this in token exchange
expExpiry; typically 10 minutes after login
Used in Step 2

The ID Token is never sent to the resource server. It's consumed in Step 2 (Token Exchange) to obtain an ID-JAG.


ID-JAG

Issued by: IDP (https://idp.xaa.dev/token) via Token Exchange
Lifetime: 5 minutes
Purpose: asserts the user's identity to a third-party Authorization Server; carries the delegation grant

JSON
ClaimDescription
typ headerMust be oauth-id-jag+jwt; the Auth Server rejects anything else
issIDP URL; verified against the Auth Server's trusted issuers list
subUser identifier, forwarded from the ID Token's sub
audThe Authorization Server's URL (not the resource server); set via the audience param in token exchange. MAY be a string or an array of strings per RFC 7519.
client_idThe resource client ID ({client_id}-at-{resource_id}); the Auth Server checks this against the authenticating client in Step 3
scopeScopes authorized by the IDP for this delegation
resourceThe resource server's API URL (RFC 8707); embedded from the resource param in token exchange. This is the exact value that will become the aud of the resulting access token — the two are identical by construction.
jtiUnique identifier; prevents replays
iatIssued-at (Unix timestamp)
nbfNot-before (Unix timestamp); the Auth Server rejects the ID-JAG if the current time is earlier than nbf
expShort expiry; 5 minutes from issuance
`aud` vs `resource`

These two claims answer different questions. aud names the token's consumer (the Auth Server that will validate this ID-JAG in Step 3). resource names the target (the API URL that the downstream access token will be used against). The AS copies resource verbatim into the access token's aud — so whatever you put here is what your resource-server middleware must validate against.

Short lifetime

The ID-JAG expires in 5 minutes. Present it to the Auth Server immediately in Step 3. If Step 3 fails and you retry more than 5 minutes later, get a fresh ID-JAG from Step 2 first.


Access Token

Issued by: Auth Server (https://auth.resource.xaa.dev/token) via JWT Bearer Grant
Lifetime: 2 hours
Purpose: grants your application access to the resource server for the specified scopes

JSON
ClaimDescription
issAuth Server URL (https://auth.resource.xaa.dev); configure your JWT middleware's issuer to match exactly. Signatures are verified against https://auth.resource.xaa.dev/jwks using RS256.
sub{providerName}:{userSub}; see note below
audYour resource server URL, exactly as registered. The Auth Server does not add or strip trailing slashes — match whatever you entered in the registration wizard.
client_idYour resource client ID
scopeIntersection of requested and authorized scopes; see note below
app_orgProvider (tenant) name that authenticated the user; same prefix used in sub
jtiUnique access-token identifier (enables revocation and replay detection)
iatIssued-at (Unix timestamp)
expExpiry; typically 2 hours
Header `typ`

The access-token header uses typ: at+jwt (RFC 9068). If your JWT library enforces a specific typ, configure it to accept at+jwt in addition to the default JWT.


sub claim format

The sub in an access token is not the raw user email. It is prefixed with the IDP's provider name:

Plain Text

For example, if the IDP is configured as customer1 and the user is alice@example.com:

Plain Text
Resource server impact

If your resource server identifies users by the sub claim, you must handle this prefix. Either strip the prefix or store it as-is and query with it.


Scope intersection rule

The scope in the issued access token is:

Plain Text

Example:

Scopes
You requested in Step 3todos.read files.read
ID-JAG authorized in Step 2todos.read
Issued in access tokentodos.read

If the intersection is empty, the token is issued with an empty scope and every protected endpoint will return 403 insufficient_scope.

How to avoid this

Always request scopes in Step 2 (token exchange) that are at least as broad as what you'll request in Step 3. The safest approach: use the same scope string in both steps.


Clock skew tolerance

The Auth Server allows 30 seconds of clock skew when validating the ID-JAG's iat claim. If your server clock is more than 30 seconds ahead of the IDP's clock, ID-JAGs will be rejected with invalid_grant.


Token flow summary

TokenIssued byValidates usingLifetimeUsed in
ID TokenIDPIDP's JWKS~10 minStep 2 only
ID-JAGIDP (via exchange)Auth Server verifies against IDP's JWKS5 minStep 3 only
Access TokenAuth ServerResource server verifies against Auth Server's JWKS2 hoursStep 4 + all API calls

Next step

These materials and any recommendations within are not legal, privacy, security, compliance, or business advice. These materials are intended for general informational purposes only and may not reflect the most current security, privacy, and legal developments nor all relevant issues. You are responsible for obtaining legal, security, privacy, compliance, or business advice from your own lawyer or other professional advisor and should not rely on the recommendations herein.

Presented byOkta Developer

Copyright © 2026 Okta. All rights reserved.