OAuth has a delegation visibility problem.

In many deployments, a token tells you that someone is acting, but not clearly who is acting for whom. Delegation is implied by context, encoded in sub by convention, or inferred from client_id heuristics. That might be acceptable inside a single product boundary. It is not acceptable for standards-based interoperability.

The fix is not a new claim. The primitives already exist. What is missing is a shared profile that applies them consistently.

The OAuth WG should standardize one actor model across both:

Standardizing act using OAuth Entity Profiles (Entity Profiles) would make delegation and principal type explicit and machine-processable at both surfaces.

๐Ÿ” Where the Current Specs Leave a Gap

act Exists, But Is Not Profiled Where It Matters

OAuth 2.0 Token Exchange (Token Exchange) defines act for token exchange. That is a good start. But the ecosystem treats actor semantics as optional and context-specific. Two standards-compliant implementations can still disagree about delegation meaning when a token crosses a domain boundary.

The gap runs deeper at the assertion boundary. JWT assertion grant processing (RFC 7523 and related profiles, such as ID-JAG) does not require a typed, interoperable actor model. So delegation introduced at the federation boundary does not survive into token exchange or downstream access tokens in any consistent way.

JWT Access Tokens Overload sub

In most RFC 9068 deployments today, sub is doing too much:

  • Is sub the end-user who authorized access?
  • Is sub the workload client acting autonomously?
  • Is sub an AI agent acting on someone’s behalf?

This is not just an OAuth plumbing question. As explored in You Don’t Give Agents Credentials. You Grant Them Power of Attorney., the identity and delegation semantics carried in tokens are the foundation on which any meaningful authority governance for agents has to be built. You cannot govern what you cannot see.

When a resource server cannot answer that question from the token alone, it cannot reliably distinguish:

  • direct subject access,
  • user-delegated client access, or
  • non-user workload or agent access.

Some implementations fall back on client_id == sub to infer “client acting as itself.” That heuristic can work in simple single-hop cases, but it does not survive delegation chains and it says nothing about what kind of principal either party is.

That ambiguity creates policy drift. Authorization decisions diverge, audit trails lose fidelity, and security reviews turn into arguments over interpretation.

Actor Identity Is Asserted, Not Bound

Even after act is populated, the actor claim is just a string. There is no cryptographic guarantee that the entity presenting the token is the same entity identified in act.sub. A stolen token can be replayed by a different actor, and the resource server has no way to tell.

Proof-of-Possession Key Semantics for JWTs (RFC 7800) defines cnf to bind a token to a holder’s key. The jkt member carries a SHA-256 JWK Thumbprint of the holder’s public key. DPoP-Protected JWT-Secured Authorization Grants (PoP JAG) and Identity Assertion JWT Authorization Grant (ID-JAG), Section 8.4 both support sender-constraining the current presenter: the assertion includes cnf.jkt, the client presents a DPoP proof, and the authorization server validates that the proof’s public key matches the asserted thumbprint. No matching proof, no token.

Key binding is not required for every delegation scenario. Many internal workload deployments are fine with bearer semantics. But when an actor presents a DPoP key binding at the token endpoint, the issued token should carry that actor’s key at the top-level cnf. On the next exchange hop, the newly issued token should repeat that pattern for the next presenter while preserving prior actor keys in the act chain as delegation history. If a cnf.jkt established at the assertion grant boundary is silently dropped in the issued access token, the resource server cannot enforce a sender constraint the authorization server already accepted.

Identity semantics and delegation semantics must be explicit protocol data, not institutional memory. Key binding makes actor identity verifiable, not just visible.

๐Ÿ”ง The Proposal: act + Entity Profiles

The approach combines existing pieces:

  • act as the explicit delegation vehicle (per Token Exchange).
  • Entity profile claims (sub_profile, client_profile) to make principal type explicit on each principal, including within nested act nodes.
  • Top-level cnf with jkt for the current token presenter when that presenter established a DPoP key binding, consistent with PoP JAG, ID-JAG Section 8.4, and RFC 7800. Prior actor keys may be preserved inside nested act nodes as delegation history, but only the top-level cnf is the live sender constraint the resource server enforces.
  • The same model applied to both surfaces: JWT assertion grants and JWT access tokens.

