-
-
Notifications
You must be signed in to change notification settings - Fork 204
Open
Labels
enhancementNew feature or requestNew feature or request
Description
RFC: Token Exchange (RFC 8693)
Phase: 5 — Advanced Security & Enterprise
Priority: P3 — Medium
Estimated Effort: Medium
Depends on: OIDC Provider (#514)
Problem Statement
In microservice and agent architectures, services need to exchange tokens for different scopes and audiences. A frontend service with a user token needs a downstream-specific token. Admin impersonation needs a token swap. Cross-org access requires token exchange. Keycloak 26.2 added this. RFC 8693 is the standard mechanism.
Proposed Solution
1. Token Exchange Endpoint
Extend POST /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:token-exchange
Request parameters (RFC 8693 §2.1):
| Parameter | Required | Description |
|---|---|---|
grant_type |
Yes | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token |
Yes | Token being exchanged |
subject_token_type |
Yes | urn:ietf:params:oauth:token-type:access_token or urn:ietf:params:oauth:token-type:refresh_token |
requested_token_type |
No | Desired token type (default: access_token) |
audience |
No | Target service/resource |
scope |
No | Requested scopes for new token |
actor_token |
No | Token of the acting party (for delegation) |
actor_token_type |
No | Type of actor token |
2. Supported Exchange Patterns
Pattern 1: Delegation — User token → service-scoped token
Subject: User's access token (full permissions)
Result: Access token scoped to specific service/audience with reduced permissions
Use case: Frontend has user token, backend-for-frontend needs a token
that only works with the billing microservice.
// Request
{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "eyJ...(user's token)",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"audience": "https://billing.internal",
"scope": "billing:read billing:write"
}
// Response token claims
{
"sub": "user_123",
"aud": "https://billing.internal",
"scope": "billing:read billing:write",
"act": { "sub": "app_frontend" }, // acting party
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}Pattern 2: Impersonation — Admin token → user token (with audit)
Subject: Admin's access token
Actor: The user being impersonated
Result: Token that looks like the user's but is logged as impersonation
Use case: Support agent needs to reproduce a user's issue.
Requires --enable-impersonation=true
// Request
{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "eyJ...(admin's token)",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"actor_token": "user_id_to_impersonate",
"scope": "impersonate"
}
// Response token claims
{
"sub": "user_456", // impersonated user
"act": { "sub": "admin_1" }, // actual actor (admin)
"is_impersonated": true,
"impersonation_reason": "support ticket #1234"
}Pattern 3: Cross-Organization — Org A token → Org B token
Subject: Token valid for Org A
Result: Token valid for Org B (if user is member of both)
Use case: User switches organization context without re-authenticating.
// Request
{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "eyJ...(org A token)",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"audience": "org_B_id"
}
// Validation: verify user is member of Org B
// Response: new token with org_id=org_B, org_role=user's role in Org B3. Implementation
case "urn:ietf:params:oauth:grant-type:token-exchange":
subjectToken := c.PostForm("subject_token")
subjectTokenType := c.PostForm("subject_token_type")
audience := c.PostForm("audience")
requestedScope := c.PostForm("scope")
actorToken := c.PostForm("actor_token")
// 1. Validate subject token
subjectClaims, err := tokenProvider.ValidateToken(subjectToken)
if err != nil {
return tokenError(c, "invalid_grant", "invalid subject_token")
}
// 2. Determine exchange pattern
if actorToken != "" && hasScope(subjectClaims, "impersonate") {
return handleImpersonationExchange(c, subjectClaims, actorToken)
} else if audience != "" {
return handleDelegationExchange(c, subjectClaims, audience, requestedScope)
}
// 3. Generate new token with appropriate claims
newClaims := buildExchangedTokenClaims(subjectClaims, audience, requestedScope)
newClaims["act"] = map[string]string{"sub": subjectClaims["sub"].(string)}
newToken, _ := tokenProvider.SignToken(newClaims)
// 4. Audit log
auditProvider.Log(ctx, audit.AuditEvent{
Action: "token.exchanged",
Metadata: map[string]interface{}{
"exchange_type": "delegation",
"audience": audience,
"original_sub": subjectClaims["sub"],
},
})
c.JSON(200, gin.H{
"access_token": newToken,
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3600,
"scope": requestedScope,
})4. Security Controls
- Token exchange requires authenticated client (client_id/secret or the subject token itself)
- Exchanged tokens have shorter TTL than the original (default: 1 hour max)
- Scope can only be reduced, never expanded beyond the subject token's scope
- Impersonation exchange requires admin role +
--enable-impersonation=true - Cross-org exchange validates user membership in target org
- All exchanges logged in audit trail with full context
CLI Configuration Flags
--enable-token-exchange=false # Enable RFC 8693 token exchange
--token-exchange-max-ttl=3600 # Max TTL for exchanged tokens (seconds)
Testing Plan
- Test delegation exchange (user token → service-scoped token)
- Test scope reduction (can't expand beyond original)
- Test impersonation exchange (admin → user token with audit)
- Test cross-org exchange (validates membership)
- Test invalid subject token rejection
- Test exchanged token has shorter TTL
- Test audit logging for all exchange patterns
References
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request