From 3094df84c8e8bd8ce5a52906b0c485eab9ca5332 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 17 Jun 2026 17:11:31 -0400 Subject: [PATCH] =?UTF-8?q?feat(19-05):=20Option=20C=20=E2=80=94=20devSess?= =?UTF-8?q?ionCookieMiddleware=20issues=20real=20local-session=20cookie=20?= =?UTF-8?q?under=20bypass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add devSessionCookieMiddleware() to devBypass.ts (production hard-guard FIRST) - Issues local-session JWT cookie for DEV_USER when no cookie present under bypass - Pure no-op when NODE_ENV=production, DEV_AUTH_BYPASS!=true, or secret not set - Mount devSessionCookieMiddleware() after devAuthBypass() in index.ts - Existing devBypass tests: 3/3 pass; typecheck: exit 0 --- apps/api/src/auth/devBypass.ts | 57 +++++++++++++++++++++++++++++++++- apps/api/src/index.ts | 8 ++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/apps/api/src/auth/devBypass.ts b/apps/api/src/auth/devBypass.ts index 2050c59..d810838 100644 --- a/apps/api/src/auth/devBypass.ts +++ b/apps/api/src/auth/devBypass.ts @@ -16,15 +16,24 @@ * skipping the DB upsert and getAuth path entirely. Other routes (e.g. events) * also read c.get('user') directly — same pattern, no change needed there. * + * Phase 19 — Option C (AUTH-LOCAL-16, D-14/D-15): + * devSessionCookieMiddleware() complements devAuthBypass() by issuing a real + * local-session JWT cookie for DEV_USER on each request that lacks one. This lets + * the PWA login gate (which checks the local-session cookie) see a valid session and + * skip to the app, so existing Phase 7/8 Playwright specs still reach the authed PWA + * without manual login. Mount AFTER devAuthBypass() in index.ts. + * * Security: * - The FIRST conditional is always `NODE_ENV === 'production'` — checked before reading - * any other env var. This is the hard guard (T-02-01). Even if DEV_AUTH_BYPASS is + * any other env var. This is the hard guard (T-02-01 / T-19-24). Even if DEV_AUTH_BYPASS is * accidentally set in production config, the guard fires and returns a no-op. * - The production Docker Compose MUST NOT set DEV_AUTH_BYPASS. See docs/deployment.md. * - This file must never be removed — the pattern is referenced by Plan 02 routes. */ import type { MiddlewareHandler } from 'hono'; +import { getCookie } from 'hono/cookie'; +import { issueLocalSessionCookie } from './localSession.js'; import { COLOR_PALETTE } from './user.js'; export const DEV_USER = { @@ -74,3 +83,49 @@ export function devAuthBypass(): MiddlewareHandler { await next(); }; } + +/** + * Phase 19 Option C (AUTH-LOCAL-16): issues a real local-session JWT cookie for DEV_USER + * so the PWA login gate sees a valid session and skips /login during dev-bypass runs. + * + * Mount AFTER devAuthBypass() on /api/* in index.ts. This middleware is a pure no-op + * passthrough in all non-bypass contexts: + * 1. NODE_ENV === 'production' → immediate no-op (hard guard, T-19-24 / D-15) + * 2. DEV_AUTH_BYPASS !== 'true' → immediate no-op (inactive outside bypass mode) + * 3. LOCAL_SESSION_SECRET not set → no-op (issueLocalSessionCookie will throw, but + * in bypass mode the boot guard exempts the secret check — skip gracefully) + * 4. 'local-session' cookie already present → no-op (avoids re-signing on every request) + * + * Security: the production hard-guard is the FIRST check — identical guard order to + * devAuthBypass() so assertNotDevBypassInProduction (IMG-01) catches both at boot. + */ +export function devSessionCookieMiddleware(): MiddlewareHandler { + // Hard production guard — FIRST check, before reading any other env var. + // Ensures this middleware can never issue a session cookie in production. + if (process.env.NODE_ENV === 'production') { + return async (_c, next) => next(); + } + + // Bypass flag not set — passthrough; no cookie is issued. + if (process.env.DEV_AUTH_BYPASS !== 'true') { + return async (_c, next) => next(); + } + + // LOCAL_SESSION_SECRET not set — bypass mode exempts the secret requirement + // (assertLocalSessionSecretSet skips when DEV_AUTH_BYPASS=true), but we cannot + // issue a cookie without it. Degrade gracefully so devAuthBypass still works. + if (!process.env.LOCAL_SESSION_SECRET) { + return async (_c, next) => next(); + } + + // Bypass active + secret set: issue a real local-session cookie for DEV_USER + // on each request that does not already carry one. + return async (c, next) => { + const existing = getCookie(c, 'local-session'); + if (!existing) { + // issueLocalSessionCookie is async (JWT sign) — await before next() + await issueLocalSessionCookie(c, DEV_USER.id); + } + await next(); + }; +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 205afe7..6616545 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -18,7 +18,7 @@ import { processOAuthCallback, oidcConfigFallbackMiddleware, } from './auth/middleware.js'; -import { devAuthBypass } from './auth/devBypass.js'; +import { devAuthBypass, devSessionCookieMiddleware } from './auth/devBypass.js'; import { localAuthMiddleware } from './auth/localAuthMiddleware.js'; import { persistSessionCookie } from './auth/persistSessionCookie.js'; import { startBrokerPoller } from './broker/poller.js'; @@ -119,6 +119,12 @@ app.route('/api/auth', localAuthRouter); // Must be mounted BEFORE oidcAuthMiddleware (T-02-01 mitigation; see auth/devBypass.ts). app.use('/api/*', devAuthBypass()); +// Phase 19 Option C (AUTH-LOCAL-16, D-14/D-15): issue a real local-session cookie for DEV_USER +// under bypass so the PWA login gate sees a valid session and skips /login. Pure no-op outside +// bypass mode (production guard is FIRST check — T-19-24; see auth/devBypass.ts). +// Mount AFTER devAuthBypass() so DEV_USER is already in context; BEFORE localAuthMiddleware. +app.use('/api/*', devSessionCookieMiddleware()); + // Phase 19 — local-session middleware: sets c.get('user') from 'local-session' JWT cookie. // No-op passthrough when no cookie is present — the OIDC guard fires for unauthenticated. // Runs AFTER devAuthBypass (which may set c.get('user') first) and BEFORE the OIDC guard.