The natural vocabulary for profile semantics is Entity Profiles, which defines sub_profile and client_profile. The actor-chain profile should align with that vocabulary rather than creating a parallel taxonomy. In the current draft, standardized values include user, device, native_app, web_app, browser_app, service, and ai_agent, and profile claims are space-delimited strings when multiple values are present.

For interoperable processing across trust domains, each principal and delegation relationship must be explicit. The resulting contract is:

ClaimMeaning
issIssuer asserting the principal and delegation relationship
subPrincipal whose authority is being exercised
actPrincipal currently acting on behalf of sub (when delegation exists)
sub_profilePrincipal profile value(s) for sub or for any act node
cnf.jktSHA-256 JWK Thumbprint of the current presenter’s key; carried at the top level when the current actor established a DPoP binding
act.cnf.jktOptional preserved thumbprint for a prior actor in the delegation chain; useful for audit and provenance, not live sender-constrained enforcement

No new grant type. No new token type. A shared profile and processing rules built on what already exists.

๐Ÿ’ก What This Looks Like in Practice

JWT Access Token: Before

1
2
3
4
5
6
7
8
{
  "iss": "https://as.example.com",
  "aud": "https://api.example.com",
  "client_id": "client-123",
  "sub": "248289761001",
  "scope": "payments:write",
  "exp": 1773076800
}

A resource server cannot determine whether sub is a user, a client instance, or an agent identifier. It cannot tell whether this is direct access or delegated access. That ambiguity is exactly what a standard should eliminate.

JWT Access Token: After

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "iss": "https://as.example.com",
  "aud": "https://api.example.com",
  "client_id": "client-123",
  "sub": "248289761001",
  "sub_profile": "user",
  "cnf": {
    "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
  },
  "act": {
    "sub": "agent-7f3c",
    "iss": "https://as.example.com",
    "sub_profile": "ai_agent"
  },
  "scope": "payments:write",
  "exp": 1773076800
}

Now the resource server can evaluate both principals explicitly. Here the current actor established a DPoP key binding during token exchange, so cnf.jkt is present at the top level and the agent must present a matching DPoP proof. If no key binding was established, top-level cnf is absent and bearer semantics apply. No heuristics. No local conventions.

๐ŸŒ The Canonical Cross-Domain Case

The most important scenario to get right is cross-domain delegation, where a federation boundary is crossed and then token exchange carries the principal chain downstream.

planner-agent is an external AI agent from assistant.example acting on behalf of Alice. It uses hotel-tool at tools.example, which must in turn call an internal backend at inventory.example, a service the agent cannot reach directly.

The flow covers four steps. Alice signs in and the agent receives a DPoP-bound refresh token. The agent exchanges that for a cross-domain ID-JAG. The agent presents the ID-JAG to obtain a tool access token, still bound to the agent’s key. Finally, hotel-tool performs its own Token Exchange to reach the backend, shifting the live sender constraint to the tool’s key. That last step is the point: the originating agent and the current presenter are not the same actor on every hop.

RoleEntity TypeSubject IDOAuth ClientKey Binding (jkt)
Useruseruser-aliceNon/a
AI agentai_agentplanner-agentYesNzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs
Toolservicehotel-toolYesR3g0XuBFHRTqAq3HyOVSLWLBXb-RypXz6NsKrOSqMmE

Step 1: Authorization Request

Alice is using planner-agent and signs in with assistant.example. The agent sends her through a standard OIDC authorization flow, using authorization code binding via dpop_jkt so the resulting code is tied to the agent’s key.

The authorization request looks roughly like this:

