Authentication for Microservices
Learn Java Authentication Pattern - Part 032
Authentication untuk microservices Java: edge authentication, service identity, token relay, token exchange, audience boundary, mTLS, gateway pattern, BFF, asynchronous propagation, Spring Security Resource Server, JAX-RS filters, observability, testing, dan failure modes.
Part 032 — Authentication for Microservices
Target part ini: memahami autentikasi di microservices secara production-grade: edge authentication, internal service identity, token relay, token exchange, audience boundary, mTLS, gateway/BFF, service-to-service call, dan failure modes yang sering menyebabkan privilege leak.
Microservices authentication bukan hanya “setiap service validate JWT”.
Itu hanya satu potongan.
Problem sebenarnya:
Who is calling?
On behalf of whom?
For which audience?
With which assurance?
Through which trust boundary?
Can this identity be safely propagated?
Dalam monolith, request masuk ke satu boundary.
Dalam microservices, satu user action bisa menjadi 10 service calls.
Jika identity propagation salah, service internal bisa menerima token yang tidak seharusnya, audience bisa kacau, audit bisa menuduh user yang salah, dan authorization bisa terjadi di tempat yang tidak punya konteks cukup.
1. Three Identities You Must Not Mix
Microservices auth punya minimal tiga identity.
| Identity | Contoh | Digunakan untuk |
|---|---|---|
| End-user identity | sub=user-123 | Audit user action, user-level authorization |
| Client/application identity | client_id=web-bff | OAuth client, app-level policy |
| Workload/service identity | spiffe://prod/ns/case/sa/case-api atau service=case-api | Service-to-service trust |
Kesalahan umum:
Service A calls Service B with user token.
Service B treats caller as user only.
Service B ignores that real network caller is Service A.
Yang benar:
Effective request context = user subject + client/app + calling workload + tenant + audience + assurance + scopes
Model Java:
public record EffectiveCaller(
UserIdentity user,
ClientIdentity client,
WorkloadIdentity workload,
TenantIdentity tenant,
Set<String> scopes,
Set<String> authorities,
Assurance assurance,
DelegationMode delegationMode
) {}
Jangan simpan sebagai string tunggal username.
2. Microservice Authentication Topologies
2.1 Edge Authentication Only
Karakteristik:
- gateway memvalidasi login/token
- downstream menerima identity via headers
- service internal tidak validate token lagi
Kelebihan:
- sederhana
- performa bagus
- kebijakan auth terpusat
Risiko:
- header spoofing jika network boundary bocor
- internal service terlalu percaya gateway
- sulit enforce audience per service
- compromised service bisa impersonate user ke service lain
Boleh dipakai hanya jika:
network boundary kuat + headers stripped at edge + internal mTLS + service authorization tetap ada
2.2 Token Relay
Service meneruskan access token user ke downstream.
Kelebihan:
- audit user mudah
- service bisa validate token sendiri
- cocok untuk simple chain
Risiko:
- token audience terlalu luas
- downstream mendapatkan token dengan scope berlebih
- confused deputy
- token leak berdampak besar
- service internal bisa menyalahgunakan user token
Token relay cocok hanya jika access token memang intended untuk semua resource server yang menerima token.
Jika token aud=case-api, jangan relay ke payment-api.
2.3 Token Exchange
Token exchange membuat token baru untuk downstream audience.
Kelebihan:
- audience tepat
- delegation eksplisit
- scope bisa diturunkan
- actor bisa tercatat
- blast radius lebih kecil
Risiko:
- lebih kompleks
- butuh STS/authorization server support
- latency dan caching perlu desain
- policy delegation harus jelas
2.4 Service Identity + User Context Header
Service-to-service authenticated by mTLS/service identity, user context dikirim sebagai signed context/header.
Risiko:
- custom protocol mudah salah
- signature/expiry/audience harus benar
- jangan kirim raw untrusted header
Gunakan hanya jika OAuth token exchange tidak tersedia dan boundary sangat jelas.
2.5 Backend-for-Frontend Pattern
BFF menyimpan token server-side dan browser hanya memegang session cookie.
Kelebihan:
- token tidak terekspos ke JavaScript
- CSRF/Cookie controls bisa dikelola
- cocok untuk browser app enterprise
Risiko:
- BFF jadi high-value service
- session store harus aman
- BFF harus enforce audience/scope downstream
3. Authentication Boundary Map
Setiap call harus punya boundary map.
Internet -> CDN/WAF -> Gateway -> BFF/API -> Internal Services -> Data Stores/Event Brokers
Untuk setiap boundary, jawab:
| Question | Example |
|---|---|
| Siapa caller langsung? | Browser, gateway, service A |
| Bukti apa yang diterima? | cookie, bearer JWT, mTLS cert, HMAC |
| Siapa yang memvalidasi? | gateway, service, sidecar, library |
| Audience siapa? | gateway, BFF, specific API |
| Identity apa yang dipropagasi? | user, client, workload, tenant |
| Apa yang tidak boleh dipercaya? | incoming headers, forwarded proto, raw claims |
Contoh boundary:
Browser -> BFF:
cookie session + CSRF + SameSite
BFF -> Case API:
OAuth access token aud=case-api, client=bff, sub=user
Case API -> Evidence API:
token exchange aud=evidence-api, actor=case-api, sub=user
Case API -> Kafka:
service identity for producer + signed audit context in event payload
4. Token Audience Is A Hard Boundary
Audience (aud) adalah salah satu claim paling penting di microservices.
Bad:
{
"aud": "all-services",
"scope": "*"
}
Better:
{
"aud": "case-api",
"scope": "case:read case:submit",
"sub": "user-123",
"azp": "web-bff",
"tenant_id": "acme"
}
For downstream:
{
"aud": "evidence-api",
"scope": "evidence:read",
"sub": "user-123",
"act": {
"sub": "service:case-api"
},
"tenant_id": "acme"
}
Rules:
A service must reject tokens not intended for it.
A downstream service must not accept a token just because signature is valid.
Signature validity != audience validity.
Spring Security resource server should configure audience validation explicitly.
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri("https://id.example.com/.well-known/jwks.json")
.build();
OAuth2TokenValidator<Jwt> issuer = JwtValidators.createDefaultWithIssuer("https://id.example.com");
OAuth2TokenValidator<Jwt> audience = token -> {
return token.getAudience().contains("case-api")
? OAuth2TokenValidatorResult.success()
: OAuth2TokenValidatorResult.failure(
new OAuth2Error("invalid_token", "Missing required audience", null)
);
};
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(issuer, audience));
return decoder;
}
5. Service Identity
User token answers:
who is the user?
Service identity answers:
which workload made this network call?
They are different.
Service identity mechanisms:
| Mechanism | Use case |
|---|---|
| mTLS client certificate | Strong service-to-service authentication |
| SPIFFE/SPIRE SVID | Workload identity across platforms |
| OAuth client credentials | Service obtains token as client |
| Kubernetes service account token | Cluster-local identity input, often exchanged |
| Cloud workload identity | AWS/Azure/GCP native service auth |
| HMAC request signing | Webhooks/partner integration |
In Java, service identity often appears as:
- TLS client cert in servlet request
- bearer token from client credentials
- sidecar-injected mTLS identity
- gateway-injected verified identity header
But application code must know whether identity was verified by itself or by infrastructure.
public enum IdentityTrustSource {
LOCAL_VERIFICATION,
TRUSTED_PROXY,
SERVICE_MESH,
UNSAFE_HEADER
}
Reject unsafe header.
6. Internal Headers Are Dangerous
Bad pattern:
X-User-Id: user-123
X-Tenant-Id: acme
X-Roles: admin
If any external client can send these headers, auth is broken.
Minimum protection:
- Strip identity headers at edge.
- Only gateway/mesh may inject verified headers.
- Use mTLS between gateway and services.
- Services reject requests missing trusted proxy proof.
- Prefer signed context or tokens over raw headers.
Example Nginx/gateway policy:
remove incoming X-User-Id, X-Tenant-Id, X-Roles
validate Authorization token
inject X-Verified-User-Id only after validation
Better: pass structured token.
7. Token Relay Pattern in Spring
Token relay is common in Spring-based systems.
The application receives an authenticated OAuth2 context and propagates access token downstream.
Simplified client:
@Component
public class DownstreamCaseClient {
private final WebClient webClient;
public DownstreamCaseClient(WebClient.Builder builder) {
this.webClient = builder.baseUrl("https://evidence-api.internal").build();
}
public Mono<EvidenceDto> getEvidence(String id) {
return webClient.get()
.uri("/evidence/{id}", id)
.attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId("idp"))
.retrieve()
.bodyToMono(EvidenceDto.class);
}
}
But token relay must be reviewed:
Is the token audience valid for downstream?
Are scopes minimal?
Is the downstream allowed to see user token?
Could Service A use the token for another service?
Does audit need actor identity?
If answers are weak, use token exchange.
8. Token Exchange Pattern
OAuth 2.0 Token Exchange defines a way to request a new security token from an authorization server/STS.
Conceptual request:
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic service-a-client-credentials
grant_type=urn:ietf:params:oauth:grant-type:token-exchange&
subject_token=eyJ...user-token...&
subject_token_type=urn:ietf:params:oauth:token-type:access_token&
audience=evidence-api&
scope=evidence:read
Response:
{
"access_token": "eyJ...exchanged-token...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "evidence:read"
}
Token claims should preserve delegation:
{
"iss": "https://id.example.com",
"sub": "user-123",
"aud": "evidence-api",
"scope": "evidence:read",
"act": {
"sub": "service:case-api"
},
"azp": "web-bff",
"tenant_id": "acme",
"acr": "aal2",
"auth_time": 1783090100
}
This lets Evidence API know:
User = user-123
Acting service = case-api
Original client = web-bff
Audience = evidence-api
Tenant = acme
Assurance = aal2
That is much better than raw token relay.
9. Java Token Exchange Client
Simplified implementation with Java HttpClient:
public final class TokenExchangeClient {
private final HttpClient httpClient;
private final URI tokenEndpoint;
private final String clientId;
private final String clientSecret;
public TokenExchangeClient(
HttpClient httpClient,
URI tokenEndpoint,
String clientId,
String clientSecret
) {
this.httpClient = httpClient;
this.tokenEndpoint = tokenEndpoint;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
public ExchangedToken exchange(String subjectToken, String audience, String scope) throws IOException, InterruptedException {
String form = "grant_type=" + enc("urn:ietf:params:oauth:grant-type:token-exchange")
+ "&subject_token=" + enc(subjectToken)
+ "&subject_token_type=" + enc("urn:ietf:params:oauth:token-type:access_token")
+ "&audience=" + enc(audience)
+ "&scope=" + enc(scope);
String basic = Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder(tokenEndpoint)
.header("Authorization", "Basic " + basic)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new TokenExchangeException("Token exchange failed with status " + response.statusCode());
}
return parse(response.body());
}
private static String enc(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}
Production requirements:
- use mTLS/private key/client assertion instead of static secret where possible
- cache exchanged tokens by subject/audience/scope/tenant/assurance carefully
- short TTL
- do not log subject token or exchanged token
- handle STS outage explicitly
- apply circuit breaker/backoff
10. Resource Server Pattern
Every service that accepts bearer tokens must validate:
| Validation | Why |
|---|---|
| Signature | Token integrity |
| Issuer | Trusted authorization server |
| Audience | Token intended for this service |
| Expiry / not-before | Time validity |
| Scope / authorities | Permission boundary |
| Tenant | Tenant isolation |
| Subject | User identity |
| Actor/client | Delegation/service policy |
| Assurance/auth_time | Sensitive action control |
Spring Security config shape:
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/cases/**").hasAuthority("SCOPE_case:read")
.requestMatchers(HttpMethod.POST, "/cases/*/submit").access(assurance("aal2", Duration.ofMinutes(15)))
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())))
.build();
}
Converter:
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
scopes.setAuthorityPrefix("SCOPE_");
scopes.setAuthoritiesClaimName("scope");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(scopes);
converter.setPrincipalClaimName("sub");
return converter;
}
But avoid blindly mapping roles from token if roles are tenant-specific or stale.
11. JAX-RS Resource Server Filter
If not using Spring Security, a JAX-RS filter can validate token.
@Provider
@Priority(Priorities.AUTHENTICATION)
public final class BearerTokenAuthenticationFilter implements ContainerRequestFilter {
private final TokenVerifier verifier;
public BearerTokenAuthenticationFilter(TokenVerifier verifier) {
this.verifier = verifier;
}
@Override
public void filter(ContainerRequestContext requestContext) {
String authorization = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authorization == null || !authorization.startsWith("Bearer ")) {
abortUnauthorized(requestContext);
return;
}
String token = authorization.substring("Bearer ".length());
VerifiedToken verified;
try {
verified = verifier.verify(token, "case-api");
} catch (TokenVerificationException ex) {
abortUnauthorized(requestContext);
return;
}
requestContext.setSecurityContext(new AuthSecurityContext(
verified.subject(),
verified.authorities(),
requestContext.getSecurityContext().isSecure(),
"Bearer"
));
}
private void abortUnauthorized(ContainerRequestContext ctx) {
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
}
}
Token verifier must enforce issuer/audience/expiry/algorithm.
12. Authorization Placement
Authentication validates caller. Authorization decides allowed action.
In microservices, authorization can happen in multiple places.
| Location | Strength | Risk |
|---|---|---|
| Gateway only | Central, simple | Lacks domain context |
| Service local | Domain-aware | Repeated policy logic |
| Central PDP | Consistent | Latency/availability complexity |
| Hybrid | Practical | Requires clear contracts |
Practical enterprise pattern:
Gateway:
coarse authentication, route-level allow/deny, abuse controls
Service:
domain authorization, tenant enforcement, object-level checks
PDP/policy engine:
shared high-level decisions if needed
Do not rely only on gateway for object-level decisions.
Gateway does not know whether user can approve this specific enforcement case.
13. Tenant Propagation
Tenant must be authenticated/authorized, not just accepted from header.
Bad:
X-Tenant-Id: acme
Authorization: Bearer valid-user-token
Better:
{
"sub": "user-123",
"tenant_id": "acme",
"tenant_memberships": ["acme"],
"aud": "case-api"
}
Even better for multi-tenant:
token tenant binding + route tenant binding + data partition tenant binding
Service must check:
if (!token.tenantId().equals(pathTenantId)) {
throw new ForbiddenException("Tenant mismatch");
}
For service-to-service:
user tenant = acme
service workload = case-api in prod
requested resource tenant = acme
All must align.
14. Async and Event-Driven Boundary
HTTP request identity does not automatically survive Kafka/event boundaries.
Bad event:
{
"caseId": "CASE-123",
"approvedBy": "user-123"
}
Better event metadata:
{
"eventId": "01J...",
"eventType": "CaseApproved",
"occurredAt": "2026-07-03T10:15:30Z",
"tenantId": "acme",
"actor": {
"type": "USER",
"subjectId": "user-123",
"sessionId": "sess-456",
"assuranceLevel": "aal2",
"authTime": "2026-07-03T10:10:00Z"
},
"producer": {
"service": "case-api",
"instance": "case-api-7f8d9"
},
"traceId": "trace-789",
"payload": {
"caseId": "CASE-123"
}
}
For event consumers:
Do not re-authenticate the historical user.
Do authenticate the producing service.
Do preserve actor context for audit.
Do not let actor context authorize new unrelated action unless policy says so.
This topic will be expanded in Part 033.
15. Gateway Pattern
Gateway responsibilities:
- TLS termination or pass-through policy
- strip untrusted headers
- validate external token/session
- route to service
- inject correlation id
- apply coarse rate limiting
- optionally perform token relay/exchange
Gateway must not become a blind identity header factory.
Gateway output contract should be explicit:
Request to service contains:
Authorization: Bearer token with aud=<service>
X-Request-Id: generated by gateway
X-Forwarded-* only if sanitized
Request must not contain:
user-controlled X-User-Id
user-controlled X-Roles
original unverified identity headers
16. Service Mesh and mTLS
Service mesh can solve transport-level service authentication.
It does not automatically solve user authentication.
mTLS says: request came from workload case-api.
JWT says: request is on behalf of user-123.
Authorization says: user-123 through case-api may read evidence-789.
Do not collapse these into one.
If mesh validates JWT at sidecar, app still needs the verified claims through a trusted channel or local validation.
Questions:
| Question | Why |
|---|---|
| Does app know which workload called it? | Service-level policy |
| Does app know user subject? | User-level authorization/audit |
| Can external client bypass mesh? | Header spoofing risk |
| Are sidecar-injected headers stripped at ingress? | Trust boundary |
| Is mTLS identity stable across deploys? | Policy reliability |
17. Token Caching
Microservices often call token endpoint too often.
Bad:
Every downstream request -> token exchange call
Better:
Cache exchanged token by:
tenant + subject + audience + scope + actor + assurance + token expiry
Cache key:
public record ExchangedTokenCacheKey(
String tenantId,
String subject,
String actorService,
String audience,
Set<String> scopes,
String assuranceLevel
) {}
Security rules:
- never cache beyond token expiry
- avoid cache key missing tenant
- avoid cache key missing audience
- expire early with skew
- clear on incident/revocation if needed
- do not share user token across subjects
18. Failure Modes
| Failure mode | Root cause | Consequence | Mitigation |
|---|---|---|---|
| Token relay everywhere | Same token accepted by all services | Overbroad blast radius | Audience-specific tokens/token exchange |
| Signature-only validation | Audience/issuer ignored | Token substitution | Validate issuer/audience/expiry/scope |
| Header trust | Service trusts X-User-Id | Spoofing | Strip headers + mTLS + signed/tokenized context |
| User/service identity mixed | Only sub tracked | Bad audit/authorization | Track user + client + workload |
| Gateway-only auth | Downstream no domain checks | Object-level access bug | Service-level authorization |
| Tenant header accepted | Tenant from request not token | Cross-tenant access | Tenant binding checks |
| Exchanged token cached wrong | Cache key missing subject/scope | Privilege leak | Complete cache key |
| Event uses live auth assumption | Consumer treats old actor as current | Unauthorized side effect | Event actor context + service auth |
| Service account overprivileged | Broad client credentials | Lateral movement | Minimal scopes/audience |
| STS outage unhandled | Token exchange dependency down | Cascade failure | Cache, fallback policy, circuit breaker |
19. Confused Deputy in Microservices
Classic confused deputy:
User can call Service A.
Service A has powerful access to Service B.
User tricks Service A into using its power on Service B for unauthorized resource.
Example:
User has case:read for CASE-1.
Service A has evidence:read-all.
User requests evidence id for CASE-2 through Service A.
Service A calls Evidence API with its broad service token.
Evidence API returns CASE-2 evidence.
Mitigation:
- Service A checks object-level authorization before downstream call.
- Evidence API also enforces user/resource relationship.
- Token exchange produces user-bound token with limited scope/resource context.
- Downstream receives enough context to verify.
Better token:
{
"sub": "user-123",
"aud": "evidence-api",
"scope": "evidence:read",
"case_id": "CASE-1",
"act": {"sub": "service:case-api"}
}
Even better: downstream checks evidence belongs to authorized case.
20. Authentication Contract in OpenAPI
For every service API, document accepted auth schemes.
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
paths:
/cases/{caseId}:
get:
security:
- bearerAuth: []
x-required-audience: case-api
x-required-scopes:
- case:read
x-required-assurance: aal1
This is not just documentation. Use it for contract tests.
21. Contract Tests
Each service should have authentication contract tests.
| Test | Expected |
|---|---|
| Missing token | 401 |
| Expired token | 401 |
| Wrong issuer | 401 |
| Wrong audience | 401 |
| Missing scope | 403 |
| Wrong tenant | 403 |
| Valid token low assurance sensitive action | 401/step-up required |
| Valid token correct audience/scope | 200 |
| Header-only identity without token | 401 |
Token with alg=none or wrong algorithm | 401 |
Example:
@Test
void rejectsTokenWithWrongAudience() {
String token = jwt()
.issuer("https://id.example.com")
.subject("user-123")
.audience("other-api")
.scope("case:read")
.sign();
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/cases/CASE-1")
.then()
.statusCode(401);
}
22. Observability
Logs must preserve all identity dimensions.
{
"event": "API_AUTHENTICATED",
"service": "case-api",
"tenant_id": "acme",
"subject": "user-123",
"client_id": "web-bff",
"actor_service": "gateway",
"audience": "case-api",
"scopes": ["case:read"],
"assurance": "aal2",
"auth_time": "2026-07-03T10:10:00Z",
"trace_id": "abc",
"request_id": "def"
}
Metrics:
auth_token_validation_success_total{service="case-api"}
auth_token_validation_failure_total{reason="wrong_audience"}
auth_token_validation_failure_total{reason="expired"}
auth_token_exchange_total{audience="evidence-api"}
auth_token_exchange_failure_total{reason="sts_unavailable"}
auth_service_identity_failure_total{reason="mtls_missing"}
auth_tenant_mismatch_total{service="case-api"}
Never log token values.
23. Performance Engineering
Token validation cost matters.
Common costs:
| Operation | Cost |
|---|---|
| JWT local signature validation | Low/Medium |
| JWKS refresh | Occasional network |
| Opaque token introspection | Network per validation unless cached |
| Token exchange | Network call |
| mTLS handshake | Higher at connection setup |
| Authorization policy call | Network if external PDP |
Strategies:
- local JWT validation where possible
- JWKS cache with rotation handling
- short but cacheable exchanged tokens
- connection pooling
- circuit breakers for STS/PDP
- avoid introspection on every high-QPS request unless caching works
- fail closed for auth uncertainty, but design graceful degradation for non-critical features
24. Incident Scenarios
24.1 User Token Leak
Actions:
- Revoke refresh token/session family.
- Short-lived access token expires naturally or add denylist for critical leak.
- Search logs by
jti, subject, client, audience. - Detect downstream calls using leaked token.
- Force reauth/step-up.
24.2 Service Credential Leak
Actions:
- Rotate service credential immediately.
- Revoke client credentials / certificates.
- Audit token endpoint usage by
client_id. - Review downstream access by actor service.
- Reduce scopes if overbroad.
24.3 JWKS Key Compromise
Actions:
- Rotate signing keys.
- Invalidate tokens signed by compromised key if possible.
- Force clients/resource servers to refresh JWKS.
- Monitor
kidusage. - Run token forgery detection.
24.4 Gateway Header Spoofing Bug
Actions:
- Block external traffic path.
- Strip identity headers at edge.
- Patch service to reject header-only identity.
- Search for requests with suspicious headers.
- Add regression tests.
25. Design Decision Matrix
| Situation | Recommended pattern |
|---|---|
| Browser app | BFF + session cookie + server-side token handling |
| Public SPA without BFF | Authorization Code + PKCE, careful token storage; prefer BFF for high-risk enterprise |
| Simple internal API chain | Token relay if audience/scope valid |
| Complex service delegation | Token exchange |
| High-security service-to-service | mTLS + OAuth token or token exchange |
| Partner API | OAuth client credentials, mTLS, or HMAC depending partner maturity |
| Kafka/event propagation | Service auth + signed/auditable actor context |
| Multi-tenant services | Tenant-bound tokens + service-level tenant checks |
| Regulated sensitive actions | Assurance claim + step-up + action-level authorization |
26. Minimal Production Blueprint
Blueprint rules:
- Browser never sees downstream API tokens if BFF pattern is used.
- Each service validates tokens intended for itself.
- Service-to-service caller identity is known.
- User identity and actor service are both preserved.
- Tenant is bound in token and checked against resource.
- Sensitive actions require assurance and freshness.
- Events preserve actor context but do not reuse live tokens.
- Audit logs contain subject, client, workload, tenant, audience, scope, and trace id.
27. Code Review Checklist
When reviewing microservice authentication code, ask:
- Does the service validate issuer?
- Does the service validate audience?
- Does the service validate expiry and not-before?
- Does the service reject wrong algorithm/key?
- Are scopes mapped explicitly?
- Are roles tenant-safe?
- Is tenant from token checked against path/body/resource?
- Are internal identity headers stripped at edge?
- Does service distinguish user identity from service identity?
- Is token relay justified?
- Would token exchange reduce risk?
- Are exchanged tokens cached safely?
- Are tokens redacted in logs?
- Are STS/JWKS outages handled?
- Are sensitive actions assurance-aware?
- Are contract tests covering wrong issuer/audience/scope?
28. Closing Mental Model
Authentication in microservices is about preserving identity semantics across trust boundaries.
The useful model:
External user proof -> edge session/token
Edge -> service token/audience
Service -> downstream delegation
Workload identity -> network caller proof
Actor context -> audit and policy
Assurance -> sensitive action control
Tenant binding -> isolation
The dangerous model:
JWT valid -> everything is fine
A valid token can still be wrong for the service. A user can still lack authority for the object. A service can still be overprivileged. A header can still be spoofed. A tenant can still be confused.
The top-tier engineering move is not to add more tokens.
It is to make identity boundaries explicit.
References
- Spring Security Reference — OAuth2 Resource Server JWT
- Spring Security Reference — OAuth2 Client
- RFC 8693 — OAuth 2.0 Token Exchange
- RFC 6750 — OAuth 2.0 Bearer Token Usage
- RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- RFC 8725 — JSON Web Token Best Current Practices
- RFC 9700 — Best Current Practice for OAuth 2.0 Security
- SPIFFE/SPIRE documentation for workload identity concepts
You just completed lesson 32 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.