Home Java Spring WebClient and OAuth2: Client Credentials & Authorization Code Flow
Advanced 5 min · July 14, 2026

Spring WebClient and OAuth2: Client Credentials & Authorization Code Flow

Master OAuth2 with Spring WebClient: implement client credentials and authorization code flows.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 20-25 min read
  • Java 17 or later
  • Spring Boot 3.x
  • Basic knowledge of OAuth2 concepts
  • Familiarity with reactive programming (Mono/Flux)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use WebClient with OAuth2 support via spring-boot-starter-webflux and spring-security-oauth2-client.
  • For client credentials flow, configure a ClientRegistration with authorization-grant-type=client_credentials.
  • For authorization code flow, use the reactive OAuth2AuthorizedClientManager to handle token refresh automatically.
  • In production, always add retry logic for token acquisition failures and monitor token expiry.
  • Never share the same WebClient instance across tenants without proper context propagation.
✦ Definition~90s read
What is Spring WebClient and OAuth2 Support?

Spring WebClient OAuth2 support is a set of reactive integrations in Spring Security that automatically handle OAuth2 token acquisition, refresh, and propagation for client credentials and authorization code flows.

Think of OAuth2 like a hotel key card system.
Plain-English First

Think of OAuth2 like a hotel key card system. The client credentials flow is like a maintenance worker who has a master key (client ID and secret) that works on all doors (APIs) without asking a guest. The authorization code flow is like a guest checking in at the front desk (authorization server), getting a key card (authorization code), then exchanging it for a room key (access token) that only opens their room (specific user data). WebClient is the bellhop who carries your requests and automatically handles the key card exchange for you.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

If you're building Spring Boot applications that talk to external APIs secured with OAuth2, you've probably felt the pain. I've been there — debugging a production outage at 2 AM because the token refresh logic silently failed, and our payment processing pipeline ground to a halt. The worst part? The logs showed nothing because we were using RestTemplate with a custom interceptor that swallowed exceptions.

Spring WebClient, introduced in Spring 5, is the modern answer. It's reactive, non-blocking, and has first-class OAuth2 support. But here's the hard truth: most teams get the integration wrong. They either over-engineer with custom token managers or under-engineer with no retry logic.

In this article, I'll show you how to implement both the client credentials and authorization code flows using Spring Security's OAuth2 client support. I'll share the patterns I've used in production for SaaS billing systems and real-time analytics pipelines. We'll cover configuration, code examples, debugging techniques, and the gotchas that the official docs gloss over.

By the end, you'll know exactly how to build resilient, secure integrations that won't fail at 3 AM on a Saturday.

Setting Up Dependencies and Configuration

Let's start with the foundation. You need two main dependencies: spring-boot-starter-webflux for WebClient and spring-security-oauth2-client for OAuth2 support. I recommend using the latest Spring Boot 3.x (3.2 at the time of writing) because it includes reactive OAuth2 support out of the box.

``xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> </dependency> ``

Now, configure your OAuth2 client properties. For client credentials flow, you'll need something like this in application.yml:

``yaml spring: security: oauth2: client: registration: my-api: client-id: my-client-id client-secret: my-client-secret authorization-grant-type: client_credentials scope: read,write provider: my-api: token-uri: https://auth.example.com/oauth/token ``

For authorization code flow, it's similar but you need the authorization-uri and redirect-uri:

``yaml spring: security: oauth2: client: registration: my-user-api: client-id: my-user-client-id client-secret: my-user-client-secret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: openid,profile provider: my-user-api: authorization-uri: https://auth.example.com/oauth/authorize token-uri: https://auth.example.com/oauth/token user-info-uri: https://auth.example.com/userinfo user-name-attribute: sub ``

One thing the docs don't tell you: never hardcode client secrets in your config files. Use environment variables or a vault. I've seen secrets leaked to GitHub more times than I care to count.

application.ymlJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
  security:
    oauth2:
      client:
        registration:
          my-api:
            client-id: ${CLIENT_ID}
            client-secret: ${CLIENT_SECRET}
            authorization-grant-type: client_credentials
            scope: read,write
        provider:
          my-api:
            token-uri: https://auth.example.com/oauth/token
