dorfteich/docs/architecture/adr/0007-auth-sessions-oidc-ready.md
Claude Fable 5 db4c5ce9ca
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m53s
CI / Build container images (pull_request) Successful in 3m55s
CI / Auth e2e pack (pull_request) Successful in 7m53s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m53s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m37s
CI / Import/export fidelity gate (push) Successful in 52s
#190: configurable session lifetime with a server-side idle timeout
SESSION_ABSOLUTE_HOURS (default 168 h) caps a session's total lifetime
from login: expiresAt is set once at creation and never extended — the
old sliding 30-day renewal is gone. SESSION_IDLE_HOURS (default 72 h)
ends sessions unused for that long, enforced server-side against
lastSeenAt with a write throttle scaled to the idle bound so short idle
windows still renew. Expired rows are removed on validation and the
session list applies both bounds, so idle-dead sessions never show as
active. The cookie maxAge follows the configured absolute bound.

Documented in .env.example (with the VS-NfD reference values for the
upcoming hardening guide #227), compose passes the variables through,
security.md and ADR 0007 record the amendment.

Refs #190

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 11:11:15 +02:00

3.0 KiB

ADR 0007: Cookie sessions, Argon2id, OIDC-ready identity model

  • Status: accepted
  • Date: 2026-07-04

Context

Kickoff decisions: self-contained user management (username, e-mail, password) with mandatory e-mail verification (double opt-in), password reset, rate limiting, and the option to disable self-registration per instance. SSO is not in the MVP but the design must allow adding OIDC login later without schema surgery.

Decision

Authentication

  • Server-side sessions stored in PostgreSQL, referenced by an opaque HttpOnly; Secure; SameSite=Lax cookie. No JWTs for browser sessions (revocability and simplicity win). Sliding expiration, default 30 days. Amended by issue #190 (2026-07-30): a configurable absolute bound (SESSION_ABSOLUTE_HOURS, default 7 days, never extended by activity) plus a configurable idle bound (SESSION_IDLE_HOURS, default 3 days, renewed by activity, enforced server-side against lastSeenAt). The sliding 30-day expiry is gone.
  • Passwords hashed with Argon2id (tuned parameters documented in code).
  • E-mail flows (verification, password reset) use single-use, expiring, hashed tokens; mail is sent via SMTP (instance-configured, see setup wizard) through a mail outbox table with retry.
  • Rate limiting on signup, login, password reset, and token endpoints: fixed-window counters in PostgreSQL keyed by IP and by account — no Redis (ADR 0002).
  • Self-registration can be disabled instance-wide (instance_settings.registration_mode: open | closed). The kickoff also asked for the option of admin approval as a later hardening step; the setting is an enum so approval_required can be added without migration pain.
  • Collaboration tokens: the API issues short-lived (≤ 60 s validity for connect) signed JWTs solely for the WebSocket handshake with the collab server (ADR 0003). These are the only JWTs in the system.

OIDC readiness (not in MVP)

  • The identity model separates account from login method: users (profile, status) and user_identities (provider = password | future oidc:<issuer>, subject, credential). Password login is just one identity row.
  • E-mail is unique per user and verified; future OIDC linking matches on verified e-mail or explicit account linking.

Consequences

  • Logout and account deactivation are immediate (session rows deleted).
  • No shared secret sprawl: one signing key for collab tokens, rotated via environment configuration.
  • Adding OIDC later means: new identity provider rows, an authorization-code flow module, and a login button — no changes to sessions, permissions, or user references.

Alternatives considered

  • JWT access/refresh tokens in the browser: harder revocation, XSS exposure of tokens, no benefit for a same-origin SPA. Rejected.
  • Auth libraries/services (Keycloak, Authentik as mandatory): heavy extra container contradicting easy self-hosting; external IdPs remain possible later through the OIDC path.