Skip to content

RFC 8693 — OAuth 2.0 Token Exchange

Spec: datatracker.ietf.org/doc/html/rfc8693Status: Partial

RFC 8693 defines a Security Token Service (STS) endpoint that exchanges one token for another. AuthHero implements one well-scoped slice of it: a confidential client can exchange a self-issued access token for a new access token scoped to a different organization, optionally with a narrower scope set. The new token carries an act claim identifying the exchanging client.

The primary use case is: a control-plane access token (representing a user with broad permissions across a tenant) is presented to a backend service, which exchanges it for an organization-scoped token before calling tenant resources.

How a client uses it

The exchanging client posts to /oauth/token:

text
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<self-issued access token>
subject_token_type=urn:ietf:params:oauth:token-type:access_token
organization=<target organization id>
client_id=<exchanging client>
client_secret=<...>           # or client_assertion (RFC 7523)
audience=<resource server>    # optional — defaults to the subject token's aud
scope=<requested scopes>      # optional — defaults to the subject token's scope

The response is a standard token response:

json
{
  "access_token": "<JWT>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read:things",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

The new access token has:

  • sub — the original user (preserved from the subject token).
  • aud — the requested (or inherited) audience.
  • org_id — the target organization.
  • scope — the requested scopes (must be a subset of the subject token's).
  • act{ "sub": "<exchanging client_id>", "client_id": "<exchanging client_id>" }, per RFC 8693 §4.1.

No refresh token is issued. The flow is meant to be re-run on demand, not refreshed.

Validation pipeline

Each request runs through, in order:

  1. Client authentication. The exchanging client must authenticate via client_secret (RFC 6749 client_secret_post/client_secret_basic) or client_assertion (RFC 7523). Public clients are rejected.
  2. organization_usage gate. The exchanging client's organization_usage must not be deny. New clients (including those registered via Dynamic Client Registration and CIMD) default to deny, so token exchange is opt-in per client.
  3. grant_types allowlist. The exchanging client must list urn:ietf:params:oauth:grant-type:token-exchange in its grant_types (standard RFC 6749 §5.2 enforcement).
  4. Subject token signature and issuer. The subject_token is verified against the tenant's JWKS. Tokens signed by a different issuer are rejected with invalid_grant.
  5. Subject token freshness. Expired tokens are rejected with invalid_grant.
  6. No chained exchange. If the subject_token already carries an act claim, the request is rejected — exchanged tokens are not themselves exchangeable.
  7. User resolution. The sub claim must resolve to a known user. Linked users follow the same linked_to chain the other grants use.
  8. Organization exists. The organization parameter must identify a real organization in the tenant.
  9. Audience is a resource server. The resolved audience must be a registered resource server. This stops the exchange from minting tokens for an audience that isn't authoritatively configured.
  10. Authorization. The user must be a member of the target organization, OR hold the global admin:organizations permission on the target resource server when the tenant has inherit_global_permissions_in_organizations enabled (the same bypass used by the refresh-token grant).
  11. Downscope. Any requested scope value must be present in the subject token's scope. Exceeding the subject token's scope returns invalid_scope.

What's not implemented

The accepted slice is narrow on purpose. The following parts of RFC 8693 are not implemented:

  • Other subject_token_type values. Only urn:ietf:params:oauth:token-type:access_token is accepted. id_token, refresh_token, jwt, saml1, saml2 are rejected at the schema layer. Foreign token types (tokens issued by another IdP) would require a per-tenant registration flow similar to Auth0's "Custom Token Exchange profile" — out of scope for now.
  • actor_token / actor_token_type. Delegation chains with an explicit actor token are not supported. The acting party is always the authenticated exchanging client, recorded in act automatically.
  • requested_token_type. AuthHero always returns an access token. Asking for a different requested_token_type (e.g. id_token) is ignored.
  • Resource indicators (RFC 8707). The resource parameter is not honored; use audience to specify the target.
  • Chained exchange. Tokens minted via token-exchange are not themselves exchangeable. RFC 8693 §4.1 allows nested actors; AuthHero deliberately rejects them.

Configuration

Token exchange requires no special client type, just the standard fields:

jsonc
{
  "client_id": "exchange-service",
  "client_secret": "...",
  "grant_types": [
    "client_credentials",
    "urn:ietf:params:oauth:grant-type:token-exchange",
  ],
  "organization_usage": "allow", // or "require" — must NOT be "deny"
}

For the admin:organizations bypass, set the tenant flag inherit_global_permissions_in_organizations and assign the permission to the user (or via a role) at tenant level on the target resource server.

Use with CIMD-registered clients

CIMD clients are public clients (no client_secret) and their grant_types are filtered to authorization_code and refresh_token. They cannot be the exchanging client for a token-exchange call. They can, however, be the subject — the typical pattern is:

  1. A CIMD-registered client (e.g. an MCP client) obtains a user-context access token via authorization code.
  2. The user presents that token to a backend service.
  3. The backend service — registered as a confidential client with token-exchange in its grant_types — calls /oauth/token to exchange the subject token for an org-scoped one.

This split keeps the exchange capability gated behind a server-side credential while still allowing public CIMD clients to drive the user-facing flow.

Audit and logging

Successful exchanges emit SUCCESS_EXCHANGE_SUBJECT_TOKEN_FOR_ACCESS_TOKEN (sestft); failed exchanges emit FAILED_EXCHANGE_SUBJECT_TOKEN_FOR_ACCESS_TOKEN (festft) with a description of which validation step rejected the request. The exchanging client_id is recorded in the new token's act claim, so resource servers and audit consumers can distinguish exchanged tokens from directly issued ones.

Released under the MIT License.