docs(ST-1008): document QR-pairing endpoints, flow, threat-model + pattern
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>
This commit is contained in:
parent
3a90fa9d13
commit
c87b6156ae
4 changed files with 269 additions and 0 deletions
94
docs/API.md
94
docs/API.md
|
|
@ -323,6 +323,100 @@ source.onmessage = (e) => console.log(JSON.parse(e.data))
|
|||
|
||||
---
|
||||
|
||||
## Auth — QR-pairing (M10)
|
||||
|
||||
Drie anonieme/cookie-geauthenticeerde endpoints voor de password-loze inlog
|
||||
via QR-pairing. Worden door de browser gebruikt (niet door Claude Code) —
|
||||
gedocumenteerd voor volledigheid en voor handmatige curl-tests.
|
||||
|
||||
**Cookie-mechaniek:** `pair/start` zet een korte `s4m_pair`-HttpOnly-cookie
|
||||
(`Path=/api/auth/pair`, `Max-Age=300`, `SameSite=Lax`, `Secure` in productie).
|
||||
`pair/stream` en `pair/claim` authenticeren tegen die cookie. Geheim materiaal
|
||||
zit nooit in URL-paden of querystrings — `mobileSecret` reist alleen via QR-
|
||||
fragment (`#s=…`) en POST-body, `desktopToken` alleen via cookie.
|
||||
|
||||
### `POST /api/auth/pair/start`
|
||||
|
||||
Anon. Maakt een nieuwe `LoginPairing` aan en zet de pre-auth cookie.
|
||||
|
||||
**Auth:** geen.
|
||||
**Body:** geen.
|
||||
**Rate-limit:** 10 per IP per minuut (zelfde patroon als `/login`).
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"pairingId": "cmoh...",
|
||||
"mobileSecret": "<43-char base64url>",
|
||||
"expiresAt": "2026-04-27T20:30:00.000Z",
|
||||
"qrUrl": "https://.../m/pair#id=cmoh...&s=<mobileSecret>"
|
||||
}
|
||||
```
|
||||
Plus `Set-Cookie: s4m_pair=<desktopToken>; HttpOnly; Path=/api/auth/pair; Max-Age=300; SameSite=Lax`.
|
||||
|
||||
**Foutcodes:** `429` bij rate-limit overschreden.
|
||||
|
||||
**Voorbeeld:**
|
||||
```bash
|
||||
curl -i -X POST -c /tmp/jar http://localhost:3000/api/auth/pair/start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/auth/pair/stream/:pairingId`
|
||||
|
||||
Server-Sent Events stream die de desktop opent direct na `pair/start` om op
|
||||
de approve-bevestiging van de mobiel te wachten.
|
||||
|
||||
**Auth:** `s4m_pair`-cookie. Werkt vanuit `EventSource` met `withCredentials: true`.
|
||||
**Path:** `pairingId` is niet vertrouwelijk; cookie is het bewijs.
|
||||
**Stream-duur:** maximaal 240s (Vercel-buffer onder de 300s `maxDuration`); sluit
|
||||
zodra status `consumed` of `cancelled` doorkomt.
|
||||
|
||||
**Events:**
|
||||
- `event: state` — eenmalig direct na connect, met `{ pairing_id, status }` (status van pairing op moment van connecten — voorkomt race wanneer approve net vóór SSE-open landt).
|
||||
- `data: {...}` — bij elke status-overgang. Payload:
|
||||
```json
|
||||
{ "op": "I" | "U", "pairing_id": "cmoh...", "status": "pending" | "approved" | "consumed" | "cancelled" }
|
||||
```
|
||||
- `: heartbeat` — SSE-comment elke 25s.
|
||||
|
||||
**Foutcodes:** `401` zonder/foute cookie, `404` als pairing onbekend, `410` als pairing verlopen.
|
||||
|
||||
**Voorbeeld:**
|
||||
```bash
|
||||
curl -N -i -b /tmp/jar http://localhost:3000/api/auth/pair/stream/<pairingId>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/auth/pair/claim`
|
||||
|
||||
Cookie-auth. Atomisch consume van een approved pairing → schrijft de echte
|
||||
`scrum4me-session` cookie zodat de desktop is ingelogd.
|
||||
|
||||
**Auth:** `s4m_pair`-cookie.
|
||||
**Body:** `{ "pairingId": "cmoh..." }`.
|
||||
|
||||
**Response 200:** `{ "ok": true }` plus
|
||||
- `Set-Cookie: scrum4me-session=...; HttpOnly; SameSite=Lax` — paired-sessie met `paired: true` en `pairedExpiresAt = now + 8h` payload-velden.
|
||||
- `Set-Cookie: s4m_pair=...; Max-Age=0` — pre-auth cookie wordt gewist.
|
||||
|
||||
**Foutcodes:**
|
||||
- `400` bij ontbrekende of malformed body
|
||||
- `401` zonder cookie of bij hash-mismatch (cookie matcht geen pairing)
|
||||
- `410` als pairing al consumed/cancelled is (replay) of verlopen
|
||||
|
||||
**Voorbeeld:**
|
||||
```bash
|
||||
curl -i -X POST -b /tmp/jar -c /tmp/jar \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"pairingId":"<pairingId>"}' \
|
||||
http://localhost:3000/api/auth/pair/claim
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Voorbeeldworkflow voor Claude Code
|
||||
|
||||
1. **Probe:** `GET /api/health?db=1` — bevestig dat de service en DB bereikbaar zijn.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue