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:
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 the global
admin:organizationspermission on the target resource server when the tenant hasinherit_global_permissions_in_organizationsenabled (the same bypass used by the refresh-token grant). - 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 slice is narrow on purpose. The following parts of RFC 8693 are not implemented:
- Other
subject_token_typevalues. Onlyurn:ietf:params:oauth:token-type:access_tokenis accepted.id_token,refresh_token,jwt,saml1,saml2are 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 inactautomatically.requested_token_type. AuthHero 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"
}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:
- 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
- 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