⚠ Never Hardcode Secrets
📊 Production Insight
Spring Boot 3.2 introduced auto-configuration for reactive OAuth2 client. If you're on an older version, you may need to manually define beans. Always check the compatibility matrix.
🎯 Key Takeaway
Use spring-security-oauth2-client with spring-boot-starter-webflux for reactive OAuth2 support. Configure client registration properties in application.yml, but keep secrets out of version control.

Creating WebClient with OAuth2 Support

Now let's create a WebClient that automatically handles token acquisition and refresh. The key is to use ServerOAuth2AuthorizedClientExchangeFilterFunction — this is the reactive equivalent of the servlet-based OAuth2AuthorizedClientExchangeFilterFunction.

```java @Configuration public class WebClientConfig {

@Bean public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oauth2Filter.setDefaultClientRegistrationId("my-api"); return WebClient.builder() .filter(oauth2Filter) .build(); }

@Bean public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( ClientRegistrationRepository clientRegistrationRepository, ReactiveOAuth2AuthorizedClientService authorizedClientService) { ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .refreshToken() .build(); AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientService); manager.setAuthorizedClientProvider(authorizedClientProvider); return manager; } } ```

Notice I included .refreshToken() in the provider builder. This is critical — without it, token refresh won't work. The official docs show this, but many developers miss it.

```java @Service public class PaymentService { private final WebClient webClient;

public PaymentService(WebClient webClient) { this.webClient = webClient; }

public Mono<Payment> processPayment(PaymentRequest request) { return webClient.post() .uri("https://api.paymentgateway.com/v1/charges") .bodyValue(request) .retrieve() .bodyToMono(Payment.class); } } ```

What about the authorization code flow? You need to propagate the authenticated user's context. Use @RegisteredOAuth2AuthorizedClient annotation in a controller:

```java @RestController public class UserController { private final WebClient webClient;

public UserController(WebClient webClient) { this.webClient = webClient; }

@GetMapping("/user/profile") public Mono<UserProfile> getProfile( @RegisteredOAuth2AuthorizedClient("my-user-api") OAuth2AuthorizedClient authorizedClient) { return webClient.get() .uri("https://api.example.com/user/profile") .attributes(oauth2AuthorizedClient(authorizedClient)) .retrieve() .bodyToMono(UserProfile.class); } } ```

But wait — in a reactive service layer, you won't have access to the authorized client directly. Instead, inject the WebClient and let the filter handle it. The filter uses the current authentication context from Spring Security's ReactiveSecurityContextHolder. This works seamlessly if you're using Spring Security's OAuth2 login.

WebClientConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2Filter.setDefaultClientRegistrationId("my-api");
        return WebClient.builder()
                .filter(oauth2Filter)
                .build();
    }

    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            ReactiveOAuth2AuthorizedClientService authorizedClientService) {
        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .clientCredentials()
                        .refreshToken()
                        .build();
        AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager manager =
                new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientService);
        manager.setAuthorizedClientProvider(authorizedClientProvider);
        return manager;
    }
}
💡Always Include Refresh Token Provider
📊 Production Insight
If you're using multiple OAuth2 client registrations (e.g., one for internal APIs, one for external), create separate WebClient beans, each with its own default registration ID. Mixing them leads to token leaks.
🎯 Key Takeaway
Use ServerOAuth2AuthorizedClientExchangeFilterFunction for reactive WebClient. Include refreshToken provider. For authorization code flow, use @RegisteredOAuth2AuthorizedClient in controllers or rely on security context.

Handling Token Refresh and Retry Logic

Token refresh is where most production issues occur. The default behavior is to try refreshing once and fail if it doesn't work. In a real system, you need retries with exponential backoff.

```java @Bean public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oauth2Filter.setDefaultClientRegistrationId("my-api");

return WebClient.builder() .filter(oauth2Filter) .filter(RetryFilter.create()) // custom retry filter .build(); } ```

