Files

28 lines
2.1 KiB
Markdown

# refresh-tokens
Companion code for [JWT Refresh Token Rotation in Spring Security 7 (With Reuse Detection)](https://ankurm.com/spring-security-jwt-refresh-token-rotation-reuse-detection/).
Short-lived access JWT, opaque rotating refresh token, token families, reuse detection, logout on one device and everywhere. H2 in memory, no external services.
Run it: `mvn test` (JDK 25, Spring Boot 4.1.2, Spring Security 7.1.2). Every test writes what it observed to [`output/`](output), and `scripts/run-all.sh` regenerates all of it, including a real HTTP walkthrough.
| file | what it shows |
|---|---|
| `01-rotation.txt` | login, two rotations, one family; the access token carries the family id as `sid` |
| `02-storage.txt` | the table holds the SHA-256 of the token, never the token |
| `03-reuse-detection.txt` | a replayed token revokes the family, including the innocent holder's newest token |
| `04-families-are-independent.txt` | a replay on the phone does not touch the laptop's login |
| `05-concurrent-refresh-strict.txt` | 8 simultaneous requests with one token: 1 wins, the family is then revoked |
| `06-concurrent-refresh-grace.txt` | the same stampede with `reuse-grace=5s`: the family survives, the late replay still kills it |
| `07-logout.txt` | single-device logout, logout everywhere, and the access token that outlives both |
| `08-check-sid.txt` | `demo.check-sid=true` makes the access token die with its family |
| `09-expiry.txt` | idle timeout versus the absolute family cap |
| `10-clock-skew.txt` | Spring Security's default validator accepts a token 30 seconds past `exp` |
| `11-identical-refusals.txt` | unknown, reused, revoked and expired answer with the same bytes |
| `12-http-walkthrough.txt` | the same story over real HTTP with curl |
Settings in `application.yaml`: `demo.jwt.access-ttl` (5m), `demo.refresh.token-ttl` (7d), `demo.refresh.family-max-age` (30d), `demo.refresh.reuse-grace` (0s), `demo.check-sid` (false).
Token fingerprints and family ids differ on every run because the tokens are random. Statuses, counts and HTTP codes do not.
The signing secret in `application.yaml` is a demo value. Read yours from a secret manager.