Phase 19: Local Auth (No-OIDC Mode) #23
@@ -16,15 +16,24 @@
|
|||||||
* skipping the DB upsert and getAuth path entirely. Other routes (e.g. events)
|
* 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.
|
* 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:
|
* Security:
|
||||||
* - The FIRST conditional is always `NODE_ENV === 'production'` — checked before reading
|
* - 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.
|
* 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.
|
* - 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.
|
* - This file must never be removed — the pattern is referenced by Plan 02 routes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { MiddlewareHandler } from 'hono';
|
import type { MiddlewareHandler } from 'hono';
|
||||||
|
import { getCookie } from 'hono/cookie';
|
||||||
|
import { issueLocalSessionCookie } from './localSession.js';
|
||||||
import { COLOR_PALETTE } from './user.js';
|
import { COLOR_PALETTE } from './user.js';
|
||||||
|
|
||||||
export const DEV_USER = {
|
export const DEV_USER = {
|
||||||
@@ -74,3 +83,49 @@ export function devAuthBypass(): MiddlewareHandler {
|
|||||||
await next();
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
processOAuthCallback,
|
processOAuthCallback,
|
||||||
oidcConfigFallbackMiddleware,
|
oidcConfigFallbackMiddleware,
|
||||||
} from './auth/middleware.js';
|
} from './auth/middleware.js';
|
||||||
import { devAuthBypass } from './auth/devBypass.js';
|
import { devAuthBypass, devSessionCookieMiddleware } from './auth/devBypass.js';
|
||||||
import { localAuthMiddleware } from './auth/localAuthMiddleware.js';
|
import { localAuthMiddleware } from './auth/localAuthMiddleware.js';
|
||||||
import { persistSessionCookie } from './auth/persistSessionCookie.js';
|
import { persistSessionCookie } from './auth/persistSessionCookie.js';
|
||||||
import { startBrokerPoller } from './broker/poller.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).
|
// Must be mounted BEFORE oidcAuthMiddleware (T-02-01 mitigation; see auth/devBypass.ts).
|
||||||
app.use('/api/*', devAuthBypass());
|
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.
|
// 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.
|
// 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.
|
// Runs AFTER devAuthBypass (which may set c.get('user') first) and BEFORE the OIDC guard.
|
||||||
|
|||||||
Reference in New Issue
Block a user