1
2
3
4
5
6
7
8
9
GET /authorize?
  response_type=code
  &client_id=planner-agent
  &redirect_uri=https%3A%2F%2Fagent.assistant.example%2Fcallback
  &scope=openid profile offline_access hotels:search hotels:book
  &state=af0ifjsldkj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &dpop_jkt=NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs

After Alice authenticates, the agent redeems the code at the token endpoint and proves possession of the same key with DPoP:

1
2
3
4
5
6
7
8
9
POST /token
Content-Type: application/x-www-form-urlencoded
DPoP: <proof signed by key with jkt NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs>

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fagent.assistant.example%2Fcallback
&client_id=planner-agent
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

The response below is abbreviated but still shows the surrounding token fields for realism. The artifact that matters for the next step is the DPoP-bound refresh token, which the agent will later use to mint an ID-JAG for cross-domain access.

1
2
3
4
5
6
{
  "access_token": "...",
  "id_token": "<id token for user-alice>",
  "refresh_token": "<refresh token bound to jkt NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs>",
  "token_type": "DPoP"
}

At this point the agent has a user-authenticated session and a refresh token bound to its key, but the cross-domain delegation semantics are not yet packaged into a JWT grant another authorization server can consume directly.

Step 2: Refresh Token to ID-JAG

planner-agent uses that refresh token as the subject_token in Token Exchange to obtain an ID-JAG for hotel-tool in another trust domain.

The request might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
POST /token
Content-Type: application/x-www-form-urlencoded
DPoP: <proof signed by key with jkt NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<refresh token>
&subject_token_type=urn:ietf:params:oauth:token-type:refresh_token
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&audience=https%3A%2F%2Fauth.tools.example%2Ftoken
&scope=hotels:search hotels:book

That exchange does not change the delegation model. It packages the delegated user and acting agent into an ID-JAG that another authorization server can validate directly.

The token exchange response might look like this:

1
2
3
4
5
{
  "issued_token_type": "urn:ietf:params:oauth:token-type:id-jag",
  "token_type": "N_A",
  "access_token": "<id-jag>"
}

Even though the issued artifact is an ID-JAG rather than an access token, Token Exchange still returns it in the access_token response field and relies on issued_token_type to tell the client what it actually received.

The resulting ID-JAG might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "iss": "https://idp.assistant.example",
  "sub": "user-alice",
  "sub_profile": "user",
  "cnf": {
    "jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
  },
  "act": {
    "iss": "https://idp.assistant.example",
    "sub": "planner-agent",
    "sub_profile": "ai_agent"
  },
  "scope": "hotels:search hotels:book",
  "aud": "https://auth.tools.example/token",
  "exp": 1773076800
}

The delegation is explicit in the ID-JAG itself: Alice authorized planner-agent to act for her. planner-agent established a DPoP key binding when the grant was issued, so cnf.jkt appears at the top level. That binding remains part of the chain until a later token exchange mints a new access token for a different current presenter.

Step 3: ID-JAG to Tool Access Token

planner-agent, not hotel-tool, presents the ID-JAG to auth.tools.example. Because the grant is DPoP-bound, the agent uses the JWT-DPoP grant and proves possession of the same key that is referenced in the ID-JAG’s top-level cnf.jkt.

The request might look like this:

1
2
3
4
5
6
7
POST /token
Content-Type: application/x-www-form-urlencoded
DPoP: <proof signed by key with jkt NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs>

grant_type=urn:ietf:params:oauth:grant-type:jwt-dpop
&assertion=<id-jag>
&scope=hotels:search hotels:book

The tools authorization server validates federation trust, validates the ID-JAG and the agent’s proof of possession, and issues an access token for hotel-tool still bound to planner-agent’s key.

That tool access token might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "iss": "https://auth.tools.example",
  "aud": "https://api.tools.example/hotel-tool",
  "sub": "user-alice",
  "sub_profile": "user",
  "scope": "hotels:search hotels:book",
  "cnf": {
    "jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
  },
  "act": {
    "iss": "https://auth.tools.example",
    "sub": "planner-agent",
    "sub_profile": "ai_agent"
  },
  "exp": 1773078600
}

