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.
When pre-built agents in your enterprise reach third-party tools (via XAA or a gateway), see Manage agents in your enterprise. If you host an MCP server for customers and accept their XAA tokens, see Let customers manage their agents (also covered under SSO Setup Suite and Cross App Access for B2B MCP Servers).
The problem XAA solves
Without XAA, an agent that already signed in through the company IdP still has to run a separate OAuth consent flow for every MCP server or API it wants to use. Each product becomes its own authorization island. Enterprise IT cannot answer "which agents can reach which tools for which users?" in one place.
Cross App Access (XAA) reuses the SSO trust the enterprise already has. The IdP that signed the user in also vouches, in a short-lived JWT, that this user, through this client, may reach this resource. The client presents that JWT to the resource's authorization server and receives a normal access token. There is no second browser redirect and no per-tool consent screen on the resource side.
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 |
|---|---|---|---|
| Manage agents in your enterprise | IdP | Descope | Third-party MCP / API authorization server |
| Let customers manage their agents | 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 manage agents in your enterprise, 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 Manage 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). |
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 derives aud from the authorization server that protects that resource.
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.
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.
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 Let customers manage their agents.
Other Notes on XAA and ID-JAG
- No second consent UI on the resource, because the enterprise already authorized the hop at the IdP.
- Short-lived assertion: the XAA token is meant to be exchanged quickly, not stored as a long-lived API key.
- Audience-restricted access token: the token the resource AS issues should be bound to the resource named in the assertion.
- Confidential clients: anyone holding a valid XAA token can attempt redemption, so Descope expects authenticated clients when minting ID-JAGs.
- Per-tenant trust on the validation side: each customer's issuer is registered on their tenant so one customer's assertions cannot buy access for another.