Token Exchange and Assertion Grants
|
This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against. OAuth 2.1 is still an Internet-Draft ( This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
The grants documented on Client Credentials and Client Authentication answer "who is this service?". This page answers a different question that shows up the moment a request has to cross more than one service on a user’s behalf: "who is this service acting for, and on whose authority?". RFC 8693 (Token Exchange) and the RFC 7521 assertion framework — with its RFC 7522 (SAML) and RFC 7523 (JWT) bearer-grant profiles — are the OAuth answers to that question, and they are the pieces that make a user’s identity survive a call chain instead of evaporating at the first internal hop.
The problem: a user’s identity does not survive a call chain by default
A client obtains an access token scoped to the first service it calls — call it service A. Service A now needs to call service B to satisfy the request. Three bad options present themselves immediately:
-
Forward the user’s original access token as-is. Service B now accepts a token whose audience (
aud) was never it; if the token is a JWT, the audience check that RFC 8725 requires will (correctly) reject it, and if the token is opaque, introspection will report an audience mismatch. Even where nothing enforcesaud, this quietly defeats audience restriction (RFC 8707) and lets any downstream service replay the token anywhere. -
Have service A call service B with its own client-credentials token. Service B now sees "service A", not "service A, acting for user U". Every authorization decision, audit-log entry, and rate limit downstream loses the user’s identity — the exact accountability gap delegated authorization was invented to close.
-
Have service A re-run a full authorization-code flow against service B, impersonating the user. There is no user present to authenticate; this is not a flow that exists in the model and should not be improvised.
RFC 8693 and the assertion grants are the two purpose-built mechanisms that avoid all three: a service presents a token it already holds — as proof of what it wants to assert — to an authorization server, and receives back a new, correctly audienced token that still carries the original user’s identity, explicitly and verifiably.
RFC 8693: OAuth 2.0 Token Exchange
RFC 8693 defines a new grant type, urn:ietf:params:oauth:grant-type:token-exchange, that trades one or two
security tokens for another. It is deliberately token-format-agnostic — the tokens involved may be JWTs, SAML
assertions, or opaque strings — which is what makes it usable as the connective tissue between
internally-issued OAuth tokens, externally-issued OIDC ID tokens, and legacy SAML assertions in the same
architecture.
Wire format: the exchange request
POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic c2VydmljZS1hOnNlY3JldA==
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzZXJ2aWNlLWEifQ...
&actor_token_type=urn:ietf:params:oauth:token-type:jwt
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=service-b
&resource=https://api.example.com/service-b
&scope=orders.read
Every parameter is named and defined in RFC 8693 §2.1:
| Parameter | Meaning |
|---|---|
|
Always |
|
The token that identifies the party on whose behalf the exchange is being requested — typically the user’s original access token or ID token. This is the identity the new token is about. |
|
A URI identifying the type of |
|
A token identifying the party that is doing the acting — the calling service itself. Present only for delegation (see below); absent for pure impersonation. |
|
Required whenever |
|
The token type the client wants back. Defaults to |
|
The logical name of the service the new token is intended for — distinct from |
|
A resource-indicator URI per RFC 8707, giving the exact API the new token should be scoped to. |
|
A (typically narrower) scope for the new token — token exchange is a natural down-scoping point. |
Wire format: the exchange response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "orders.read"
}
issued_token_type echoes back what was actually issued, which may legitimately differ from
requested_token_type if the authorization server substitutes a type it prefers. Exchanged tokens are
conventionally issued with a short lifetime (note the 300-second expires_in above) — they are minted for
one hop of a call chain, not for long-term storage.
|
RFC 8693 is a grant type, so it is requested at the standard |
Impersonation vs. delegation, and the act claim
RFC 8693 draws a sharp line between two shapes of exchange, and the distinction matters for every downstream authorization decision:
-
Impersonation — only
subject_tokenis presented, noactor_token. The new token asserts that the calling service is the subject:subin the issued token is the user’s own subject identifier, with no trace that a service acted on their behalf. This is the weaker audit trail; use it only when the receiving service genuinely has no need to know that a chain of services was involved. -
Delegation — both
subject_tokenandactor_tokenare presented. The new token keepssubas the user, but adds anactclaim identifying the actor:
{
"iss": "https://as.example.com",
"sub": "user-42",
"aud": "service-b",
"scope": "orders.read",
"act": {
"sub": "service-a"
},
"exp": 1893456600
}
The act claim can nest, so a chain of three services produces a chain of act claims — act.act identifies
the service that acted for the actor that acted for the user, and so on. A resource server that receives a
delegated token can therefore reconstruct the exact call path an identity travelled, which is what makes
delegation the right default for audit-sensitive systems: every hop is named, not just the originating user.
A separate may_act claim (RFC 8693 §4.4) can be embedded in a token in advance to pre-authorize which actors
are allowed to request delegation on that subject’s behalf — useful when the authorization server wants to
constrain delegation to a known allow-list of internal services rather than trusting whatever presents a valid
actor_token.
|
Impersonation silently discards the calling service’s identity from the issued token; delegation preserves it in
|
The assertion framework: RFC 7521, RFC 7522, RFC 7523
Token exchange trades a token the authorization server itself understands as an OAuth artefact. The assertion framework solves an adjacent but different problem: a client already holds a pre-existing security assertion issued by some other party — a SAML assertion from an enterprise identity provider, or a JWT signed by a trusted issuer — and wants to trade it directly for an OAuth access token, or use it as a client-authentication credential, without a browser round-trip.
RFC 7521 defines the generic framework: an assertion can be used either as a grant (the assertion parameter
at the token endpoint, requesting an access token) or as a client authentication method (a client_assertion
parameter, proving the client’s identity instead of a shared secret). Two profiles instantiate the framework for
the two assertion formats in wide use:
-
RFC 7522 — SAML 2.0 Bearer Assertion Profile — for organisations with an existing SAML identity provider (common in enterprise single sign-on) that want to bridge SAML-authenticated identities into an OAuth/OIDC-speaking API layer without re-implementing authentication.
-
RFC 7523 — JWT Bearer Token Profile — the far more common case today: a JWT, signed by a key the authorization server trusts, is exchanged directly for an access token, or used to authenticate the client itself (
private_key_jwt, already covered on the client-authentication page).
Wire format: the JWT bearer grant (RFC 7523) as an authorization grant
POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6ImlkcC1rZXktMSJ9.eyJpc3MiOiJodHRwczovL3BhcnRuZXItaWRwLmV4YW1wbGUuY29tIiwic3ViIjoidXNlci00MiIsImF1ZCI6Imh0dHBzOi8vYXMuZXhhbXBsZS5jb20vdG9rZW4iLCJleHAiOjE4OTM0NTQwMDB9.signature
&scope=orders.read
The assertion is a JWT whose iss identifies the trusted issuer (a partner authorization server, an
enterprise identity provider, or the client’s own signing key for self-issued assertions), whose sub identifies
the subject the access token should be issued for, and whose aud names the token endpoint itself so the
assertion cannot be replayed against a different authorization server. The response is an ordinary OAuth token
response — access_token, token_type, expires_in, optionally scope — with no refresh token, matching the
same "no interactive user present to re-consent" reasoning that applies to client_credentials.
Wire format: JWT bearer as client authentication (RFC 7523 §2.2)
The same profile also defines client_assertion_type / client_assertion as a client-authentication method
usable with any grant type, most often paired with authorization_code or client_credentials:
POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6ImNsaWVudC1rZXktMSJ9.eyJpc3MiOiJzZXJ2aWNlLWEiLCJzdWIiOiJzZXJ2aWNlLWEiLCJhdWQiOiJodHRwczovL2FzLmV4YW1wbGUuY29tL3Rva2VuIiwianRpIjoiYWJjMTIzIiwiZXhwIjoxODkzNDU0MDYwfQ.signature
&scope=orders.write
This is exactly private_key_jwt, already introduced on
Client Credentials and Client
Authentication — it is listed here because it is the same RFC 7523 machinery, applied to authenticating the
client rather than asserting a subject.
|
|
Token exchange vs. the assertion grants: when to reach for which
| RFC 8693 token exchange | RFC 7521/7523 assertion grant | |
|---|---|---|
What is presented |
A token the same authorization server (or a federated one it trusts) already issued |
A pre-existing assertion from an external issuer (partner IdP, SAML source, self-signed client JWT) |
Typical use |
Internal service-to-service hop, narrowing scope/audience for the next call |
Bridging an external identity into this authorization server for the first time |
Identity chaining |
Native support via |
No built-in actor-chaining; the JWT’s own claims must carry any lineage |
Grant type |
|
|
Refresh token issued |
No |
No |
In practice the two compose: a SAML or JWT bearer grant is often the entry point that gets an external identity into the system as a first OAuth token, and RFC 8693 token exchange is what then carries that identity down an internal call chain, narrowing audience and scope at each hop.
Identity chaining across trust domains
Everything above assumes a single authorization server, or a small number of authorization servers that directly trust each other’s tokens. Real organisations increasingly span multiple trust domains — a subsidiary’s authorization server, a partner’s authorization server, a SaaS vendor’s authorization server — and need a user’s identity to cross those boundaries verifiably, not just internally.
draft-ietf-oauth-identity-chaining (in the RFC Editor queue at the time of writing) standardises this: it
defines how an authorization server in domain B can accept a token exchange request whose subject_token was
issued by a different authorization server in domain A, verify that trust relationship, and issue its own
token that still carries the originating identity forward — effectively RFC 8693’s act chaining, but across
an organisational trust boundary instead of within one authorization server’s own token issuance. It builds
directly on RFC 8693’s wire format rather than introducing a new grant type, which is why it is documented here
alongside token exchange rather than as a separate flow.
|
Cross-domain identity chaining is only as strong as the trust relationship between the two authorization
servers. Verify the issuing authorization server’s identity (via its |
Where each fits a microservice topology
| Topology | Recommended mechanism |
|---|---|
API gateway fronting internal microservices, single authorization server |
RFC 8693 token exchange at (or just behind) the gateway: exchange the user’s front-door token for a short-lived, audience-scoped token per downstream service. |
Service A calls service B calls service C, want a full audit trail |
RFC 8693 delegation ( |
Enterprise SSO (SAML) bridging into an OAuth-speaking API layer |
RFC 7522 SAML bearer grant once, at the boundary, to mint the first OAuth token; RFC 8693 from there on. |
Partner or vendor integration issuing its own signed JWTs |
RFC 7523 JWT bearer grant to trade the partner’s JWT for a token your resource servers understand. |
Multi-organisation identity crossing separately-operated authorization servers |
|
Machine-to-machine call with no user identity involved at all |
Plain |
Sequence: a user token exchanged down a three-service call chain
The diagram below follows a single user request from an API gateway, through service A and service B, down to
service C, with a delegated token exchange narrowing audience and accumulating act at every hop.
Each exchange narrows the audience to exactly the next hop and adds one more level of act nesting, so service
C can see — and log — the entire path the user’s identity travelled to reach it, not just the fact that
someone eventually called it with a token naming user-42.
References
-
RFC 7521 — Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants
-
RFC 7522 — SAML 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants
-
RFC 7523 — JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
-
draft-ietf-oauth-identity-chaining — OAuth Identity and Authorization Chaining Across Domains