Interview Prep Zoneby CuriouserLabs

Question 2 of 5 · staff level

Design stateless API security with OAuth2/JWT. What does the resource server validate, and what are the classic JWT mistakes?

🎤 Say this first

Pattern: an authorization server (Keycloak/Auth0/Entra) issues short-lived JWT access tokens; your Boot services are resource servers (oauth2ResourceServer().jwt()) that validate locally: signature against the issuer's rotating keys (JWKS fetched from the issuer), iss, aud, exp/nbf — no network hop per request, which is the entire scaling point. Claims map to authorities for authorization. Classic mistakes: no aud check (token for service A accepted by service B), long-lived access tokens with no revocation story, putting PII in claims, alg: none/key-confusion handling (use the library, never hand-roll), and validating only 'is it a valid JWT' instead of 'is it a valid token for me, for this action'.

The full picture

Resource server with real validation + authority mapping
# application.yml
spring.security.oauth2.resourceserver.jwt:
  issuer-uri: https://auth.acme.com/realms/acme   # discovers JWKS, validates iss

@Bean
JwtDecoder jwtDecoder(OAuth2ResourceServerProperties props) {
    var decoder = JwtDecoders.<NimbusJwtDecoder>fromIssuerLocation(
        props.getJwt().getIssuerUri());
    decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
        JwtValidators.createDefaultWithIssuer(issuer),
        new JwtClaimValidator<List<String>>("aud",                 // AUDIENCE ✅
            aud -> aud != null && aud.contains("orders-api"))));
    return decoder;
}

@Bean
JwtAuthenticationConverter authoritiesConverter() {      // claims -> authorities
    var granted = new JwtGrantedAuthoritiesConverter();
    granted.setAuthoritiesClaimName("roles");
    granted.setAuthorityPrefix("ROLE_");
    var conv = new JwtAuthenticationConverter();
    conv.setJwtGrantedAuthoritiesConverter(granted);
    return conv;
}
  • Token lifecycle: access tokens 5–15 min; refresh tokens (rotated, revocable) live in the client's secure storage — revocation of access tokens is by expiry, which is why they must be short. Need instant kill? That's a denylist/introspection trade-off — accept the network hop where it matters.
  • Opaque tokens + introspection as the alternative: revocable and smaller, but a network call per request (cacheable) — pick per trust boundary: JWTs inside your platform, introspection at the edge is a common hybrid.
  • Claims discipline: authorization claims (roles/scopes/tenant) yes; PII/email as little as possible (JWTs are only base64 — anyone can read them); keep them small (they ride every request).
  • Web clients: browser apps should get tokens via BFF/cookie patterns rather than storing JWTs in localStorage (XSS-readable) — the answer differs for SPAs vs service-to-service, and saying so is the senior move.

🔄 Likely follow-up questions

  • JWT vs opaque + introspection — full trade-off table and where you'd use each.
  • How does JWKS key rotation work, and what breaks if you pin keys?
  • Design service-to-service auth: client-credentials, mTLS, or both?