# ADR-0008: Short access tokens, rotating refresh tokens, separate audiences - **Status:** Accepted - **Date:** 2026-08-11 ## Context Storefront customers and back-office staff authenticate against the same API. A single token type shared between them means an XSS on the storefront is a path into the admin. ## Decision A short-lived (15 min) stateless JWT access token carrying the permission set, plus a long-lived refresh token that is opaque to the client, delivered as an httpOnly SameSite cookie, stored server-side only as a SHA-256 hash, and rotated on every use. Rotation is tracked with `Session.replacedById`. Presenting an already-rotated refresh token means the token leaked, so the entire token family is revoked — theft detection, not just theft mitigation. Every token carries an audience (`storefront` or `admin`). Admin controllers declare `@RequireAudience('admin')`, and the check runs before any permission logic. ## Consequences A stolen access token expires in minutes. A stolen refresh token is detectable and self-revoking. A stolen storefront token is rejected by admin endpoints on audience alone, before permissions are consulted. The trade-off is that permission changes are not instantaneous — bounded by the access token lifetime. For an immediate kill switch, `CACHE_KEYS.revokedSession` exists as a Redis denylist checked per request; it is deliberately not enabled by default because it reintroduces a hot-path lookup. ## Alternatives considered Server-side sessions — simpler to revoke, but adds a datastore read to every request and complicates a future mobile client. Long-lived access tokens — rejected: the blast radius of a leak is unacceptable for a system holding payment and address data.