```java public class RetryFilter implements ExchangeFilterFunction { private static final int MAX_RETRIES = 3; private static final Duration INITIAL_BACKOFF = Duration.ofMillis(500);

public static ExchangeFilterFunction create() { return (request, next) -> next.exchange(request) .flatMap(clientResponse -> { if (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().equals(HttpStatus.UNAUTHORIZED)) { return Mono.error(new RetryableException(clientResponse.statusCode())); } return Mono.just(clientResponse); }) .retryWhen(Retry.backoff(MAX_RETRIES, INITIAL_BACKOFF) .filter(throwable -> throwable instanceof RetryableException) .onRetryExhaustedThrow((spec, signal) -> signal.failure())); } } ```

But be careful: retrying on 401 after token refresh may cause infinite loops if the token endpoint itself is down. I recommend using a circuit breaker pattern with resilience4j. Here's a production pattern I use:

```java @Bean public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager, RetryRegistry retryRegistry) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oauth2Filter.setDefaultClientRegistrationId("my-api");

Retry retry = retryRegistry.retry("oauth2-api");

return WebClient.builder() .filter(oauth2Filter) .filter((request, next) -> Retry.decoratePublisher(retry, () -> next.exchange(request)) ) .build(); } ```

This way, if the token server is down, you don't keep hammering it. The circuit breaker opens after a threshold of failures.

RetryFilter.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RetryFilter implements ExchangeFilterFunction {
    private static final int MAX_RETRIES = 3;
    private static final Duration INITIAL_BACKOFF = Duration.ofMillis(500);

    public static ExchangeFilterFunction create() {
        return (request, next) -> 
            next.exchange(request)
                .flatMap(clientResponse -> {
                    if (clientResponse.statusCode().is5xxServerError() ||
                        clientResponse.statusCode().equals(HttpStatus.UNAUTHORIZED)) {
                        return Mono.error(new RetryableException(clientResponse.statusCode()));
                    }
                    return Mono.just(clientResponse);
                })
                .retryWhen(Retry.backoff(MAX_RETRIES, INITIAL_BACKOFF)
                        .filter(throwable -> throwable instanceof RetryableException)
                        .onRetryExhaustedThrow((spec, signal) -> signal.failure()));
    }
}
⚠ Avoid Infinite Retry Loops
📊 Production Insight
I once saw a system where the retry logic was retrying the entire request including the token refresh. The token server was rate-limiting them, causing a cascade of 429s. Always separate token acquisition retries from API call retries.
🎯 Key Takeaway
Add retry with exponential backoff for token acquisition failures. Use circuit breakers to prevent cascading failures when the token server is down.

What the Official Docs Won't Tell You

  1. Reactive vs. Servlet stack: If you're using Spring MVC (servlet stack), you cannot use ServerOAuth2AuthorizedClientExchangeFilterFunction. You must use the servlet-based OAuth2AuthorizedClientExchangeFilterFunction. Mixing them will cause ClassCastException at runtime. I've seen teams waste days on this.
  2. Token caching: The default InMemoryReactiveOAuth2AuthorizedClientService stores tokens in memory. If your application restarts, all tokens are lost. For production, consider using a distributed cache like Redis, especially for authorization code flows where user tokens are long-lived.
  3. Token refresh race conditions: When multiple requests arrive simultaneously with an expired token, they all trigger a refresh. This can overwhelm your token server. The fix is to use a synchronized token refresh — but in reactive programming, that's tricky. Spring Security's reactive client manager handles this internally by using a lock per registration ID, but only if you use the default implementation.
  4. Scope handling: Some providers require specific scopes for client credentials. If you omit scopes in your config, the token request may fail. Always include the scopes your API needs.
  5. Custom token endpoint parameters: Some OAuth2 providers require additional parameters (e.g., audience, resource). The standard client registration properties don't support this. You'll need to implement a custom ReactiveOAuth2AccessTokenResponseClient. Here's a snippet:

