module-17/token-rotator.mdPRODUCTION MODULE

The Token Rotator

Sessions that feel permanent to users and expire in minutes for attackers.

A stolen JWT with a long expiry is a skeleton key — and short expiries punish every legitimate user instead.
Sliding sessions: 15-minute access tokens, HttpOnly refresh tokens with rotation, and family revocation for an instant kill switch across every device.

The incident

A JWT with a 30-day expiry leaked through a logged request header. For the rest of those 30 days it was a skeleton key — no way to revoke it, because the whole point of stateless tokens was that the server kept no state to revoke against.

Why the obvious approach fails

Stateless JWTs trade revocability for scale, and naive implementations pay that price at the worst moment: after a theft. Long expiries mean a stolen token works for weeks. Short expiries punish every legitimate user with re-logins. Storing tokens in localStorage hands them to any XSS. The resolution isn't picking a better expiry — it's splitting the problem: a short-lived token for API calls, a long-lived, httpOnly, rotating token to mint new ones, and server-side state for exactly one thing — the ability to say no.

The standard

  • Access tokens live ~15 minutes and are the only credential API calls accept.
  • Refresh tokens live in HttpOnly, Secure, SameSite cookies — never readable by JavaScript, never in localStorage.
  • Every refresh rotates the token and invalidates its predecessor; a replayed old refresh token kills the whole token family (theft detection built in).
  • Password change, explicit logout, and admin action revoke the family — sessions die on every device within one access-token lifetime.
  • Mobile and SPA flows use OAuth2/OIDC with PKCE; implicit flow is banned.

How Cortex validates it

At plan time

Auth blueprints must declare token lifetimes, storage, rotation, and revocation semantics; localStorage token storage fails plan review.

At validation

assert: access_token.ttl <= 15massert: refresh_token.cookie_flags == [HttpOnly, Secure, SameSite]assert: replay(rotated_refresh) → family_revoked

Assertion syntax is illustrative.

At review

The approver sees the theft simulation: stolen refresh token replayed, family revoked, all sessions dead, with the audit trail attached.

Composes with

See where your codebase stands against this standard.