Careeroza
Service-to-Service Authentication
Options
| Approach | How it works | Pros | Cons |
|---|---|---|---|
| OAuth2 Client Credentials | The service logs in to Keycloak with its own client ID/secret and gets a token with its own scopes | Standard, fine-grained scopes, works with Spring Security | Token fetch and caching to manage (Spring does it) |
| mTLS | Both sides present certificates; identity = certificate | Encrypts traffic and authenticates the machine | Certificate issuing and rotation |
| Service mesh (Istio, Linkerd) | Sidecar proxies do mTLS automatically; policies in YAML | No code changes | Extra infrastructure to run and learn |
| Shared API key / static header | A secret string in a header | Very simple | No expiry, hard to rotate; avoid |
Client Credentials with RestClient
order-service
application.yml
spring:
security:
oauth2:
client:
provider:
keycloak:
token-uri: http://keycloak:8180/realms/shopease/protocol/openid-connect/token
registration:
payment-api:
provider: keycloak
client-id: order-service
client-secret: ${ORDER_SERVICE_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: payments:write@Configuration
public class PaymentClientConfig {
// Works outside an HTTP request too (Kafka listeners, schedulers)
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository registrations,
OAuth2AuthorizedClientService clients) {
var manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrations, clients);
manager.setAuthorizedClientProvider(
OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build());
return manager;
}
@Bean
RestClient paymentClient(RestClient.Builder builder, OAuth2AuthorizedClientManager manager) {
var oauth = new OAuth2ClientHttpRequestInterceptor(manager); // Spring Security 6.4+
oauth.setClientRegistrationIdResolver(request -> "payment-api");
return builder.baseUrl("http://payment-service").requestInterceptor(oauth).build();
}
}Example
What happens on the wire (the token is cached until it nearly expires):
POST http://keycloak:8180/realms/shopease/protocol/openid-connect/token
grant_type=client_credentials&scope=payments:write (Basic auth: order-service:****)
POST http://payment-service/api/payments
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzZXJ2aWNlLWFjY291bnQtb3JkZXItc2VydmljZSIs...Example output
HTTP/1.1 200 OK
{"access_token":"eyJhbGciOiJSUzI1NiJ9...","expires_in":300,"token_type":"Bearer","scope":"payments:write"}
HTTP/1.1 201 Created
{"paymentId":"pay_NqX81kLm2","status":"CAPTURED"}mTLS with Spring Boot SSL Bundles
payment-service
Spring Boot 3.1+ SSL bundles configure certificates in one place for the server and for clients:
application.yml
server:
port: 8443
ssl:
bundle: internal
client-auth: need # callers MUST present a valid certificate
spring:
ssl:
bundle:
pem:
internal:
keystore:
certificate: file:/etc/certs/payment-service.crt
private-key: file:/etc/certs/payment-service.key
truststore:
certificate: file:/etc/certs/shopease-internal-ca.crtTip: In Kubernetes, most teams let a service mesh (Istio, Linkerd) handle mTLS and keep OAuth2 tokens for authorization (scopes/roles). Certificates prove which service is calling; tokens say what it may do.