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