Three modules on Spring Boot 4.1.1 with Spring Authorization Server 7.1.1: the provider
itself, a relying party, and an API that trusts its tokens. Client registration, PKCE,
a custom consent page and token customisation, with profiles that make each failure
reproducible.
Every claim is backed by captured output in docs/output/as-*.txt, regenerated by
authorization-server/scripts/run-all.sh. Notable findings, verified against the jars:
- OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(HttpSecurity) was deleted
in 7.0, and both configuration classes moved into spring-security-config
- ClientSettings.requireProofKey flipped from false to true, on the authorization server
(1.5.8 -> 7.1.1) and on the OAuth2 client (6.5.1 -> 7.1.1)
- requireProofKey(false) does not make PKCE optional for a public client; the code
verifier is that client's only authentication at the token endpoint
- MediaTypeRequestMatcher(TEXT_HTML) matches Accept: */*, so the token endpoint answers
API callers with 302 -> /login unless setIgnoredMediaTypes(ALL) is called
Also renames the repository to spring-auth-demo and cross-links the new chapter set from
the existing documentation.
104 lines
5.0 KiB
Markdown
104 lines
5.0 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`](../jwt-authentication/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`](../jwt-authentication/src/main/java/com/ankurm/jwtauth/config/AppUsers.java) + `DaoAuthenticationProvider` | — |
|
|
| token minting | [`TokenService`](../jwt-authentication/src/main/java/com/ankurm/jwtauth/auth/TokenService.java) | [05](05-hs256-vs-rs256.md) |
|
|
| token verifying | [`JwtAuthenticationFilter`](../jwt-authentication/src/main/java/com/ankurm/jwtauth/auth/JwtAuthenticationFilter.java) | [02](02-filter-chain-and-ordering.md) |
|
|
| claim validation | [`JwtValidatorFactory`](../jwt-authentication/src/main/java/com/ankurm/jwtauth/config/JwtValidatorFactory.java) | [07](07-edge-cases.md) |
|
|
| key material | [`Hs256KeyConfig`](../jwt-authentication/src/main/java/com/ankurm/jwtauth/config/Hs256KeyConfig.java) / [`Rs256KeyConfig`](../jwt-authentication/src/main/java/com/ankurm/jwtauth/config/Rs256KeyConfig.java) | [05](05-hs256-vs-rs256.md) |
|
|
| authorization rules | [`SecurityConfig`](../jwt-authentication/src/main/java/com/ankurm/jwtauth/config/SecurityConfig.java) | [03](03-401-vs-403.md) |
|
|
| revocation | [`RevokedTokenStore`](../jwt-authentication/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.
|
|
|
|
---
|
|
|
|
A third project joined this repository later: a real OAuth2 / OIDC provider, with its own
|
|
client and resource server. Its architecture is a superset of the one drawn above —
|
|
see [`docs/authorization-server/`](authorization-server/README.md).
|