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.
5.0 KiB
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'salgheader. → 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
nandeonly — grep it for"d","p","q"before exposing it.
Claims and validation
audis validated. It is not validated by default. → 07 §1issis 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
tokensValidAftertimestamp compared againstiat.
Chain configuration
- CSRF decision is deliberate and matches where the token lives: disabled only if no credential is ambient. → 04
SessionCreationPolicy.STATELESSandNullSecurityContextRepository. Verify noSet-Cookieappears in a response. → 06formLogin,httpBasicandlogoutare explicitly disabled if unused — otherwise a browser-shaped fallback exists on your API.- Custom filter is
addFilterBefore(..., UsernamePasswordAuthenticationFilter.class), extendsOncePerRequestFilter, and is not also registered as a servlet filter. → 02 anyRequest()is the last rule. → 07 §12- The filter clears the
SecurityContexton every failure path. → 02 AuthenticationEntryPointandAccessDeniedHandlerare both configured, and login failures have a@RestControllerAdvice. → 03- The filter-chain diagnostic endpoint (
/api/public/filtershere) is removed.
Responses
- Login failures are indistinguishable across bad-password, unknown-user, locked and disabled. → 03
error_descriptiondoes not leak expiry timestamps or internal URLs in production. → 07 §16- 401 carries
WWW-Authenticatewith a real RFC 6750 error code, not a bare realm. → 07 §15 - Rate limiting on
/loginand/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
allowUriQueryParameteris off. → 07 §13 - Access logs do not record the
Authorizationheader. - Authentication failures are logged with enough context to alert on, and a spike in
invalid_tokenis alertable. @Async/executor boundaries wrap theSecurityContext. → 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.