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

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
Manage agents in your enterpriseIdPDescopeThird-party MCP / API authorization server
Let customers manage their agentsResource 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 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 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 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:

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).
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 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 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.

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:

  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 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.
Was this helpful?

On this page