Runnable companion for https://ankurm.com/spring-security-7-1-jwt-authentication-guide/ - login -> token issue -> OncePerRequestFilter -> SecurityContext, end to end - HS256 and RS256 variants (RS256 publishes a real JWKS endpoint) - the same API secured by the built-in oauth2ResourceServer().jwt(), for comparison - 11 documentation chapters under docs/, interlinked with the code - docs/output/ is real captured output, regenerated by scripts/run-all.sh - 13 passing tests pinning the 401-vs-403 contract and the CSRF failure Verified against Spring Boot 4.1.1, Spring Security 7.1.1, JDK 25.0.4.1.
133 lines
5.6 KiB
Markdown
133 lines
5.6 KiB
Markdown
# 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.
|