Skip to content
TDACorpIDENTITYSign in

Token exchange

How a service trades a token it holds for one addressed to another service, the exact request, and every refusal.

On this page

A token is addressed to one audience, and a service must refuse a token that is not addressed to it. Token exchange (RFC 8693) is how a service obtains a token that another service will accept. Service A holds a token addressed to A, asks identity to exchange it, and receives a token addressed to B.

This page covers the exchange itself. Service to service puts it together with client_credentials and shows the SDK doing both in one call.

When to use it#

  • Service A calls service B and has no person involved. A gets its own token with client credentials, then exchanges it for B.
  • Do not use it to widen anything. An exchange can only narrow: the new token never carries a scope the old one lacked.

A public client may not use this grant. The exchange of a token that stands for a person, or one that adds an actor to the token, is not described here.

The request#

POST /tokenContent-Type: application/x-www-form-urlencodedDPoP: <proof>grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id=<your client id>&subject_token=<the token you hold>&subject_token_type=urn:ietf:params:oauth:token-type:access_token&requested_token_type=urn:ietf:params:oauth:token-type:access_token&audience=<the target's client id>&scope=<optional, space separated>&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=<assertion>
ParameterRule
subject_tokenAn access token identity issued, exactly once in the request.
subject_token_type and requested_token_typeBoth are the access token URN above. A request without requested_token_type is refused with invalid_request.
audienceThe client id of the service you want to call. It names the target.
scopeOptional, at most once. A subset of what the new token may carry. Without it, or if it is repeated, you get the full allowed set.
resourceNot allowed. It is refused with invalid_target. Use audience.

You authenticate as your client with an assertion and prove your DPoP key, as for any token request.

What identity checks#

  1. The subject_token is an access token (an ID token is refused) and verifies: its signature, the issuer, that it has not expired or been revoked, that its session (if it has one) is still live, and that the identity behind it is active.
  2. The token was issued to you: its aud or azp is your client id.
  3. If the token is bound to a key, your request proves the same key. The SDK uses one key for both steps.
  4. The target exists, is a registered client, holds the token_exchange grant, and may enter the token's organization: it is a first-party client, belongs to that organization, or its app has an active installation there. A token with no organization reaches a first-party target only.
  5. The token's authorization version is still current. If the underlying permissions changed since it was issued, the exchange is refused and you start again from client_credentials.
  6. If the token's organization requires single sign-on (the sso_enforcement flag, default OFF), the session behind the token must hold a recent enough single sign-on login. Otherwise the answer is insufficient_user_authentication with acr_values and max_age.
  7. The scopes: the new token carries the scopes of the old one that the target is registered to hold. If you name scopes, they must be inside that set. If the intersection is empty, the exchange fails.

The answer#

{  "access_token": "<jwt>",  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",  "token_type": "DPoP",  "expires_in": 600,  "scope": "<granted scopes>"}

There is no refresh token and no ID token. The lifetime is the shortest of: the time left on the token you presented, 900 seconds, and the target's own access token setting. An exchanged token therefore never outlives its parent.

ClaimValue
audThe target's client id.
azpYour client id: who asked for the token.
subUnchanged from the token you presented.
scopeThe narrowed set.
cnfThe thumbprint of the key proved on this request.

The target verifies it with createResourceServer, giving its own client id as audience. See Protect your API.

Refusals#

AnswerCause
unauthorized_clientA public client used the grant, or the client does not hold token_exchange.
invalid_requestA missing or wrong requested_token_type, subject_token_type or subject_token.
invalid_grantThe subject token does not verify, was not issued to you, is bound to a key you did not prove, is stale, or the identity behind it is no longer active.
invalid_targetaudience is missing, not a client id, not registered, lacks token_exchange, its app is not installed in the token's organization, or you sent resource.
insufficient_user_authenticationRFC 9470, only with sso_enforcement on. The session needs a fresh single sign-on login.
invalid_scopeYou named a scope outside the allowed set, or the sets share nothing.
invalid_clientClient authentication or DPoP failed.
use_dpop_nonceRetry with the DPoP-Nonce value and a fresh assertion.

The description sometimes names the parameter. The SDK throws OAuthEndpointError with oauthError set to the code.

With the SDK#

serviceClient.exchangeToken({ subjectToken, audience, scope }) makes this one request. Most code uses tokenFor(audience) instead, which mints the first token, exchanges it, caches the result and retries once if the first token had gone stale.

Verified against commit ca46b57