Files
HealthLog/prisma/migrations/0076_v1447_withings_oauth_state/migration.sql
T
Marc-André Bombeck 66a7f8b6fa chore(release): v1.4.47 — audit-backlog closure + drag-to-reorder + Coach disable + OAuth nonce ledger
Eight changes: W0 idempotency test pin, W1 legacy column drop, W2 Textarea primitive, W3 Coach disable toggle, W4 dnd-kit drag-to-reorder, W5 onboarding 24h gate, W6 Withings OAuth nonce ledger, W7+W8 legacy_form_total drop + Coach onLine pre-check. 6-axis QA reconciled. Migrations 0076/0077/0078 required pre-image. Tests 5093 → ~5170.
2026-05-22 09:18:52 +02:00

60 lines
2.6 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- v1.4.47 W6 — Withings OAuth state nonce ledger.
--
-- The v1.4.43 security audit (L-1) flagged the legacy state shape
-- `${user.id}:${random16}` as cosmetic-but-fragile: the user id is
-- recoverable from any request log entry where the cookie isn't
-- redacted, and from a network-traffic capture of the redirect. The
-- cookie is `httpOnly` + `Secure` + `sameSite:lax` today so the XSS
-- exfiltration surface is closed — but a future refactor that flips
-- the cookie to a non-httpOnly variant would silently expose the user
-- id. Audit recommendation was to switch the state to a fully-random
-- 16-byte nonce backed by a short-lived `(nonce → userId)` row.
--
-- This table is that ledger. Each `withings/connect` mint persists one
-- row; `withings/callback` looks it up, asserts `expiresAt > now()`,
-- and DELETEs the row in a single transaction so a replay of the same
-- state nonce against a fresh user session fails the CSRF check (the
-- row is gone after first consumption). A daily cron sweeps any
-- abandoned rows whose `expiresAt` has passed (user closed the
-- Withings tab without completing the handshake).
--
-- Schema choices:
--
-- * `nonce` is the primary key — it's already random and
-- 22-character base64url, so a dedicated `id` column would just
-- duplicate the key without buying anything.
-- * `userId` carries `ON DELETE CASCADE` so removing a user cleans
-- up their in-flight nonces atomically.
-- * `expiresAt` is indexed for the cleanup sweep — without it the
-- daily `DELETE … WHERE expiresAt < now()` would seq-scan the
-- entire table once a day even though the typical row count is
-- bounded by `users × concurrent in-flight handshakes`.
--
-- Idempotent guards (`IF NOT EXISTS`) match the 0067 / 0070 / 0071 /
-- 0074 / 0075 pattern so reruns are safe.
--
-- Reversibility: down migration is `DROP TABLE IF EXISTS
-- "withings_oauth_states"`. Additive-only; no existing row is
-- touched.
CREATE TABLE IF NOT EXISTS "withings_oauth_states" (
"nonce" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "withings_oauth_states_pkey" PRIMARY KEY ("nonce")
);
CREATE INDEX IF NOT EXISTS "withings_oauth_states_expires_at_idx"
ON "withings_oauth_states" ("expires_at");
DO $$ BEGIN
ALTER TABLE "withings_oauth_states"
ADD CONSTRAINT "withings_oauth_states_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;