The current actor is still planner-agent, because it is the entity presenting this access token to hotel-tool. The live sender constraint remains the agent’s top-level cnf.jkt. At this point there has been no key transition yet.

Step 4: Tool Token Exchange for Backend Service

hotel-tool receives the inbound access token when planner-agent calls it. To reach inventory.example, it performs one more Token Exchange using that token as the subject_token and presenting its own DPoP proof. This is where the live sender constraint shifts from the agent’s key to the tool’s key.

The request might look like this:

1
2
3
4
5
6
7
8
9
POST /token
Content-Type: application/x-www-form-urlencoded
DPoP: <proof signed by key with jkt R3g0XuBFHRTqAq3HyOVSLWLBXb-RypXz6NsKrOSqMmE>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<tool access token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=https%3A%2F%2Finventory.example
&scope=inventory:reserve

The backend authorization server validates the inbound tool token, preserves the actor chain, validates hotel-tool’s proof of possession for the new audience, and issues a backend access token bound to the tool’s key.

The resulting backend access token might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "iss": "https://auth.inventory.example",
  "aud": "https://api.inventory.example/reservations",
  "sub": "user-alice",
  "sub_profile": "user",
  "scope": "inventory:reserve",
  "cnf": {
    "jkt": "R3g0XuBFHRTqAq3HyOVSLWLBXb-RypXz6NsKrOSqMmE"
  },
  "act": {
    "iss": "https://auth.inventory.example",
    "sub": "hotel-tool",
    "sub_profile": "service",
    "act": {
      "iss": "https://auth.tools.example",
      "sub": "planner-agent",
      "sub_profile": "ai_agent",
      "cnf": {
        "jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
      }
    }
  },
  "exp": 1773078600
}

The current presenter is now hotel-tool, so the live sender constraint is the tool’s top-level cnf.jkt. The nested act chain preserves planner-agent and the upstream key that established the original delegation. This is the key transition the profile needs to make explicit: each new access token binds only the current presenter, while the chain preserves who previously acted for whom.

What this scenario demonstrates is why both surfaces must align. The ID-JAG and the downstream access tokens encode the same delegation, in the same model. The first cross-domain hop keeps planner-agent as the current presenter. The later backend hop changes the current presenter to hotel-tool and mints a new top-level sender constraint without losing the upstream actor chain. The AS does not need to translate between different actor representations as it crosses issuance boundaries. The resource server receives a token where principal types and delegation history are unambiguous across the full chain.

If assertion grants and access tokens use different delegation conventions, the ambiguity just moves one layer up. The alignment is the point.

โš™๏ธ Resource Server Processing

With a standardized model, the resource server can implement stable, issuer-agnostic logic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
input: access_token jwt
       dpop_proof jwt   // required when token.cnf.jkt is present

1) Validate token (sig, iss, aud, exp) per JWT AT profile (RFC 9068).
2) Resolve subject          := token.sub
   Resolve subject_profiles := parse_profiles(token.sub_profile)
3) If token.cnf.jkt present:                  // live sender constraint for current presenter
   Compute thumbprint(dpop_proof.header.jwk)
   Verify computed thumbprint == token.cnf.jkt
   Verify dpop_proof signature valid for the key in dpop_proof.header.jwk
   Reject if mismatch
4) If token.act present:
   actor                := token.act.sub      // current actor (RFC 8693)
   actor_profiles       := parse_profiles(token.act.sub_profile)
   evaluate policy.allow_delegate(actor_profiles, subject_profiles)
   evaluate policy.allow_actor_for_subject(actor, subject)
   evaluate policy.allow_scope_for_pair(token.scope, actor, subject)
5) If token.act absent:
   evaluate policy.allow_subject_self(token.scope, subject, subject_profiles)
6) Optionally log nested token.act.act... as delegation history for audit.
   Nested cnf.jkt values record the key binding of each prior actor in the chain.
7) Enforce decision.

