teecup/004_auth.sql
Erol Haagenrud fbd3f58a1c Ekte autentisering er bygget og verifisert. X-Debug-User-Id-stubben er helt fjernet, ingen fallback beholdt.
Ny flyt: magic-link (POST /auth/request-link → POST /auth/verify-link) + JWT-sesjon i HttpOnly/SameSite=Lax/dynamisk-Secure-cookie (30 dager), pluss /auth/logout og /auth/me. Ny migrasjon 004_auth.sql (unik e-post-indeks + magic_link_token-tabell).

Sikkerhetsdesignet fra Plan-agent-gjennomgangen holdt gjennom testing:

Token: secrets.token_urlsafe(32), kun SHA-256-hash lagres
Atomisk forbruk (UPDATE...RETURNING, ikke les-sjekk-skriv) — hindrer replay
Generisk respons uansett om e-posten finnes — hindrer enumerering
app_user opprettes først ved vellykket verifisering, ikke ved forespørsel — hindrer massopprettelse
Gamle uforbrukte lenker ugyldiggjøres når en ny utstedes
PyJWT (byttet fra python-jose pga. bredere sårbarhetsflate) med eksplisitt algorithms=["HS256"]
Ekte eksistens-sjekk mot app_user på hvert kall — en slettet bruker mister tilgang umiddelbart, ikke etter 30 dager
Alle 12 planlagte tester bestått, inkludert cooldown, token-ugyldiggjøring, utløp, tuklet JWT, slettet bruker, og at debug-headeren nå er helt uten effekt.

To ting funnet og fikset/dokumentert underveis:

ON CONFLICT (email) matchet ikke den nye partielle unike indeksen uten eksplisitt WHERE-klausul — fikset.
En reell, dypere RLS-bug (dokumentert i FEATURE_BACKLOG.md, ikke fikset her): organization-tabellens RLS-policy kaster en 500 i stedet for "se ingenting" når app.current_org leses tilbake som tomstreng (ikke NULL) på en gjenbrukt pool-tilkobling. Berører trolig alle 15 RLS-policyer i skjemaet — for stort og sensitivt (ADR-003-grunnmuren) til å hastefikse her, så jeg mitigerte det lokalt i /auth/me og satte det som punkt 1 i neste-steg-listen.
2026-07-16 15:16:53 +02:00

40 lines
1.9 KiB
SQL

-- =====================================================================
-- TeeCup — migrasjon 004
-- Ekte autentisering: magic-link + JWT-sesjon
-- =====================================================================
-- Kjøres etter 001 (skjema), 002 (roller) og 003 (scoring/blind draw).
--
-- MERK om nummerering: dette klaimer migrasjonsnummer 004, som CLAUDE.md/
-- FEATURE_BACKLOG løst har forutsatt til en fremtidig "kommunikasjon"-
-- migrasjon — den blir 005 i stedet. Ren nummerkonvensjon, ingen teknisk
-- konflikt.
-- =====================================================================
\set ON_ERROR_STOP on
-- ---------------------------------------------------------------------
-- 1. Unik e-post (mangler i 001 — nødvendig for at magic-link-flyten skal
-- kunne bruke e-post som race-trygg oppslagsnøkkel via ON CONFLICT).
-- ---------------------------------------------------------------------
CREATE UNIQUE INDEX app_user_email_unique ON app_user (email) WHERE email IS NOT NULL;
-- ---------------------------------------------------------------------
-- 2. Magic-link-tokens
-- ---------------------------------------------------------------------
-- Lagrer e-post, IKKE en user_id-FK: app_user-raden opprettes først når
-- noen faktisk beviser eierskap ved å løse inn en gyldig, uforbrukt token
-- (se app/routers/auth.py) — ikke når lenken bare forespørres. Dette er
-- identitetsnivå (som app_user), ingen organization_id/RLS.
CREATE TABLE magic_link_token (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email citext NOT NULL,
token_hash text NOT NULL UNIQUE,
expires_at timestamptz NOT NULL,
consumed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON magic_link_token (email);
-- Grants til runtime-rollen (den eier ikke tabellen, så den trenger eksplisitt DML).
GRANT SELECT, INSERT, UPDATE, DELETE ON magic_link_token TO teecup_app;