```java public class CustomTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> { private final WebClient webClient = WebClient.create();

@Override public Mono<OAuth2AccessTokenResponse> getTokenResponse( OAuth2ClientCredentialsGrantRequest request) { ClientRegistration registration = request.getClientRegistration(); return webClient.post() .uri(registration.getProviderDetails().getTokenUri()) .body(BodyInserters.fromFormData("grant_type", "client_credentials") .with("client_id", registration.getClientId()) .with("client_secret", registration.getClientSecret()) .with("audience", "https://api.example.com")) .retrieve() .bodyToMono(OAuth2AccessTokenResponse.class); } } ```

Then register it in your configuration.

CustomTokenResponseClient.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CustomTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
    private final WebClient webClient = WebClient.create();

    @Override
    public Mono<OAuth2AccessTokenResponse> getTokenResponse(
            OAuth2ClientCredentialsGrantRequest request) {
        ClientRegistration registration = request.getClientRegistration();
        return webClient.post()
                .uri(registration.getProviderDetails().getTokenUri())
                .body(BodyInserters.fromFormData("grant_type", "client_credentials")
                        .with("client_id", registration.getClientId())
                        .with("client_secret", registration.getClientSecret())
                        .with("audience", "https://api.example.com"))
                .retrieve()
                .bodyToMono(OAuth2AccessTokenResponse.class);
    }
}
🔥Custom Token Endpoint Parameters
📊 Production Insight
The race condition on token refresh nearly brought down a fintech startup I consulted for. They had 100+ concurrent requests hitting an API with an expired token. The token server got 100 simultaneous refresh requests, rate-limited them, and all requests failed. The fix was to use a distributed lock for token refresh.
🎯 Key Takeaway
Watch out for reactive vs. servlet stack mismatches, token cache persistence, race conditions, scope requirements, and custom token endpoint parameters.

Testing OAuth2 with WebClient

Testing OAuth2 flows can be painful because you don't want to hit real authorization servers in unit tests. Use WireMock to mock the token endpoint and the downstream API.

``xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> <scope>test</scope> </dependency> ``

```java @SpringBootTest @AutoConfigureWebTestClient @AutoConfigureMockMvc public class PaymentServiceTest {

@Autowired private WebTestClient webTestClient;

@Test public void testProcessPayment() { // Mock token endpoint stubFor(post(urlEqualTo("/oauth/token")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody("{\"access_token\":\"test-token\",\"token_type\":\"Bearer\",\"expires_in\":3600}")));

// Mock downstream API stubFor(post(urlEqualTo("/v1/charges")) .withHeader("Authorization", equalTo("Bearer test-token")) .willReturn(aResponse() .withStatus(200) .withBody("{\"id\":\"ch_123\",\"status\":\"succeeded\"}")));

webTestClient.post() .uri("/api/payments") .bodyValue(new PaymentRequest(100, "usd")) .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$.status").isEqualTo("succeeded"); } } ```

For authorization code flow, you need to simulate a logged-in user. Use @WithMockUser with OAuth2 attributes:

