1.7 KiB
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.