Skip to Content
ConceptsToken Exchange (RFC 8693)

Token Exchange (RFC 8693)

The Problem: One Token Does Not Fit All

In a monolithic application, a single access token is sufficient: the user authenticates, receives a token, and uses it for every API call. But modern architectures are not monolithic. A typical request might traverse multiple services:

User → Frontend → API Gateway → Order Service → Payment Service → Notification Service

Each service in this chain has different trust levels, different scopes, and different audiences. Using the same token everywhere creates problems:

ProblemDescription
Over-privileged tokensThe frontend token has read:users write:orders manage:payments but the Payment Service only needs process:payments. If the Payment Service is compromised, the attacker has access to all scopes.
Wrong audienceA token issued for frontend-app should not be accepted by payment-service. Without audience restriction, any service accepting the token is a valid target.
ImpersonationAn admin needs to debug a user’s account by acting as that user. There is no standard way to “become” another user without knowing their credentials.
DelegationService A needs to call Service B on behalf of the user, but Service B needs to know both the original user and the calling service’s identity.

OAuth 2.0 Token Exchange (RFC 8693) provides a standardized protocol for exchanging one security token for another, with precise control over the subject, audience, and scope of the resulting token.

What Token Exchange Does

Token Exchange defines a new grant type at the authorization server’s token endpoint. A client sends an existing token (the subject_token) and optionally an actor_token, and receives back a new token with different properties.

The key operations it enables:

  1. Impersonation: Admin obtains a token with sub set to another user
  2. Delegation: Service obtains a token that preserves the original user’s identity but records the service as the actor
  3. Audience restriction: Service obtains a token scoped to a specific downstream API
  4. Scope reduction: Service obtains a token with fewer permissions than the original

How Token Exchange Works

The Token Exchange Request

A token exchange request is a POST to the standard token endpoint with grant_type=urn:ietf:params:oauth:grant-type:token-exchange:

POST /api/auth/token HTTP/1.1 Host: auth.example.com Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange &subject_token=eyJhbGciOiJSUzI1NiJ9... &subject_token_type=urn:ietf:params:oauth:token-type:access_token &requested_token_type=urn:ietf:params:oauth:token-type:access_token &audience=payment-service &scope=process:payments &actor_token=eyJhbGciOiJSUzI1NiJ9... &actor_token_type=urn:ietf:params:oauth:token-type:access_token

Request Parameters

ParameterRequiredDescription
grant_typeYesAlways urn:ietf:params:oauth:grant-type:token-exchange
subject_tokenYesThe token being exchanged — represents the identity of the party on whose behalf the request is being made
subject_token_typeYesThe type of the subject token (see Token Types below)
requested_token_typeOptionalThe desired type for the output token. Defaults to access_token.
audienceOptionalThe target service or API that will consume the new token
scopeOptionalThe requested scopes for the new token. Must be a subset of the subject token’s scopes.
actor_tokenOptionalThe token of the party performing the exchange (the “actor”)
actor_token_typeConditionalRequired if actor_token is present

Token Types

RFC 8693 defines standardized URNs for token types:

Token Type URNDescription
urn:ietf:params:oauth:token-type:access_tokenOAuth 2.0 access token
urn:ietf:params:oauth:token-type:refresh_tokenOAuth 2.0 refresh token
urn:ietf:params:oauth:token-type:id_tokenOpenID Connect ID token
urn:ietf:params:oauth:token-type:jwtGeneric JWT

The Response

A successful token exchange returns a new token:

{ "access_token": "eyJhbGciOiJSUzI1NiJ9...", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 1800, "scope": "process:payments" }

The issued_token_type confirms what kind of token was actually issued (it may differ from requested_token_type if the server applied policies).

Impersonation vs Delegation

Token Exchange supports two fundamentally different patterns: impersonation and delegation. Understanding the difference is critical for building secure systems.

Impersonation

In impersonation, the exchanged token’s sub claim is set to the target user. The downstream service sees requests “from” the impersonated user. The original actor is recorded in the act claim for audit purposes, but the service treats the request as if it came from the target user.

Admin (sub: admin-456) exchanges token for User (sub: user-123) → New token: sub=user-123, act.sub=admin-456 → Payment Service sees: "Request from user-123 (impersonated by admin-456)"

Use case: An admin needs to reproduce a bug that only affects a specific user’s account. They impersonate the user to see the same data and behavior.

