# jwt-auth-demo Runnable companion code for **[Spring Security 7.1 JWT Authentication: The Complete Guide (Spring Boot 4.1)](https://ankurm.com/spring-security-7-1-jwt-authentication-guide/)** on ankurm.com. Everything here was compiled and executed. Every file under [`docs/output/`](docs/output) is real program output, regenerated by [`scripts/run-all.sh`](scripts/run-all.sh) — not transcribed by hand. | | | |---|---| | JDK | Temurin **25.0.4.1+1** (current LTS) | | Spring Boot | **4.1.1** | | Spring Framework | **7.0.9** | | Spring Security | **7.1.1** | | Nimbus JOSE+JWT | **10.9.1** | | Tomcat | **11.0.24** | | Jackson | **3.1.5** (`tools.jackson`) | --- ## Quickstart ```bash git clone https://ankurm.com/git.app/asmhatre/jwt-auth-demo.git cd jwt-auth-demo ./scripts/run.sh hs256 # or: mvn spring-boot:run -Dspring-boot.run.profiles=hs256 # in another shell ./scripts/curl-transcript.sh # the whole flow, end to end ``` Three demo users: | username | password | authorities | |---|---|---| | `alice` | `alice-password` | `ROLE_USER`, `SCOPE_profile:read` | | `root` | `root-password` | `ROLE_USER`, `ROLE_ADMIN`, `SCOPE_profile:read`, `SCOPE_admin:read` | | `locked` | `locked-password` | locked account — always fails login | --- ## Profiles The same application demonstrates four axes. Combine them freely. | profile | what it changes | |---|---| | `hs256` *(default)* | Symmetric HMAC signing. One secret signs and verifies. | | `rs256` | RSA signing, plus a real `/.well-known/jwks.json` endpoint. | | *(none)* | Validation by a hand-written `OncePerRequestFilter`. | | `resourceserver` | Validation by Spring Security's built-in `oauth2ResourceServer().jwt()`. | | `strict` | Adds the `token_type` validator to the resource-server chain. | | `csrfon` | Turns CSRF on, reproducing the "permitAll() returns 403" failure. | | `shortlived` | 2-second access tokens, for observing expiry and clock skew. | | `trace` | `TRACE` logging for `org.springframework.security`. | ```bash ./scripts/run.sh rs256 ./scripts/run.sh hs256,resourceserver,strict ./scripts/run.sh hs256,csrfon,trace ``` --- ## Endpoints | method | path | rule | why it exists | |---|---|---|---| | `POST` | `/api/auth/login` | `permitAll()` | issues an access + refresh token pair | | `POST` | `/api/auth/refresh` | `permitAll()` | rotates the refresh token | | `POST` | `/api/auth/logout` | authenticated | revokes the presented token by `jti` | | `GET` | `/api/public/ping` | `permitAll()` | reachable with no token at all | | `GET` | `/api/me` | authenticated | **401** without a token | | `GET` | `/api/admin/stats` | `hasRole('ADMIN')` | **403** with a valid non-admin token | | `GET` | `/api/reports` | `@PreAuthorize` scope | the method-security twin of the above | | `GET` | `/api/public/filters` | `permitAll()` | prints the live filter chain | | `GET` | `/api/async-demo` | authenticated | `SecurityContext` across a thread boundary | | `GET` | `/.well-known/jwks.json` | `permitAll()` | `rs256` profile only | --- ## Documentation Start with [`docs/01-architecture.md`](docs/01-architecture.md) and follow the trail. | doc | covers | |---|---| | [01 — Architecture](docs/01-architecture.md) | the whole request path, drawn | | [02 — Filter chain and ordering](docs/02-filter-chain-and-ordering.md) | where a custom filter goes, and the four ways to place it wrong | | [03 — 401 vs 403](docs/03-401-vs-403.md) | `ExceptionTranslationFilter`'s actual decision, and RFC 6750 headers | | [04 — CSRF vs permitAll](docs/04-csrf-permitall-403.md) | why `permitAll()` still returns 403, and when to disable CSRF | | [05 — HS256 vs RS256](docs/05-hs256-vs-rs256.md) | key handling, JWKS, rotation, algorithm confusion | | [06 — SecurityContext and statelessness](docs/06-securitycontext-and-statelessness.md) | explicit save, repositories, thread boundaries | | [07 — Edge cases](docs/07-edge-cases.md) | 18 things that bite, each with the fix | | [08 — Testing](docs/08-testing.md) | what to pin, and the Boot 4 test-slice split | | [09 — Manual filter vs resource server](docs/09-manual-filter-vs-resource-server.md) | a side-by-side, and which to pick | | [10 — Production checklist](docs/10-production-checklist.md) | the list to run before you ship | | [11 — What changed in Spring Security 7](docs/11-spring-security-7-changes.md) | the 7.x-specific surprises this repo hit | --- ## Captured output | file | what it shows | |---|---| | [`curl-transcript-hs256.txt`](docs/output/curl-transcript-hs256.txt) | 20 steps: login → token → 401 → 403 → tamper → refresh → revoke | | [`rs256-demo.txt`](docs/output/rs256-demo.txt) | JWKS, `alg=RS256`, signature sizes, tamper rejection | | [`csrf-vs-permitall.txt`](docs/output/csrf-vs-permitall.txt) | the 403 on a `permitAll()` endpoint | | [`csrf-trace.txt`](docs/output/csrf-trace.txt) | the TRACE log proving the chain stops at filter 5 of 12 | | [`expiry-and-clock-skew.txt`](docs/output/expiry-and-clock-skew.txt) | a token still accepted 5s after `exp` | | [`resource-server-loose.txt`](docs/output/resource-server-loose.txt) | a refresh token accepted as an access token | | [`resource-server-strict.txt`](docs/output/resource-server-strict.txt) | the same request, refused | | [`test-run.txt`](docs/output/test-run.txt) | 13 passing tests | Regenerate all of it: ```bash ./scripts/run-all.sh ``` --- ## Security note The keys in `src/main/resources/` and the HMAC secret in `application.yaml` are **demo values committed on purpose** so the repository runs with no setup. They are public. Never point them at anything you care about — see [docs/10-production-checklist.md](docs/10-production-checklist.md). ## License MIT.