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 ServiceEach service in this chain has different trust levels, different scopes, and different audiences. Using the same token everywhere creates problems:
| Problem | Description |
|---|---|
| Over-privileged tokens | The 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 audience | A token issued for frontend-app should not be accepted by payment-service. Without audience restriction, any service accepting the token is a valid target. |
| Impersonation | An 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. |
| Delegation | Service 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:
- Impersonation: Admin obtains a token with
subset to another user - Delegation: Service obtains a token that preserves the original user’s identity but records the service as the actor
- Audience restriction: Service obtains a token scoped to a specific downstream API
- 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_tokenRequest Parameters
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Always urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | Yes | The token being exchanged — represents the identity of the party on whose behalf the request is being made |
subject_token_type | Yes | The type of the subject token (see Token Types below) |
requested_token_type | Optional | The desired type for the output token. Defaults to access_token. |
audience | Optional | The target service or API that will consume the new token |
scope | Optional | The requested scopes for the new token. Must be a subset of the subject token’s scopes. |
actor_token | Optional | The token of the party performing the exchange (the “actor”) |
actor_token_type | Conditional | Required if actor_token is present |
Token Types
RFC 8693 defines standardized URNs for token types:
| Token Type URN | Description |
|---|---|
urn:ietf:params:oauth:token-type:access_token | OAuth 2.0 access token |
urn:ietf:params:oauth:token-type:refresh_token | OAuth 2.0 refresh token |
urn:ietf:params:oauth:token-type:id_token | OpenID Connect ID token |
urn:ietf:params:oauth:token-type:jwt | Generic 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_tokenThe 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
| Dimension | Impersonation | Delegation |
|---|---|---|
sub in new token | Target user | Original user (unchanged) |
act claim | Original admin/actor | Intermediary service |
| Downstream sees | ”Request from target user" | "Request from original user via service” |
| Permission required | impersonate:users | delegate:tokens |
| Risk level | High (full identity assumption) | Medium (scope can be reduced) |
| Audit trail | act records who impersonated | act records which service delegated |
| Typical actor | Admin user | Backend 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-gatewayChain 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:
| Operation | Required Permission | Who Has It |
|---|---|---|
| Impersonation | impersonate:users | Admin applications only |
| Delegation | delegate:tokens | Backend services |
| Audience restriction | delegate:tokens | Backend services |
| Scope reduction | No special permission | Any 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:
- Restrict the permission: Only grant
impersonate:usersto admin applications, never to user-facing apps - Require MFA: Admin must have completed MFA in the current session to perform impersonation
- Log and alert: All impersonation events trigger audit log entries and (optionally) notifications
- 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
actchain) - 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
| Approach | Standardized | Identity Preserved | Audit Trail | Scope Control |
|---|---|---|---|---|
| Token Exchange (RFC 8693) | Yes | Yes (act claim) | Full chain | Per-exchange |
| API Gateway token transformation | No | Varies | Gateway-only | Gateway-level |
| Custom middleware headers | No | Often lost | Manual | Ad-hoc |
| Shared service account | No | No (user identity lost) | Minimal | None |
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:
| Setting | Default | Description |
|---|---|---|
| Enable Token Exchange | false | Master toggle |
| Allow impersonation | false | Whether this application can perform impersonation exchanges |
| Allow delegation | false | Whether this application can perform delegation exchanges |
| Max chain depth | 5 | Maximum nesting depth of act claims |
| Exchanged token lifetime | 50% | Lifetime of exchanged token as percentage of subject token’s remaining lifetime |
API Endpoint
Token Exchange uses the standard token endpoint:
| Endpoint | Method | Grant Type |
|---|---|---|
/api/auth/token | POST | urn: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.
Related Concepts
- Tokens Explained — JWT structure, the
actclaim, and token lifetimes - OAuth 2.0 & OIDC — The authorization framework Token Exchange extends
- FGA / Zanzibar Model — Fine-grained authorization that can gate impersonation permissions
- DPoP (Proof of Possession) — Sender-constrained tokens that can be combined with Token Exchange