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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions