How to Configure Spring Security 6.3 for OAuth 2.0 Token Exchange on Resource Server
The Problem
When I built a microservice architecture where one service needed to call another, I got this problem:
The incoming JWT token has audience claim [user-service], but the downstream message-service requires audience [message-service].My user-service receives a JWT token meant for itself, but when it tries to call the message-service downstream, the token gets rejected because the audience doesn’t match.
Environment
- Spring Boot 3.2+
- Spring Security 6.3+
- Java 17+
- OAuth2 Authorization Server
What I Tried First
I thought I could just pass the original token to the downstream service:
@Servicepublic class UserService {
@Autowired private RestTemplate restTemplate;
public List<Message> getMessages(String token) { HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(token); // Pass original token HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Message[]> response = restTemplate.exchange( "http://message-service/api/messages", HttpMethod.GET, entity, Message[].class ); return Arrays.asList(response.getBody()); }}But when I tested this, I got a 403 Forbidden:
403 Forbidden: Insufficient scope or invalid audienceThe problem is that the original token was issued for user-service audience, but message-service expects its own audience claim.
Understanding Token Exchange
I discovered that OAuth 2.0 Token Exchange (RFC 8693) solves this exact problem. The resource server can exchange the incoming token for a new token with the correct audience.
Here’s how the flow works:
+-------------+ +-------------+ +-------------+ +-------------+| Client |---->| User-Service|---->| Auth-Server |---->| Message-Svc |+-------------+ +-------------+ +-------------+ +-------------+ | | | | | 1. Request with | | | | user token | | | |------------------>| | | | | 2. Exchange token | | | for message-service audience | | |------------------>| | | | | 3. New token | | | |<------------------| | | 4. Call with new token | | |-------------------------------------->| | | | | | | 5. Response | | |<------------------|<--------------------------------------|The key insight: the resource server plays two roles - it validates incoming tokens as a resource server, AND it acts as an OAuth2 client to exchange tokens.
The Solution: Spring Security 6.3 Configuration
Spring Security 6.3 introduced TokenExchangeOAuth2AuthorizedClientProvider specifically for this use case.
Step 1: Add Dependencies
I needed both resource-server and client dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency>I initially forgot the oauth2-client dependency and got this error:
No qualifying bean of type 'ClientRegistrationRepository' availableStep 2: Configure Resource Server
First, I configured JWT validation for incoming tokens:
spring: security: oauth2: resourceserver: jwt: issuer-uri: http://localhost:9001 audiences: user-serviceThis tells Spring Security to validate incoming JWTs against my authorization server.
Step 3: Configure OAuth2 Client for Token Exchange
This is where it gets tricky. I needed to register a client with a special grant type:
spring: security: oauth2: client: registration: my-message-service: provider: my-auth-server client-id: "message-service" client-secret: "token" authorization-grant-type: "urn:ietf:params:oauth:grant-type:token-exchange" scope: - message.read provider: my-auth-server: issuer-uri: http://localhost:9001Critical detail: The authorization-grant-type MUST be exactly urn:ietf:params:oauth:grant-type:token-exchange. I made the mistake of using token-exchange without the URN prefix and got:
Unsupported grant type: token-exchangeStep 4: Create OAuth2AuthorizedClientManager Bean
The TokenExchangeOAuth2AuthorizedClientProvider needs to be registered:
@Configurationpublic class TokenExchangeConfig {
@Bean public OAuth2AuthorizedClientManager authorizedClientManager( ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .provider(tokenExchangeProvider) .build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientRepository); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager; }
@Bean public OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient() { return new RestClientTokenExchangeTokenResponseClient(); }}Step 5: Use RestClient with Token Exchange
Spring Security 6.3 recommends using RestClient (not the old RestTemplate) with ServletOAuth2AuthorizedClientManager:
@Servicepublic class MessageServiceClient {
private final RestClient restClient; private final OAuth2AuthorizedClientManager authorizedClientManager;
public MessageServiceClient( RestClient.Builder restClientBuilder, OAuth2AuthorizedClientManager authorizedClientManager) { this.authorizedClientManager = authorizedClientManager; this.restClient = restClientBuilder .baseUrl("http://message-service") .build(); }
public List<Message> getMessages(Authentication principal) { // Get or exchange token OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize( new OAuth2AuthorizeRequest() .authorizedClient(null) // Will trigger token exchange .clientRegistrationId("my-message-service") .principal(principal) .build() );
String token = authorizedClient.getAccessToken().getTokenValue();
return restClient.get() .uri("/api/messages") .headers(headers -> headers.setBearerAuth(token)) .retrieve() .body(new ParameterizedTypeReference<List<Message>>() {}); }}Step 6: Security Filter Chain Configuration
Make sure the security filter chain allows both resource server and client authentication:
@Configuration@EnableWebSecuritypublic class SecurityConfig {
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/**").authenticated() .anyRequest().permitAll() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(Customizer.withDefaults()) ) .oauth2Client(Customizer.withDefaults());
return http.build(); }}Why This Works
I think the key reasons this architecture works are:
-
Dual Role Support: Spring Security allows a resource server to also act as an OAuth2 client. The same application validates incoming tokens AND exchanges them for new ones.
-
Token Exchange Grant: The
urn:ietf:params:oauth:grant-type:token-exchangegrant type (RFC 8693) is specifically designed for this use case where one token is exchanged for another. -
AuthorizedClientManager: This component manages token acquisition and caching. When a token is requested, it checks if an exchange is needed and performs it automatically.
-
RestClient Integration: The modern RestClient with OAuth2 support makes it easy to inject tokens into downstream calls.
Common Mistakes I Made
Mistake 1: Missing oauth2-client dependency
I only added the resource-server dependency initially:
<!-- Missing this! --><!-- <dependency> --><!-- <groupId>org.springframework.boot</groupId> --><!-- <artifactId>spring-boot-starter-oauth2-client</artifactId> --><!-- </dependency> -->This caused ClientRegistrationRepository bean not found error.
Mistake 2: Wrong grant type format
I used token-exchange instead of the full URN:
authorization-grant-type: "token-exchange" # Missing URN prefix!Always use the full URN: urn:ietf:params:oauth:grant-type:token-exchange
Mistake 3: Not configuring RestClient properly
I tried to use the old RestTemplate but RestClient is the recommended approach in Spring Security 6.3:
// RestTemplate is not ideal for OAuth2 token exchangeprivate final RestTemplate restTemplate;Use RestClient with OAuth2AuthorizedClientManager instead.
Complete Project Structure
src/main/java/com/example/userservice/├── config/│ ├── SecurityConfig.java│ └── TokenExchangeConfig.java├── client/│ └── MessageServiceClient.java├── controller/│ └── UserController.java└── service/ └── UserService.java
src/main/resources/└── application.ymlTesting the Configuration
I tested this setup with:
# 1. Get initial token from auth servercurl -X POST http://localhost:9001/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials&client_id=user-service&client_secret=secret"
# 2. Call user-service with the tokencurl http://localhost:8080/api/users/messages \ -H "Authorization: Bearer <token_from_step_1>"
# The user-service should:# 1. Validate the incoming token# 2. Exchange it for a new token with message-service audience# 3. Call message-service with the new token# 4. Return the responseSummary
In this post, I showed how to configure Spring Security 6.3 for OAuth 2.0 Token Exchange when a resource server needs to call downstream services. The key points are:
- Add both
oauth2-resource-serverandoauth2-clientdependencies - Configure the client registration with
urn:ietf:params:oauth:grant-type:token-exchangegrant type - Register
TokenExchangeOAuth2AuthorizedClientProviderwithOAuth2AuthorizedClientManager - Use
RestClientwithOAuth2AuthorizedClientManagerfor downstream calls
This pattern enables secure microservice-to-microservice communication while maintaining user identity across service calls.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 RFC 8693 - OAuth 2.0 Token Exchange
- 👨💻 Spring Security OAuth2 Client Documentation
- 👨💻 Spring Security 6.3 Release Notes
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments