Token Exchange
Token exchange (RFC 8693) lets a client trade a token it already holds, the subject token, for a new token scoped to a downstream Resource. Inbound Apps and Agentic Clients use it to narrow or retarget a credential without sending the user through login again. Credentials stored in Connections come from a different endpoint, covered in Fetching a Connection Credential.
Token exchange works differently from the authentication grants, which decide how a client gets its first token and are turned on per app. Token exchange is always available at the project's token endpoint, with no per-app toggle and no consent screen. A policy with the Delegated access (token exchange) grant type is the only thing that decides whether an exchange succeeds.
How It Works
- The client gets a subject token with an authentication grant, such as authorization code, client credentials, or JWT bearer.
- The client calls
/tokenagain withgrant_type=urn:ietf:params:oauth:grant-type:token-exchange, naming the target Resource and, optionally, the scopes it wants. - Descope evaluates policies. If a rule allows Delegated access (token exchange) for that client, target, and scopes, Descope returns the new token. Otherwise it denies the request and logs a policy violation.
Which Tokens You Can Exchange
The subject token doesn't have to come from an Inbound App. Descope accepts any access token or ID token that your project issued, so the client that exchanges a token can be a different app from the one that received it. Set subject_token_type to urn:ietf:params:oauth:token-type:access_token or urn:ietf:params:oauth:token-type:id_token to match the token you send.
- Inbound App and Agentic Client tokens: A token from any Inbound App or Agentic Client in the project, including a Resource token from an earlier exchange.
- Federated App tokens: A token from an OIDC Federated App, where Descope is the identity provider for one of your own applications. A user who already signed in to that application doesn't have to go through a new OAuth flow to reach your APIs.
- Descope session tokens: The session token from your own app's login with a Descope SDK or Flow.
Descope validates every subject token in two ways. The token must come from the same project as the exchanging client, so a token from one project can't be exchanged in another. The token's aud must also include your project ID or the exchanging client's client ID. Federated App tokens and session tokens carry the project ID by default. An Inbound App token carries both values unless its Default Audience is set to Client ID only, in which case only that same app can exchange it.
An exchange for an XAA token (ID-JAG) has its own rules. It requires an ID token or a SAML assertion as the subject token, and it skips the aud check. The usual subject token there is the ID token from the user's federated login, which was issued to a different app than the one that exchanges it.
Exchanging from Your Backend
Token exchange always requires client authentication, with a client secret, private key JWT, or Workload Identity Federation. Only a confidential client can authenticate that way, so a public client such as a single-page app, a mobile app, or a CLI can't exchange tokens.
The usual pattern puts the exchange in your backend. The public client signs in and calls your backend with its token. Your backend is a Resource to that client, and it's also registered as its own confidential Inbound App. It validates the incoming token, exchanges it for a token to the downstream Resource, and calls that Resource. The secret or private key stays on your server, and the public client only ever holds a token for your backend. Downstream credential access walks through this setup for an API or MCP server.
Configuring the Policy
On the Access Policy page, create or update a policy with:
- Subjects: The Inbound App or Agentic Client that may perform the exchange, or conditions on client tags, tenant, and so on
- Targets: The Resource or Connection, and the scopes this policy grants
- Grant types: Delegated access (token exchange)
The new token only carries scopes that the policy allows, the target defines, and the client requested. See Policies → Grant Types for the full model and examples such as tenant-scoped delegated access.
Exchanging for a Resource Token
Use this when a client holds a Descope access token and needs a separate token for your API or MCP server, with a different aud or scope. The client keeps its original token and sends it to Descope as the subject token. Descope checks policy and returns a new JWT for the target Resource.
The exchange can happen in the client itself, or in your backend with its own client, as described in Exchanging from Your Backend. You can switch between the two in the diagram:
Select any step to see what happens. In phase 1 the client gets token A for Resource A. In phase 2 it trades token A for token B, scoped to Resource B, and keeps token A for calls to Resource A.
Send a POST to the token endpoint:
| Parameter | Description |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | The access token (or other token type) being exchanged |
subject_token_type | Usually urn:ietf:params:oauth:token-type:access_token |
resource | Target Resource URI (RFC 8707) |
scope | Optional subset of scopes; must be allowed by policy |
| Client authentication | client_id / client_secret, or other method configured on the app |
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 "client_id=<CLIENT_ID>" \
-d "client_secret=<CLIENT_SECRET>" \
-d "subject_token=<SUBJECT_TOKEN>" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "resource=https://your-api.example.com" \
-d "scope=orders.read"The new token still represents the same user and tenant as the subject token. Your Resource validates it (iss, aud, exp, scope) like any other token. See Developing APIs and Using a Resource Token for how to validate it.
The aud check is what keeps each hop separate. If your Resource rejects any token whose aud isn't its own URI, a token issued for another service can't be replayed against it, and a caller can't skip a gateway in front of it by presenting the token it was given for that gateway.
The Actor Claim
The exchanged token's sub is still the user, so your Resource can't tell from sub alone which service asked for the token. Descope records that service in the actor claim (act), as RFC 8693 §4.1 defines it. The sub inside act is the client ID of the app that made the exchange:
{
"sub": "<USER_ID>",
"aud": ["https://your-api.example.com"],
"act": {
"sub": "<BACKEND_CLIENT_ID>"
}
}When the subject token already has an act claim, Descope keeps the earlier actors. It nests the subject token's act inside the new one, so every exchange adds one layer. The outer sub is the most recent actor, and the innermost sub is the first service in the chain. After a second service exchanges the token above, the new token looks like this:
{
"sub": "<USER_ID>",
"aud": ["https://orders-api.example.com"],
"act": {
"sub": "<SECOND_SERVICE_CLIENT_ID>",
"act": {
"sub": "<BACKEND_CLIENT_ID>"
}
}
}Your Resource should base access decisions on the user and the current actor, the outermost act.sub. RFC 8693 treats nested actors as informational only, so use them for logging and auditing, not to grant access. Descope also limits how long the chain can grow. It rejects an exchange when the subject token's act chain is already more than three levels deep. If your services need a longer chain, you can contact Descope Support to raise the limit. The per-request TokenExchangeSuccess audit event records the same chain in its actor_chain and delegation_depth fields.
Token exchange and impersonation
Descope also writes act during user impersonation, but the actor is different. In a token exchange, the actor is an app, so act.sub holds a client ID. In impersonation, the actor is a person, so act.sub holds the impersonator's Descope user ID, and sub holds the impersonated user.
The two can appear in one chain. When a service exchanges a session token from an impersonated session, Descope nests the impersonation act inside the new one. The impersonator's user ID is then the innermost sub, under the client IDs of every service that exchanged the token. If your Resource applies stricter handling to impersonated requests, it should look for a user ID anywhere in the chain, not only in the outer act. A nested actor can make your Resource refuse or limit a request, but it should never be the reason to allow one.
Fetching a Connection Credential
Credentials from the Connections vault don't come from the token endpoint. When an agent, MCP server, or gateway needs to call a third-party API, it presents its Descope access token to the Connection token endpoints instead. Descope still checks a policy with the Delegated access (token exchange) grant type, with the Connection as the target, before it returns the stored OAuth token or API key. See Downstream credential access for the request.
When to Use It
| Scenario | See also |
|---|---|
| A user signed in once, and the client needs a scoped token for one API or MCP server | Resources, MCP server |
| A gateway exchanges an agent's token for a token scoped to one of the Resources behind it | Gateways |
| An autonomous agent with an M2M token needs a narrower Resource token | Client credentials |
| An agent needs an XAA token (ID-JAG) to present to a third-party authorization server that protects a resource server, such as the Notion or Supabase MCP server | How XAA Works |
Troubleshooting
When no policy permits the requested subject, target, or scopes, Descope returns no token and writes a Warning audit event with the client, target, requested scopes, and evaluation outcome. The Agentic Activity Dashboard rolls these up. Common fixes:
- Add or update a policy with Delegated access (token exchange) for the client and the Resource or Connection
- Confirm the Resource or Connection defines the scopes you're requesting
- Make sure the subject token is still valid and was issued to the same client, or satisfies your policy's subject conditions
- If the error says the delegation depth limit was exceeded, the subject token's
actchain is already more than three levels deep. You can exchange a token from earlier in the chain instead, or ask Descope Support to raise the limit
For the endpoint and parameter reference, see Authorization Server. For end-to-end request examples, see Using Inbound Apps.