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

5.0 KiB

01 — Architecture

← README · next: filter chain and ordering →

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 and step 19 of curl-transcript-hs256.txt.

Where each concern lives

concern class doc
password check AppUsers + DaoAuthenticationProvider
token minting TokenService 05
token verifying JwtAuthenticationFilter 02
claim validation JwtValidatorFactory 07
key material Hs256KeyConfig / Rs256KeyConfig 05
authorization rules SecurityConfig 03
revocation RevokedTokenStore 07

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/.