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);
}
}