main
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.
jwt-auth-demo
Runnable companion code for Spring Security 7.1 JWT Authentication: The Complete Guide (Spring Boot 4.1) on ankurm.com.
Everything here was compiled and executed. Every file under docs/output/
is real program output, regenerated by 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
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. |
./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 and follow the trail.
| doc | covers |
|---|---|
| 01 — Architecture | the whole request path, drawn |
| 02 — Filter chain and ordering | where a custom filter goes, and the four ways to place it wrong |
| 03 — 401 vs 403 | ExceptionTranslationFilter's actual decision, and RFC 6750 headers |
| 04 — CSRF vs permitAll | why permitAll() still returns 403, and when to disable CSRF |
| 05 — HS256 vs RS256 | key handling, JWKS, rotation, algorithm confusion |
| 06 — SecurityContext and statelessness | explicit save, repositories, thread boundaries |
| 07 — Edge cases | 18 things that bite, each with the fix |
| 08 — Testing | what to pin, and the Boot 4 test-slice split |
| 09 — Manual filter vs resource server | a side-by-side, and which to pick |
| 10 — Production checklist | the list to run before you ship |
| 11 — What changed in Spring Security 7 | the 7.x-specific surprises this repo hit |
Captured output
| file | what it shows |
|---|---|
curl-transcript-hs256.txt |
20 steps: login → token → 401 → 403 → tamper → refresh → revoke |
rs256-demo.txt |
JWKS, alg=RS256, signature sizes, tamper rejection |
csrf-vs-permitall.txt |
the 403 on a permitAll() endpoint |
csrf-trace.txt |
the TRACE log proving the chain stops at filter 5 of 12 |
expiry-and-clock-skew.txt |
a token still accepted 5s after exp |
resource-server-loose.txt |
a refresh token accepted as an access token |
resource-server-strict.txt |
the same request, refused |
test-run.txt |
13 passing tests |
Regenerate all of it:
./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.
License
MIT.
Languages
Java
81.6%
Shell
18.4%