Token Exchange
This document describes the token exchange implementation that allows Keycloak and Entra tokens to be exchanged for enriched governance platform tokens. The examples below use Keycloak because it has a checked-in local compose setup.
Overview
Section titled “Overview”The token exchange pattern implements RFC 8693 (OAuth 2.0 Token Exchange) to allow clients to exchange their IDP access tokens for governance platform tokens that include enriched claims such as organization memberships, project assignments, and DID keys.
Architecture
Section titled “Architecture”┌─────────────┐ ┌──────────────┐ ┌─────────────────┐│ Client │────▶│ Keycloak │ │ Auth Service │└─────────────┘ └──────────────┘ └─────────────────┘ │ │ │ │ 1. Authenticate │ │ │────────────────────▶│ │ │ │ │ │ 2. Access Token │ │ │◀────────────────────│ │ │ │ │ │ 3. Token Exchange Request │ │───────────────────────────────────────────▶│ │ │ │ │ │ 4. Validate Token │ │ │◀─────────────────────│ │ │ │ │ 5. Enriched Token │ │ │◀───────────────────────────────────────────│Implementation Components
Section titled “Implementation Components”1. Keycloak Token Validation (auth-service/backend/pkg/idp/providers/keycloak/token_validation.go)
Section titled “1. Keycloak Token Validation (auth-service/backend/pkg/idp/providers/keycloak/token_validation.go)”- Validates Keycloak JWT tokens using Keycloak’s JWKS endpoint
- Caches public keys for performance
- Extracts user claims from validated tokens
2. Token Issuer (auth-service/backend/pkg/services/token_issuer.go)
Section titled “2. Token Issuer (auth-service/backend/pkg/services/token_issuer.go)”- Issues new JWT tokens with enriched claims
- Manages signing keys (generates or loads from environment)
- Provides JWKS endpoint for token validation
3. Token Exchange Handler (auth-service/backend/pkg/handlers/token_exchange.go)
Section titled “3. Token Exchange Handler (auth-service/backend/pkg/handlers/token_exchange.go)”- Implements the token exchange endpoint
- Orchestrates validation, user lookup, and token issuance
- Enriches tokens with governance platform claims
4. Claims Builder (auth-service/backend/pkg/claims/claims.go)
Section titled “4. Claims Builder (auth-service/backend/pkg/claims/claims.go)”- Shared functions for building organization and project claims
- Consistent claim structure across different auth flows
API Endpoints
Section titled “API Endpoints”Token Exchange
Section titled “Token Exchange”POST /api/v1/auth/tokenContent-Type: application/json
{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "<keycloak_access_token>", "subject_token_type": "urn:ietf:params:oauth:token-type:access_token"}
Response:{ "access_token": "<enriched_token>", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "<refresh_token>", "refresh_expires_in": 2592000, "issued_token_type": "urn:ietf:params:oauth:token-type:access_token"}POST /api/v1/auth/token-exchange is still registered for backward compatibility and routes to the same handler. Both routes are only registered when IDP_PROVIDER is keycloak or entra; Auth0 and generic OIDC deployments do not expose the token-exchange endpoint and rely on the provider-issued access token directly.
JWKS Endpoint
Section titled “JWKS Endpoint”GET /api/v1/auth/.well-known/jwks.json
Response:{ "keys": [ { "kty": "RSA", "use": "sig", "kid": "auth-service-12345678", "alg": "RS256", "n": "...", "e": "AQAB" } ]}Configuration
Section titled “Configuration”Environment Variables
Section titled “Environment Variables”# Keycloak ConfigurationIDP_PROVIDER=keycloakIDP_ISSUER=http://localhost:8081/realms/governanceIDP_CLIENT_ID=governance-platform-backendIDP_CLIENT_SECRET=backend-secretIDP_KEYCLOAK_REALM=governanceIDP_KEYCLOAK_ADMIN_URL=http://localhost:8081
# Token Issuer ConfigurationAUTH_SERVICE_URL=http://localhost:8080/api/v1/authAUTH_SERVICE_PRIVATE_KEY=<PEM_ENCODED_RSA_KEY>AUTH_SERVICE_KEY_ID=<CUSTOM_KEY_ID>AUTH_SERVICE_KEY_PATH=/path/to/key.pemToken Claims
Section titled “Token Claims”Input (Keycloak Token)
Section titled “Input (Keycloak Token)”{ "sub": "f:12345678-1234-1234-1234-123456789012:johndoe", "email": "john@example.com", "name": "John Doe", "preferred_username": "johndoe", "groups": ["project-123", "admin"], "exp": 1234567890, "iat": 1234567890}Output (Enriched Token)
Section titled “Output (Enriched Token)”{ "sub": "12345678-1234-1234-1234-123456789012", "email": "john@example.com", "name": "John Doe", "preferred_username": "johndoe", "app_organizations": [ { "id": 1, "roles": ["organization_owner"], "name": "Example Org", "auth0_org_id": "org_123" } ], "app_projects": [ { "id": 123, "roles": ["project_owner"] } ], "user_context": { "total_organizations": 1, "total_projects": 1, "has_admin_access": true }, "did_key_id": "did:key:z6Mk...", "groups": ["project-123", "admin"], "idp_provider": "keycloak", "idp_user_id": "f:12345678-1234-1234-1234-123456789012:johndoe", "https://governance.eqtylab.io/organization_id": 1, "organization_id": "1", "iss": "http://localhost:8080/api/v1/auth", "aud": "http://localhost:8080/api/v1/auth", "exp": 1234567890, "iat": 1234567890, "nbf": 1234567890, "jti": "87654321-4321-4321-4321-210987654321"}Client Integration
Section titled “Client Integration”JavaScript/TypeScript Example
Section titled “JavaScript/TypeScript Example”async function exchangeKeycloakToken(keycloakToken: string): Promise<string> { const response = await fetch("https://auth-service/api/v1/auth/token", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", subject_token: keycloakToken, subject_token_type: "urn:ietf:params:oauth:token-type:access_token", }), });
if (!response.ok) { throw new Error(`Token exchange failed: ${response.statusText}`); }
const data = await response.json(); return data.access_token;}Go Example
Section titled “Go Example”type TokenExchangeRequest struct { GrantType string `json:"grant_type"` SubjectToken string `json:"subject_token"` SubjectTokenType string `json:"subject_token_type"`}
type TokenExchangeResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` RefreshExpiresIn int `json:"refresh_expires_in"` IssuedTokenType string `json:"issued_token_type"`}
func exchangeToken(keycloakToken string) (string, error) { reqBody := TokenExchangeRequest{ GrantType: "urn:ietf:params:oauth:grant-type:token-exchange", SubjectToken: keycloakToken, SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token", }
jsonBody, _ := json.Marshal(reqBody) resp, err := http.Post( "https://auth-service/api/v1/auth/token", "application/json", bytes.NewBuffer(jsonBody), ) if err != nil { return "", err } defer resp.Body.Close()
var tokenResp TokenExchangeResponse if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { return "", err }
return tokenResp.AccessToken, nil}Testing
Section titled “Testing”For local testing, obtain a Keycloak access token, POST it to /api/v1/auth/token, then verify the returned access token against /api/v1/auth/.well-known/jwks.json.
Security Considerations
Section titled “Security Considerations”- Token Validation: Incoming IDP tokens are validated against the configured provider’s JWKS
- Token Expiration: Enriched tokens respect the remaining lifetime of the original IDP token
- Key Management: Private keys for token signing can be provided via environment or auto-generated
- HTTPS: Always use HTTPS in production for token exchange
- Token Scope: Enriched tokens are only valid for the governance platform services
Troubleshooting
Section titled “Troubleshooting”Token Exchange Fails
Section titled “Token Exchange Fails”- Verify Keycloak configuration in environment variables
- Check auth service logs for validation errors
- Ensure the incoming IDP token is not expired
Missing Claims
Section titled “Missing Claims”- Verify user exists in governance platform database
- Check organization/project memberships
- Review auth service logs for enrichment errors
Performance Issues
Section titled “Performance Issues”- Token exchange adds ~50-100ms latency
- Consider implementing client-side token caching
- Monitor JWKS cache hit rates