Article
In distributed systems and microservices architectures, tokens often need to be transformed, exchanged, or delegated. A service might need to act on behalf of a user when calling another service, or a client might need to exchange a token from one identity provider for a token from another. OAuth 2.0 Token Exchange (RFC 8693) provides a standardized mechanism for these scenarios, enabling secure token transformation and delegation. For Java developers, implementing token exchange is essential for building sophisticated identity and authorization flows.
What is Token Exchange (RFC 8693)?
Token Exchange is an OAuth 2.0 extension that allows a client to request a new security token based on an existing token. The specification defines:
- Subject Token: The original token being exchanged
- Actor Token: A token representing the acting party (for delegation)
- Requested Token Type: The type of token desired (e.g., JWT, access token)
- Resource: The target service or resource
- Scope: The requested scope for the new token
Key Use Cases
┌──────────┐ ┌──────────┐ ┌──────────┐ │ User │ │ Service A│ │ Service B│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ 1. User Token │ │ ├─────────────────>│ │ │ │ 2. Token Exchange│ │ │─────────────────>│ │ │ │ │ │ 3. Service Token │ │ │<─────────────────│ │ │ │ │ 4. Act on behalf │ │ │ ├─────────────────>│ │ │ │ │ │ 5. Response │ │ │<─────────────────│ │ │ │
- Delegation: Service acts on behalf of a user
- Impersonation: One entity impersonates another
- Token Translation: Convert between different token formats
- Downstream Authentication: Propagate identity across service boundaries
Complete Token Exchange Implementation in Java
1. Dependencies and Setup
<dependencies> <!-- Spring Security OAuth2 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> <version>6.1.5</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> <version>6.1.5</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-resource-server</artifactId> <version>6.1.5</version> </dependency> <!-- Nimbus JOSE + JWT --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.37.3</version> </dependency> <!-- Web and REST --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- For JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- For testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2. Token Exchange Request/Response Models
package com.example.tokenexchange.model; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Map; import java.util.Set; /** * Token Exchange Request (RFC 8693) */ public class TokenExchangeRequest { @JsonProperty("grant_type") private String grantType = "urn:ietf:params:oauth:grant-type:token-exchange"; @JsonProperty("subject_token") private String subjectToken; @JsonProperty("subject_token_type") private String subjectTokenType; @JsonProperty("actor_token") private String actorToken; @JsonProperty("actor_token_type") private String actorTokenType; @JsonProperty("resource") private String resource; @JsonProperty("audience") private String audience; @JsonProperty("scope") private String scope; @JsonProperty("requested_token_type") private String requestedTokenType = "urn:ietf:params:oauth:token-type:access_token"; // Additional custom parameters private Map<String, Object> additionalParameters; // Constructors public TokenExchangeRequest() {} public TokenExchangeRequest(String subjectToken, String subjectTokenType) { this.subjectToken = subjectToken; this.subjectTokenType = subjectTokenType; } // Builder pattern public static class Builder { private TokenExchangeRequest request = new TokenExchangeRequest(); public Builder subjectToken(String subjectToken) { request.subjectToken = subjectToken; return this; } public Builder subjectTokenType(String subjectTokenType) { request.subjectTokenType = subjectTokenType; return this; } public Builder actorToken(String actorToken) { request.actorToken = actorToken; return this; } public Builder actorTokenType(String actorTokenType) { request.actorTokenType = actorTokenType; return this; } public Builder resource(String resource) { request.resource = resource; return this; } public Builder audience(String audience) { request.audience = audience; return this; } public Builder scope(String scope) { request.scope = scope; return this; } public Builder requestedTokenType(String requestedTokenType) { request.requestedTokenType = requestedTokenType; return this; } public Builder additionalParameters(Map<String, Object> params) { request.additionalParameters = params; return this; } public TokenExchangeRequest build() { return request; } } // Getters and setters public String getGrantType() { return grantType; } public void setGrantType(String grantType) { this.grantType = grantType; } public String getSubjectToken() { return subjectToken; } public void setSubjectToken(String subjectToken) { this.subjectToken = subjectToken; } public String getSubjectTokenType() { return subjectTokenType; } public void setSubjectTokenType(String subjectTokenType) { this.subjectTokenType = subjectTokenType; } public String getActorToken() { return actorToken; } public void setActorToken(String actorToken) { this.actorToken = actorToken; } public String getActorTokenType() { return actorTokenType; } public void setActorTokenType(String actorTokenType) { this.actorTokenType = actorTokenType; } public String getResource() { return resource; } public void setResource(String resource) { this.resource = resource; } public String getAudience() { return audience; } public void setAudience(String audience) { this.audience = audience; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public String getRequestedTokenType() { return requestedTokenType; } public void setRequestedTokenType(String requestedTokenType) { this.requestedTokenType = requestedTokenType; } public Map<String, Object> getAdditionalParameters() { return additionalParameters; } public void setAdditionalParameters(Map<String, Object> additionalParameters) { this.additionalParameters = additionalParameters; } } /** * Token Exchange Response */ public class TokenExchangeResponse { @JsonProperty("access_token") private String accessToken; @JsonProperty("issued_token_type") private String issuedTokenType; @JsonProperty("token_type") private String tokenType = "Bearer"; @JsonProperty("expires_in") private Long expiresIn; @JsonProperty("scope") private String scope; @JsonProperty("refresh_token") private String refreshToken; private Map<String, Object> additionalParameters; // Constructors public TokenExchangeResponse() {} public TokenExchangeResponse(String accessToken, String issuedTokenType, Long expiresIn) { this.accessToken = accessToken; this.issuedTokenType = issuedTokenType; this.expiresIn = expiresIn; } // Builder pattern public static class Builder { private TokenExchangeResponse response = new TokenExchangeResponse(); public Builder accessToken(String accessToken) { response.accessToken = accessToken; return this; } public Builder issuedTokenType(String issuedTokenType) { response.issuedTokenType = issuedTokenType; return this; } public Builder tokenType(String tokenType) { response.tokenType = tokenType; return this; } public Builder expiresIn(Long expiresIn) { response.expiresIn = expiresIn; return this; } public Builder scope(String scope) { response.scope = scope; return this; } public Builder refreshToken(String refreshToken) { response.refreshToken = refreshToken; return this; } public Builder additionalParameters(Map<String, Object> params) { response.additionalParameters = params; return this; } public TokenExchangeResponse build() { return response; } } // Getters and setters public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getIssuedTokenType() { return issuedTokenType; } public void setIssuedTokenType(String issuedTokenType) { this.issuedTokenType = issuedTokenType; } public String getTokenType() { return tokenType; } public void setTokenType(String tokenType) { this.tokenType = tokenType; } public Long getExpiresIn() { return expiresIn; } public void setExpiresIn(Long expiresIn) { this.expiresIn = expiresIn; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public String getRefreshToken() { return refreshToken; } public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } public Map<String, Object> getAdditionalParameters() { return additionalParameters; } public void setAdditionalParameters(Map<String, Object> additionalParameters) { this.additionalParameters = additionalParameters; } } 3. Token Exchange Service
package com.example.tokenexchange.service; import com.example.tokenexchange.model.TokenExchangeRequest; import com.example.tokenexchange.model.TokenExchangeResponse; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jwt.SignedJWT; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.security.interfaces.RSAPublicKey; import java.time.Instant; import java.util.*; /** * Token Exchange Service implementing RFC 8693 */ @Service public class TokenExchangeService { private static final Logger logger = LoggerFactory.getLogger(TokenExchangeService.class); // Token type URNs public static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token"; public static final String TOKEN_TYPE_REFRESH_TOKEN = "urn:ietf:params:oauth:token-type:refresh_token"; public static final String TOKEN_TYPE_ID_TOKEN = "urn:ietf:params:oauth:token-type:id_token"; public static final String TOKEN_TYPE_SAML1 = "urn:ietf:params:oauth:token-type:saml1"; public static final String TOKEN_TYPE_SAML2 = "urn:ietf:params:oauth:token-type:saml2"; public static final String TOKEN_TYPE_JWT = "urn:ietf:params:oauth:token-type:jwt"; @Value("${token-exchange.default-ttl:3600}") private long defaultTtl; @Value("${token-exchange.issuer:https://token-exchange.example.com}") private String issuer; private final TokenValidator tokenValidator; private final TokenGenerator tokenGenerator; private final TokenPolicyEnforcer policyEnforcer; public TokenExchangeService(TokenValidator tokenValidator, TokenGenerator tokenGenerator, TokenPolicyEnforcer policyEnforcer) { this.tokenValidator = tokenValidator; this.tokenGenerator = tokenGenerator; this.policyEnforcer = policyEnforcer; } /** * Exchange a token according to RFC 8693 */ public TokenExchangeResponse exchangeToken(TokenExchangeRequest request) { logger.info("Processing token exchange request"); // 1. Validate grant type validateGrantType(request); // 2. Validate subject token TokenValidationResult subjectValidation = validateSubjectToken(request); // 3. Validate actor token if present (delegation) TokenValidationResult actorValidation = validateActorToken(request); // 4. Apply policy enforcement TokenExchangeContext context = new TokenExchangeContext( request, subjectValidation, actorValidation); policyEnforcer.enforcePolicies(context); // 5. Generate new token String newToken = generateExchangedToken(context); // 6. Build response return buildResponse(newToken, request); } private void validateGrantType(TokenExchangeRequest request) { String expectedGrantType = "urn:ietf:params:oauth:grant-type:token-exchange"; if (!expectedGrantType.equals(request.getGrantType())) { throw new TokenExchangeException( "Invalid grant_type. Expected: " + expectedGrantType); } } private TokenValidationResult validateSubjectToken(TokenExchangeRequest request) { if (request.getSubjectToken() == null || request.getSubjectToken().isEmpty()) { throw new TokenExchangeException("subject_token is required"); } if (request.getSubjectTokenType() == null) { throw new TokenExchangeException("subject_token_type is required"); } return tokenValidator.validateToken( request.getSubjectToken(), request.getSubjectTokenType() ); } private TokenValidationResult validateActorToken(TokenExchangeRequest request) { if (request.getActorToken() != null) { if (request.getActorTokenType() == null) { throw new TokenExchangeException( "actor_token_type is required when actor_token is present"); } return tokenValidator.validateToken( request.getActorToken(), request.getActorTokenType() ); } return null; } private String generateExchangedToken(TokenExchangeContext context) { // Determine token type based on requested_token_type or default String requestedType = context.getRequest().getRequestedTokenType(); if (requestedType == null) { requestedType = TOKEN_TYPE_ACCESS_TOKEN; } // Build claims for new token Map<String, Object> claims = new HashMap<>(); // Subject from original token claims.put("sub", context.getSubjectValidation().getSubject()); // Actor if present (delegation) if (context.getActorValidation() != null) { claims.put("act", Map.of( "sub", context.getActorValidation().getSubject(), "token_type", context.getRequest().getActorTokenType() )); } // Issuer claims.put("iss", issuer); // Issued at claims.put("iat", Instant.now().getEpochSecond()); // Expiration long expiry = Instant.now().getEpochSecond() + defaultTtl; claims.put("exp", expiry); // Audience if (context.getRequest().getAudience() != null) { claims.put("aud", context.getRequest().getAudience()); } // Scope if (context.getRequest().getScope() != null) { claims.put("scope", context.getRequest().getScope()); } // Resource if (context.getRequest().getResource() != null) { claims.put("resource", context.getRequest().getResource()); } // Add original token context claims.put("original_token_type", context.getRequest().getSubjectTokenType()); claims.put("exchange_time", Instant.now().toString()); // Generate token based on requested type return tokenGenerator.generateToken(claims, requestedType); } private TokenExchangeResponse buildResponse(String newToken, TokenExchangeRequest request) { TokenExchangeResponse response = new TokenExchangeResponse(); response.setAccessToken(newToken); response.setIssuedTokenType( request.getRequestedTokenType() != null ? request.getRequestedTokenType() : TOKEN_TYPE_ACCESS_TOKEN ); response.setTokenType("Bearer"); response.setExpiresIn(defaultTtl); if (request.getScope() != null) { response.setScope(request.getScope()); } return response; } // Exception class public static class TokenExchangeException extends RuntimeException { public TokenExchangeException(String message) { super(message); } public TokenExchangeException(String message, Throwable cause) { super(message, cause); } } // Context class for exchange processing public static class TokenExchangeContext { private final TokenExchangeRequest request; private final TokenValidationResult subjectValidation; private final TokenValidationResult actorValidation; public TokenExchangeContext(TokenExchangeRequest request, TokenValidationResult subjectValidation, TokenValidationResult actorValidation) { this.request = request; this.subjectValidation = subjectValidation; this.actorValidation = actorValidation; } public TokenExchangeRequest getRequest() { return request; } public TokenValidationResult getSubjectValidation() { return subjectValidation; } public TokenValidationResult getActorValidation() { return actorValidation; } } } 4. Token Validator Implementation
package com.example.tokenexchange.service; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jwt.SignedJWT; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.KeyFactory; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; import java.time.Instant; import java.util.Base64; import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Token validation service */ @Component public class TokenValidator { @Value("${token-exchange.jwks-url:https://auth.example.com/.well-known/jwks.json}") private String jwksUrl; private final Map<String, RSAPublicKey> publicKeyCache = new ConcurrentHashMap<>(); /** * Validate a token and return validation result */ public TokenValidationResult validateToken(String token, String tokenType) { try { // Parse token based on type if (tokenType.startsWith("urn:ietf:params:oauth:token-type:jwt") || tokenType.equals(TokenExchangeService.TOKEN_TYPE_ACCESS_TOKEN)) { return validateJWT(token); } else { throw new TokenExchangeService.TokenExchangeException( "Unsupported token type: " + tokenType); } } catch (Exception e) { throw new TokenExchangeService.TokenExchangeException( "Token validation failed: " + e.getMessage(), e); } } private TokenValidationResult validateJWT(String jwtString) throws Exception { SignedJWT signedJWT = SignedJWT.parse(jwtString); // Get key ID from header String keyId = signedJWT.getHeader().getKeyID(); if (keyId == null) { throw new TokenExchangeService.TokenExchangeException("No key ID in token header"); } // Get public key RSAPublicKey publicKey = getPublicKey(keyId); // Verify signature JWSVerifier verifier = new RSASSAVerifier(publicKey); if (!signedJWT.verify(verifier)) { throw new TokenExchangeService.TokenExchangeException("Invalid signature"); } // Get claims var claims = signedJWT.getJWTClaimsSet(); // Check expiration Date expiration = claims.getExpirationTime(); if (expiration != null && expiration.before(Date.from(Instant.now()))) { throw new TokenExchangeService.TokenExchangeException("Token expired"); } // Check not before Date notBefore = claims.getNotBeforeTime(); if (notBefore != null && notBefore.after(Date.from(Instant.now()))) { throw new TokenExchangeService.TokenExchangeException("Token not yet valid"); } // Build validation result TokenValidationResult result = new TokenValidationResult(); result.setSubject(claims.getSubject()); result.setIssuer(claims.getIssuer()); result.setAudience(claims.getAudience() != null ? String.join(",", claims.getAudience()) : null); result.setExpirationTime(expiration); result.setNotBeforeTime(notBefore); result.setIssueTime(claims.getIssueTime()); result.setClaims(claims.getClaims()); result.setValid(true); return result; } private RSAPublicKey getPublicKey(String keyId) { // In production, fetch from JWKS endpoint // This is a simplified example return publicKeyCache.computeIfAbsent(keyId, k -> { try { // In real implementation, fetch from JWKS // For demo, return a placeholder return loadPublicKeyFromString(keyId); } catch (Exception e) { throw new TokenExchangeService.TokenExchangeException( "Failed to load public key: " + keyId, e); } }); } private RSAPublicKey loadPublicKeyFromString(String keyId) throws Exception { // This would load from keystore or JWKS // Simplified for example byte[] keyBytes = Base64.getDecoder().decode( "MIIBCgKCAQEA..." // Your public key here ); X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance("RSA"); return (RSAPublicKey) kf.generatePublic(spec); } } /** * Token validation result */ class TokenValidationResult { private String subject; private String issuer; private String audience; private Date expirationTime; private Date notBeforeTime; private Date issueTime; private Map<String, Object> claims; private boolean valid; // Getters and setters public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getIssuer() { return issuer; } public void setIssuer(String issuer) { this.issuer = issuer; } public String getAudience() { return audience; } public void setAudience(String audience) { this.audience = audience; } public Date getExpirationTime() { return expirationTime; } public void setExpirationTime(Date expirationTime) { this.expirationTime = expirationTime; } public Date getNotBeforeTime() { return notBeforeTime; } public void setNotBeforeTime(Date notBeforeTime) { this.notBeforeTime = notBeforeTime; } public Date getIssueTime() { return issueTime; } public void setIssueTime(Date issueTime) { this.issueTime = issueTime; } public Map<String, Object> getClaims() { return claims; } public void setClaims(Map<String, Object> claims) { this.claims = claims; } public boolean isValid() { return valid; } public void setValid(boolean valid) { this.valid = valid; } } 5. Token Generator Implementation
package com.example.tokenexchange.service; import com.nimbusds.jose.*; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.time.Instant; import java.util.Date; import java.util.Map; import java.util.UUID; /** * Token generator for exchange responses */ @Component public class TokenGenerator { private final RSAKey signingKey; private final JWSSigner signer; @Value("${token-exchange.issuer}") private String issuer; public TokenGenerator() throws JOSEException { // Generate signing key (in production, load from keystore) this.signingKey = new RSAKeyGenerator(2048) .keyID(UUID.randomUUID().toString()) .generate(); this.signer = new RSASSASigner(signingKey); } /** * Generate a token based on requested type */ public String generateToken(Map<String, Object> claims, String tokenType) { if (tokenType.equals(TokenExchangeService.TOKEN_TYPE_ACCESS_TOKEN) || tokenType.equals(TokenExchangeService.TOKEN_TYPE_JWT)) { return generateJWT(claims); } else { throw new TokenExchangeService.TokenExchangeException( "Cannot generate token type: " + tokenType); } } private String generateJWT(Map<String, Object> claims) { try { // Build JWT claims set JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); claims.forEach((key, value) -> { if (value instanceof String) { claimsBuilder.claim(key, (String) value); } else if (value instanceof Long) { claimsBuilder.claim(key, (Long) value); } else if (value instanceof Date) { claimsBuilder.claim(key, (Date) value); } else { claimsBuilder.claim(key, value); } }); // Add standard claims if missing if (!claims.containsKey("jti")) { claimsBuilder.jwtID(UUID.randomUUID().toString()); } if (!claims.containsKey("iat")) { claimsBuilder.issueTime(Date.from(Instant.now())); } if (!claims.containsKey("exp")) { claimsBuilder.expirationTime( Date.from(Instant.now().plusSeconds(3600))); } if (!claims.containsKey("iss") && issuer != null) { claimsBuilder.issuer(issuer); } JWTClaimsSet claimsSet = claimsBuilder.build(); // Create JWS header JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256) .keyID(signingKey.getKeyID()) .type(JOSEObjectType.JWT) .build(); // Sign JWT SignedJWT signedJWT = new SignedJWT(header, claimsSet); signedJWT.sign(signer); return signedJWT.serialize(); } catch (JOSEException e) { throw new TokenExchangeService.TokenExchangeException( "Failed to generate token", e); } } /** * Get the public JWK for token verification */ public RSAKey getPublicKey() { return signingKey.toPublicJWK(); } } 6. Policy Enforcer Implementation
package com.example.tokenexchange.service; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; /** * Policy enforcement for token exchange */ @Component public class TokenPolicyEnforcer { private final List<TokenExchangePolicy> policies = new ArrayList<>(); public TokenPolicyEnforcer() { // Register default policies registerDefaultPolicies(); } private void registerDefaultPolicies() { // Policy 1: Subject token must be valid addPolicy(context -> { if (!context.getSubjectValidation().isValid()) { throw new TokenExchangeService.TokenExchangeException( "Subject token is invalid"); } }); // Policy 2: Check expiration addPolicy(context -> { var exp = context.getSubjectValidation().getExpirationTime(); if (exp != null && exp.before(new java.util.Date())) { throw new TokenExchangeService.TokenExchangeException( "Subject token has expired"); } }); // Policy 3: Delegation constraints addPolicy(context -> { if (context.getActorValidation() != null) { // Check if delegation is allowed var claims = context.getSubjectValidation().getClaims(); if (claims.containsKey("delegation") && !Boolean.TRUE.equals(claims.get("delegation"))) { throw new TokenExchangeService.TokenExchangeException( "Delegation not allowed for this token"); } } }); // Policy 4: Audience constraints addPolicy(context -> { String requestedAudience = context.getRequest().getAudience(); if (requestedAudience != null) { var claims = context.getSubjectValidation().getClaims(); if (claims.containsKey("aud")) { // Check if requested audience is allowed // This is simplified - actual implementation would check // against allowed audiences for the token } } }); // Policy 5: Scope constraints addPolicy(context -> { String requestedScope = context.getRequest().getScope(); if (requestedScope != null) { var claims = context.getSubjectValidation().getClaims(); if (claims.containsKey("scope")) { String tokenScope = (String) claims.get("scope"); // Check if requested scope is subset of token scope // This is simplified - actual implementation would do proper scope validation } } }); } /** * Add a custom policy */ public void addPolicy(TokenExchangePolicy policy) { policies.add(policy); } /** * Enforce all policies for a token exchange context */ public void enforcePolicies(TokenExchangeService.TokenExchangeContext context) { policies.forEach(policy -> policy.enforce(context)); } /** * Policy interface */ @FunctionalInterface public interface TokenExchangePolicy { void enforce(TokenExchangeService.TokenExchangeContext context); } } 7. REST Controller for Token Exchange
package com.example.tokenexchange.controller; import com.example.tokenexchange.model.TokenExchangeRequest; import com.example.tokenexchange.model.TokenExchangeResponse; import com.example.tokenexchange.service.TokenExchangeService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.Map; /** * REST controller for token exchange endpoint */ @RestController @RequestMapping("/oauth2") public class TokenExchangeController { private static final Logger logger = LoggerFactory.getLogger(TokenExchangeController.class); private final TokenExchangeService tokenExchangeService; public TokenExchangeController(TokenExchangeService tokenExchangeService) { this.tokenExchangeService = tokenExchangeService; } /** * Token Exchange endpoint (RFC 8693) */ @PostMapping("/token") public ResponseEntity<?> exchangeToken(@RequestBody TokenExchangeRequest request) { logger.info("Received token exchange request"); try { TokenExchangeResponse response = tokenExchangeService.exchangeToken(request); return ResponseEntity.ok(response); } catch (TokenExchangeService.TokenExchangeException e) { logger.error("Token exchange failed: {}", e.getMessage()); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(Map.of( "error", "invalid_request", "error_description", e.getMessage() )); } catch (Exception e) { logger.error("Unexpected error during token exchange", e); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of( "error", "server_error", "error_description", "An unexpected error occurred" )); } } /** * Health check endpoint */ @GetMapping("/token/health") public ResponseEntity<Map<String, String>> health() { return ResponseEntity.ok(Map.of( "status", "up", "service", "token-exchange", "rfc", "8693" )); } /** * Error handler for unsupported grant types */ @ExceptionHandler(UnsupportedOperationException.class) public ResponseEntity<Map<String, String>> handleUnsupportedGrantType( UnsupportedOperationException e) { return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(Map.of( "error", "unsupported_grant_type", "error_description", e.getMessage() )); } } 8. Client Implementation for Token Exchange
package com.example.tokenexchange.client; import com.example.tokenexchange.model.TokenExchangeRequest; import com.example.tokenexchange.model.TokenExchangeResponse; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; /** * Client for performing token exchange */ @Component public class TokenExchangeClient { private final RestTemplate restTemplate; private final String tokenExchangeUrl; public TokenExchangeClient( RestTemplate restTemplate, @Value("${token-exchange.url:http://localhost:8080/oauth2/token}") String tokenExchangeUrl) { this.restTemplate = restTemplate; this.tokenExchangeUrl = tokenExchangeUrl; } /** * Exchange a token for a new token */ public TokenExchangeResponse exchangeToken( String subjectToken, String subjectTokenType, String requestedTokenType, String audience, String scope) { TokenExchangeRequest request = new TokenExchangeRequest.Builder() .subjectToken(subjectToken) .subjectTokenType(subjectTokenType) .requestedTokenType(requestedTokenType) .audience(audience) .scope(scope) .build(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<TokenExchangeRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<TokenExchangeResponse> response = restTemplate.exchange( tokenExchangeUrl, HttpMethod.POST, entity, TokenExchangeResponse.class ); if (!response.getStatusCode().is2xxSuccessful()) { throw new RuntimeException("Token exchange failed: " + response.getStatusCode()); } return response.getBody(); } /** * Exchange a JWT for a new access token with specific audience */ public String exchangeForAudience(String jwt, String audience) { TokenExchangeResponse response = exchangeToken( jwt, TokenExchangeService.TOKEN_TYPE_JWT, TokenExchangeService.TOKEN_TYPE_ACCESS_TOKEN, audience, null ); return response.getAccessToken(); } /** * Delegate token (act on behalf) */ public String delegateToken(String userToken, String actorToken) { TokenExchangeRequest request = new TokenExchangeRequest.Builder() .subjectToken(userToken) .subjectTokenType(TokenExchangeService.TOKEN_TYPE_JWT) .actorToken(actorToken) .actorTokenType(TokenExchangeService.TOKEN_TYPE_JWT) .build(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<TokenExchangeRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<TokenExchangeResponse> response = restTemplate.exchange( tokenExchangeUrl, HttpMethod.POST, entity, TokenExchangeResponse.class ); return response.getBody().getAccessToken(); } } 9. Security Configuration
package com.example.tokenexchange.config; import com.example.tokenexchange.service.TokenGenerator; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; /** * Security configuration for token exchange service */ @Configuration @EnableWebSecurity public class SecurityConfig { private final TokenGenerator tokenGenerator; public SecurityConfig(TokenGenerator tokenGenerator) { this.tokenGenerator = tokenGenerator; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/oauth2/token").permitAll() .requestMatchers("/oauth2/token/health").permitAll() .requestMatchers("/.well-known/jwks.json").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(Customizer.withDefaults()) ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .csrf(csrf -> csrf.disable()); return http.build(); } @Bean public JwtDecoder jwtDecoder() { RSAKey publicKey = tokenGenerator.getPublicKey(); return NimbusJwtDecoder.withPublicKey(publicKey.toRSAPublicKey()).build(); } @Bean public JWKSet jwkSet() { return new JWKSet(tokenGenerator.getPublicKey()); } } 10. Application Configuration
# application.yml server: port: 8080 spring: application: name: token-exchange-service token-exchange: issuer: https://token-exchange.example.com default-ttl: 3600 jwks-url: https://auth.example.com/.well-known/jwks.json allowed-token-types: - urn:ietf:params:oauth:token-type:access_token - urn:ietf:params:oauth:token-type:jwt - urn:ietf:params:oauth:token-type:id_token logging: level: com.example.tokenexchange: DEBUG org.springframework.security: INFO
11. Testing the Token Exchange
package com.example.tokenexchange; import com.example.tokenexchange.model.TokenExchangeRequest; import com.example.tokenexchange.model.TokenExchangeResponse; import com.example.tokenexchange.service.TokenExchangeService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.*; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class TokenExchangeIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test public void testTokenExchange() { // Create token exchange request TokenExchangeRequest request = new TokenExchangeRequest.Builder() .subjectToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...") // Valid JWT .subjectTokenType(TokenExchangeService.TOKEN_TYPE_JWT) .requestedTokenType(TokenExchangeService.TOKEN_TYPE_ACCESS_TOKEN) .audience("https://api.example.com") .scope("read write") .build(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<TokenExchangeRequest> entity = new HttpEntity<>(request, headers); // Perform exchange ResponseEntity<TokenExchangeResponse> response = restTemplate.postForEntity( "/oauth2/token", entity, TokenExchangeResponse.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().getAccessToken()).isNotBlank(); assertThat(response.getBody().getExpiresIn()).isGreaterThan(0); assertThat(response.getBody().getIssuedTokenType()) .isEqualTo(TokenExchangeService.TOKEN_TYPE_ACCESS_TOKEN); } @Test public void testInvalidTokenShouldFail() { TokenExchangeRequest request = new TokenExchangeRequest.Builder() .subjectToken("invalid.token.format") .subjectTokenType(TokenExchangeService.TOKEN_TYPE_JWT) .build(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<TokenExchangeRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<Object> response = restTemplate.postForEntity( "/oauth2/token", entity, Object.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } } Token Exchange Flow Examples
1. Basic Token Exchange
POST /oauth2/token HTTP/1.1 Host: token-exchange.example.com Content-Type: application/json { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "subject_token_type": "urn:ietf:params:oauth:token-type:access_token", "audience": "https://api.example.com", "scope": "read write" } 2. Delegation (Act on Behalf)
POST /oauth2/token HTTP/1.1 Host: token-exchange.example.com Content-Type: application/json { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "subject_token_type": "urn:ietf:params:oauth:token-type:access_token", "actor_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "actor_token_type": "urn:ietf:params:oauth:token-type:access_token", "audience": "https://downstream.example.com" } 3. Successful Response
HTTP/1.1 200 OK Content-Type: application/json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 3600, "scope": "read write" } Security Considerations
| Aspect | Best Practice |
|---|---|
| Token Validation | Always validate signatures, expiration, and audience |
| Delegation | Implement proper authorization checks for delegation |
| Scope Reduction | Ensure exchanged tokens have equal or reduced scope |
| Audience Restriction | Validate that the client is allowed to request specific audiences |
| Token Type Enforcement | Only support necessary token types |
| Rate Limiting | Implement rate limiting to prevent abuse |
| Audit Logging | Log all token exchanges for security monitoring |
Conclusion
Token Exchange (RFC 8693) provides a powerful, standardized mechanism for token transformation and delegation in OAuth 2.0 ecosystems. The Java implementation presented here demonstrates:
- Complete RFC compliance with proper request/response handling
- Flexible token validation supporting multiple token types
- Delegation support for acting on behalf of users or services
- Policy enforcement for security and business rules
- Spring integration for production-ready services
For Java developers building microservices, API gateways, or identity platforms, token exchange is an essential capability. It enables sophisticated identity flows while maintaining security and standards compliance. The implementation provided serves as a solid foundation that can be extended with additional features like:
- Integration with external identity providers
- Caching of exchanged tokens
- Advanced policy engines
- Monitoring and metrics
- Support for additional token formats (SAML, etc.)
As distributed systems become more complex, the ability to securely transform and delegate identity becomes increasingly critical. Token exchange provides the standardized foundation for these patterns, and Java's rich ecosystem makes implementation straightforward and robust.
Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)
Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/
Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/
Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/
Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/
Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/
Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/
Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/
Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/
Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/