OAuth 2.0 Token Exchange in Java: Implementing RFC 8693 for Modern Identity Flows

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:

  1. Subject Token: The original token being exchanged
  2. Actor Token: A token representing the acting party (for delegation)
  3. Requested Token Type: The type of token desired (e.g., JWT, access token)
  4. Resource: The target service or resource
  5. 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      │
│                  │<─────────────────│
│                  │                  │
  1. Delegation: Service acts on behalf of a user
  2. Impersonation: One entity impersonates another
  3. Token Translation: Convert between different token formats
  4. 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

AspectBest Practice
Token ValidationAlways validate signatures, expiration, and audience
DelegationImplement proper authorization checks for delegation
Scope ReductionEnsure exchanged tokens have equal or reduced scope
Audience RestrictionValidate that the client is allowed to request specific audiences
Token Type EnforcementOnly support necessary token types
Rate LimitingImplement rate limiting to prevent abuse
Audit LoggingLog 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:

  1. Complete RFC compliance with proper request/response handling
  2. Flexible token validation supporting multiple token types
  3. Delegation support for acting on behalf of users or services
  4. Policy enforcement for security and business rules
  5. 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/

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper