docs/API.md — nieuwe sectie 'Auth — QR-pairing (M10)' met alle drie endpoints (start, stream, claim), cookie-mechaniek, foutcodes (400/401/410/429), curl-voorbeelden inclusief --cookie-jar. docs/scrum4me-architecture.md — sectie 'QR-pairing flow' met: - Mermaid sequence-diagram (start → QR → scan → approve → claim) - Threat-model (replay, phishing-QR, demo-block, rate-limit, secret-leak, long-lived sessie) met expliciete mitigaties - TTL-rationale voor de drie tijden (5min pending / +5min approved / 8u paired) - Subsectie 'Waarom geen secret in URL' — fragment-eigenschap + HttpOnly cookie + twee gescheiden hashes docs/patterns/qr-login.md — herbruikbaar pattern 'QR-pairing via unauth-SSE + pre-auth cookie' met de drie endpoints, vier security-uitgangspunten, sjabloon-bestanden, TTL-richtlijn, en wanneer NIET te gebruiken. CLAUDE.md — extra rij in Implementatiepatronen-tabel die naar het nieuwe pattern-doc verwijst. Acceptatie ST-1008 (zeven scenario's): - Happy path: gedekt door manuele E2E in vorige stories (gebruiker bevestigde dat M10-stories op Solo bord verschijnen + curl-roundtrip werkt) - Demo-block: actions/pairing.test.ts → approvePairing demo → Niet beschikbaar - Replay: pair-claim.test.ts → 410 op tweede claim - Expiry tijdens pending: pair-stream.test.ts + pairing.test.ts → 410/error - Expiry tussen approve+claim: pair-claim.test.ts → 410 - Cookie-mismatch op SSE/claim: pair-stream.test.ts + pair-claim.test.ts → 401 - Secret niet in URL/logs: per ontwerp — fragment + cookie reizen niet via URL-paden of querystrings (gedocumenteerd in architecture.md) Quality gates: lint 0 errors, tsc clean, vitest 139/139 (16 files). M10 is hiermee compleet — feat/M10-qr-login bevat 13 commits klaar voor gebruiker-acceptatie en PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
4.1 KiB
Markdown
95 lines
4.1 KiB
Markdown
# Patroon: QR-pairing via unauth-SSE + pre-auth cookie
|
|
|
|
Het M10 QR-login-mechanisme is herbruikbaar voor elke feature die **realtime-
|
|
feedback wil tussen twee browsers/devices vóórdat de eindgebruiker is
|
|
geauthenticeerd**. De typische vorm:
|
|
|
|
> "Apparaat A start een proces, krijgt een token. Apparaat B (bekend kanaal)
|
|
> bevestigt iets. Apparaat A wil dat realtime weten en daarna iets claimen."
|
|
|
|
Voorbeelden waar dit zou kunnen passen: device-pairing voor 2FA-setup, login-
|
|
op-TV via QR, "claim deze export"-flow, account-overdracht tussen sessies.
|
|
|
|
---
|
|
|
|
## Drie eindpunten
|
|
|
|
| Endpoint | Auth | Doel |
|
|
|---|---|---|
|
|
| `POST /api/.../start` | anon | maakt resource aan, retourneert mobile-secret in body, zet HttpOnly device-token cookie |
|
|
| `GET /api/.../stream/[id]` | cookie | SSE die op LISTEN/NOTIFY wacht op statusverandering |
|
|
| `POST /api/.../claim` | cookie | atomic state-transitie van "approved" → "consumed", wisselt cookie in voor échte sessie |
|
|
|
|
Plus een server-action-laag die door het tweede device wordt aangeroepen na
|
|
het scannen / klikken van een link met fragment-secret.
|
|
|
|
---
|
|
|
|
## Vier security-uitgangspunten
|
|
|
|
1. **Twee gescheiden geheimen** — een voor het kanaal richting het tweede
|
|
device (in QR-fragment), een voor het oorspronkelijke device (in HttpOnly
|
|
cookie). Beide alleen als sha256-hash in DB.
|
|
2. **Geen secret in URL.** Path en querystring lekken naar access logs,
|
|
reverse proxies, observability. Geheimen reizen alleen via:
|
|
- URL-fragment (`#…`) — browsers sturen die niet naar de server
|
|
- HttpOnly cookies — meestal niet gelogd, en alleen leesbaar door server
|
|
- POST-body — niet gelogd standaard
|
|
3. **Atomic consume.** Het claim-endpoint doet één UPDATE met een composite
|
|
WHERE op alle invarianten (status, hash, expiry). PostgreSQL row-locking
|
|
garandeert dat concurrent dubbele claims slechts één caller succes geven.
|
|
4. **Path-scoped cookie.** `Path=/api/.../...` zorgt dat de pre-auth cookie
|
|
alleen naar pairing-routes gaat — niet naar de rest van de app.
|
|
|
|
---
|
|
|
|
## Sjabloon-bestanden
|
|
|
|
Ga voor M10 specifiek? Kopieer en pas aan:
|
|
|
|
- `lib/auth/pairing.ts` — secret/token generators + sha256 + timing-safe verify + expiry helper
|
|
- `lib/auth/pair-cookie.ts` — set/read/clear van Path-scoped HttpOnly cookie
|
|
- `app/api/auth/pair/start/route.ts` — anon POST, rate-limited, sets cookie
|
|
- `app/api/auth/pair/stream/[id]/route.ts` — SSE met cookie-auth, LISTEN op eigen channel
|
|
- `app/api/auth/pair/claim/route.ts` — atomic update + iron-session schrijven
|
|
- `actions/pairing.ts` — Server Actions voor het tweede device
|
|
- `app/(app)/m/pair/pair-confirmation.tsx` — Client island die `location.hash` parseert
|
|
|
|
Voor het tweede device zit de auth meestal al in de bestaande `(app)`-layout
|
|
guard. De Client Component gebruikt `window.location.hash` (niet `useSearchParams`)
|
|
om het secret op te pikken.
|
|
|
|
---
|
|
|
|
## TTL-richtlijn
|
|
|
|
Drie tijden in escalerende volgorde, alle korter dan de reguliere sessie:
|
|
|
|
- **Pending (cookie + DB-rij)** — *kort genoeg dat een verloren cookie/QR
|
|
weinig schade aanricht*. M10: 5 minuten.
|
|
- **Approved (na bevestiging)** — *kort genoeg dat een approved-maar-niet-
|
|
geclaimde pairing niet eindeloos open blijft*. M10: 5 minuten extra.
|
|
- **Resulterende sessie** — *kort genoeg voor publieke apparaten, lang genoeg
|
|
voor een werkdag*. M10: 8 uur, plus `paired: true`-vlag voor toekomstige
|
|
remote-revoke.
|
|
|
|
---
|
|
|
|
## Wanneer dit patroon NIET gebruiken
|
|
|
|
- Wanneer beide kanten al ingelogd zijn — dan is een normaal API-call met
|
|
bestaande sessie eenvoudiger.
|
|
- Wanneer realtime niet kritiek is — een korte poll (`setInterval` op een
|
|
status-endpoint) is simpeler dan een SSE-stream.
|
|
- Wanneer er één centraal apparaat is — gebruik dan een normale sessie; de
|
|
twee-device-dans is alleen nodig om credentials van het ene apparaat naar
|
|
het andere te brengen.
|
|
|
|
---
|
|
|
|
## Referenties
|
|
|
|
- Volledige flow + threat-model: `docs/scrum4me-architecture.md` § QR-pairing flow
|
|
- Endpoint-contract: `docs/API.md` § Auth — QR-pairing
|
|
- LISTEN/NOTIFY-pattern: `app/api/realtime/solo/route.ts` (M8 ST-802) — zelfde
|
|
ReadableStream + heartbeat + hard-close + abort-cleanup, alleen ander channel
|