Sessions are stored as hashes, so a database dump contains nothing that can be pasted into a browser. A login that has passed the password but not the second factor is modelled as a pending session: it expires, is revocable, and reaches exactly one endpoint. Signup is invite-only and takes the email address from the invite rather than the form, so a link cannot be redirected to somebody else. The first admin is created by an endpoint that refuses once any account exists, checked and inserted in one transaction because it is necessarily unauthenticated. Login failures and forgotten-password requests answer identically whether or not the address has an account — otherwise the form is an account-existence oracle. Password rules are a length floor and nothing else: composition rules push people towards worse passwords. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
24 lines
1.1 KiB
SQL
24 lines
1.1 KiB
SQL
-- Sessions gain a two-step state, and TOTP gains a way back in.
|
|
|
|
-- A session created by a correct password but not yet by a correct TOTP code.
|
|
-- Such a session authenticates exactly one thing — completing the TOTP step — and
|
|
-- nothing else. Modelling the half-finished login as a session rather than as a
|
|
-- separate token means it expires, is listed, and can be revoked by the same code
|
|
-- that handles every other session.
|
|
ALTER TABLE sessions ADD COLUMN pending_totp INTEGER NOT NULL DEFAULT 0;
|
|
|
|
-- Single-use codes for someone who has lost their authenticator.
|
|
--
|
|
-- Without these, losing a phone means losing the account, and the only remaining
|
|
-- repair is editing SQLite by hand — which is not a recovery story you can put in
|
|
-- front of somebody who invited their partner to a shared shopping list.
|
|
CREATE TABLE totp_recovery_codes (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
|
code_hash TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
used_at INTEGER
|
|
);
|
|
|
|
CREATE INDEX idx_recovery_codes_user ON totp_recovery_codes (user_id) WHERE used_at IS NULL;
|