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 two slices of it on /oauth/token, selected by subject_token_type:
subject_token_type | Flow | Subject token |
|---|---|---|
urn:ietf:params:oauth:token-type:access_token | Organization switching (this page) | An access token AuthHero issued itself |
A custom URI you register, e.g. urn:acme:session-token | Custom Token Exchange | A token your own backend signed, verified against your JWKS or by an action |
Use Custom Token Exchange when a trusted backend that runs its own login needs AuthHero tokens for one of its users. It is compatible with Auth0's feature of the same name. It returns tokens only and does not create an AuthHero browser session. See the Custom Token Exchange guide for profiles, user mapping and errors.
The rest of this page covers organization switching. 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:
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 scopeThe response is a standard token response:
{
"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:
- Client authentication. The exchanging client must authenticate via
client_secret(RFC 6749client_secret_post/client_secret_basic) orclient_assertion(RFC 7523). Public clients are rejected. organization_usagegate. The exchanging client'sorganization_usagemust not bedeny. New clients (including those registered via Dynamic Client Registration and CIMD) default todeny, so token exchange is opt-in per client.grant_typesallowlist. The exchanging client must listurn:ietf:params:oauth:grant-type:token-exchangein itsgrant_types(standard RFC 6749 §5.2 enforcement).- Subject token signature and issuer. The
subject_tokenis verified against the tenant's JWKS. Tokens signed by a different issuer are rejected withinvalid_grant. - Subject token freshness. Expired tokens are rejected with
invalid_grant. - No chained exchange. If the
subject_tokenalready carries anactclaim, the request is rejected — exchanged tokens are not themselves exchangeable. - User resolution. The
subclaim must resolve to a known user. Linked users follow the samelinked_tochain the other grants use. - Organization exists. The
organizationparameter must identify a real organization in the tenant. - Audience is a resource server. The resolved
audiencemust be a registered resource server. This stops the exchange from minting tokens for an audience that isn't authoritatively configured. - Authorization. The user must be a member of the target organization, OR hold, at global scope on the Management API (
urn:authhero:management), eitheraccess:all_organizationsoradmin:organizationswith the tenant'sinherit_global_permissions_in_organizationsflag enabled. Every organization gate (login, silent auth, refresh token) applies the same rule. - Downscope. Any requested
scopevalue must be present in the subject token's scope. Exceeding the subject token's scope returnsinvalid_scope.
What's not implemented
The accepted surface is narrow on purpose. The following parts of RFC 8693 are not implemented:
- Other standard
subject_token_typevalues. Of the IETF-registered types, onlyurn:ietf:params:oauth:token-type:access_tokenis accepted.id_token,refresh_token,jwt,saml1andsaml2are rejected. Tokens from another issuer are accepted only through Custom Token Exchange, under a customsubject_token_typeregistered in a token exchange profile. As on Auth0, profiles cannot claim the reservedurn:ietf:params:oauth:namespace. 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 inactautomatically.requested_token_type. Organization switching always returns an access token. Asking for a differentrequested_token_type(e.g.id_token) is ignored.- Resource indicators (RFC 8707). The
resourceparameter is not honored; useaudienceto 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:
{
"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"
}To bypass membership, assign access:all_organizations to the user (or via a role) at tenant level on the Management API. The older admin:organizations bypass also works but needs the tenant flag inherit_global_permissions_in_organizations. See Access to every organization.
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:
- A CIMD-registered client (e.g. an MCP client) obtains a user-context access token via authorization code.
- The user presents that token to a backend service.
- The backend service — registered as a confidential client with
token-exchangein itsgrant_types— calls/oauth/tokento 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.
Related AuthHero documentation
- Custom Token Exchange — exchanging a token your backend signed
- RFC 6749 — OAuth 2.0 Authorization Framework
- RFC 7523 — JWT Client Authentication — used to authenticate the exchanging client
- Client ID Metadata Documents (CIMD) — typical source of subject tokens
- AuthHero vs Auth0 — Token Exchange differences