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.
This commit is contained in:
97
docs/01-architecture.md
Normal file
97
docs/01-architecture.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 01 — Architecture
|
||||
|
||||
[← README](../README.md) · [next: filter chain and ordering →](02-filter-chain-and-ordering.md)
|
||||
|
||||
There are two paths through this application, and confusing them is the source of most
|
||||
JWT bugs. The **login path** runs once and is stateful in the only sense that matters:
|
||||
it sees a password. The **request path** runs on every subsequent call and sees nothing
|
||||
but a string.
|
||||
|
||||
## The login path
|
||||
|
||||
```
|
||||
POST /api/auth/login {"username":"alice","password":"..."}
|
||||
|
|
||||
v
|
||||
AuthController <-- the ONLY place a password is read
|
||||
|
|
||||
| authenticationManager.authenticate(
|
||||
| UsernamePasswordAuthenticationToken.unauthenticated(user, pass))
|
||||
v
|
||||
ProviderManager
|
||||
|
|
||||
v
|
||||
DaoAuthenticationProvider
|
||||
| loadUserByUsername -> UserDetails
|
||||
| passwordEncoder.matches(raw, encoded)
|
||||
v
|
||||
Authentication (authenticated=true, authorities=[ROLE_USER, SCOPE_profile:read])
|
||||
|
|
||||
v
|
||||
TokenService.issueAccessToken(authentication)
|
||||
| JwtClaimsSet: iss aud sub jti iat nbf exp scope roles token_type
|
||||
| NimbusJwtEncoder.encode(...)
|
||||
v
|
||||
200 {"accessToken":"eyJ...","refreshToken":"eyJ...","tokenType":"Bearer",...}
|
||||
```
|
||||
|
||||
Note what does **not** happen: no session is created, no `SecurityContext` is saved, no
|
||||
cookie is set. The `Authentication` object built here is used to fill in claims and is
|
||||
then discarded.
|
||||
|
||||
## The request path
|
||||
|
||||
```
|
||||
GET /api/me
|
||||
Authorization: Bearer eyJ...
|
||||
|
|
||||
v
|
||||
FilterChainProxy ---------------------------------------------+
|
||||
| |
|
||||
| 1 DisableEncodeUrlFilter |
|
||||
| 2 WebAsyncManagerIntegrationFilter |
|
||||
| 3 SecurityContextHolderFilter loads context |
|
||||
| 4 HeaderWriterFilter |
|
||||
| 5 JwtAuthenticationFilter <-- ours |
|
||||
| resolve Bearer token |
|
||||
| jwtDecoder.decode(token) |
|
||||
| verify signature |
|
||||
| exp / nbf (+/- 60s skew), iss, aud |
|
||||
| token_type == "access", jti not revoked |
|
||||
| JwtAuthenticationToken -> SecurityContext |
|
||||
| 6 RequestCacheAwareFilter |
|
||||
| 7 SecurityContextHolderAwareRequestFilter |
|
||||
| 8 AnonymousAuthenticationFilter |
|
||||
| 9 SessionManagementFilter |
|
||||
| 10 ExceptionTranslationFilter catches what follows |
|
||||
| 11 AuthorizationFilter permitAll / hasRole |
|
||||
| |
|
||||
+--------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
DispatcherServlet -> @PreAuthorize -> controller
|
||||
```
|
||||
|
||||
That list is not from memory. It is printed by `GET /api/public/filters`, which reads
|
||||
`FilterChainProxy.getFilterChains()` at runtime — see
|
||||
[`FilterChainReport`](../src/main/java/com/ankurm/jwtauth/diag/FilterChainReport.java)
|
||||
and step 19 of [`curl-transcript-hs256.txt`](output/curl-transcript-hs256.txt).
|
||||
|
||||
## Where each concern lives
|
||||
|
||||
| concern | class | doc |
|
||||
|---|---|---|
|
||||
| password check | [`AppUsers`](../src/main/java/com/ankurm/jwtauth/config/AppUsers.java) + `DaoAuthenticationProvider` | — |
|
||||
| token minting | [`TokenService`](../src/main/java/com/ankurm/jwtauth/auth/TokenService.java) | [05](05-hs256-vs-rs256.md) |
|
||||
| token verifying | [`JwtAuthenticationFilter`](../src/main/java/com/ankurm/jwtauth/auth/JwtAuthenticationFilter.java) | [02](02-filter-chain-and-ordering.md) |
|
||||
| claim validation | [`JwtValidatorFactory`](../src/main/java/com/ankurm/jwtauth/config/JwtValidatorFactory.java) | [07](07-edge-cases.md) |
|
||||
| key material | [`Hs256KeyConfig`](../src/main/java/com/ankurm/jwtauth/config/Hs256KeyConfig.java) / [`Rs256KeyConfig`](../src/main/java/com/ankurm/jwtauth/config/Rs256KeyConfig.java) | [05](05-hs256-vs-rs256.md) |
|
||||
| authorization rules | [`SecurityConfig`](../src/main/java/com/ankurm/jwtauth/config/SecurityConfig.java) | [03](03-401-vs-403.md) |
|
||||
| revocation | [`RevokedTokenStore`](../src/main/java/com/ankurm/jwtauth/auth/RevokedTokenStore.java) | [07](07-edge-cases.md) |
|
||||
|
||||
## The one-sentence version
|
||||
|
||||
A JWT deployment is an **issuer** that trades a password for a signed claims set, and a
|
||||
**verifier** that trades a signed claims set for an `Authentication` — and every failure
|
||||
mode in this repository comes from one of the two doing slightly less checking than the
|
||||
other assumed.
|
||||
Reference in New Issue
Block a user