Security Engineering · Authorization
Constrained Delegation and Token Exchange in OAuth 2.0
How the token exchange grant carries a subject through a chain of services, what scopes and audience restrict, and why impersonation and delegation differ in what the audit log records.
A request that crosses five services needs an answer to a question the original bearer token does not answer: on whose behalf is this hop acting, and with what authority. Passing the user’s token to every downstream service works until one of those services is compromised, at which point the token grants access to every service in the chain. Passing a service credential works until the audit trail needs to name the user. The token exchange grant exists to make that middle case expressible: a service holding a token for a user requests a new token, scoped to a different audience, that still records the user’s identity.
Subject token, actor token, and who is who
The exchange request names a subject token, which represents the party on whose behalf the request is being made, and optionally an actor token, which represents the party making the request. The response is a new token whose subject reflects the subject token and whose authorization reflects the scopes the exchange endpoint chose to grant. For pure impersonation the request carries only the subject token; for delegation it carries both, and the issued token records both identities.
The distinction is not cosmetic. With delegation, a downstream service can distinguish “the user asked for this and service A relayed it” from “service A decided this on its own”. An audit log that records only the subject cannot tell those apart, and the difference is exactly what a post-incident review needs. The RFC’s model puts this in the token claims: the subject identifies the party the token is about, and an act claim identifies the current actor, with nesting for chains.
Nesting is the part that implementations get wrong. A chain of three services produces an act claim nested three deep, innermost actor outermost in the nesting, and each hop must add its own actor rather than replacing the previous one. A service that validates the token and re-issues it without preserving the chain has destroyed the property the whole grant exists to provide, and the resulting token is indistinguishable from one issued directly to the last service.
Scopes do not compose the way people expect
A scope is a string the authorization server defines and the resource server interprets. The protocol does not specify their structure or their relationship, so read write on the original grant does not formally imply that read can be requested for a downstream audience, nor that the exchange endpoint will grant it.
The rule that makes delegation meaningful is that every hop can only narrow. A service that requests a token with scopes the original token did not carry should be refused by the exchange endpoint, and the response should not silently grant the intersection. Implementations that grant the intersection produce a working system whose failure is invisible: the downstream service believes it has authority it does not have, and the error appears later as a mysterious authorization failure rather than at the exchange.
Narrowing also applies to audience. A token minted for the billing service should be rejected by the records service even if the records service would otherwise accept the issuer’s tokens. Audience restriction is the mechanism that prevents a compromised service from replaying a token it received against a different service, and it requires the resource server to actually check the audience claim rather than only the signature.
POST /token HTTP/1.1
Host: authn.example.net
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&subject_token=eyJhbGciOiJSUzI1NiIs...
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&audience=https%3A%2F%2Frecords.example.net
&scope=records%3Aread
The grant type URI and the token type URIs are registered in the RFC and must match exactly; a typo produces a request the server rejects as an unsupported grant, which is at least a loud failure.
What binds a token to the channel it travelled on
A bearer token is a bearer instrument: whoever holds the bytes can use them. Delegation multiplies the number of parties that hold the bytes, since every hop in the chain has a copy, so the window in which a leaked token is usable is the union of every hop’s storage.
Certificate-bound tokens address this by recording the hash of the client’s TLS certificate in the token and requiring the presenting client to prove possession of the corresponding key. A stolen token presented over a different connection, even by an attacker with the same network position, fails the check. The binding is only as strong as the proof of possession on each hop, and it must be re-established on the exchanged token rather than copied from the original.
The alternative binding is proof-of-possession through a signed request, where the client signs a structured representation of the request with its own key. That covers the case a certificate does not: a token that is presented by a service which is not the one it was issued to, where the certificate would fail but a stolen key would succeed.
Revocation across a chain
Revoking the original token does not revoke the tokens exchanged from it. Each exchanged token is an independent artifact with its own lifetime, and the authorization server may have no record linking them unless it chooses to maintain one.
Two designs exist. The first is short lifetimes for exchanged tokens combined with introspection at the resource server, which makes revocation effective within the introspection cache’s lifetime. The second is narrowing the window at issuance time: an exchanged token that lives for minutes limits the damage a leaked copy can do, at the cost of an exchange per request or per short batch.
Introspection has its own cost. Every introspected request becomes a call to the authorization server, which puts that server on the critical path of every downstream request, and its availability becomes the availability of the whole chain. Caching the introspection result moves back toward staleness, and the cache lifetime is once again the revocation delay.
The practical arrangement is that the chain is bounded in length and in lifetime. A delegation chain that can extend indefinitely, with each hop able to request a fresh token for a fresh audience, is a mechanism for generating authority that outlives the context that justified it. Bounding chain depth and requiring each token’s expiry to be at most the subject token’s expiry keeps the whole chain inside the original grant’s validity window.
Where the model meets enforcement
The exchange grant produces tokens; it does not enforce anything. Enforcement is at the resource server, and the resource server’s correctness depends on claims it may treat as advisory. A resource server that checks the signature and the expiry but not the audience, the issuer, or the scope accepts tokens minted for other services, which is the confused deputy in token form: the service’s own trust in the issuer substitutes for a check that the caller’s authority actually covers this resource.
The requirement, stated plainly, is that every resource server independently verifies issuer and audience, then applies its own scope check to the specific operation. The exchange endpoint’s decision is an input to that check, not a substitute for it. That layering of decision and enforcement is the arrangement described in the reference monitor article, and the tenant-binding problem in multi-tenant isolation applies here directly: a token scoped to a tenant must not be usable to name a different one.
Service credentials that are not user tokens
A large share of intra-service traffic has no user behind it: a scheduled job, a message consumer, a reconciliation task. The exchange grant covers this case with a subject token that represents the service itself, so the issued token names the service as subject and carries no act chain. The distinction from a user-initiated request is then visible in the token, which matters for policy: a rule that requires a human subject should be expressible as a check on the subject type, and a system that mints service tokens indistinguishable from user tokens cannot enforce that rule.
The alternative, which many deployments adopt first, is a long-lived credential in configuration. Its failure mode is well understood and still common: the credential is copied into several places, has no expiry, and grants whatever the service was first authorised for rather than what it currently needs. Exchange does not fix that by itself, but it makes the credential short-lived and audience-bound, and it puts issuance in a place where the requested scopes can be logged and compared against a policy.
There is a cost to routing service-to-service auth through the same exchange endpoint as user traffic. The endpoint becomes a dependency of every service’s every call, so its availability is the platform’s availability, and its latency is added once per hop unless tokens are reused across calls. Reuse reintroduces the narrowing problem: a token cached for a downstream audience must be cached keyed by the full requested scope and audience pair, not by the service alone, or the first caller’s narrower scope leaks into the next caller’s request.
References
- RFC 8693: OAuth 2.0 Token Exchange — IETF
- RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens — IETF
- RFC 6749: The OAuth 2.0 Authorization Framework — IETF
- Zero Trust Architecture — NIST Special Publication 800-207