The full picture
# 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.