``java @Test @WithMockUser(authorities = "SCOPE_openid") public void testGetProfile() { // Mock token endpoint with refresh token // ... } ``

But the real trick is testing token refresh scenarios. I recommend creating a test that: 1. First call succeeds with a short-lived token. 2. Second call triggers a refresh (mock token endpoint to return a new token). 3. Third call uses the new token.

This ensures your retry and refresh logic works end-to-end.

PaymentServiceTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@SpringBootTest
@AutoConfigureWebTestClient
public class PaymentServiceTest {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    public void testProcessPayment() {
        // Mock token endpoint
        stubFor(post(urlEqualTo("/oauth/token"))
                .willReturn(aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withBody("{\"access_token\":\"test-token\",\"token_type\":\"Bearer\",\"expires_in\":3600}")));

        // Mock downstream API
        stubFor(post(urlEqualTo("/v1/charges"))
                .withHeader("Authorization", equalTo("Bearer test-token"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withBody("{\"id\":\"ch_123\",\"status\":\"succeeded\"}")));

        webTestClient.post()
                .uri("/api/payments")
                .bodyValue(new PaymentRequest(100, "usd"))
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$.status").isEqualTo("succeeded");
    }
}
💡Test Token Refresh Scenarios
📊 Production Insight
I always include a test that simulates the token server being down. This ensures our circuit breaker kicks in and we don't hang the application.
🎯 Key Takeaway
Use WireMock to mock token and API endpoints. Test both happy path and token refresh scenarios. Use @WithMockUser for authorization code flow tests.

Production Best Practices and Monitoring

You've got WebClient working with OAuth2. Now let's make it production-ready.

1. Monitor token expiry and refresh latency. Add metrics for: - Token acquisition time (histogram) - Token refresh count (counter) - Token expiry time (gauge) - Number of 401 responses before successful refresh (counter)

``java @Bean public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager, MeterRegistry meterRegistry) { // ... return WebClient.builder() .filter(oauth2Filter) .filter(MetricsFilter.create(meterRegistry)) .build(); } ``

2. Use a dedicated connection pool. WebClient uses a connection pool from Reactor Netty. Configure it properly:

``java @Bean public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { ConnectionProvider provider = ConnectionProvider.builder("oauth2-pool") .maxConnections(50) .pendingAcquireTimeout(Duration.ofSeconds(30)) .maxIdleTime(Duration.ofSeconds(20)) .build(); return WebClient.builder() .clientConnector(new ReactorClientHttpConnector( HttpClient.create(provider))) .filter(oauth2Filter) .build(); } ``

3. Logging and debugging. Enable DEBUG logging for OAuth2 classes:

``yaml logging: level: org.springframework.security.oauth2: DEBUG org.springframework.web.reactive.function.client: DEBUG ``

But be careful in production — these logs can be verbose. Use a conditional logger or sample rates.

4. Graceful degradation. If token acquisition fails, don't let your entire service go down. Return fallback responses or cached data. Use Hystrix or resilience4j for bulkheading.

5. Secret rotation. If your client secret changes, you need a way to update it without restarting the application. Use Spring Cloud Config with a refresh scope or a secrets manager that supports dynamic secrets.

application.ymlJAVA
1
2
3
4
logging:
  level:
    org.springframework.security.oauth2: DEBUG
    org.springframework.web.reactive.function.client: DEBUG
🔥Monitor Token Metrics
📊 Production Insight
I once had a client where the token server was in a different region, causing 2-second latency per token acquisition. We added a cache with a short TTL to reduce the number of token requests.
🎯 Key Takeaway
Monitor token metrics, configure connection pools, enable OAuth2 debug logging carefully, implement graceful degradation, and handle secret rotation.
● Production incidentPOST-MORTEMseverity: high

The Silent Token Refresh Failure in Our Payment Gateway

Symptom
Every 30 minutes, payment processing would hang for exactly 15 minutes. Users saw 'Payment Pending' status, and our monitoring showed no errors — just a slowdown in API response times.
Assumption
The developer assumed the OAuth2 token refresh was working fine because the initial authentication succeeded. They thought the issue was network latency.
Root cause
The WebClient was configured with an ExchangeFilterFunction that called a blocking OAuth2AuthorizedClientManager.getAuthorizedClient() on a reactive thread. When the access token expired, the blocking call caused the scheduler to hang, leading to thread starvation and cascading timeouts.
Fix
Switched to reactive OAuth2AuthorizedClientManager (ServerOAuth2AuthorizedClientExchangeFilterFunction) and added a retry mechanism with exponential backoff for token acquisition.
Key lesson
  • Never block in a reactive pipeline — use the reactive OAuth2 support classes.
  • Always add retry logic for token acquisition; token servers can fail.
  • Monitor token expiry and refresh latency as separate metrics.
  • Test token refresh under load — not just happy path.
  • Use separate WebClient instances for different OAuth2 client registrations to avoid context leaks.
Production debug guideSymptom to Action4 entries
Symptom · 01
401 Unauthorized after token refresh
Fix
Check if the token endpoint returns a new refresh token. Some providers rotate refresh tokens; your configuration must handle it.
Symptom · 02
WebClient hangs or times out
Fix
Look for blocking calls in reactive chain. Check thread dumps for 'blocking' threads. Ensure you use ServerOAuth2AuthorizedClientExchangeFilterFunction.
Symptom · 03
Token refresh not happening
Fix
Verify that the OAuth2AuthorizedClientManager is configured with a ReactiveOAuth2AuthorizedClientProvider that includes refresh_token grant type.
Symptom · 04
Memory leak in token cache
Fix
Check if you're storing OAuth2AuthorizedClient in a non-expiring cache. Use the default InMemoryOAuth2AuthorizedClientService or implement a bounded cache.
★ Quick Debug Cheat SheetImmediate actions for common OAuth2 WebClient issues
401 Unauthorized
Immediate action
Check if token is expired and refresh logic is triggered.
Commands
curl -v -X POST <token-endpoint> -d 'grant_type=client_credentials&client_id=...&client_secret=...'
Check WebClient filter: ensure ServerOAuth2AuthorizedClientExchangeFilterFunction is applied.
Fix now
Add a retry filter with fallback to re-authenticate.
WebClient hangs+
Immediate action
Look for blocking calls in reactive pipeline.
Commands
jstack <pid> | grep -A 20 'blocking'
Enable Reactor debugging: -Dreactor.debug=true
Fix now
Replace blocking OAuth2AuthorizedClientManager with reactive ServerOAuth2AuthorizedClientExchangeFilterFunction.
FeatureClient CredentialsAuthorization Code
Use caseMachine-to-machineUser-facing apps
Token refreshNot typical (short-lived tokens)Yes, via refresh tokens
User contextNoneRequires user authentication
Spring Security supportReactiveOAuth2AuthorizedClientProviderBuilder.clientCredentials()ReactiveOAuth2AuthorizedClientProviderBuilder.authorizationCode()
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
application.ymlspring:Setting Up Dependencies and Configuration
WebClientConfig.java@ConfigurationCreating WebClient with OAuth2 Support
RetryFilter.javapublic class RetryFilter implements ExchangeFilterFunction {Handling Token Refresh and Retry Logic
CustomTokenResponseClient.javapublic class CustomTokenResponseClient implements ReactiveOAuth2AccessTokenRespo...What the Official Docs Won't Tell You
PaymentServiceTest.java@SpringBootTestTesting OAuth2 with WebClient
application.ymllogging:Production Best Practices and Monitoring

Key takeaways

1
Use reactive OAuth2 support with ServerOAuth2AuthorizedClientExchangeFilterFunction for non-blocking WebClient.
2
Always include refresh token provider and retry logic to handle token expiry gracefully.
3
Monitor token metrics and use circuit breakers to prevent cascading failures.
4
Test token refresh scenarios with WireMock to ensure your logic works under load.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how to configure WebClient for client credentials OAuth2 flow in...
Q02SENIOR
How do you handle token refresh race conditions in a reactive WebClient?
Q03SENIOR
What metrics would you monitor for OAuth2 WebClient in production?
Q01 of 03SENIOR

Explain how to configure WebClient for client credentials OAuth2 flow in a Spring Boot reactive application.

ANSWER
Add dependencies spring-boot-starter-webflux and spring-security-oauth2-client. Configure client registration properties in application.yml with authorization-grant-type=client_credentials. Create a WebClient bean with ServerOAuth2AuthorizedClientExchangeFilterFunction that uses a ReactiveOAuth2AuthorizedClientManager configured with clientCredentials() and refreshToken() providers.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between client credentials and authorization code flow?
02
How do I handle token refresh in WebClient?
03
Can I use WebClient with OAuth2 in a non-reactive Spring MVC application?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

5 min read · try the examples if you haven't

Previous
Spring WebClient Filters: Logging, Retry, Authentication, and Custom Filters
118 / 121 · Spring Boot
Next
Making and Combining Multiple Simultaneous WebClient Calls in Spring