POST /api/auth/token HTTP/1.1 Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange &subject_token=<user-123-token> &subject_token_type=urn:ietf:params:oauth:token-type:access_token &actor_token=<admin-456-token> &actor_token_type=urn:ietf:params:oauth:token-type:access_token &requested_token_type=urn:ietf:params:oauth:token-type:access_token

The resulting JWT:

{ "sub": "user-123", "iss": "https://auth.example.com", "aud": "frontend-app", "exp": 1739882700, "act": { "sub": "admin-456" } }

Delegation

In delegation, the exchanged token’s sub claim remains the original user. The actor is recorded in the act claim, but the token still represents the original user’s request, now mediated by a service.

User (sub: user-123) authenticates → Frontend calls Order Service → Order Service exchanges token to call Payment Service → New token: sub=user-123, act.sub=order-service → Payment Service sees: "Request from user-123, delegated via order-service"

Use case: Service-to-service calls in a microservice architecture where each service needs to call downstream services on behalf of the user, with the downstream service knowing both who the user is and which service is calling.

The resulting JWT:

{ "sub": "user-123", "iss": "https://auth.example.com", "aud": "payment-service", "exp": 1739882700, "scope": "process:payments", "act": { "sub": "order-service" } }

Key Differences

DimensionImpersonationDelegation
sub in new tokenTarget userOriginal user (unchanged)
act claimOriginal admin/actorIntermediary service
Downstream sees”Request from target user""Request from original user via service”
Permission requiredimpersonate:usersdelegate:tokens
Risk levelHigh (full identity assumption)Medium (scope can be reduced)
Audit trailact records who impersonatedact records which service delegated
Typical actorAdmin userBackend service

Impersonation is a privileged operation. In Auris, only clients with the impersonate:users permission can perform impersonation token exchanges. This permission should be restricted to admin applications and never granted to user-facing clients.

The act Claim: Delegation Chains

The act (actor) claim is a JSON object embedded in the JWT that records who performed the token exchange. Actor claims can be nested to represent a chain of delegations:

{ "sub": "user-123", "iss": "https://auth.example.com", "aud": "notification-service", "act": { "sub": "payment-service", "act": { "sub": "order-service", "act": { "sub": "api-gateway" } } } }

This token tells a story: user-123 made a request through api-gateway, which delegated to order-service, which delegated to payment-service, which is now calling notification-service.

Reading the Chain

The outermost act.sub is the immediate caller. Each nested act represents the previous hop in the delegation chain. The sub at the top level is always the original user.

notification-service receives the token and sees: - Who is the user? → sub: user-123 - Who is calling me? → act.sub: payment-service - Who called payment-service? → act.act.sub: order-service - Who called order-service? → act.act.act.sub: api-gateway

Chain Depth Limits

Auris limits the delegation chain depth to prevent confused deputy attacks and unbounded token growth. The default maximum chain depth is 5. If a token exchange would exceed this depth, the request is rejected with:

{ "error": "invalid_request", "error_description": "Maximum delegation chain depth exceeded" }

Security Properties

Token Exchange is a powerful mechanism that requires careful security controls.

Permission Checks

Auris enforces explicit permission checks on every token exchange:

OperationRequired PermissionWho Has It
Impersonationimpersonate:usersAdmin applications only
Delegationdelegate:tokensBackend services
Audience restrictiondelegate:tokensBackend services
Scope reductionNo special permissionAny client can reduce its own scope

Scope Reduction (Never Expansion)

The exchanged token can never have more permissions than the original. If the subject token has scopes read:users write:orders, the exchanged token can request read:users (subset) but not read:users delete:users (superset). The authorization server enforces this by intersecting the requested scope with the subject token’s scope.

Audit Trail

Every token exchange is logged with:

  • The subject token’s identity (sub)
  • The actor token’s identity (if present)
  • The requested audience and scope
  • Whether the exchange was impersonation or delegation
  • The IP address and timestamp

This audit trail is visible in the Auris Console under Logs and can be queried via the API.

Token Lifetime Reduction

Exchanged tokens always have a shorter lifetime than the original token. If the subject token expires in 1 hour, the exchanged token might expire in 30 minutes. This limits the exposure window if the exchanged token is compromised.

The specific reduction is configurable per-application, with a default of 50% of the remaining lifetime of the subject token.

Security Considerations

Impersonation Risks

