Careeroza
Service-to-Service with Client Credentials
Configuration
When order-service calls payment-service, it uses its own identity. Spring Security fetches, caches and renews the client-credentials token automatically:
order-service application.yml
spring:
security:
oauth2:
client:
registration:
payment-service:
provider: keycloak
client-id: order-service
client-secret: ${ORDER_SERVICE_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: payments.write
provider:
keycloak:
issuer-uri: http://keycloak:8080/realms/shopeaseRestClient with an OAuth2 Interceptor
@Configuration
public class PaymentClientConfig {
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository registrations,
OAuth2AuthorizedClientService clientService) {
OAuth2AuthorizedClientProvider provider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
// works outside an HTTP request too (schedulers, Kafka listeners)
var manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrations, clientService);
manager.setAuthorizedClientProvider(provider);
return manager;
}
@Bean
RestClient paymentRestClient(RestClient.Builder builder, OAuth2AuthorizedClientManager manager) {
OAuth2ClientHttpRequestInterceptor oauth2 = new OAuth2ClientHttpRequestInterceptor(manager);
oauth2.setClientRegistrationIdResolver(request -> "payment-service");
return builder
.baseUrl("http://payment-service")
.requestInterceptor(oauth2) // adds Authorization: Bearer <service token>
.build();
}
}
@Service
public class PaymentGateway {
private final RestClient paymentRestClient;
public PaymentGateway(RestClient paymentRestClient) { this.paymentRestClient = paymentRestClient; }
public PaymentResult charge(Long orderId, BigDecimal amount) {
return paymentRestClient.post().uri("/api/payments")
.body(new ChargeRequest(orderId, amount, "INR"))
.retrieve()
.body(PaymentResult.class);
}
}Note: OAuth2ClientHttpRequestInterceptor is available from Spring Security 6.4. On WebClient use ServletOAuth2AuthorizedClientExchangeFilterFunction; with OpenFeign, add a RequestInterceptor that calls manager.authorize(...).
payment-service Protects Itself
http.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/api/payments").hasAuthority("SCOPE_payments.write")
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));Wrong token type
A user token (no payments.write) is rejected even if it reaches payment-service:
curl -i -X POST http://payment-service/api/payments -H "Authorization: Bearer $ASHA_TOKEN" -d "{}"Example output
HTTP/1.1 403 WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"