Skip to content

How to Use OAuth2AuthorizedClientManager for Programmatic Token Exchange in Spring

Purpose

This post demonstrates how to programmatically perform OAuth 2.0 Token Exchange using OAuth2AuthorizedClientManager in Spring Security.

Environment

  • Spring Boot 3.3.x
  • Spring Security 6.3.x
  • Java 21
  • OAuth 2.0 Authorization Server

The Problem

When I built a microservices architecture with OAuth 2.0 authentication, I got this problem:

The Problem Statement
How do I call a downstream service from my controller using a token exchanged from the original JWT?

My frontend sends a JWT to Service A. Service A needs to call Service B, but Service B requires a different scope. I needed to exchange the incoming token for a new one with the right permissions.

At first, I tried to manually extract the token and call the authorization server:

WrongApproach.java
@RestController
public class UserController {
@GetMapping("/user/message")
public String message(@RequestHeader("Authorization") String authHeader) {
// Extract token manually
String token = authHeader.replace("Bearer ", "");
// Call authorization server to exchange token - THIS IS WRONG
String newToken = callAuthServerForExchange(token);
// Call downstream service
return callDownstreamService(newToken);
}
private String callAuthServerForExchange(String token) {
// This is complex and error-prone!
// Need to handle token caching, refresh, expiration...
throw new UnsupportedOperationException("Too complex!");
}
}

This approach has several problems:

  • Manual HTTP calls to the authorization server
  • No token caching
  • No automatic refresh when tokens expire
  • Lots of boilerplate code
  • Error-prone token handling

What I tried first

I searched for Spring Security’s built-in support and found OAuth2AuthorizedClientManager. But when I tried to use it, I got confused about how to set it up:

FirstAttempt.java
@RestController
public class UserController {
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
@GetMapping("/user/message")
public String message(JwtAuthenticationToken jwtAuthentication) {
// How do I use this manager?
// What parameters do I need?
OAuth2AuthorizedClient client = authorizedClientManager.authorize(???);
// ...
}
}

The documentation showed examples, but I couldn’t figure out how to wire everything together for programmatic token exchange.

The Solution: OAuth2AuthorizeRequest

After reading the Spring Security documentation and source code, I found that OAuth2AuthorizeRequest is the key class for building token exchange requests.

Here’s the complete solution:

Step 1: Configure the OAuth2 Client

src/main/resources/application.yml
spring:
security:
oauth2:
client:
registration:
my-message-service:
client-id: ${CLIENT_ID}
client-secret: ${CLIENT_SECRET}
authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange
scope: message:read
provider:
my-message-service:
token-uri: https://auth.example.com/oauth2/token

Step 2: Configure OAuth2AuthorizedClientManager Bean

src/main/java/com/example/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}

Step 3: Use the Manager in Controller

src/main/java/com/example/controller/UserController.java
@RestController
public class UserController {
private final OAuth2AuthorizedClientManager authorizedClientManager;
private final RestClient restClient;
public UserController(OAuth2AuthorizedClientManager authorizedClientManager,
RestClient.Builder restClientBuilder) {
this.authorizedClientManager = authorizedClientManager;
this.restClient = restClientBuilder.build();
}
@GetMapping("/user/message")
public String message(JwtAuthenticationToken jwtAuthentication) {
// Build authorize request
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("my-message-service")
.principal(jwtAuthentication)
.build();
// Perform token exchange
OAuth2AuthorizedClient authorizedClient =
this.authorizedClientManager.authorize(authorizeRequest);
// Get the new access token
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
// Call downstream service with exchanged token
RestClient.ResponseSpec responseSpec = restClient
.get()
.uri("http://localhost:8082/message")
.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue()))
.retrieve();
ResponseEntity<String> responseEntity = responseSpec.toEntity(String.class);
return responseEntity.getBody();
}
}

Now test it:

curl command to test the endpoint
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..." http://localhost:8080/user/message

Returns the response from the downstream service:

Response from downstream service
{"message": "Hello from downstream service!"}

You can see that I succeeded in exchanging tokens programmatically and calling the downstream service.

Why This Works

I think the key reasons this approach works are:

  1. Automatic Token Detection: The OAuth2AuthorizedClientManager automatically detects the incoming JWT from the security context. When you pass JwtAuthenticationToken as the principal, the manager knows to use it for token exchange.

  2. Token Caching: The manager caches tokens using OAuth2AuthorizedClientRepository, so repeated calls don’t require new token exchanges until the token expires.

  3. Automatic Refresh: If the exchanged token expires, the manager automatically refreshes it using the refresh token flow.

  4. Clean Separation: The controller only needs to build a request and call authorize(). All the complexity of HTTP calls, token parsing, and caching is handled internally.

