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
67 lines
3.0 KiB
Markdown
67 lines
3.0 KiB
Markdown
# 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.
|