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