Deepen PracticeOrdered learning track

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.

13 min read2588 words
PrevNext
Lesson 3240 lesson track23–33 Deepen Practice
#java#authentication#microservices#oauth2+11 more

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.

IdentityContohDigunakan untuk
End-user identitysub=user-123Audit user action, user-level authorization
Client/application identityclient_id=web-bffOAuth client, app-level policy
Workload/service identityspiffe://prod/ns/case/sa/case-api atau service=case-apiService-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:

QuestionExample
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:

MechanismUse case
mTLS client certificateStrong service-to-service authentication
SPIFFE/SPIRE SVIDWorkload identity across platforms
OAuth client credentialsService obtains token as client
Kubernetes service account tokenCluster-local identity input, often exchanged
Cloud workload identityAWS/Azure/GCP native service auth
HMAC request signingWebhooks/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:

  1. Strip identity headers at edge.
  2. Only gateway/mesh may inject verified headers.
  3. Use mTLS between gateway and services.
  4. Services reject requests missing trusted proxy proof.
  5. 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:

ValidationWhy
SignatureToken integrity
IssuerTrusted authorization server
AudienceToken intended for this service
Expiry / not-beforeTime validity
Scope / authoritiesPermission boundary
TenantTenant isolation
SubjectUser identity
Actor/clientDelegation/service policy
Assurance/auth_timeSensitive 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.

LocationStrengthRisk
Gateway onlyCentral, simpleLacks domain context
Service localDomain-awareRepeated policy logic
Central PDPConsistentLatency/availability complexity
HybridPracticalRequires 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:

QuestionWhy
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 modeRoot causeConsequenceMitigation
Token relay everywhereSame token accepted by all servicesOverbroad blast radiusAudience-specific tokens/token exchange
Signature-only validationAudience/issuer ignoredToken substitutionValidate issuer/audience/expiry/scope
Header trustService trusts X-User-IdSpoofingStrip headers + mTLS + signed/tokenized context
User/service identity mixedOnly sub trackedBad audit/authorizationTrack user + client + workload
Gateway-only authDownstream no domain checksObject-level access bugService-level authorization
Tenant header acceptedTenant from request not tokenCross-tenant accessTenant binding checks
Exchanged token cached wrongCache key missing subject/scopePrivilege leakComplete cache key
Event uses live auth assumptionConsumer treats old actor as currentUnauthorized side effectEvent actor context + service auth
Service account overprivilegedBroad client credentialsLateral movementMinimal scopes/audience
STS outage unhandledToken exchange dependency downCascade failureCache, 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.

TestExpected
Missing token401
Expired token401
Wrong issuer401
Wrong audience401
Missing scope403
Wrong tenant403
Valid token low assurance sensitive action401/step-up required
Valid token correct audience/scope200
Header-only identity without token401
Token with alg=none or wrong algorithm401

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:

OperationCost
JWT local signature validationLow/Medium
JWKS refreshOccasional network
Opaque token introspectionNetwork per validation unless cached
Token exchangeNetwork call
mTLS handshakeHigher at connection setup
Authorization policy callNetwork 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:

  1. Revoke refresh token/session family.
  2. Short-lived access token expires naturally or add denylist for critical leak.
  3. Search logs by jti, subject, client, audience.
  4. Detect downstream calls using leaked token.
  5. Force reauth/step-up.

24.2 Service Credential Leak

Actions:

  1. Rotate service credential immediately.
  2. Revoke client credentials / certificates.
  3. Audit token endpoint usage by client_id.
  4. Review downstream access by actor service.
  5. Reduce scopes if overbroad.

24.3 JWKS Key Compromise

Actions:

  1. Rotate signing keys.
  2. Invalidate tokens signed by compromised key if possible.
  3. Force clients/resource servers to refresh JWKS.
  4. Monitor kid usage.
  5. Run token forgery detection.

24.4 Gateway Header Spoofing Bug

Actions:

  1. Block external traffic path.
  2. Strip identity headers at edge.
  3. Patch service to reject header-only identity.
  4. Search for requests with suspicious headers.
  5. Add regression tests.

25. Design Decision Matrix

SituationRecommended pattern
Browser appBFF + session cookie + server-side token handling
Public SPA without BFFAuthorization Code + PKCE, careful token storage; prefer BFF for high-risk enterprise
Simple internal API chainToken relay if audience/scope valid
Complex service delegationToken exchange
High-security service-to-servicemTLS + OAuth token or token exchange
Partner APIOAuth client credentials, mTLS, or HMAC depending partner maturity
Kafka/event propagationService auth + signed/auditable actor context
Multi-tenant servicesTenant-bound tokens + service-level tenant checks
Regulated sensitive actionsAssurance claim + step-up + action-level authorization

26. Minimal Production Blueprint

Blueprint rules:

  1. Browser never sees downstream API tokens if BFF pattern is used.
  2. Each service validates tokens intended for itself.
  3. Service-to-service caller identity is known.
  4. User identity and actor service are both preserved.
  5. Tenant is bound in token and checked against resource.
  6. Sensitive actions require assurance and freshness.
  7. Events preserve actor context but do not reuse live tokens.
  8. 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
Lesson Recap

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.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.