Files
HealthLog/prisma/migrations/0256_step_up_elevation/migration.sql
T
Marc-André Bombeck e557d9e9e6 feat(auth): add token-bound step-up elevations for second-factor management
Second-factor management has been cookie-only since v1.23. The gate is right:
a Bearer token is a credential whose provenance the server cannot see, and
tearing down a second factor must not be reachable from the weaker of an
interactive login and a value pasted into a script. What was missing was never
Bearer access to those routes — it was a way to say, over a transport with no
session row, that the human holding this device just re-proved a factor.

An elevation is that proof. It is minted only against a re-proved account
password or primary passkey, bound to the exact token that re-proved, single-use
via a conditional UPDATE, five-minute-lived to match the cookie path's step-up
window, and stored hashed. A stolen token mints nothing; a second token cannot
redeem another's elevation; a password change drops every one the account holds.

requireMfaManagementAuth is the one gate the management routes share. On a
cookie session it delegates to the original helpers unchanged, so the web flow
runs the code it always did. requireAdmin stays cookie-only and out of reach.
2026-07-19 12:42:00 +02:00

33 lines
1.5 KiB
SQL

-- v1.30.34 — token-bound step-up elevations.
--
-- The Bearer-transport equivalent of `sessions.mfa_verified_at`. A row is
-- minted only after a password or primary-passkey re-proof, is bound to the
-- exact API token that re-proved, is single-use (`consumed_at`), and expires
-- within minutes. Only the HMAC of the opaque secret is stored.
CREATE TABLE "step_up_elevations" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"api_token_id" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"method" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"consumed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "step_up_elevations_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "step_up_elevations_token_hash_key" ON "step_up_elevations"("token_hash");
CREATE INDEX "step_up_elevations_user_id_idx" ON "step_up_elevations"("user_id");
CREATE INDEX "step_up_elevations_api_token_id_idx" ON "step_up_elevations"("api_token_id");
CREATE INDEX "step_up_elevations_expires_at_idx" ON "step_up_elevations"("expires_at");
ALTER TABLE "step_up_elevations" ADD CONSTRAINT "step_up_elevations_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Cascade on the token binding: revoking or deleting a token must never leave a
-- redeemable elevation behind.
ALTER TABLE "step_up_elevations" ADD CONSTRAINT "step_up_elevations_api_token_id_fkey"
FOREIGN KEY ("api_token_id") REFERENCES "api_tokens"("id") ON DELETE CASCADE ON UPDATE CASCADE;