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

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.