Files
Clarium/docs/adr/0002-server-side-sessions.md
T
mroberts de38590f46 docs: add book tracker design spec and auth ADRs
Spec covers data model, API, auth, frontend, error handling, and testing.
ADR-0001 records PBKDF2 + pepper (FIPS) over argon2; ADR-0002 records
revocable server-side sessions over Flask's signed cookie.
2026-10-02 13:38:46 -05:00

2.4 KiB

ADR-0002: Server-side sessions with opaque tokens hashed at rest

Date: 2026-10-02 Status: Accepted

Context

After login, the browser needs a credential for subsequent API calls. Flask's default session is a signed cookie (itsdangerous, HMAC-SHA1 by default) holding the session data client-side. That cookie cannot be revoked before it expires: logout only asks the browser to forget it, and a stolen cookie stays valid until SECRET_KEY rotates. Healthcare applications are expected to support automatic logoff after inactivity (HIPAA Security Rule, 45 CFR 164.312(a)(2)(iii)) and real revocation.

Decision

Store sessions in PostgreSQL, keyed by the SHA-256 of an opaque random token.

  • Issue: on login/register, token = secrets.token_urlsafe(32) (256 bits). Insert sha256(token), user_id, created_at, expires_at = now() + 30 minutes. The raw token exists only in the cookie.
  • Cookie: sid=<token>; HttpOnly; Secure; SameSite=Lax; Path=/api.
  • Validate and slide: one statement per authenticated request:
    UPDATE sessions
       SET expires_at = LEAST(now() + interval '30 minutes', created_at + interval '12 hours')
     WHERE token_hash = %s AND expires_at > now()
    RETURNING user_id;
    
    This gives a 30-minute idle timeout and a 12-hour absolute cap. No row → 401.
  • Revoke: logout deletes the row. Deleting a user cascades to their sessions.
  • Cleanup: a user's expired rows are deleted when they log in; no scheduled job.

CSRF

The cookie is SameSite=Lax, so browsers do not send it on cross-site POST/PATCH/DELETE. In addition, every mutating route requires Content-Type: application/json; a cross-origin request with that type triggers a CORS preflight, and the API never grants CORS. Together these remove the need for a CSRF token.

Consequences

  • Logout and server-side expiry actually revoke access.
  • A database leak exposes only token hashes, which cannot be replayed as cookies.
  • One indexed UPDATE per authenticated request; negligible at this scale.
  • Hashing tokens with plain SHA-256 (not a slow KDF) is correct here: tokens carry 256 bits of entropy, so brute force is infeasible.

Known gaps (out of scope)

  • No login rate limiting or lockout. Online password guessing is bounded only by PBKDF2 cost. Before production, add per-username and per-IP throttling.
  • No "log out other devices" UI, though the schema supports it (DELETE … WHERE user_id = %s).