1
0
Files
spring-auth-demo/docs/01-architecture.md
Ankur Mhatre 38c0a5f358 Add Spring Authorization Server project: OAuth2/OIDC provider, client and resource server
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.
2026-08-24 08:12:36 +05:30

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 &mdash;
see [`docs/authorization-server/`](authorization-server/README.md).