Token exchange
RFC 8693 — a first-party admin console swaps a session token for a short-lived token of another audience.
Third-party applications do not use this flow
Token exchange is available only to first_party clients, used by MuoCeon's own admin console to carry an
already signed-in user's session into the oauth / account resource endpoints. Third parties obtain
tokens only through the authorization code flow.
What it solves
A first-party admin console holds an auth audience session token in the browser but needs to call endpoints under oauth (authorized application management) or account (account profile). Calling them with an auth token is blocked by audience validation — audience is a hard boundary, not a suggestion. Token exchange is the narrow door cut through that boundary: mint a short-lived, narrowly-scoped token for one specific audience.
Request
curl -X POST 'https://id.example.com/v1/oauth/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u "$FIRST_PARTY_CLIENT_ID:$FIRST_PARTY_CLIENT_SECRET" \
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
-d 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
-d "subject_token=$AUTH_ACCESS_TOKEN" \
-d 'audience=oauth' \
-d 'scope=oauth:grants:read'| Parameter | Required | Notes |
|---|---|---|
grant_type | ✅ | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | ✅ | A valid auth audience access token |
subject_token_type | ✅ | Only urn:ietf:params:oauth:token-type:access_token |
audience | ✅ | Only oauth or account |
scope | Optional | Only self-management scopes of the target audience |
Explicitly rejected usages:
actor_token/ delegated exchange — unsupported; passing it yields a parameter error.requested_token_typeother than an access token (e.g. requesting anid_token) — unsupported.subject_token_typeofrefresh_token, SAML assertion, etc. — unsupported.audience=auth— exchanging back to the session audience is not allowed.
Response
{
"token_type": "Bearer",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"expires_in": 300,
"access_token": "eyJ...",
"scope": "oauth:grants:read"
}How exchanged tokens differ
| Property | Exchanged token |
|---|---|
| Lifetime | Fixed at 5 minutes |
| Storage | Not persisted; no server-side record |
| Revocation | Cannot be revoked individually — wait for expiry |
aud | Target audience only (oauth or account), never auth |
| Refresh | No refresh token issued |
| Scope of use | The handful of requests needed for one page load |
Because nothing is persisted, it inherently cannot be revoked or introspected. The 5-minute lifetime is therefore the only security boundary: exchange, use immediately, discard — never cache it in local storage for reuse across pages.
Choosing an audience
- Reading authorized applications or revoking grants:
audience=oauthwithoauth:grants:read/oauth:grants:revoke. - Reading or updating the account profile:
audience=accountwithaccount:profile:read/account:profile:write. - Needing both audiences: exchange twice; do not expect one token to cover two audiences.
The full scope list is in Scopes and audiences.