Table of Contents
- ADR-0003: Auth — short-lived access JWT + rotating refresh tokens, with RBAC
- Context
- Decision
- Consequences
ADR-0003: Auth — short-lived access JWT + rotating refresh tokens, with RBAC¶
- Status: Accepted
- Date: 2026-06-26
- Deciders: Backend team
Context¶
The API is stateless and horizontally scaled behind a load balancer, so it must not hold per-instance session state. We need authentication that (a) lets any replica verify a caller without a shared session store on the hot path, (b) limits the blast radius of a leaked credential, © supports logout/revocation, and (d) gates each operation by the caller's tenant role.
Decision¶
Access tokens: short-lived JWT (HS256).
- Claims:
sub(user),org_id(tenant),role,scope=access,iss,aud,iat,exp,jti. TTL ~15 minutes. - Verified in-process by every replica (
platform/auth.JWTIssuer) — no store lookup on the request path. The signing algorithm is pinned (HS256) to defeatalgconfusion, andiss/audare validated.
Refresh tokens: opaque, rotating, hashed at rest.
- A 256-bit random value; only its SHA-256 hash is stored in
refresh_tokens. - On
/auth/refreshthe presented token is revoked and a successor is issued (replaced_bylinks the chain) — single-use rotation. - Reuse detection: presenting an already-revoked refresh token implies theft; we revoke the user's entire token family and force re-login.
/auth/logoutrevokes the active token. TTL ~30 days.
Authorization: RBAC by permission, not by role.
- Roles
owner > admin > member; the role→permission matrix lives in exactly one place (domain.Role.Can). Handlers and services check permissions (project:create,member:manage, …), not roles directly. - Checks happen twice: a fast route-level
requirePermissionmiddleware and an authoritative check inside each use-case (defense in depth). Special cases (admins cannot modify owners; members may delete only their own tasks) live in the use-case.
Passwords: bcrypt, cost ≥ 12, timing-safe compare; the same vague
invalid credentials error for unknown-user and wrong-password.
Consequences¶
Positive
- Stateless verification scales horizontally; the DB is touched only on login/refresh/logout, not on every request.
- Short access TTL caps exposure; rotation + reuse detection turns a stolen refresh token into a detectable, self-limiting event.
- One RBAC source of truth is exhaustively unit-tested across the matrix.
Negative / costs
- A leaked signing key forges access tokens until rotated — mitigated by a
rotatable
kidstrategy (future) and key management via secrets, never code. - A valid access token cannot be revoked before its (short) expiry; we accept this for the 15-minute window rather than checking a denylist per request.
Neutral
org_idlives in the token, so switching active organization means issuing a token for another membership (a future/auth/switchendpoint).