Skip to main content

Token Exchange

Token exchange, defined by RFC 8693, instead takes a token the caller already holds — the subject_token — and issues a new token derived from it. subject_token and subject_token_type are required on every request; there is no defined way to invoke this grant without presenting an existing credential.

Exactly one of two outcomes results, determined by whether actor_token is also present:

  • Downscoping (no actor_token): the new token is issued for the same subject as subject_token, with a scope equal to or narrower than the original. This is typically used before passing a token to code that should not be able to exceed one specific task.
  • Delegation (actor_token and actor_token_type present): the new token is issued for the same subject, and additionally records that a second party — the actor — is currently acting on the subject's behalf. The subject retains ownership of the token; only the fact that another party is acting on it is recorded. For a worked example of delegation — an AI agent redeeming a human's mandate — see Authenticating AI Agents with O2ID.
Downscoping:
Caller ──(already has)──► subject_token
Caller ──POST /oauth2/token (subject_token + optional scope)──► new, narrower token

Delegation:
Actor ──(already has, e.g. via client_credentials)──► actor_token
Caller ──POST /oauth2/token (subject_token + actor_token)──► new, delegated token

Token Request

POST /oauth2/token

Content type: application/x-www-form-urlencoded

Always required:

FieldValue
grant_typeurn:ietf:params:oauth:grant-type:token-exchange — identifies the request as token exchange, as distinct from authorization_code or client_credentials
client_idA registered application's clientId. As with every other grant, the application's allowedScopes apply as an outer ceiling: this grant cannot return more than the requesting application was itself ever allowed to have
subject_tokenThe access token presented as proof of the caller's current identity. The new token is issued for this identity
subject_token_typeMust be the literal string urn:ietf:params:oauth:token-type:access_token. RFC 8693 defines several possible token types (ID tokens, SAML assertions, and others); O2ID accepts only an access token here, so this field exists but has a single valid value

Optional:

FieldValue
scopeA space-delimited list of the scopes requested for the new token. If omitted, the new token receives the full intersection of every applicable ceiling, described below
actor_tokenAn access token identifying the second party currently acting on the subject's behalf. Present only for delegation — see Actor-token delegation. actor_token_type is required whenever actor_token is present, and neither may be sent without the other
actor_token_typeMust be urn:ietf:params:oauth:token-type:access_token, required exactly when actor_token is present; both are omitted for plain downscoping
resource / audienceRecords the downstream service the caller intends to use this token with, as either an absolute URI (resource) or a free-form name (audience). Both are validated and recorded on the minted token — see resource / audience

Rejected outright, not silently ignored: a requested_token_type other than access_token. RFC 8693 permits a caller to request a different token type than the one it started with — for example, exchanging a SAML assertion for a JWT. O2ID does not support this: the grant issues only access tokens, and a request for any other type is treated as a request error.

Success response — the same overall shape returned by any other grant, with one field added and two omitted:

