Release 0.11.0: trust device for 30 days
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
-- §6.2 / v0.11.0: trust device for 30 days (roadmap item #9).
|
||||
--
|
||||
-- After a successful OTC or passcode sign-in, the user can check
|
||||
-- "trust this device for 30 days." The framework then issues a
|
||||
-- server-issued opaque device-trust token, stores its hash on this
|
||||
-- table, and sets a long-lived HttpOnly + Secure + SameSite=Lax
|
||||
-- cookie carrying the raw token. On a subsequent visit, the cookie is
|
||||
-- presented at `/auth/device-trust/start`; if the server can match the
|
||||
-- hash to a non-expired non-revoked row, the user is signed in without
|
||||
-- another OTC / passcode round-trip.
|
||||
--
|
||||
-- v0.11.0 introduces no new env vars. The 30-day window is hard-coded
|
||||
-- in `backend/app/device_trust.py`; raising or lowering it (or making
|
||||
-- it user-selectable) is a §19.2 candidate, alongside the cross-device
|
||||
-- session-revocation surface this table will eventually share with the
|
||||
-- v0.10.0 passcode-lockout shape (see SPEC §19.2 / SESSIONS-AND-DEVICES).
|
||||
--
|
||||
-- Storage shape:
|
||||
--
|
||||
-- * `id` — surrogate key. Lets the revoke-device UI address a single
|
||||
-- row by id without leaking the token shape.
|
||||
-- * `user_id` — FK into users(id) with cascade on delete. A deleted
|
||||
-- user automatically loses every trusted device.
|
||||
-- * `device_token_hash` — bcrypt hash of the random opaque token
|
||||
-- issued at trust-time. The raw token only ever lives in the
|
||||
-- outbound `Set-Cookie` header and the inbound `Cookie` header;
|
||||
-- server-side storage is the hash, so a DB compromise does not
|
||||
-- hand attackers a stash of valid device tokens.
|
||||
-- * `created_at` — when the row was issued.
|
||||
-- * `expires_at` — `created_at + 30 days`. A row past this timestamp
|
||||
-- is dead; the lookup path refuses it without further checks.
|
||||
-- * `user_agent` — the User-Agent header captured at issuance.
|
||||
-- Stored verbatim (truncated to 1024 chars at the application
|
||||
-- layer) so the revoke-device UI can show a rough device label.
|
||||
-- Not used for any auth decision — purely a hint to the user
|
||||
-- reviewing their device list.
|
||||
-- * `last_seen_at` — refreshed every time the row authenticates a
|
||||
-- request. Lets the revoke-device UI surface "last used 3 days
|
||||
-- ago" so the user can tell which row corresponds to which
|
||||
-- device.
|
||||
-- * `revoked_at` — NULL means active; non-NULL stamps when the user
|
||||
-- (or admin) revoked the row. Lookups treat any non-NULL value
|
||||
-- as "this row is dead" without consulting the expiry; the
|
||||
-- revoke gesture is intentionally one-way (a revoked device must
|
||||
-- re-trust to come back online).
|
||||
--
|
||||
-- Indexing: a unique index on `device_token_hash` so collisions are
|
||||
-- detectable at insert time (the token space is 256 bits of CSPRNG
|
||||
-- entropy, so a collision is structurally impossible, but the
|
||||
-- declaration documents the invariant). A separate index on
|
||||
-- `(user_id, revoked_at)` so the revoke-device UI's list query is
|
||||
-- a covering walk.
|
||||
--
|
||||
-- The bcrypt dependency reused here was added in v0.7.0 for OTC and
|
||||
-- extended in v0.10.0 for passcodes; v0.11.0 needs no new dep.
|
||||
--
|
||||
-- The cookie shape: `rfc_device_trust` carries the raw token,
|
||||
-- HttpOnly, Secure, SameSite=Lax, Max-Age=2592000 (30 days). It is
|
||||
-- "essential" per the v0.13.0 cookie-consent banner (it is part of
|
||||
-- authentication, not analytics), so it is set regardless of the
|
||||
-- user's analytics / other-cookies choices.
|
||||
|
||||
CREATE TABLE device_trust (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
device_token_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL,
|
||||
user_agent TEXT NOT NULL DEFAULT '',
|
||||
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
revoked_at TEXT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_device_trust_token_hash ON device_trust (device_token_hash);
|
||||
CREATE INDEX idx_device_trust_user ON device_trust (user_id, revoked_at);
|
||||
Reference in New Issue
Block a user