Something goes wrong in a downstream system and you go to the audit log to find out who did it. The log says the user did it. The user did not do it — a service did, on the user’s behalf, three hops away. Whether your log can tell the difference was decided long before the incident, by which token that service was carrying.
The problem in one paragraph
A user calls service A. Service A needs to call service B to finish the job. What token should A present to B?
Three options get used in practice, and two are bad.
Pass the user’s token straight through. Simple, and it works until it doesn’t. B now holds a credential minted for A, with A’s audience and A’s scopes, and every service in the chain accumulates a token that is more powerful than its job requires. The audit trail at B records the user. It has no idea A was involved.
Use a service account. Now B knows exactly which service called — and has completely lost the user. Your authorization decisions at B can no longer depend on who the work is for, so they usually degrade into “service A is allowed to read everything,” which is how one compromised service becomes a full data breach.
Exchange the token. A presents what it has to the authorization server and asks for a token appropriate to calling B. This is what RFC 8693 standardizes, and its real contribution is not the plumbing — it is forcing you to state which of two very different things you meant.
The exchange request
Token exchange is an OAuth grant type, so it is a form POST to the token endpoint like any other:
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<the token representing who the work is for>
subject_token_type=urn:ietf:params:oauth:token-type:access_token
audience=https://service-b.internal
scope=reports:read
subject_token and subject_token_type are required — the subject is the party on whose behalf the request is made. The optional resource, audience and scope parameters say what the new token is for, which is how you narrow authority on the way down a call chain rather than widening it. requested_token_type asks for a particular output format. Token types are named by URN, including urn:ietf:params:oauth:token-type:access_token, :refresh_token, :id_token, :jwt, and the two SAML variants.
That request, as written above, asks for impersonation. One more parameter changes it.
Impersonation versus delegation
This is the distinction the whole RFC exists to make, and it is worth stating precisely.
Impersonation. The caller is given the rights of the subject and becomes indistinguishable from them in that context. Downstream, there is one identity: the user. Nothing in the token records that a service was in the middle.
Delegation. The caller keeps its own identity, separate from the subject’s, and it is explicitly understood that this party is acting as an agent for the other. Downstream, there are two identities: who it is for, and who is doing it.
The request shape selects between them. A subject_token on its own asks for impersonation. Adding an actor_token — plus actor_token_type, which becomes required once an actor token is present — identifies the acting party and asks for delegation.
The difference shows up where it matters most: after something has gone wrong. Under impersonation the downstream log cannot distinguish the user opening a report from a compromised middle service reading every report. Under delegation it can, because the token said so.
How delegation is written into the token: act and may_act
Two JWT claims carry this.
act records the current actor. Its value is a JSON object whose members identify the party acting on the subject’s behalf — so the token’s sub remains the user while act names the service. Because the value is an object, it can itself contain an act claim, and that nesting expresses a chain: B acted for A, which acted for the user. The token carries the whole path, not just the last hop.
may_act is the permission to do this, stated ahead of time. It says a named party is authorized to become the actor for this subject. This matters because otherwise any service holding a valid token could ask to act for anyone; may_act lets the authorization server check a requested delegation against policy and refuse the ones nobody sanctioned.
Read together: may_act is the grant, act is the receipt.
Why this stopped being an enterprise-SSO footnote
Token exchange sat in the background for years as something large SSO deployments did between SAML and OAuth worlds. Agents dragged it back to the centre, because an agent doing work for a person is delegation in its purest form — and the industry’s default shortcut is the worst of the three options above.
Hand an agent a shared API key and you have erased both identities in one move. The logs show a service account touching data. Which agent ran? For which user? Under what grant? Nothing in the request answers those, which is the argument for giving agents real identities instead of shared keys and, one level up, why agent authorization is turning into an audit-trail problem rather than an access-control one.
Delegation-style exchange gives you the shape you actually want at each hop: the user as sub, the agent as act, and a scope narrowed to the immediate task. It composes with the flow most interactive apps already run — Authorization Code with PKCE gets you the first token; exchange is what you do with it afterwards.
Practical notes
Narrow on every hop. The point of specifying audience and scope is that authority should shrink as it travels. If the token B receives can do everything the token A received could, you have built pass-through with extra steps.
Keep the exchanged tokens short-lived. A delegated token is a standing statement that one party may act for another; that statement should expire quickly. Short lifetimes remain the practical revocation story for anything self-contained.
Log the actor, not just the subject. Emitting a delegated token and then recording only sub in your logs throws away the thing you paid for. The audit record should name both.
Support varies. This is an authorization-server capability, not a library you drop into a service. Check what your identity provider actually implements — several support the grant but not actor_token, which means you get impersonation under a delegation-shaped name. That is exactly the case worth catching in a test rather than in an incident.
The rule worth remembering
Impersonation answers “what may be done”; delegation also answers “by whom, for whom.” RFC 8693 makes that a parameter rather than an accident: subject_token alone erases the caller, actor_token preserves it, act records the chain and may_act authorizes it in advance. Pick deliberately — and remember that the audit log you will read during an incident was configured by the token you issued long before it.
Comments