Skip to content

ADR-0003: Session-Scoped JWT Signing (SCv2)

Status: ACCEPTED Date: 2026-03-19 Supersedes: N/A Superseded by: N/A

Context

The platform originally used a single application-wide JWT signing secret (slaunchx.security.jwt.secret). All tokens were verified against this global secret. Revoking a session required either maintaining a blacklist or waiting for token expiry, because any valid token could pass signature verification regardless of session state.

Source: docs/archive/plans/2026-03-19-session-scoped-jwt-signing-implementation-plan.md, docs/archive/specs/2026-03-19-scv2-crypto-hardfix-design-spec.md

Decision

Replace the global JWT signing secret with one random HMAC secret per authenticated session. The secret is generated at login, stored inside UserSessionInfo in Redis alongside session metadata, and used to sign both access and refresh tokens. JwtTokenHandler decodes sid from the token, loads the session from Redis, and verifies the signature using that session's secret.

Consequences

  • Session termination (logout, admin kill, Redis TTL expiry) immediately invalidates all tokens for that session without a blacklist.
  • Each session is cryptographically isolated; compromising one session's secret does not affect others.
  • Token verification now requires a Redis lookup per request (already performed for fingerprint validation, so no additional round-trip).
  • Global secret rotation is no longer needed for routine security hygiene.

Alternatives Considered

  • Token blacklist with global secret: Rejected because blacklists grow unboundedly and require distributed consistency.
  • Separate secrets for access and refresh tokens: Rejected because both tokens share the same session lifecycle; a single secret per session is simpler and equally secure.
  • Short-lived tokens without session binding: Rejected because it does not provide immediate revocation capability.

Internal Handbook