Scrum4Me/lib/auth/pair-cookie.ts
Madhura68 b4813e6e54 feat(ST-1002): add pairing helpers, pre-auth cookie + paired-session guard
lib/auth/pairing.ts: pure crypto-helpers voor de QR-pairing flow.
- generateMobileSecret() / generateDesktopToken() — beide 32 bytes base64url, los
  zodat ze elkaar niet onthullen
- hashToken(t) — sha256-hex
- verifyToken(t, hash) — timingSafeEqual met length-guard
- isPairedSessionExpired(session) — geëxtraheerde helper zodat de Server-
  Component-render Date.now() niet rechtstreeks aanroept (React Compiler-flag)

lib/auth/pair-cookie.ts: HttpOnly pre-auth cookie helpers (s4m_pair).
- Path=/api/auth/pair, Max-Age=120s (gelijk aan pending-TTL pairing),
  SameSite=Lax, Secure in productie

lib/session.ts: SessionData uitgebreid met optionele paired + pairedExpiresAt.

app/(app)/layout.tsx: guard die paired-sessies vernietigt zodra
pairedExpiresAt verstreken is en redirect naar /login.

Tests: 14 unit-tests in __tests__/lib/auth/pairing.test.ts dekken hash-
determinisme, timing-safe verify (true/false/length-mismatch), generator-
uniciteit en vier expiry-scenario's voor isPairedSessionExpired.

Quality gates: npm run lint (0 errors), tsc --noEmit clean, vitest 111/111.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:23:00 +02:00

33 lines
1.1 KiB
TypeScript

// ST-1002: HttpOnly pre-auth cookie voor de QR-pairing desktop-side.
//
// Wordt gezet door /api/auth/pair/start (ST-1003), gelezen door
// /api/auth/pair/stream/[id] (ST-1004) en /api/auth/pair/claim (ST-1006),
// en gewist op claim of cancel. Path-scoped naar /api/auth/pair zodat de
// cookie niet naar andere routes lekt.
import { cookies } from 'next/headers'
const COOKIE_NAME = 's4m_pair'
const MAX_AGE_SECONDS = 120 // gelijk aan pending-TTL van LoginPairing
const COOKIE_PATH = '/api/auth/pair'
export async function setPairCookie(desktopToken: string): Promise<void> {
const jar = await cookies()
jar.set(COOKIE_NAME, desktopToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: COOKIE_PATH,
maxAge: MAX_AGE_SECONDS,
})
}
export async function readPairCookie(): Promise<string | null> {
const jar = await cookies()
return jar.get(COOKIE_NAME)?.value ?? null
}
export async function clearPairCookie(): Promise<void> {
const jar = await cookies()
jar.delete({ name: COOKIE_NAME, path: COOKIE_PATH })
}