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

RoleWhat it isExample
UserThe human (or service identity) the agent acts forAn employee in your Okta / Entra / Descope tenant
Requesting application (client)The agent or app that already has a session with the IdPClaude Code, VS Code, your internal agent
Identity provider (IdP)Issues the XAA token after checking enterprise policyDescope (when you manage your agents) or the customer's Okta / Entra (when they manage theirs)
Resource authorization serverTrusts the IdP, accepts the XAA token, issues its own access tokenDescope protecting an MCP server you sell, or a third-party MCP vendor's AS
ResourceThe API or MCP server the access token is forLinear 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 jobDescope's role in the diagramWho mints the XAA tokenWho redeems it
Issue XAA tokens for your agentsIdPDescopeThird-party MCP / API authorization server
Accept your customers' XAA tokensResource authorization serverCustomer's Okta, Entra, or DescopeDescope (for your MCP Resource)

End-to-end flow

Two OAuth profiles, in sequence:

  1. 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).
  2. 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:

ConceptPurpose
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.
ResourceThe concrete API or MCP server URL the access is for.
SubjectThe user (or identity) the agent acts for.
ClientWhich client requested the grant, so the AS can bind the grant to the authenticated client.
ScopesWhat the IdP is willing to allow for this hop (the resource AS may narrow further).
Expiry (exp) / jtiShort 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:

  1. Authenticates the client (confidential clients are strongly preferred).
  2. Validates the subject token.
  3. Checks enterprise policy (which clients, users, and resources are allowed).
  4. 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 tokensubject_token_type
A SAML assertion issued by Descopeurn:ietf:params:oauth:token-type:saml2
A Descope refresh tokenurn:ietf:params:oauth:token-type:refresh_token
A Descope ID tokenurn: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:

ClaimMeaning
audThe authorization server that will validate this assertion (in the example, Asana's AS at https://mcp.asana.com).
resourceThe specific protected resource the agent wants to reach (in the example, https://mcp.asana.com/v2/mcp).
client_idThe 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.
issYour Descope project as the issuer, <base-url>/v1/apps/__ProjectID__. The downstream AS fetches Descope's JWKs from here.
subThe Descope user the agent is acting for.
exp / iatShort-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 typeCan request an ID-JAGNotes
Confidential (recommended)YesAuthenticates with a client secret or private key JWT.
PublicPossible, but not the defaultCannot 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:

  1. Authenticates the client.
  2. Verifies the JWT signature against the IdP's JWKs.
  3. Checks iss, aud, exp, resource, and client binding.
  4. 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
Was this helpful?

On this page