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.
98 lines
4.6 KiB
Markdown
98 lines
4.6 KiB
Markdown
# 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.
|