How XAA and ID-JAG Work
This page explains the protocol: what Cross App Access (XAA) is, what an XAA token (ID-JAG) contains, and how the two OAuth grants fit together.
To set up XAA for pre-built agents in your enterprise reaching third-party tools, see Issue XAA Tokens for Your Agents.
If you host an MCP server for customers and accept their XAA tokens, see Accept Your Customers' XAA Tokens (also covered under SSO Setup Suite and Cross App Access for B2B MCP Servers).
For why XAA exists, see Enterprise-Managed Authorization. In short, the IdP that signed the user in vouches, in a short-lived JWT, that this user, through this client, may reach this resource, and the resource's authorization server trades that JWT for a normal access token.
The IETF draft calls this pattern XAA and defines the JWT as an Identity Assertion JWT Authorization Grant (ID-JAG). In Descope docs, XAA token and ID-JAG refer to the same credential.
Parties
| Role | What it is | Example |
|---|---|---|
| User | The human (or service identity) the agent acts for | An employee in your Okta / Entra / Descope tenant |
| Requesting application (client) | The agent or app that already has a session with the IdP | Claude Code, VS Code, your internal agent |
| Identity provider (IdP) | Issues the XAA token after checking enterprise policy | Descope (when you manage your agents) or the customer's Okta / Entra (when they manage theirs) |
| Resource authorization server | Trusts the IdP, accepts the XAA token, issues its own access token | Descope protecting an MCP server you sell, or a third-party MCP vendor's AS |
| Resource | The API or MCP server the access token is for | Linear MCP, your product's MCP server |
The IdP and the resource authorization server are often different organizations. That split is the point: the IdP decides whether the hop is allowed; the resource AS decides what access token the resource gets.
How the two Descope use cases map onto the protocol
Descope actually plays both roles with XAA: when you manage your agents, it is the IdP; when customers manage their agents, it is the resource authorization server.
| Your job | Descope's role in the diagram | Who mints the XAA token | Who redeems it |
|---|---|---|---|
| Issue XAA tokens for your agents | IdP | Descope | Third-party MCP / API authorization server |
| Accept your customers' XAA tokens | Resource authorization server | Customer's Okta, Entra, or Descope | Descope (for your MCP Resource) |
End-to-end flow
Two OAuth profiles, in sequence:
- Token exchange (RFC 8693) at the IdP: the client trades a token it already holds (typically an ID token or access token from SSO) for an XAA token (ID-JAG).
- JWT Bearer grant (RFC 7523) at the resource authorization server: the client presents that XAA token as an assertion and receives the resource's access token.
The user is not redirected to the resource authorization server's /authorize endpoint on this path. Consent and access decisions already happened at the IdP (and in any policies you configure there).
What an XAA token (ID-JAG) is
An XAA token is a signed JWT. It is not a bearer access token for the MCP server. It is an assertion: a statement the IdP makes that the resource authorization server can verify and then exchange for its own access token.
Typical header:
{
"alg": "RS256",
"typ": "oauth-id-jag+jwt",
"kid": "..."
}The typ (or equivalent profile marker) distinguishes an ID-JAG from a normal access token so a random JWT cannot be replayed as a grant.
Claims that matter
Exact claim names follow the IETF draft and the IdP's profile. Conceptually the assertion binds:
| Concept | Purpose |
|---|---|
Issuer (iss) | Which IdP signed the assertion. The resource AS must trust this issuer and fetch its JWKs. |
Audience (aud) | The resource authorization server that is allowed to redeem the assertion. |
| Resource | The concrete API or MCP server URL the access is for. |
| Subject | The user (or identity) the agent acts for. |
| Client | Which client requested the grant, so the AS can bind the grant to the authenticated client. |
| Scopes | What the IdP is willing to allow for this hop (the resource AS may narrow further). |
Expiry (exp) / jti | Short lifetime; optional single-use tracking. |
After a successful JWT Bearer exchange, the resource AS returns an ordinary access token whose audience is the resource. From that point on, the MCP server or API validates that access token the way it always does.
Minting the XAA token (IdP side)
At the IdP token endpoint, the client sends a token exchange roughly like:
curl -X POST https://idp.example.com/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "requested_token_type=urn:ietf:params:oauth:token-type:id-jag" \
-d "subject_token=$EXISTING_IDP_TOKEN" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "resource=https://mcp.example.com" \
-d "scope=tools:read"The IdP:
- Authenticates the client (confidential clients are strongly preferred).
- Validates the subject token.
- Checks enterprise policy (which clients, users, and resources are allowed).
- Returns the XAA token (ID-JAG), often with a very short lifetime.
When Descope is the IdP
When you issue XAA tokens for your agents, Descope is this IdP. The agent never talks to Descope and the downstream server in one step: Descope mints an XAA token (ID-JAG), then the downstream authorization server trades that assertion for its own access token.
Exchange a Descope token for an XAA token
The agent calls Descope's token endpoint with the token exchange grant (RFC 8693), asking for an ID-JAG as the requested token type.
The subject token is a credential Descope already issued for your project. Descope accepts three kinds:
| Subject token | subject_token_type |
|---|---|
| A SAML assertion issued by Descope | urn:ietf:params:oauth:token-type:saml2 |
| A Descope refresh token | urn:ietf:params:oauth:token-type:refresh_token |
| A Descope ID token | urn:ietf:params:oauth:token-type:id_token |
The token has to come from your own Descope project. That ties the assertion to a real user in your tenant, which is what policy is then evaluated against.
curl -X POST "__BaseURL__/oauth2/v1/apps/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "requested_token_type=urn:ietf:params:oauth:token-type:id-jag" \
-d "subject_token=<DESCOPE_ID_TOKEN>" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:id_token" \
-d "resource=https://mcp.asana.com/v2/mcp" \
-d "client_id=<CLIENT_ID>" \
-d "client_secret=<CLIENT_SECRET>"resource is the downstream service the agent wants to reach. Descope matches it to the Resource you registered, checks your token exchange policy, and mints the assertion only for what that policy allows. For Console setup (Resource, scopes, policy), see Issue XAA Tokens for Your Agents → XAA setup.
What the XAA token contains
The response carries a signed JWT. Its header marks it as an ID-JAG rather than an ordinary access token:
{
"alg": "RS256",
"kid": "SK3ICInXRuWz7Dw0BAvLObJktyKeK",
"typ": "oauth-id-jag+jwt"
}{
"aud": "https://mcp.asana.com",
"client_id": "UDNJQ0luWW5XVE13ZmZXekt4S3QzTGFFUXZvRzpUUEEzSU5nbXRiSlZVN2RpYkVMeTR6WXZDSGlsVUUj",
"exp": 1787611589,
"iat": 1787611289,
"iss": "https://api.descope.com/v1/apps/P3ICInYnWTMwffWzKxKt3LaEQvoG",
"jti": "LJ3INiBL8Bwa6RXVs55q0pcoo0e91",
"resource": "https://mcp.asana.com/v2/mcp",
"scope": "",
"sub": "U3ICdOPhMIascVJHWE49w92v8v3R"
}aud and resource are not the same value:
| Claim | Meaning |
|---|---|
aud | The authorization server that will validate this assertion (in the example, Asana's AS at https://mcp.asana.com). |
resource | The specific protected resource the agent wants to reach (in the example, https://mcp.asana.com/v2/mcp). |
client_id | The ID the downstream AS knows the client by, from the client's Cross App Access target for this Resource. It can be a CIMD URL. With no mapping, it falls back to the client's Descope client ID, as in the example above. |
iss | Your Descope project as the issuer, <base-url>/v1/apps/__ProjectID__. The downstream AS fetches Descope's JWKs from here. |
sub | The Descope user the agent is acting for. |
exp / iat | Short-lived (five minutes in the example). Minted per request, not stored. |
In the request you send only resource. Descope takes aud from the authorization server set in the Resource's Cross App Access section.
Note
The resource value you send and the resource claim you get back must both match the downstream server exactly. A trailing slash or path difference produces an assertion the downstream authorization server will reject.
Redeem the XAA token for an access token
The agent presents the assertion to the downstream authorization server named in aud, using the JWT Bearer grant (RFC 7523):
curl -X POST "https://mcp.asana.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=<ID_JAG>" \
-d "client_id=<CLIENT_ID_AT_THE_DOWNSTREAM_AS>"That server validates the assertion against Descope's public keys, confirms it trusts your project as an issuer, and returns its own access token. Descope's involvement ends when the ID-JAG is minted.
Client authentication for XAA
The ID-JAG is a bearer assertion. Anyone holding one can present it to the downstream authorization server and receive an access token, so Descope requires the client requesting an ID-JAG to authenticate as a confidential client (client ID and secret, or private key JWT).
| Client type | Can request an ID-JAG | Notes |
|---|---|---|
| Confidential (recommended) | Yes | Authenticates with a client secret or private key JWT. |
| Public | Possible, but not the default | Cannot hold a secret, so the assertion is harder to protect against replay. |
Private key JWT
Signing a client assertion with your own private key avoids distributing a shared secret. It is behind a feature flag — contact Descope to enable it. See Client authentication for the supported methods.
Registering the client with the downstream authorization server
Descope being a trusted issuer is not enough on its own. The downstream AS must also recognize your client:
- CIMD — the client presents a metadata URL; the downstream server registers it on the spot (usual path).
- Manual registration — you create the client at the downstream server and configure its ID (and secret) in your agent.
Use that ID or the CIMD URL as the client's Cross App Access target for the Resource in Descope, so the ID-JAG's client_id matches what the downstream server expects.
This is separate from registering the client in Descope. Clients that arrive in Descope through CIMD or DCR get access to the Resource they registered with automatically. A client you create by hand in Descope needs a token exchange policy or the exchange fails.
Redeeming the XAA token (resource AS side)
At the resource authorization server:
curl -X POST https://resource-as.example.com/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=$XAA_TOKEN"The resource AS:
- Authenticates the client.
- Verifies the JWT signature against the IdP's JWKs.
- Checks
iss,aud,exp, resource, and client binding. - Issues an access token for that resource (and may map the subject into a local user or tenant).
In Descope, when customers manage their agents against an MCP server you host for them, Descope is this resource authorization server: it accepts the customer's XAA token and returns a Descope access token. See Accept Your Customers' XAA Tokens to set it up.
Other Notes on XAA and ID-JAG
- Audience-restricted access token: The token the resource AS issues should be bound to the resource named in the assertion
- Per-tenant trust on the validation side: Each customer's issuer is registered on their tenant, so one customer's assertions can't buy access for another
Enterprise-Managed Authorization
Use Cross App Access (XAA) so an enterprise identity provider decides which agents reach which MCP servers, for your agents or your customers'.
Issue XAA Tokens for Your Agents
Let Claude Code, VS Code, and other pre-built clients reach third-party MCP servers with XAA tokens that Descope issues and governs by policy.