1
0
Files
jwt-auth-demo/docs/10-production-checklist.md
asmhatre 4a8dab6739 Spring Security 7.1 JWT authentication on Spring Boot 4.1
Runnable companion for https://ankurm.com/spring-security-7-1-jwt-authentication-guide/

- login -> token issue -> OncePerRequestFilter -> SecurityContext, end to end
- HS256 and RS256 variants (RS256 publishes a real JWKS endpoint)
- the same API secured by the built-in oauth2ResourceServer().jwt(), for comparison
- 11 documentation chapters under docs/, interlinked with the code
- docs/output/ is real captured output, regenerated by scripts/run-all.sh
- 13 passing tests pinning the 401-vs-403 contract and the CSRF failure

Verified against Spring Boot 4.1.1, Spring Security 7.1.1, JDK 25.0.4.1.
2026-08-22 06:34:43 +00:00

5.0 KiB

10 — Production checklist

← manual vs resource server · next: Spring Security 7 changes →

Run this list before shipping. Each item links to the section that explains it.

Keys and algorithms

  • Signing key comes from a secret manager or KMS, never from application.yaml, and never from an environment variable baked into an image. The values in this repository are public demo values.
  • HMAC secret is ≥ 32 bytes of random data (openssl rand -base64 48), not a passphrase that happens to be long enough. → 05
  • The algorithm is pinned on the decoder (.macAlgorithm(..) / .signatureAlgorithm(..)), not left to the token's alg header. → 05
  • If more than one service verifies tokens, the algorithm is asymmetric (RS256 / ES256). With HS256 every verifier can mint admin tokens. → 05
  • Every key has a kid, and a rotation procedure exists and has been rehearsed. → 05
  • A published JWKS contains n and e only — grep it for "d", "p", "q" before exposing it.

Claims and validation

  • aud is validated. It is not validated by default. → 07 §1
  • iss is validated (JwtValidators.createDefaultWithIssuer).
  • Access and refresh tokens are distinguishable, and the distinction is enforced on every request. → 07 §2
  • Clock skew is a deliberate number, not an accepted default of 60s. → 07 §3
  • No PII in claims. A JWT is signed, not encrypted. → 07 §7
  • Token size measured against your proxy's header limit, with the most privileged user's token. → 07 §8

Lifetimes and revocation

  • Access-token TTL is minutes, not hours or days.
  • Refresh tokens rotate on use, and a replay invalidates the family. → 07 §5
  • Every token carries a jti, and a denylist exists for logout, password change, and compromise. → 07 §4
  • The denylist is shared across instances (Redis, not a ConcurrentHashMap) and entries expire.
  • "Log out everywhere" is possible — usually a per-user tokensValidAfter timestamp compared against iat.

Chain configuration

  • CSRF decision is deliberate and matches where the token lives: disabled only if no credential is ambient. → 04
  • SessionCreationPolicy.STATELESS and NullSecurityContextRepository. Verify no Set-Cookie appears in a response. → 06
  • formLogin, httpBasic and logout are explicitly disabled if unused — otherwise a browser-shaped fallback exists on your API.
  • Custom filter is addFilterBefore(..., UsernamePasswordAuthenticationFilter.class), extends OncePerRequestFilter, and is not also registered as a servlet filter. → 02
  • anyRequest() is the last rule. → 07 §12
  • The filter clears the SecurityContext on every failure path. → 02
  • AuthenticationEntryPoint and AccessDeniedHandler are both configured, and login failures have a @RestControllerAdvice. → 03
  • The filter-chain diagnostic endpoint (/api/public/filters here) is removed.

Responses

  • Login failures are indistinguishable across bad-password, unknown-user, locked and disabled. → 03
  • error_description does not leak expiry timestamps or internal URLs in production. → 07 §16
  • 401 carries WWW-Authenticate with a real RFC 6750 error code, not a bare realm. → 07 §15
  • Rate limiting on /login and /refresh. Nothing in Spring Security does this for you, and an unthrottled login endpoint with bcrypt is also a CPU denial-of-service.

Transport and operations

  • HTTPS enforced; HSTS on.
  • Tokens never in URLs, and allowUriQueryParameter is off. → 07 §13
  • Access logs do not record the Authorization header.
  • Authentication failures are logged with enough context to alert on, and a spike in invalid_token is alertable.
  • @Async/executor boundaries wrap the SecurityContext. → 06
  • Dependency scanning covers nimbus-jose-jwt — it is where JOSE CVEs land.

Before you build any of this

Ask whether you should. If you need sessions and have one server-rendered application, a session cookie is simpler, revocable by design, and has no key management. If you need federated identity, an authorization server (Keycloak, Auth0, Okta, Spring Authorization Server) already implements every item on this list. A hand-rolled JWT layer is the right answer for a stateless API you own end to end — and a lot of work everywhere else.