Here’s the flow diagram:

Token Exchange Flow
+----------------+ +------------------+ +--------------------+
| Frontend | | Service A | | Authorization |
| (Browser) | | (Controller) | | Server |
+-------+--------+ +--------+---------+ +----------+---------+
| | |
| 1. JWT with scope A | |
|---------------------->| |
| | |
| | 2. OAuth2AuthorizeRequest|
| | with principal |
| |------------------------->|
| | |
| | 3. Token Exchange |
| | (scope B) |
| |<-------------------------|
| | |
| | 4. Call Service B |
| | with new token |
| |--------+ |
| | | |
| | 5. Response |
|<------------------------------| |
| | |

Common Mistakes I Made

Mistake 1: Not Injecting OAuth2AuthorizedClientManager

I initially tried to create the manager inside the controller:

Mistake1.java
@RestController
public class UserController {
@GetMapping("/user/message")
public String message(JwtAuthenticationToken jwtAuthentication) {
// WRONG: Creating manager manually
OAuth2AuthorizedClientManager manager = new DefaultOAuth2AuthorizedClientManager(...);
// This won't work - missing repositories and configuration
}
}

This fails because the manager needs ClientRegistrationRepository and OAuth2AuthorizedClientRepository beans that are auto-configured by Spring Security.

Solution: Inject the manager as a dependency through constructor injection.

Mistake 2: Using Wrong Client Registration ID

I used a client registration ID that didn’t exist:

Mistake2.java
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("non-existent-client") // WRONG!
.principal(jwtAuthentication)
.build();

This throws:

Error message for invalid client registration
IllegalArgumentException: Could not find ClientRegistration with registration id 'non-existent-client'

Solution: The client registration ID must match exactly what’s defined in application.yml.

Mistake 3: Forgetting Bearer Auth Header

I tried to call the downstream service without setting the Bearer auth header:

Mistake3.java
// WRONG: No auth header
String response = restClient
.get()
.uri("http://localhost:8082/message")
.retrieve()
.body(String.class);

The downstream service returns 401 Unauthorized.

Solution: Always set the Bearer auth header with the token value:

CorrectApproach.java
.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue()))

Mistake 4: Using Wrong Principal Type

I tried using Principal interface instead of JwtAuthenticationToken:

Mistake4.java
@GetMapping("/user/message")
public String message(Principal principal) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("my-message-service")
.principal(principal) // This might work, but loses JWT-specific info
.build();
}

While this compiles, it may not work correctly for token exchange because the manager needs access to the JWT claims.

Solution: Use JwtAuthenticationToken as the parameter type for better type safety and access to JWT-specific information.

Additional Configuration

For the token exchange grant type, you need to configure the authorization server to support it. Here’s a minimal Keycloak configuration:

Keycloak Realm Configuration (CLI)
# Create client with token exchange enabled
$ kcadm.sh create clients -r myrealm -s clientId=my-message-service \
-s secret=my-secret \
-s enabled=true \
-s protocol=openid-connect \
-s 'attributes."oauth2.grant.type"=["urn:ietf:params:oauth:grant-type:token-exchange"]'

For Spring Authorization Server:

src/main/java/com/example/config/AuthorizationServerConfig.java
@Configuration
public class AuthorizationServerConfig {
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId("my-message-service")
.clientId("my-message-service")
.clientSecret("{noop}my-secret")
.authorizationGrantType(new AuthorizationGrantType("urn:ietf:params:oauth:grant-type:token-exchange"))
.scope("message:read")
.build();
return new InMemoryRegisteredClientRepository(client);
}
}

Testing the Implementation

Here’s a test to verify the token exchange works:

src/test/java/com/example/controller/UserControllerTest.java
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockJwtAuthenticationToken
void testMessageEndpoint() throws Exception {
mockMvc.perform(get("/user/message"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").exists());
}
}

Summary

In this post, I showed how to programmatically perform OAuth 2.0 Token Exchange using OAuth2AuthorizedClientManager in Spring Security. The key points are:

  1. Inject OAuth2AuthorizedClientManager as a bean
  2. Build OAuth2AuthorizeRequest with client registration ID and principal
  3. Call authorize() to get OAuth2AuthorizedClient
  4. Extract access token and use with RestClient

This approach handles token caching, refresh, and expiration automatically, keeping your code clean and maintainable.

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