The three-case decision tree for relying parties becomes:

  1. act present: dual-principal policy evaluation (sub authority + act authority to act).
  2. No act, sub_profile contains user: direct user-context access.
  3. No act, non-user subject profile: non-delegated workload or agent self-access.

client_id may still be an input, but it is not the delegation model. It cannot represent chained delegation and does not type either principal.

This is the operational value of the profile: the resource server does not branch on issuer-specific convention to understand delegation semantics. Authorization, audit, and policy enforcement work the same way across implementations.

๐Ÿ›ก๏ธ Deployment and Backward Compatibility

This does not require a flag day.

Existing deployments that only understand subject-only tokens continue to work. Explicit act is additive, and consumers that do not understand it can ignore it (with appropriate policy defaults). Discovery or profile metadata can signal capability for implementations that want to negotiate.

The goal is not disruption. The goal is ending private delegation semantics for new and federated deployments where interoperability actually matters.

๐Ÿ”Ž Caveat: Delegation Chain Is Not Execution Identity

A nested act chain tells you who claims to act for whom. It does not prove that the current request is a valid continuation of the originating execution.

The same delegation chain can appear in multiple unrelated executions. If each presenter holds the bound key for its hop, each presentation can still be valid under proof of possession. Sender-constraining answers “who is allowed to present this token,” not “which execution produced this request.”

From a JWT, you can answer “who signed this” and, with act plus cnf, “who is acting for whom” and “who proved possession of the current key.” You cannot answer “which execution is this” unless some separate execution identifier or continuity mechanism is standardized and propagated alongside the delegation chain.

This proposal does not address execution identity or execution continuity. Its scope is limited to making delegation semantics and current presenter key binding explicit and interoperable across assertion grants and JWT access tokens.

๐Ÿ“‹ Recommendation

The OAuth WG should standardize an interoperable actor profile that:

  • reuses act from Token Exchange,
  • uses Entity Profiles vocabulary for principal typing (sub_profile) on each principal in the chain,
  • uses top-level cnf.jkt for the current presenter when a token is sender-constrained, while allowing prior actor keys to be preserved inside nested act nodes as delegation history,
  • applies uniformly to JWT assertion grants and JWT access tokens,
  • defines deterministic processing rules for AS validation and resource server authorization, including DPoP proof verification against top-level cnf.jkt when present.

The primitive work is already done. act exists. Entity Profiles are being defined. Token Exchange is standardized. DPoP key binding for the current presenter is already specified in JWT grant work and in ID-JAG Section 8.4. What is needed is the profile that stitches them together across both token surfaces, makes the processing rules explicit, and ends the cross-profile ambiguity that has so far been left to implementer judgment.

Until that profile exists, delegation will remain a private convention masquerading as a standard. Two implementations can be fully RFC-compliant and still disagree about who is acting for whom.

That is not an acceptable baseline for cross-domain identity, and it is the token-layer prerequisite for anything more ambitious. Governing delegated agent authority, enforcing mandate lifecycles, propagating revocation signals: none of it is tractable if the delegation relationship is invisible in the token.

When “who is acting for whom” is portable protocol meaning rather than local folklore, the ecosystem gets better federation, better authorization policy, and better audit trails. That is worth standardizing.

Changelog

  • 2026-03-18: Clarified sender-constrained semantics so the current presenter uses top-level cnf.jkt and prior actor keys are preserved only as historical delegation context inside nested act nodes.
  • 2026-03-18: Added explicit alignment to ID-JAG Section 8.4 for sender-constraining the current presenter while keeping nested prior-actor keys as profile-specific history.
  • 2026-03-18: Reworked the canonical cross-domain example around an AI agent, a cross-domain tool, and a backend service, with an end-to-end flow from OIDC sign-in to ID-JAG issuance to downstream token exchanges, plus a cast table covering role, entity type, subject ID, OAuth client status, and key binding.
  • 2026-03-18: Added an explicit caveat that this proposal does not address execution identity or execution continuity.