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.
ID Token
Issued by: IDP (https://idp.xaa.dev)
Lifetime: ~10 minutes
Purpose: proves the user's identity to your application
| Claim | Description |
|---|---|
iss | IDP URL |
sub | User's unique identifier (typically their email) |
aud | Must equal your client_id; the IDP enforces this in token exchange |
exp | Expiry; typically 10 minutes after login |
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
| Claim | Description |
|---|---|
typ header | Must be oauth-id-jag+jwt; the Auth Server rejects anything else |
iss | IDP URL; verified against the Auth Server's trusted issuers list |
sub | User identifier, forwarded from the ID Token's sub |
aud | The 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_id | The resource client ID ({client_id}-at-{resource_id}); the Auth Server checks this against the authenticating client in Step 3 |
scope | Scopes authorized by the IDP for this delegation |
resource | The 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. |
jti | Unique identifier; prevents replays |
iat | Issued-at (Unix timestamp) |
nbf | Not-before (Unix timestamp); the Auth Server rejects the ID-JAG if the current time is earlier than nbf |
exp | Short expiry; 5 minutes from issuance |
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.
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
| Claim | Description |
|---|---|
iss | Auth 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 |
aud | Your 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_id | Your resource client ID |
scope | Intersection of requested and authorized scopes; see note below |
app_org | Provider (tenant) name that authenticated the user; same prefix used in sub |
jti | Unique access-token identifier (enables revocation and replay detection) |
iat | Issued-at (Unix timestamp) |
exp | Expiry; typically 2 hours |
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:
For example, if the IDP is configured as customer1 and the user is alice@example.com:
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:
Example:
| Scopes | |
|---|---|
| You requested in Step 3 | todos.read files.read |
| ID-JAG authorized in Step 2 | todos.read |
| Issued in access token | todos.read |
If the intersection is empty, the token is issued with an empty scope and every protected endpoint will return 403 insufficient_scope.
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
| Token | Issued by | Validates using | Lifetime | Used in |
|---|---|---|---|---|
| ID Token | IDP | IDP's JWKS | ~10 min | Step 2 only |
| ID-JAG | IDP (via exchange) | Auth Server verifies against IDP's JWKS | 5 min | Step 3 only |
| Access Token | Auth Server | Resource server verifies against Auth Server's JWKS | 2 hours | Step 4 + all API calls |
Next step
- Step 2: Token Exchange: how the ID Token becomes an ID-JAG
- Step 3: JWT Bearer Grant: how the ID-JAG becomes an Access Token
- Error Codes: debug failed token operations
On this page