Skip to content

RFC: Token Exchange (RFC 8693) #523

@lakhansamani

Description

@lakhansamani

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 B

3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions