feat(19-03): implement localAuthMiddleware, GET /api/auth/mode, and pre-auth route mounts

- localAuthMiddleware: cookie→c.set('user') with Pitfall-1 guard (no-set on no-cookie path)
- authMode: GET /api/auth/mode pre-auth endpoint (localEnabled:true, oidcEnabled from env+config)
- localAuth: POST /api/auth/local/login (rate-limit + timing-safe), logout routes
- index.ts: mount authModeRouter + localAuthRouter pre-auth; localAuthMiddleware after devAuthBypass; OIDC guard wrapped skip-when-user-set
This commit is contained in:
Lucas Berger
2026-06-17 16:48:17 -04:00
parent ac32bd405f
commit be7a0aec90
4 changed files with 326 additions and 2 deletions
+25 -2
View File
@@ -11,12 +11,15 @@ import { listsRouter, listItemsRouter } from './routes/lists.js';
import { pushRouter } from './routes/push.js';
import { adminRouter } from './routes/admin.js';
import { setupRouter } from './routes/setup.js';
import { authModeRouter } from './routes/authMode.js';
import { localAuthRouter } from './routes/localAuth.js';
import {
oidcAuthMiddleware,
processOAuthCallback,
oidcConfigFallbackMiddleware,
} from './auth/middleware.js';
import { devAuthBypass } from './auth/devBypass.js';
import { localAuthMiddleware } from './auth/localAuthMiddleware.js';
import { persistSessionCookie } from './auth/persistSessionCookie.js';
import { startBrokerPoller } from './broker/poller.js';
import { startOutboxWorker, initOutboxTrigger } from './broker/outboxWorker.js';
@@ -48,15 +51,26 @@ app.route('/health', healthRouter);
// 423 lock after setup is complete (SETUP-04 / D-10).
app.route('/api/setup', setupRouter);
// Phase 19 — pre-auth auth routes: GET /api/auth/mode and POST /api/auth/local/login, /logout.
// Mounted BEFORE devAuthBypass so they are reachable without a session (D-01 / AUTH-LOCAL-05).
app.route('/api/auth', authModeRouter);
app.route('/api/auth', localAuthRouter);
// Dev-auth bypass — no-op passthrough unless DEV_AUTH_BYPASS=true AND NODE_ENV!='production'.
// 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());
// 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.
// The OIDC guard below is wrapped to skip when c.get('user') is already set (Pitfall 1 guard).
app.use('/api/*', localAuthMiddleware());
// 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.
// Unauthenticated requests receive a 302 redirect to the OIDC authorize endpoint.
// OIDC_AUTH_EXTERNAL_URL is MANDATORY behind Pangolin to construct the correct
// redirect_uri (Pitfall 1). Set it to https://familysync.<domain>.
if (!devBypassActive) {
@@ -67,7 +81,16 @@ if (!devBypassActive) {
// A2 CONFIRMED: oidcAuthMiddleware() reads process.env per-request (call time), so
// injecting into process.env here is safe and effective (see auth/middleware.ts A2 note).
app.use('/api/*', oidcConfigFallbackMiddleware);
app.use('/api/*', oidcAuthMiddleware());
// Phase 19 / D-03: OIDC guard wrapped to skip when c.get('user') is already set.
// A valid local-session (or dev-bypass) user must NOT be 302-redirected to the OIDC
// provider — the skip-when-set wrapper is the coexistence seam (D-03 / RESEARCH Pitfall 1).
app.use('/api/*', async (c, next) => {
if (c.get('user')) {
await next();
return;
}
await oidcAuthMiddleware()(c, next);
});
// Re-issues the session-scoped oidc-auth cookie as persistent so PWA sessions survive close (AUTH-02).
app.use('/api/*', persistSessionCookie());
}