Skip to content

Token exchange

Token exchange (RFC 8693) is the OAuth grant for transforming one token into another. You hand the platform a valid token and a request for a new one; the platform decides whether the swap is permitted and, if so, issues the new token.

This sounds abstract. Three concrete cases will make it click.

Your service holds an access token with scopes users:read users:write audit:read. It needs to call a downstream service that only needs users:read. You do not want to forward the broader token — a bug in the downstream could misuse the extra capabilities.

Exchange the broad token for a narrower one:

POST https://<your-tenant-url>/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<the-broad-token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&scope=users:read

The response is a fresh access token with only users:read. Forward it to the downstream service. If the downstream is compromised, the blast radius is limited to what users:read allows.

Service A holds a token whose audience is https://api-a.cymmetri.com. It needs to call Service B at https://api-b.cymmetri.com. Service B will reject the existing token because the aud claim is wrong.

Swap audiences:

POST https://<your-tenant-url>/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<service-a-token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=https://api-b.cymmetri.com
&scope=...

The platform checks whether Service A (the subject of the original token) is allowed to call Service B (the new audience). If allowed, the new token is issued; if not, invalid_target comes back.

A support engineer is signed in. They need to perform an action on behalf of a customer's user — a refund, an account recovery, a data lookup. The right token model is "the support engineer is acting as the customer's user" — both identities are visible in the audit log, neither is hidden.

Exchange the engineer's token for a customer-scoped token:

POST https://<your-tenant-url>/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<engineer-token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=<customer-user-token>
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
&scope=accounts:read

The platform issues a new access token whose sub is the customer's user, with an act claim recording the engineer's identity. Every API call carrying that token is traceable to both identities — the engineer who acted and the user they acted as. This is the right pattern for support tooling.

  • You just need a new token because the old one expired. That's refresh, not exchange.
  • You are a backend calling another backend in a job that has no user context. That's client credentials.
  • You are the same service calling the same downstream you've always called. No need to transform anything; just forward the token (assuming the audience is right).

Token exchange is for transformations. If nothing about the token is changing, you do not need this grant.

Not every transformation is permitted. The tenant admin controls:

  • Which application can perform exchanges at all (an opt-in flag on the application).
  • Which audiences each application may target (a policy list).
  • Whether act-as is permitted, and which subject types may be impersonated.

The platform refuses with invalid_target or invalid_grant when a request crosses a boundary the policy does not allow. The audit log records every exchange, allowed or denied.

The full set of token types you might see:

IdentifierMeans
urn:ietf:params:oauth:token-type:access_tokenA standard OAuth access token
urn:ietf:params:oauth:token-type:refresh_tokenA refresh token
urn:ietf:params:oauth:token-type:id_tokenAn OIDC id token
urn:ietf:params:oauth:token-type:jwtAny JWT

You almost always exchange access tokens for access tokens. The other types come up in federation scenarios — for example, exchanging a SAML assertion for an access token at the trust boundary.

Every exchange records:

  • The original subject_token jti.
  • The new token's jti.
  • The actor if present.
  • The application performing the exchange.

This lets you reconstruct a call chain after the fact: a single user request can hand off through three services, each performing a narrowing exchange, and the audit log shows the full lineage.