Client Credentials to Authorization Code Token Exchange
Introduction
This document outlines the design for exchanging an external Client Credentials (CC) token (e.g., from Okta) for an internal Authorization Code (AC) style token (containing user/function identity) within the light-4j ecosystem.
Use Case
External partners or applications integrate with light-portal using their own Identity Provider (e.g., Okta) via the Client Credentials flow.
- External Token: A JWT issued by Okta representing the External App.
- Internal Requirement:
light-portalservices require a JWT containing internaluserId,roles, andcustom_claimsto perform business logic (Event Sourcing/CQRS). - Goal: Bridge the external identity to the internal identity transparently at the ingress/handler layer.
Architecture
The solution uses RFC 8693 OAuth 2.0 Token Exchange.
High-Level Flow
- External Request: The external app calls
light-portalwithAuthorization: Bearer <Okta-CC-Token>. - Interception: A
TokenExchangeHandlerinlight-portalintercepts the request. - Validation: The handler validates the Okta token (signature, expiration) using
JwtVerifierand Okta’s JWK. - Exchange:
- The handler requests a token exchange from the internal
light-oauth2service. - Grant Type:
urn:ietf:params:oauth:grant-type:token-exchange - Subject Token: The verified Okta token.
- Subject Token Type:
urn:ietf:params:oauth:token-type:jwt
- The handler requests a token exchange from the internal
- Minting:
light-oauth2validates the request.- It identifies the “Shadow Project” mapped to the external client ID.
- It issues a new internal JWT containing the mapped internal
userIdand roles.
- Injection: The handler replaces the
Authorizationheader with the new internal token. - Processing: Downstream services processing the request see a standard internal user token.
Configuration
The TokenExchangeHandler configures the exchange parameters via client.yml or values.yml using OAuthTokenExchangeConfig.
client:
tokenExUri: "https://light-oauth2/oauth2/token"
tokenExClientId: "${portal.client_id}"
tokenExClientSecret: "${portal.client_secret}"
tokenExScope:
- "portal.w"
- "portal.r"
Client Identity Mapping
A critical component is mapping the External Client ID (from Okta) to an Internal Function ID.
Recommended Approach: Database (Shadow Client)
Maintain a “Shadow Client” record in the light-oauth2 database for each external partner.
- Registration: Create a client in
light-oauth2whereclient_idmatches the Okta Client ID. - Mapping: Populate the
custom_claimscolumn for this client with the internal identity:{ "functionId": "acme-integration-user", "internalRole": "partner" } - Execution: When
light-oauth2performs the exchange, it looks up this client record and automatically injects these custom claims into the new token.
Pros:
- Scalable: Manage thousands of partners via Portal UI/Database without restarts.
- Standard: Uses existing
light-oauth2features. - Decoupled: The handler code remains generic.
Alternative Approach: Configuration
Map IDs locally in the handler configuration.
tokenExchangeMapping:
"okta-client-id-1": "internal-id-1"
Pros: Simple for MVP. Cons: Requires restart to add partners; hard to manage at scale.
Security Considerations
- Trust: The internal
light-oauth2must trust the Portal Client to perform exchanges. - Validation: The
TokenExchangeHandlermust validate the external token before attempting exchange to prevent garbage requests from reaching the OAuth server. - Scope: The exchanged token should have scopes limited to what the external partner is allowed to do.