Impersonation allows complete identity assumption. An application with impersonate:users can act as any user in the system. Mitigations:

  1. Restrict the permission: Only grant impersonate:users to admin applications, never to user-facing apps
  2. Require MFA: Admin must have completed MFA in the current session to perform impersonation
  3. Log and alert: All impersonation events trigger audit log entries and (optionally) notifications
  4. Time-limit: Impersonated tokens have short lifetimes (15 minutes default)

Delegation Chain Attacks

A delegation chain A → B → C → D means each service in the chain has successively more constrained tokens. However, a compromised service in the middle of the chain can:

  • Read the token claims (including the act chain)
  • Use its delegated token to call downstream services
  • It cannot call services outside the token’s audience
  • It cannot expand the scope beyond what was delegated to it

Token Exchange vs Alternatives

ApproachStandardizedIdentity PreservedAudit TrailScope Control
Token Exchange (RFC 8693)YesYes (act claim)Full chainPer-exchange
API Gateway token transformationNoVariesGateway-onlyGateway-level
Custom middleware headersNoOften lostManualAd-hoc
Shared service accountNoNo (user identity lost)MinimalNone

Token Exchange is the standardized approach. It preserves the full identity chain, provides granular scope control, and creates an audit trail at the authorization server.

Auris Implementation Details

Prisma Model

model TokenExchange { id String @id @default(cuid()) tenantId String applicationId String subjectUserId String actorUserId String? requestedAudience String? requestedScope String? grantedScope String? exchangeType TokenExchangeType createdAt DateTime @default(now()) } enum TokenExchangeType { IMPERSONATION DELEGATION }

Configuration

Token Exchange is enabled per-application in the Auris Console under Applications > (select application) > Settings:

SettingDefaultDescription
Enable Token ExchangefalseMaster toggle
Allow impersonationfalseWhether this application can perform impersonation exchanges
Allow delegationfalseWhether this application can perform delegation exchanges
Max chain depth5Maximum nesting depth of act claims
Exchanged token lifetime50%Lifetime of exchanged token as percentage of subject token’s remaining lifetime

API Endpoint

Token Exchange uses the standard token endpoint:

EndpointMethodGrant Type
/api/auth/tokenPOSTurn:ietf:params:oauth:grant-type:token-exchange

Code Example: Microservice Delegation Chain

The following example demonstrates a three-service delegation chain:

// api-gateway receives user's access token and needs to call order-service async function callOrderService(userAccessToken: string) { // Exchange the user's token for one scoped to order-service const exchangeResponse = await fetch('https://auth.example.com/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', subject_token: userAccessToken, subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', audience: 'order-service', scope: 'read:orders write:orders', }), }) const { access_token: orderServiceToken } = await exchangeResponse.json() // Call order-service with the delegated token const orders = await fetch('https://order-service.internal/api/orders', { headers: { Authorization: `Bearer ${orderServiceToken}` }, }) return orders.json() } // order-service receives the delegated token and needs to call payment-service async function processPayment(delegatedToken: string, orderId: string) { // Further exchange: scope down to payment-service const exchangeResponse = await fetch('https://auth.example.com/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', subject_token: delegatedToken, subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', audience: 'payment-service', scope: 'process:payments', }), }) const { access_token: paymentToken } = await exchangeResponse.json() // The payment-service token now has: // sub: original-user-id // aud: payment-service // scope: process:payments // act: { sub: order-service, act: { sub: api-gateway } } return fetch('https://payment-service.internal/api/charge', { method: 'POST', headers: { Authorization: `Bearer ${paymentToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ orderId, amount: 49.99 }), }) }

Admin Impersonation Example

async function impersonateUser(adminToken: string, targetUserId: string) { const response = await fetch('https://auth.example.com/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', subject_token: adminToken, subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', requested_token_type: 'urn:ietf:params:oauth:token-type:access_token', // For impersonation, the subject is identified via a custom parameter // or the admin token's claims are used with the target user audience: 'frontend-app', }), }) if (!response.ok) { const error = await response.json() if (error.error === 'insufficient_scope') { throw new Error('Admin does not have impersonate:users permission') } throw new Error(`Token exchange failed: ${error.error_description}`) } const { access_token } = await response.json() // This token has: // sub: target-user-id // act: { sub: admin-user-id } // Downstream services see requests "from" the target user return access_token }

Every token exchange — whether impersonation or delegation — creates an entry in the Auris audit log. Administrators can filter logs by exchange type to review all impersonation activity. In high-security environments, impersonation events can trigger real-time notifications to the security team.