
DELEGATION
RFC 8693 lets an agent act for a user without becoming the user. The difference lives in one nested JWT claim, and getting it right decides whether your audit trail means anything.
TL;DR
Most platforms that advertise RFC 8693 token exchange are running impersonation, not delegation, and the
resulting token can't tell you which agent did what. The difference comes down to one optional request
parameter, actor_token, and the nested claim it produces, act. Skip the parameter and the issued token
carries a sub and nothing else. The resource server sees a user. It never sees a machine.
That single parameter matters because "was this a human or an agent" is a question almost every downstream system eventually has to answer: for rate limiting, for step-up authentication on a write, for an incident review later. RFC 8693 has answered it since January 2020. Most deployments never ask.
Delegation semantics aren't automatically the right contract, either. Sometimes impersonation is exactly what you want, when the whole point is that the agent should be invisible to the resource server. The actual problem is teams landing on impersonation by default and calling it a decision they made.
If you're building agent delegation, send the actor_token. If more than one agent is in the chain, stop
trusting anything past the current actor for authorization decisions. That's the whole argument. Everything
below is how to implement it without the details biting you.
When an agent calls an API on a user's behalf, the resource server gets a token and has to decide what it means. There are exactly two coherent answers, and OAuth has had precise language for both since January 2020.
Under impersonation, the token says the user did this. The resource server can't tell the agent from the person, because as far as the token is concerned there is no agent. Under delegation, the token says the agent did this on the user's behalf, and both identities live in the same credential. RFC 8693 puts it plainly: "when principal A impersonates principal B, A is given all the rights that B has within some defined rights context and is indistinguishable from B in that context." Delegation is different: "principal A still has its own identity separate from B, and it is explicitly understood that while B may have delegated some of its rights to A, any actions taken are being taken by A representing B."
The choice between them isn't a semantic quibble. It decides whether your audit log can answer "was this a human or a machine," whether a resource server can apply different policy to agent traffic than to interactive traffic, and whether accountability survives more than one hop. All three of those questions are answerable for one extra request parameter and one nested claim on the wire. We think that's a cheap price for what you get back.
Basic OAuth mechanics and agent permission policy (which scopes an agent ought to hold) are covered separately on this site. This post is about the exchange protocol itself: the parameters on the wire, the claim structures that come back, how a resource server should evaluate them, and where the model is known to be weak.
The worked example throughout is a customer-support system at a subscription software company, invented for illustration and nothing more. A user in the support console asks a triage agent to look into a billing complaint. The triage agent reads the ticket and the CRM record, decides the question is a proration dispute, and hands off to a billing agent that can query the billing API. One user, two agents, three services, and a requirement that the billing API be able to tell all of them apart.
The core distinction: impersonation collapses two identities into one and destroys information.
Delegation carries both identities in the same token and preserves it. RFC 8693 supports both, and the
difference on the wire is whether you send an actor_token.
RFC 8693 defines token exchange as an extension grant at the ordinary OAuth token endpoint. The authorization server acting in this role is conventionally called a Security Token Service, a component that accepts one security token and returns another with different properties: a different audience, a narrower scope, a shorter lifetime, a different format, or different subject semantics.
The pattern the RFC describes in its introduction is exactly the agent handoff problem. A resource server A needs to call a backend service C on behalf of a requesting user B. Depending on local policy, A either uses its own credentials with an annotation that it's acting for B (delegation), or is granted a limited credential to C that continues to identify B as the authorized entity (impersonation).
The structural property that matters, and the one that's easy to miss on a first read because the wire format gets all the attention, is that the STS is an authorization decision point, not a translation service. Nothing in RFC 8693 obliges an authorization server to honor an exchange request; whether a composite token is issued "is at the discretion of the authorization server and applicable policy and configuration." A well-run STS is where "may this agent act for this user against this service with these scopes" gets answered, and it's the only place in the architecture where that question is answerable with full context.
The grant type is urn:ietf:params:oauth:grant-type:token-exchange. Section 2.1 defines the request
parameters, and only two of them are actually required:
subject_token (REQUIRED): the token representing the identity of the party on whose behalf the
request is made.subject_token_type (REQUIRED): an identifier for the subject token's type.actor_token (OPTIONAL): a token representing the identity of the acting party. This is the one
parameter that turns impersonation into delegation.actor_token_type (REQUIRED when actor_token is present).requested_token_type (OPTIONAL): the type of token the client wants back.audience (OPTIONAL): the logical name of the target service.resource (OPTIONAL): the target service as an absolute URI. RFC 8707 covers the same parameter in
more depth.scope (OPTIONAL): space-delimited requested scopes.Section 3 registers five token-type identifiers (urn:ietf:params:oauth:token-type:access_token,
:refresh_token, :id_token, :saml1, :saml2) and borrows a sixth rather than owning it,
urn:ietf:params:oauth:token-type:jwt, defined in Section 9 of RFC 7519. In practice, if you're exchanging
JWTs between internal services you'll pick one identifier and never think about the other four again. The
SAML types exist for enterprises still running SAML-based SSO at the edge of an OAuth deployment; skip them
unless that's your world.
The response, defined in Section 2.2.1, requires three fields and recommends a fourth. access_token
carries the issued token regardless of its actual type. issued_token_type says what that type is, which
matters because the authorization server may return something other than what was requested. token_type
describes how to use it, and RFC 8693 registers a value for the case where the answer is "you don't":
N_A, for when the issued token isn't an access token at all. expires_in is RECOMMENDED, and we'd treat
it as required in practice. An exchanged token without an explicit lifetime is a token you can't reason
about.
Here is the triage agent's first exchange, where the console holds a token for the user and wants one the triage agent can present to the CRM:
POST /oauth2/token HTTP/1.1
Host: sts.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&subject_token=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0.eyJzdWIiOiJqb...
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt
&actor_token=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0.eyJzdWIiOiJodHRw...
&actor_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt
&resource=https%3A%2F%2Fapi.crm.internal%2F
&scope=tickets%3Aread+accounts%3AreadHTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache, no-store
{
"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjcyIn0.eyJhdWQiOiJodHRwczo...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 300
}There's an actor_token in that request, so delegation is possible. There's a resource, so the issued
token can be audience-restricted to the CRM and nothing else. There's a narrowed scope. And expires_in
is 300 seconds, not 3600: an exchanged token exists to cover one unit of work, not a session.
RFC 8693's Appendix A makes the dependency explicit in a parenthetical that's easy to skim past. In its
impersonation example the client sends only a subject_token, and the specification notes that "delegation
is impossible with only a subject_token and no actor_token." If your platform's exchange call has
nowhere to put the agent's identity, it can't produce delegation semantics, whatever the marketing page
says.
The difference is visible in the decoded token, and only there.
An impersonation exchange sends a subject_token and nothing else. The issued token's sub is the
user; the agent has vanished:
{
"iss": "https://sts.example.com",
"aud": "https://api.crm.internal/",
"exp": 1787001300,
"sub": "jordan.reyes@example.com",
"scope": "tickets:read accounts:read"
}The CRM receives this and sees Jordan Reyes. It can't rate-limit agents differently from humans, can't require step-up for machine-initiated writes, and can't record in its own logs that a machine did this. Whatever accountability exists lives entirely in the support console's logs, which the CRM's incident responders don't have.
A delegation exchange sends both tokens, and the issued token is what RFC 8693 calls a composite, "composed of information about multiple subjects":
{
"iss": "https://sts.example.com",
"aud": "https://api.crm.internal/",
"exp": 1787001300,
"sub": "jordan.reyes@example.com",
"scope": "tickets:read accounts:read",
"act": {
"sub": "https://agents.example.com/triage"
}
}Same user, same audience, same scopes. One additional claim, and now the CRM knows both who authorized the call and what performed it.
The act claim is defined in Section 4.1. Its value is a JSON object whose members are claims identifying
the actor, often iss and sub together, since either alone may not be unique. Claims inside act pertain
only to the actor's identity and aren't relevant to the containing JWT's validity, so non-identity claims
such as exp, nbf, and aud don't belong there. If you find exp inside an act object, something
generated it by copying a claims set instead of following the spec, and that's worth catching in code review
before it ships.
The complementary claim is may_act, in Section 4.4, and it runs the other direction. Where act
records delegation that has happened, may_act states that a party is authorized to become the actor for
the subject. Its stated purpose is precisely the STS decision: when a subject_token arrives at the token
endpoint, its may_act claim can be used by the authorization server to determine whether the client, or
the party named in the actor_token, is authorized to engage in the requested delegation or impersonation.
{
"iss": "https://idp.example.com",
"aud": "https://sts.example.com",
"exp": 1787000900,
"sub": "jordan.reyes@example.com",
"scope": "tickets:read accounts:read billing:read",
"may_act": {
"sub": "https://agents.example.com/triage"
}
}That's a user token stating, in the credential itself, which agent may act for this user. Both act and
may_act are also registered for OAuth token introspection responses with identical semantics, so an
authorization server using opaque tokens can convey the same structure without JWTs. We'd call may_act the
more important of the two claims to actually enforce. act just records what happened after the fact.
may_act is what stops an exchange from happening in the first place.
Figure 1 — The same exchange with and without an actor_token, and what the resource server can see as a result.
The triage agent now hands off to the billing agent, which needs a token for the billing API, and needs the record of how it got there to survive.
RFC 8693 expresses a chain of delegation by nesting one act claim inside another. The rules in Section 4.1
are specific: the outermost act claim represents the current actor, nested act claims represent prior
actors, and the least recent actor is the most deeply nested. The nested claims are a history trail
connecting the original subject through each delegation step.
After the second hop, the token the billing API receives looks like this:
{
"iss": "https://sts.example.com",
"aud": "https://api.billing.internal/",
"exp": 1787001600,
"sub": "jordan.reyes@example.com",
"scope": "invoices:read proration:read",
"act": {
"sub": "https://agents.example.com/billing",
"act": {
"sub": "https://agents.example.com/triage"
}
}
}Read outward-in: Jordan Reyes authorized this, the billing agent is doing it right now, the triage agent did it before that.
Then there's the rule most implementations get wrong, and it's a MUST:
For the purpose of applying access control policy, the consumer of a token MUST only consider the
token's top-level claims and the party identified as the current actor by the act claim. Prior actors
identified by any nested act claims are informational only and are not to be considered in access
control decisions.
This is worth sitting with, because the naive intuition runs the other way. A reasonable engineer looking at
that chain might write policy like "allow if any actor in the chain is a trusted agent," or "deny if the
chain contains an agent from an untrusted namespace." Both violate the specification. The nested history is
audit evidence, not authorization input. Your decision uses sub, aud, scope, exp, and the
outermost act.sub; everything deeper goes to the log. The reason becomes clear once you see the attack
two sections down: if prior actors were authorization-relevant, an attacker able to influence chain contents
would gain a way to manufacture privilege.
Skip this rule and here's concretely what breaks. A team builds a policy that trusts the billing agent
because the triage agent, three hops back, is on an allowlist. Six months later the triage agent is
deprecated and its identity gets reassigned to a lower-trust internal tool, and every downstream policy that
keyed off "triage is in the chain somewhere" is now trusting something it was never meant to trust, silently,
with no code change on the policy side to point at. That's the actual cost of treating act history as
authorization input, not a hypothetical purity concern.
Figure 2 — A two-hop exchange. The act chain grows outward at each hop; only the outermost actor is authorization-relevant.
The actor claim gets most of the attention when people talk about this specification, but we think the more useful idea is quieter: an exchange can happen at the moment of a tool call, not once at session start.
The default pattern is to obtain a broad user token at connection time and hold it for the session. Every tool call is then backed by the same credential, which necessarily carries the union of all permissions any tool might need. An agent that can read invoices and issue refunds holds refund capability continuously, including during the ninety-nine percent of its work that only reads.
Just-in-time downscoping inverts that. The broad token stays where it is, held by the orchestrator or the STS, never by the agent's reasoning loop. At the moment a specific tool is invoked, the platform exchanges it for a token narrow in three dimensions at once:
invoices:read, not invoices:read billing:write refunds:issue.resource set to exactly the service being called, so RFC 8707 audience restriction
makes the token useless anywhere else.RFC 8693's own security considerations point at the first and third of these, suggesting the scope claim
alongside constraints such as a limited token lifetime to mitigate abuse of delegated rights, on the grounds
that it restricts the contexts in which those rights can be exercised.
This matters more for agents than for conventional software because an agent's next action is a function of
text it did not author. A build log, a support ticket, a PDF: any of these can carry an instruction the
agent may follow. You can't fully prevent an agent from attempting an action an attacker suggested. You
can arrange for the credential in its hand at that moment to be worth almost nothing. A five-minute token
scoped to invoices:read and audience-restricted to the billing API isn't worth stealing, and it isn't
worth steering.
The pattern worth stealing from this post: hold breadth centrally, hand out narrowness at call time. An exchanged token that's scope-narrow, audience-narrow, and short-lived converts most credential-theft outcomes from an incident into a log line.
We think RFC 8693 is a mature specification and a good one, with one real weakness in the nested case. The honest description of where it stands: recent, contested, and still being argued over.
Delegation chain splicing. In late February 2026, Chiradeep Chhaya raised a security consideration on
the IETF OAuth working group mailing list under this name. The mechanism is straightforward once stated.
RFC 8693 requires the STS to validate the subject_token and the actor_token, but it requires no
cross-validation between them: nothing obliges the STS to check that the two originated from the same
delegation flow. A compromised intermediary holding two independently valid tokens from unrelated contexts
can present one as subject_token and the other as actor_token. Both validate. The STS issues a correctly
signed token asserting a delegation relationship that never occurred, and downstream consumers accept it,
because the signature is good and the claims are well-formed.
The thread is instructive on both the risk and its limits, and the disagreement matters as much as the
claim. Chhaya cited CVE-2025-55241, the Microsoft Entra ID cross-tenant impersonation vulnerability, scored
CVSS 10.0 by Microsoft and involving undocumented service-to-service "Actor tokens" together with a legacy
Azure AD Graph endpoint that failed to validate the originating tenant. Microsoft pushed a global fix on 17
July 2025, three days after the report, and published the CVE that September. Chhaya argued this is evidence
that actor-token validation failures aren't hypothetical. Jeff Lombardo of AWS, replying on 12 March 2026,
pushed back: the CVE's root cause, he argued, was unsigned JWTs and missing tenant-identifier validation, not
the act/sub binding mechanism itself. That weakens the specific example without disposing of the general
concern.
Three mitigations were proposed. Make cross-validation explicit, so the STS verifies actor_token.sub
matches an authorized actor declared in the subject_token, which amounts to making may_act normative
rather than optional. Chain by audience, so the aud of step N must match the sub of step N+1. Or issue
per-step delegation receipts, signed attestations of each hop a consumer can verify independently. Ayesha
Dissanayaka proposed a related "intermediate consent step," with the authorization server evaluating the
request against preconditions before issuance.
Lombardo's counterargument is the one to take seriously if you operate an STS: most of these are risk-based decisions that vary with deployment topology, so the authorization server should decide rather than the RFC mandating one answer. Chhaya narrowed his ask to documentation, observing that no mainstream authorization server validates delegation-chain integrity "because nobody told them to look."
That's roughly what has happened, and it's the strongest available evidence the concern is real. The IETF's
Identity Assertion JWT Authorization Grant draft (draft-ietf-oauth-identity-assertion-authz-grant-04, May
2026, a working-group document) now carries a security consideration stating that "a client could attempt to
combine a valid subject_token with an unrelated or less-trusted actor_token to obtain an ID-JAG that
overstates the actor's authority," and saying that profiles which define use of actor_token should specify
how it is validated and how any resulting act claim is derived. A separate individual submission,
draft-mw-oauth-actor-chain-01, argues that RFC 8693 permits nested act claims but "does not define
interoperable rules for preserving, extending, disclosing, and validating a delegation path across
successive exchanges," and proposes six profiles for asserting or cryptographically proving chain
continuity. That's one proposal with no working-group standing, not a direction of travel yet.
a delegation that never occurred, accepted downstream -->
Figure 3 — Delegation chain splicing. Both input tokens are genuine; the relationship between them is fabricated.
Confused deputy. An intermediary that holds a powerful credential and accepts instructions from a less-privileged caller is a confused deputy waiting to happen. Token exchange is the remedy: the intermediary presents what it holds and receives something appropriate for the caller and target rather than reusing its own credential. Token passthrough is the failure. MCP's authorization specification forbids passthrough normatively: servers MUST only accept tokens valid for use with their own resources, and MUST NOT accept or transit any other tokens.
The STS becomes the highest-value target in the system. It holds the policy deciding who may act for whom, and it can mint tokens for every downstream service. Compromising it is strictly better for an attacker than compromising any individual resource server. We'd give an STS the operational rigor of a root CA, not an internal microservice, and budget for it accordingly.
Revocation doesn't propagate down a chain by itself. This is the failure mode most likely to bite you in production, and the least discussed. Suppose Jordan Reyes revokes the support console's grant. The upstream refresh token stops working immediately, so no new exchanges succeed. But tokens already issued at hop two remain cryptographically valid until they expire, and nothing in RFC 8693 defines a mechanism to reach back and invalidate them. Two honest answers exist. Keep exchanged-token lifetimes short enough that the window is acceptable, which is the downscoping argument arriving from a different direction. Or add back-channel revocation signaling, which means adopting event propagation between systems rather than expecting the token format to solve it. The lifetime answer is the one you can implement this quarter.
There's a gap between what RFC 8693 defines and what platforms ship, and it's worth naming plainly because it's easy to be misled by a feature checklist.
The pattern looks like this. A platform announces RFC 8693 token exchange, and what it means is user-facing exchange: swapping a user's token for another token for the same user, at a different audience or with a narrower scope. That's genuinely useful, and it's genuinely most of the value of downscoping. It's not delegation, because there's no actor.
Keycloak is a useful concrete case because its documentation is unusually candid. Standard Token Exchange
became officially supported in Keycloak 26.2, released May 2025, described as compliant with RFC 8693.
Reading the documentation closely: the standard implementation covers internal-to-internal exchange within a
realm, supports access-token, ID-token and refresh-token requested_token_type values, and supports
audience for narrowing. It also states that Keycloak token exchange "does not yet have support for the
resource parameter," and treats actor-token delegation as experimental support behind a separate feature.
That's a well-engineered, honestly documented implementation of the user-facing subset. An architecture
diagram assuming full actor-chain semantics on top of it would be wrong in ways you'd only discover during
integration.
The standards picture for the agent case is similarly unfinished. An individual submission titled "OAuth 2.0
Extension: On-Behalf-Of User Authorization for AI Agents," by Thilina Senarath and Ayesha Dissanayaka,
proposed a requested_actor parameter to name the agent needing delegation and an actor_token to
authenticate it, with the issued token carrying claims documenting the chain from user to agent. Its most
recent revision is dated 25 August 2025 and it has expired. Meanwhile the working-group document that has
advanced furthest permits actor_token on the exchange but explicitly declines to define normative
processing for it.
Three things are true at once here. The claim format for delegation has been stable and IANA-registered since 2020; you can rely on it. The validation semantics for multi-hop delegation are actively contested, with a documented attack class and no consensus mitigation. And the deployed reality is predominantly impersonation and user-facing downscoping, with actor-token delegation experimental or absent in most platforms we've read documentation for.
None of that argues against using token exchange. It argues for reading your provider's documentation
instead of its landing page, for asking specifically whether actor_token is processed and whether
may_act is enforced, and for keeping multi-hop lifetimes short while the guidance settles.
The distinction RFC 8693 draws between impersonation and delegation is the most useful idea in agent
authorization, and it costs one request parameter. Send an actor_token and the resulting token carries the
user as sub and the agent as a nested act: two identities in one credential, so a resource server can
apply different policy to machine traffic and an investigator six months later can tell what happened. Omit
it and agents become indistinguishable from the people they work for, which is a decision you'll regret at
exactly the wrong moment.
Three things are worth carrying away, and we'd rank them in this order. Nested act chains record history,
but only the outermost actor is authorization-relevant: that's a MUST, and treating deeper entries as policy
input is both a spec violation and an attack surface. Just-in-time downscoping is the highest-leverage
application of the whole specification: exchange broad for narrow at the moment of the call, in scope and
audience and lifetime together. And the multi-hop story is unsettled, with a splicing attack class documented
on the OAuth working group list in February 2026 and now reflected in a working-group draft's security
considerations, so build for short lifetimes and expect revision.
What delegation actually buys you is the ability to answer a question. When something goes wrong, a refund that shouldn't have issued, a record read that shouldn't have been read, impersonation lets you answer "which user," and delegation lets you answer "which user, and which agent, and in what order." Only one of those is enough to fix anything.