Documentation
Documentation
Step 2: Token Exchange
Exchange the ID Token for an ID-JAG (Identity Assertion Authorization Grant) using RFC 8693 Token Exchange.
Steps 1, 2, and 3 are identical whether you're calling a REST API or an MCP server. The only difference is Step 4: the transport. Change the resource parameter to point at your MCP server URL instead of a REST API URL.
What is Token Exchange?
Token Exchange (RFC 8693) allows you to exchange one security token for another. In XAA, we exchange a subject token — an ID Token or Refresh Token from Step 1 — for an ID-JAG (which asserts user identity to a third-party authorization server).
- Refresh Token (recommended): needs
offline_accessscope in Step 1, but lets you mint new ID-JAGs later without repeating it - ID Token: always available, but only good for one exchange right after login
Request parameters
| Parameter | Required | Value |
|---|---|---|
grant_type | Yes | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | Yes | The ID Token, or Refresh Token, from Step 1 |
subject_token_type | Yes | urn:ietf:params:oauth:token-type:id_token or urn:ietf:params:oauth:token-type:refresh_token |
requested_token_type | Yes | urn:ietf:params:oauth:token-type:id-jag |
audience | Yes | The Authorization Server URL. Becomes the aud claim in the ID-JAG. |
resource | Yes | The Resource Server API URL, embedded as the resource claim. |
scope | Optional | Requested scopes (e.g., todos.read) |
audience and resource serve different roles: audience identifies who will validate the ID-JAG (the Authorization Server), while resource identifies what will be accessed (your API). Both are required.
Implementation
Developer-registered clients use client_secret_post: credentials go in the POST body, not in an Authorization: Basic header.
Using a Refresh Token (Recommended)
Add offline_access to the scope in Step 1 to get a refresh_token alongside the id_token — the same mechanism Okta and Microsoft Entra ID use. Swap subject_token and subject_token_type; the rest is unchanged:
Unlike the ID-JAG, the Refresh Token isn't consumed by this exchange — reuse it to mint more ID-JAGs until it expires or is revoked (RFC 8693 §2.1).
Response
A successful response:
| Field | Description |
|---|---|
access_token | The ID-JAG token |
issued_token_type | Confirms this is an ID-JAG |
token_type | N_A since ID-JAG is not used as a bearer token directly |
expires_in | Token lifetime in seconds (typically 5 minutes) |
scope | Granted scopes |
The ID-JAG contains claims that identify the user and authorize access:
iss- Issuer (the IDP that issued the original ID Token)sub- Subject (user identifier from the ID Token)aud- Audience (the authorization server URL)client_id- The resource client ID ({client_id}-at-{resource_id}), the identity your app presents to the Authorization Server in Step 3. This is distinct from the IDP client ID used in Steps 1 and 2.resource- The canonical Resource Server URLscope- Requested permissionsjti- Unique token identifier (prevents replay attacks)exp- Expiration time (short-lived, typically 5 minutes)iat- Issued at time
Decoded ID-JAG example
client_id is the resource client ID, the -at-{resource} form created when you registered. Your app authenticates with this identity in Step 3.
Error handling
| Error | Cause | Fix |
|---|---|---|
invalid_grant | Subject token is expired, invalid, or has been revoked | Re-authenticate in Step 1 to get a fresh subject token |
unauthorized_client | Client is not authorized for token exchange | Verify your client was registered with the correct grant types |
invalid_target | Resource URL is not registered or allowed | Re-register the resource using the wizard; the audience + resource must match a resource connection |
invalid_scope | Requested scope is not permitted for your client | Remove the invalid scopes, or re-register and add them in Wizard Step 3 |
Security considerations
- Short Expiry: ID-JAGs have short lifetimes (5 minutes) to minimize risk
- Single Use: ID-JAGs contain a
jticlaim and may be single-use - Audience Binding: ID-JAGs are bound to a specific authorization server
- Client Authentication: Always use client credentials when exchanging tokens
- Refresh Token storage: Treat it like any long-lived credential — never expose it in URLs or client-side code
Next step
Now that you have an ID-JAG, proceed to Step 3: JWT Bearer Grant to exchange it for an access token.
On this page