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.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# 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:
|
||||
```sql
|
||||
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`).
|
||||
Reference in New Issue
Block a user