Service-to-Service Authentication
AdvancedMutual TLS or short-lived JWT access tokens (client credentials flow) authenticate services with each other; avoid embedding long-lived secrets in code.
Overview
Service-to-service authentication solves the "how does service A prove its identity to service B?" problem. Two dominant approaches: mTLS (mutual TLS) where each service presents a client certificate, and the OAuth2 Client Credentials Flow where a service exchanges its client_id + client_secret for a short-lived JWT access token from an identity provider (Keycloak, Okta, AWS Cognito). mTLS is powerful but operationally complex (certificate rotation, PKI management); a service mesh (Istio) automates it transparently via SPIFFE/SPIRE identity. Client Credentials is simpler — the receiving service just validates the JWT signature with a public JWKS endpoint — but requires all services to have access to an auth server. In both cases, never embed long-lived shared secrets (API keys) in code or environment variables.
OAuth2 Client Credentials with Spring Security
The calling service obtains a token from an identity provider using its client ID and secret. Spring Security's OAuth2 client support automates token acquisition and refresh.
# application.properties — order-service calling payment-service
spring.security.oauth2.client.registration.payment-service.client-id=order-service
spring.security.oauth2.client.registration.payment-service.client-secret=${CLIENT_SECRET}
spring.security.oauth2.client.registration.payment-service.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.payment-service.scope=payments:create
spring.security.oauth2.client.provider.keycloak.token-uri= https://keycloak.internal/realms/platform/protocol/openid-connect/token
# Configure WebClient with OAuth2 token auto-injection
@Configuration
public class WebClientConfig {
@Bean
public WebClient paymentWebClient(
OAuth2AuthorizedClientManager clientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientManager);
oauth2.setDefaultClientRegistrationId("payment-service");
return WebClient.builder()
.baseUrl("https://payment-service.internal")
.filter(oauth2) // automatically attaches Bearer token
.build();
}
}Validating incoming service tokens (resource server)
The receiving service (payment-service) configures Spring Security as a resource server. It validates the JWT signature using the JWKS URI from the identity provider — no shared secret needed.
# payment-service application.properties
spring.security.oauth2.resourceserver.jwt.jwk-set-uri= https://keycloak.internal/realms/platform/protocol/openid-connect/certs
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/payments/**").hasAuthority("SCOPE_payments:create")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(
jwtAuthenticationConverter()))
);
return http.build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter conv = new JwtGrantedAuthoritiesConverter();
conv.setAuthorityPrefix("SCOPE_");
conv.setAuthoritiesClaimName("scope");
JwtAuthenticationConverter jwtConv = new JwtAuthenticationConverter();
jwtConv.setJwtGrantedAuthoritiesConverter(conv);
return jwtConv;
}
}mTLS with Istio service mesh
Istio automates mTLS between all pods using SPIFFE SVIDs (X.509 certificates). PeerAuthentication enforces strict mTLS mode; no application code changes are needed.
# PeerAuthentication — enforce strict mTLS in the namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # reject all non-mTLS traffic
# AuthorizationPolicy — payment-service only accepts calls from order-service
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: payment-service-authz
namespace: production
spec:
selector:
matchLabels:
app: payment-service
rules:
- from:
- source:
# Only order-service service account can call payment-service
principals:
- "cluster.local/ns/production/sa/order-service"
to:
- operation:
methods: ["POST"]
paths: ["/payments/*"]Key Points to Remember
- 1Never use long-lived shared API keys between services — use short-lived tokens (15min–1h) or mTLS certificates.
- 2Client Credentials flow: service obtains a token from the auth server, attaches it as Bearer in every request.
- 3Spring Security resource server validates JWT signature via JWKS URI — no shared secret, no round-trip to auth server per request.
- 4mTLS via Istio is transparent to application code — Envoy sidecar handles TLS handshake and certificate rotation.
- 5Inject client_secret via environment variable or Vault — never hardcode in application.properties or source code.
- 6Use scopes to limit what each service is permitted to do — payments:read vs payments:create as separate scopes.
Interview Questions
Sign in to ask AriaWhat is the OAuth2 Client Credentials flow and how does it differ from Authorization Code flow?
How does a resource server validate a JWT without calling the identity provider on every request?
What is mTLS and how does Istio automate certificate rotation for service-to-service mTLS?
How would you revoke a compromised service identity in an mTLS setup?
Compare mTLS and OAuth2 client credentials for service-to-service auth — when would you choose each?
Ask Aria about Service-to-Service Authentication
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.