feat(auth): refresh token rotation via jti tracking

Adds a `refresh_tokens` table (migration 003) with one row per live
refresh token, keyed by UUID jti. On every POST /api/auth/refresh the
old jti row is deleted and a new token pair is issued and stored. Using
a consumed token returns 401. Expired rows are pruned inline on each
successful rotation.

Server: Claims gains an optional `jti` field; make_refresh_token now
returns (jwt, jti); register/login insert the jti row; RefreshResponse
now carries both tokens. Client: stores the rotated refresh token from
the response. ARCHITECTURE.md: API table + Security Model updated.
Three new integration tests cover rotation, consumed-token rejection,
and chained rotations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-12 13:34:42 -07:00
parent 7d7c83ab28
commit b129664344
10 changed files with 280 additions and 31 deletions
+2 -1
View File
@@ -660,7 +660,7 @@ All endpoints are under the base URL configured by the user (e.g., `https://soli
|---|---|---|---|---|
| POST | `/api/auth/register` | None | `{username, password}` | `{access_token, refresh_token}` |
| POST | `/api/auth/login` | None | `{username, password}` | `{access_token, refresh_token}` |
| POST | `/api/auth/refresh` | None | `{refresh_token}` | `{access_token}` |
| POST | `/api/auth/refresh` | None | `{refresh_token}` | `{access_token, refresh_token}` (rotated) |
### Sync
@@ -1020,6 +1020,7 @@ Migrations run automatically on startup via `sqlx::migrate!()`.
| Password storage | bcrypt, cost factor 12 — never stored in plaintext |
| Token security | JWTs signed with HS256, stored in OS keychain via `keyring` crate |
| Token expiry | Access: 24h, Refresh: 30d |
| Refresh token rotation | Each `/api/auth/refresh` call consumes the incoming refresh token (deletes its jti row) and issues a new one. Reuse of a consumed token returns 401. Expired rows are pruned inline. |
| Brute force | `tower-governor`: 10 req/min per IP on `/api/auth/*` |
| Payload abuse | 1MB max request body, enforced by Axum middleware |
| Data deletion | `DELETE /api/account` removes all rows via `ON DELETE CASCADE` |