---
title: "Standardize `act` Across Assertion Grants and JWT Access Tokens"
date: "2026-03-18T10:30:00-07:00"
lastmod: "2026-03-18T10:30:00-07:00"
description: "OAuth deployments need one interoperable way to represent explicit delegation. Reusing `act` with entity profiles across JWT assertion grants and JWT access tokens closes a long-standing gap where `sub` semantics are ambiguous and delegation is implicit."
summary: "The current split between token exchange semantics and JWT access token practice creates avoidable interoperability failures. A common profile for act, grounded in entity profiles, can align JWT assertion grant and JWT access token processing."
tags:
  - "OAuth"
  - "Standards"
  - "Delegation"
  - "Token Exchange"
  - "JWT"
  - "JWT Assertion Grant"
  - "Proof of Possession"
---


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:

- JWT assertion grants ([RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) and profiles built on top of it, including [Identity Assertion JWT Authorization Grant (ID-JAG)](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/))
- JWT access tokens ([RFC 9068](https://datatracker.ietf.org/doc/html/rfc9068))

Standardizing `act` using [OAuth Entity Profiles (Entity Profiles)](https://datatracker.ietf.org/doc/html/draft-mora-oauth-entity-profiles-00) 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)](https://datatracker.ietf.org/doc/html/rfc8693) 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](https://datatracker.ietf.org/doc/html/rfc7523) and related profiles, such as [ID-JAG](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/)) 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](https://datatracker.ietf.org/doc/html/rfc9068) 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.](https://notes.karlmcguinness.com/series/you-dont-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)](https://datatracker.ietf.org/doc/html/rfc7800) 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)](https://datatracker.ietf.org/doc/html/draft-parecki-oauth-jwt-dpop-grant-01) and [Identity Assertion JWT Authorization Grant (ID-JAG), Section 8.4](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant-02#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](https://datatracker.ietf.org/doc/html/draft-parecki-oauth-jwt-dpop-grant-01), [ID-JAG Section 8.4](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant-02#section-8.4), and [RFC 7800](https://datatracker.ietf.org/doc/html/rfc7800). 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](https://datatracker.ietf.org/doc/html/draft-mora-oauth-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:

| Claim | Meaning |
|---|---|
| `iss` | Issuer asserting the principal and delegation relationship |
| `sub` | Principal whose authority is being exercised |
| `act` | Principal currently acting on behalf of `sub` (when delegation exists) |
| `sub_profile` | Principal profile value(s) for `sub` or for any `act` node |
| `cnf.jkt` | SHA-256 JWK Thumbprint of the current presenter's key; carried at the top level when the current actor established a DPoP binding |
| `act.cnf.jkt` | Optional 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

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

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

| Role | Entity Type | Subject ID | OAuth Client | Key Binding (`jkt`) |
|---|---|---|---|---|
| User | `user` | `user-alice` | No | n/a |
| AI agent | `ai_agent` | `planner-agent` | Yes | `NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs` |
| Tool | `service` | `hotel-tool` | Yes | `R3g0XuBFHRTqAq3HyOVSLWLBXb-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:

```text
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:

```text
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.

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

```text
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:

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

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

```text
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:

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

```text
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:

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

```text
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](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant-02#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.

