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
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
3.0 KiB
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=Laxcookie. 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 againstlastSeenAt). 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 soapproval_requiredcan 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) anduser_identities(provider=password| futureoidc:<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.