Files
HealthLog/prisma/migrations/0204_v123_trusted_devices/migration.sql
T
Marc-André Bombeck 135bb04f0d feat(auth): allow trusting a device to skip the second factor
Add an opt-in "remember this device" at the second-factor step. On opt-in
a random token is minted, set in an httpOnly+Secure+SameSite=Strict cookie,
and only its HMAC hash is stored in a new TrustedDevice table. A later sign-in
from the same browser still requires the password (factor 1); a matching,
unexpired trusted device lets the login skip factor 2 only.

The token is never accepted for step-up — a trusted-device login does not stamp
mfaVerifiedAt — so a stolen device cookie cannot reach a destructive action.
Trusted devices are listed and revocable from the security surface, are revoked
wholesale when the second factor is removed or the user signs out everywhere,
and cascade away on account deletion. A recovery-code login never trusts the
browser. Migration 0204 adds the table (and the instance-wide mfa_required
policy column that the enforcement work reads).
2026-06-27 21:49:39 +02:00

36 lines
1.8 KiB
SQL

-- v1.23 — admin-enforced MFA policy + "remember this device" trusted devices.
--
-- 1. `app_settings.mfa_required` — the instance-wide "require a second factor"
-- policy. When true, every account without an active second factor is sent
-- through a forced-enrollment interstitial after sign-in. The effective
-- per-user requirement is `mfa_required OR users.mfa_enforced`.
--
-- 2. `trusted_devices` — the opt-in "remember this device" store. Only the HMAC
-- hash of the cookie token is persisted (keyed by API_TOKEN_HMAC_KEY); the
-- raw token lives only in the user's httpOnly+Secure cookie. A trusted
-- device skips factor 2 within the 30-day window but never satisfies step-up
-- and never replaces the password. Revoked when the second factor is removed.
ALTER TABLE "app_settings" ADD COLUMN "mfa_required" BOOLEAN NOT NULL DEFAULT false;
CREATE TABLE "trusted_devices" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"label" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3) NOT NULL,
"last_used_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "trusted_devices_pkey" PRIMARY KEY ("id")
);
-- The token hash is the lookup key on every trusted-device sign-in, and a
-- token is single-purpose per device, so the hash is globally unique.
CREATE UNIQUE INDEX "trusted_devices_token_hash_key" ON "trusted_devices"("token_hash");
CREATE INDEX "trusted_devices_user_id_idx" ON "trusted_devices"("user_id");
-- The expired-trusted-device reaper sweeps on `expires_at`.
CREATE INDEX "trusted_devices_expires_at_idx" ON "trusted_devices"("expires_at");
ALTER TABLE "trusted_devices" ADD CONSTRAINT "trusted_devices_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;