Classic painting used as the article cover
← Back to blog

DELEGATION

Token Exchange for Agents: Delegation Without Impersonation (RFC 8693)

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.

Paulina XuAug 26, 202619 min
Token ExchangeDelegationIdentity

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.

Overview

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.

The Security Token Service Pattern

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 Wire Protocol

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:

http
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%3Aread
http
HTTP/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.

Impersonation vs. Delegation, in Claims

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:

json
{
  "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":

json
{
  "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.

json
{
  "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.

Delegation

subject_token + actor_token

STS

sub: user
act.sub: agent

RS sees both

Impersonation

subject_token only

STS

sub: user
(agent absent)

RS sees a human

Figure 1 — The same exchange with and without an actor_token, and what the resource server can see as a result.

Reading an Actor Chain

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:

json
{
  "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.

Billing APISTSBilling AgentTriage AgentSupport ConsoleUserBilling APISTSBilling AgentTriage AgentSupport ConsoleUser"look into this billing complaint"exchange (subject=user, actor=triage)token: sub=user, act={triage}run with that tokenexchange (subject=that token, actor=billing)token: sub=user, act={billing, act:{triage}}hand offcall with nested-act tokenauthorize on sub + aud + scope + act.sub (billing)log full chain as evidenceproration detail

Figure 2 — A two-hop exchange. The act chain grows outward at each hop; only the outermost actor is authorization-relevant.

Just-in-Time Downscoping

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:

  • Scope-narrow. Only the scopes this one tool needs. invoices:read, not invoices:read billing:write refunds:issue.
  • Audience-narrow. resource set to exactly the service being called, so RFC 8707 audience restriction makes the token useless anywhere else.
  • Time-narrow. A lifetime measured against the operation, not the session. Five minutes, not eight hours.

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.

Failure Modes

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.

subject_token = A
actor_token = B

Context A:
token for user X,
actor = agent P

Compromised
intermediary

Context B:
token for agent Q,
unrelated flow

STS

Validate subject_token: OK

Validate actor_token: OK

No cross-validation required
by RFC 8693

Signed token:
sub = X, act.sub = Q
delegation never happened

Downstream RS accepts

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.

The Implementation Gap

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.

Conclusion

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.

Sources