Skip to content

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:

Error: Audience Mismatch
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:

UserService.java
@Service
public 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:

Error Response
403 Forbidden: Insufficient scope or invalid audience

The 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:

Token Exchange Flow
+-------------+ +-------------+ +-------------+ +-------------+
| 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:

pom.xml
<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:

Error: Missing Bean
No qualifying bean of type 'ClientRegistrationRepository' available

Step 2: Configure Resource Server

First, I configured JWT validation for incoming tokens:

application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9001
audiences: user-service

This 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:

application.yml
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:9001

Critical 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:

Error: Unsupported Grant Type
Unsupported grant type: token-exchange

Step 4: Create OAuth2AuthorizedClientManager Bean

The TokenExchangeOAuth2AuthorizedClientProvider needs to be registered:

TokenExchangeConfig.java
@Configuration
public 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:

MessageServiceClient.java
@Service
public 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:

SecurityConfig.java
@Configuration
@EnableWebSecurity
public 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:

  1. 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.

  2. Token Exchange Grant: The urn:ietf:params:oauth:grant-type:token-exchange grant type (RFC 8693) is specifically designed for this use case where one token is exchanged for another.

  3. AuthorizedClientManager: This component manages token acquisition and caching. When a token is requested, it checks if an exchange is needed and performs it automatically.

  4. 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:

pom.xml (WRONG)
<!-- 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:

application.yml (WRONG)
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:

MessageServiceClient.java (OLD WAY)
// RestTemplate is not ideal for OAuth2 token exchange
private final RestTemplate restTemplate;

Use RestClient with OAuth2AuthorizedClientManager instead.

Complete Project Structure

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.yml

Testing the Configuration

I tested this setup with:

test-token-exchange.sh
# 1. Get initial token from auth server
curl -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 token
curl 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 response

Summary

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:

  1. Add both oauth2-resource-server and oauth2-client dependencies
  2. Configure the client registration with urn:ietf:params:oauth:grant-type:token-exchange grant type
  3. Register TokenExchangeOAuth2AuthorizedClientProvider with OAuth2AuthorizedClientManager
  4. Use RestClient with OAuth2AuthorizedClientManager for 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments