From 4b34b16f0242d212123de496dd84e9197f03f9c6 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 5 Jun 2026 13:48:00 -0400 Subject: [PATCH] fix(02): dev-auth bypass no longer blocked by oidcAuthMiddleware - index.ts: compute devBypassActive at startup; skip app.use(oidcAuthMiddleware) entirely when active so the OIDC guard never runs in local dev - routes/me.ts: read c.get('user') first; return dev identity directly when devAuthBypass injected it, bypassing getAuth() and the DB upsert - auth/devBypass.ts: add ContextVariableMap augmentation for 'user' key; correct stale comment that claimed getAuth/401 path was still active --- apps/api/src/auth/devBypass.ts | 24 ++++++++++++++++++------ apps/api/src/index.ts | 19 +++++++++++++++++-- apps/api/src/routes/me.ts | 28 +++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/apps/api/src/auth/devBypass.ts b/apps/api/src/auth/devBypass.ts index 3149473..4574e84 100644 --- a/apps/api/src/auth/devBypass.ts +++ b/apps/api/src/auth/devBypass.ts @@ -9,12 +9,12 @@ * When the bypass is inactive (wrong env, or NODE_ENV=production) the middleware is a * pure no-op passthrough — production behaviour is unchanged. * - * Context key: 'user' — matches the key read by downstream consumers (e.g. routes/me.ts - * calls getAuth(c) from @hono/oidc-auth; the events route will read c.get('user') directly). - * In dev bypass mode, c.get('user') returns DEV_USER. getAuth(c) is still called by me.ts - * but will return null because no OIDC session cookie is present; me.ts guards this with - * `if (!auth) return 401`. When using the bypass, consume c.get('user') directly in routes - * that need the user object (events route pattern in Plan 02). + * Context key: 'user' — matches the key read by downstream consumers. + * In dev bypass mode, c.get('user') returns DEV_USER. index.ts does NOT mount + * oidcAuthMiddleware when devBypassActive is true, so getAuth(c) is never called. + * routes/me.ts reads c.get('user') first and returns the dev identity directly, + * 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. * * Security: * - The FIRST conditional is always `NODE_ENV === 'production'` — checked before reading @@ -35,6 +35,18 @@ export const DEV_USER = { color: COLOR_PALETTE[0], // '#4A90D9' — first palette slot } as const +/** + * Extend Hono's ContextVariableMap so that c.get('user') / c.set('user', ...) + * are statically typed throughout the app. The value type is the DEV_USER shape, + * which is compatible with both the bypass path and any future app-level user object + * stored on context (they share the same id/displayName/color subset). + */ +declare module 'hono' { + interface ContextVariableMap { + user: typeof DEV_USER + } +} + /** * Returns a Hono MiddlewareHandler that injects DEV_USER into the request context * when the dev-auth bypass is active, or a pure passthrough when inactive. diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index db80d15..34c3aa9 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -11,6 +11,17 @@ import { startBrokerPoller } from './broker/poller.js' export const app = new Hono() +// Compute once at startup: bypass is active only in non-production with explicit opt-in. +// In production NODE_ENV='production' → devBypassActive=false → OIDC is always mounted. +const devBypassActive = + process.env.NODE_ENV !== 'production' && process.env.DEV_AUTH_BYPASS === 'true' + +if (devBypassActive) { + console.warn( + '⚠ DEV_AUTH_BYPASS active — OIDC guard DISABLED. Never use in production.', + ) +} + // OIDC callback — must be registered BEFORE oidcAuthMiddleware so the // authorization-code exchange is not itself intercepted by the auth check (T-02-02) app.get('/callback', (c) => processOAuthCallback(c)) @@ -19,15 +30,19 @@ app.get('/callback', (c) => processOAuthCallback(c)) app.route('/health', healthRouter) // Dev-auth bypass — no-op passthrough unless DEV_AUTH_BYPASS=true AND NODE_ENV!='production'. -// When active, injects a fixed dev user so the OIDC guard below is not required for local dev. +// When active, injects DEV_USER into the Hono context and the OIDC guard is NOT mounted. // Must be mounted BEFORE oidcAuthMiddleware (T-02-01 mitigation; see auth/devBypass.ts). app.use('/api/*', devAuthBypass()) // Protect all /api/* routes with OIDC session middleware (AUTH-01, T-02-05). +// Skipped entirely when devBypassActive so that local dev works without Authelia. +// In production devBypassActive is always false — OIDC is unconditionally mounted. // Unauthenticated requests receive a 302 redirect to Authelia's authorize endpoint. // OIDC_AUTH_EXTERNAL_URL is MANDATORY behind Pangolin to construct the correct // redirect_uri (Pitfall 1). Set it to https://familysync.. -app.use('/api/*', oidcAuthMiddleware()) +if (!devBypassActive) { + app.use('/api/*', oidcAuthMiddleware()) +} // Protected API routes (behind oidcAuthMiddleware) app.route('/api/me', meRouter) diff --git a/apps/api/src/routes/me.ts b/apps/api/src/routes/me.ts index 031a5f5..5c6c199 100644 --- a/apps/api/src/routes/me.ts +++ b/apps/api/src/routes/me.ts @@ -1,13 +1,19 @@ /** * GET /api/me — returns the authenticated user's identity and assigned color. * - * Flow: + * Flow (normal — OIDC active): * 1. getAuth(c) reads iss + sub from the OIDC session JWT cookie * (validated and refreshed by oidcAuthMiddleware — never reaches here unauthenticated) * 2. upsertUser(iss, sub, email) writes the user row on first visit, returns * the existing row on subsequent visits (idempotent, keyed on iss+sub, D-10) * 3. Returns { user: { id, displayName, color } } * + * Flow (dev bypass — DEV_AUTH_BYPASS=true, non-production): + * devAuthBypass() injects DEV_USER via c.set('user', DEV_USER). oidcAuthMiddleware + * is NOT mounted in index.ts when the bypass is active, so getAuth(c) is never called. + * This handler reads c.get('user') first and short-circuits to return the dev identity + * directly, skipping the DB upsert. + * * The OIDC session cookie is httpOnly + Secure + SameSite (T-02-03). * No credential or refresh-token data is included in the response (T-02-04). */ @@ -15,12 +21,28 @@ import { Hono } from 'hono' import { getAuth } from '../auth/middleware.js' import { upsertUser } from '../auth/user.js' +// Side-effect import: brings in the ContextVariableMap augmentation for c.get('user') +import '../auth/devBypass.js' export const meRouter = new Hono() meRouter.get('/', async (c) => { - // getAuth returns null only if the session is invalid — oidcAuthMiddleware on - // /api/* redirects unauthenticated requests before this handler is reached. + // Dev-auth bypass path: devAuthBypass() sets c.get('user') to DEV_USER when active. + // Return the injected dev identity directly — no DB round-trip, no OIDC session needed. + const devUser = c.get('user') + if (devUser) { + return c.json({ + user: { + id: devUser.id, + displayName: devUser.displayName, + color: devUser.color, + }, + }) + } + + // Normal OIDC path: getAuth returns null only if the session is invalid. + // oidcAuthMiddleware on /api/* redirects unauthenticated requests before this handler + // is reached, so null here indicates a genuine session error. const auth = await getAuth(c) if (!auth) { return c.json({ error: 'Unauthorized' }, 401)