{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "users:read",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

issued_token_type is present because RFC 8693 §2.2.1 requires every token-exchange response to state the type of token issued (always access_token here). A refresh_token or id_token is never returned: a caller requiring an additional token later simply invokes this grant again with a current subject_token. Re-exchanging on each request means scope is re-derived from the current state on every call and, for delegation, the mandate's revocation and expiry status is re-checked each time, rather than relying on a refresh token whose validity was established only once, at the time of the original exchange.

Downscoping

Downscoping is the simpler of the two outcomes. A caller supplies only subject_token — no actor_token — and receives a new token for the same identity with a smaller set of scopes. A typical use case: before invoking third-party or lower-trust code with a token, exchange it for one that cannot do more than the single task at hand, regardless of what the original token was capable of.

The scope returned is the intersection of three inputs:

  1. The front-door application's allowedScopes (the application-level ceiling every grant respects)
  2. The subject's current, live role scopes, evaluated at the moment of exchange rather than cached from when the original token was issued
  3. The scopes requested in scope, if provided — omitting scope entirely returns the full intersection of the two ceilings above

The second input has an immediate consequence: because role scopes are evaluated live on every exchange, revoking a role takes effect on the subject's very next exchange, not when the existing token happens to expire. This matches the live-recheck behavior of every other O2ID token; token exchange is not an exception.

curl -s -X POST http://localhost:8080/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "subject_token=${SUBJECT_TOKEN}" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
--data-urlencode "scope=users:read"

Actor-token delegation

Delegation occurs when actor_token is also present, together with actor_token_type. RFC 8693 §4.1's delegation rules apply in this case. The subject of the new token is unchanged: it remains whoever subject_token identified, never the actor. The party identified by actor_token is instead recorded separately, in an act claim internal to the token. Delegation therefore adds a record of who is currently acting, rather than replacing the record of whose authority is being exercised.

Keeping the subject fixed is what auditability depends on. If the subject were permitted to change with each delegation, a token's history would be unrecoverable beyond the most recent actor — the party that originally authorized the underlying action would be lost. With the subject fixed, every delegated action is traceable to the party that authorized it, regardless of how many actors the token subsequently passes through.

O2ID applies this delegation mechanism to one concrete case: an AI agent redeeming a human's mandate. A mandate is a record stating that a specific AI agent may act as a given human, with specified scopes, until a specified time. It need not predate the exchange by any particular margin: a mandate can be created and redeemed in the same interaction, when the human is present and authorizing exactly this action, or set up well ahead of an agent acting unattended — see Managing AI Agents for both patterns. To redeem a mandate:

  • The AI agent presents its own token as actor_token. This token is obtained separately, in advance, via the Client Credentials Grant, proving the agent's own identity independently of any human or mandate.
  • The human presents their own current access token as subject_token.

No mandate_id parameter exists in the request — RFC 8693 defines no such concept — so the mandate is instead resolved implicitly, by matching the (AI agent, human) pair identified by actor_token and subject_token. If no active mandate authorizes that pair, the exchange fails with invalid_grant.

curl -s -X POST http://localhost:8080/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "subject_token=${HUMAN_TOKEN}" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
--data-urlencode "actor_token=${AGENT_TOKEN}" \
--data-urlencode "actor_token_type=urn:ietf:params:oauth:token-type:access_token"

The scope granted is the intersection, evaluated at the moment of exchange, of:

  1. The front-door application's allowedScopes
  2. The acting AI agent's own allowedScopes — what the agent itself was permitted, independent of the human's or the mandate's permissions
  3. The mandate's scope, fixed at creation time: it can only narrow what the creating human was able to grant then, and can never be widened later without a new mandate
  4. The human's current, live role scopes, re-evaluated on every exchange as with downscoping

The fourth input has the same consequence as in downscoping: revoking the human's role immediately revokes every AI agent acting on their behalf, at each agent's next exchange, with no separate step required.

Chaining

A subject_token presented to this grant need not be an undelegated token: it may itself already be the result of an earlier delegation. In that case, its existing actor is not discarded or replaced; the new actor is nested inside it, as act.act, and so on for additional hops. Repeated re-delegation builds a chain of custody, in which each hop records the party that acted at that point, while the original human remains at the root as the token's unchanging subject.

Each hop in the chain requires its own, independently granted mandate. O2ID does not permit an actor to be trusted implicitly on the basis that an earlier hop in the chain was authorized: every actor at every position in the chain must be authorized directly by the human, rather than by transitive extension of a delegation the human never reviewed.

resource / audience

resource (an absolute URI, for example https://api.example.com) or audience (a free-form name) records the downstream resource server the caller intends to use the newly minted token with. If resource is supplied, it is validated as a well-formed absolute URI; a malformed value is rejected with invalid_target, per RFC 8707 §2. Whichever value is supplied is recorded on the minted token and can be read back later, for example via token introspection.

Supplying either one at all is optional — omit both entirely for plain downscoping with no audience restriction. But once supplied, the target must be a currently registered resource server that the calling application holds some standing authorization for; anything else — an unregistered value, or one the application was never authorized for — is rejected with invalid_target.

That check only confirms the calling application has standing for the named target; it deliberately does not change how the token's own scope is computed, which stays exactly what it already was — derived from the human's live RBAC role scopes (see Managing Roles), narrowed by the application's allowedScopes and, for delegation, the agent's own scopes and the redeemed mandate. Token exchange's scope is fundamentally about a human's own permissions carried or narrowed forward, a different authorization model from a resource server's own scope namespace. Getting a Token for a Resource Server is where a resource server's own scopes actually govern what's granted — there, an audience produces a token scoped to that resource server's authorized permissions instead.

For how to inspect what a minted token actually carries, see Token Introspection.

API Reference

See the API Reference for the full request/response schemas.