Client Credentials Flow for Service-to-Service

Client Credentials Flow for Service-to-Service

The client credentials flow enables secure service-to-service communication without user involvement. This flow is ideal for backend services, scheduled jobs, and microservice architectures where services need to authenticate with each other.

// Java Spring Boot implementation of Client Credentials flow
import org.springframework.security.oauth2.client.*;
import org.springframework.security.oauth2.client.registration.*;
import org.springframework.security.oauth2.core.*;
import org.springframework.web.reactive.function.client.*;

@Configuration
@EnableWebSecurity
public class OAuth2ClientConfig {
    
    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ReactiveOAuth2AuthorizedClientService authorizedClientService) {
        
        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
            ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials()
                .refreshToken()
                .build();
        
        DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultReactiveOAuth2AuthorizedClientManager(
                clientRegistrationRepository,
                authorizedClientService
            );
        
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        
        return authorizedClientManager;
    }
    
    @Bean
    public WebClient webClient(ReactiveOAuth2AuthorizedClientManager clientManager) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
            new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientManager);
        
        oauth.setDefaultClientRegistrationId("api-client");
        
        return WebClient.builder()
            .filter(oauth)
            .build();
    }
}

@Service
public class SecureAPIClient {
    
    private final WebClient webClient;
    private final OAuth2AuthorizedClientService clientService;
    
    public SecureAPIClient(WebClient webClient, 
                          OAuth2AuthorizedClientService clientService) {
        this.webClient = webClient;
        this.clientService = clientService;
    }
    
    public Mono<ApiResponse> callProtectedAPI(String endpoint) {
        return webClient
            .get()
            .uri(endpoint)
            .retrieve()
            .bodyToMono(ApiResponse.class)
            .doOnError(error -> {
                if (error instanceof WebClientResponseException.Unauthorized) {
                    // Handle token expiration
                    clearCachedTokens();
                }
            })
            .retry(1); // Retry once if unauthorized
    }
    
    @Scheduled(fixedDelay = 3600000) // Every hour
    public void refreshTokenProactively() {
        // Proactively refresh tokens before expiration
        OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(
            "api-client",
            "service-account"
        );
        
        if (client != null && isTokenExpiringSoon(client)) {
            // Force token refresh
            clientService.removeAuthorizedClient(
                "api-client",
                "service-account"
            );
        }
    }
    
    private boolean isTokenExpiringSoon(OAuth2AuthorizedClient client) {
        Instant expiresAt = client.getAccessToken().getExpiresAt();
        return expiresAt != null && 
               Instant.now().plus(Duration.ofMinutes(5)).isAfter(expiresAt);
    }
}

// Custom token storage for distributed systems
@Component
public class RedisOAuth2AuthorizedClientService 
        implements OAuth2AuthorizedClientService {
    
    private final RedisTemplate<String, OAuth2AuthorizedClient> redisTemplate;
    
    @Override
    public void saveAuthorizedClient(
            OAuth2AuthorizedClient authorizedClient,
            Authentication principal) {
        
        String key = generateKey(
            authorizedClient.getClientRegistration().getRegistrationId(),
            principal.getName()
        );
        
        // Store with TTL based on token expiration
        Duration ttl = Duration.between(
            Instant.now(),
            authorizedClient.getAccessToken().getExpiresAt()
        );
        
        redisTemplate.opsForValue().set(key, authorizedClient, ttl);
    }
    
    @Override
    public OAuth2AuthorizedClient loadAuthorizedClient(
            String clientRegistrationId,
            String principalName) {
        
        String key = generateKey(clientRegistrationId, principalName);
        return redisTemplate.opsForValue().get(key);
    }
    
    private String generateKey(String registrationId, String principalName) {
        return String.format("oauth2:client:%s:%s", registrationId, principalName);
    }
}