/** * OIDC authentication middleware wiring. * * Configures @hono/oidc-auth for the generic OIDC identity provider (D-06). * * Required env vars: * OIDC_AUTH_SECRET — 32+ char random string for JWT cookie signing (T-02-03) * OIDC_ISSUER — OIDC issuer URL (middleware fetches /.well-known/openid-configuration) * OIDC_CLIENT_ID — registered client ID at the OIDC provider * OIDC_CLIENT_SECRET — PLAIN text secret (NOT the pbkdf2 hash — see Pitfall 7) * OIDC_REDIRECT_URI — https://familysync./callback * OIDC_AUTH_EXTERNAL_URL — https://familysync. — MANDATORY behind Pangolin (Pitfall 1) * * Phase 12 — env-OR-app_config fallback (D-02 / D-03 / Recommendation a): * OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_AUTH_EXTERNAL_URL may be absent from env on a * fresh unconfigured instance or when the wizard has written them to app_config but * the container has not yet been restarted. oidcAuthMiddlewareWithFallback() reads * the process.env value first; when absent, reads the app_config DB value and injects * it via process.env for the duration of the request. This avoids a crash on fresh * boot and allows wizard-configured values to work before a container restart. * * A2 CONFIRMED: @hono/oidc-auth reads OIDC_ISSUER etc. at per-request call time * (inside setOidcAuthEnv → env(c) → process.env). NOT at import time. A fresh instance * without these env vars boots cleanly; the HTTP 500 only occurs if a request hits * /api/* OIDC-protected routes before setup is complete — which is acceptable since * /api/setup/* is pre-auth and is the only pre-setup surface. * * Session persistence (AUTH-02): * @hono/oidc-auth stores the refresh token in the signed JWT cookie. * Every OIDC_AUTH_REFRESH_INTERVAL (default 15 min) the middleware calls * the token endpoint with the stored refresh token — no iframe required (D-12). * Session lifespan is governed by OIDC_AUTH_EXPIRES (default 1 day) and * the OIDC provider's refresh_token_lifespan. * * Scopes: openid, profile, email only — no 'groups' scope (D-11). * * Source: https://github.com/honojs/middleware/tree/main/packages/oidc-auth */ import type { Context, Next } from 'hono'; import { eq } from 'drizzle-orm'; import { db } from '../db/client.js'; import { appConfig } from '../db/schema.js'; export { oidcAuthMiddleware, processOAuthCallback, getAuth } from '@hono/oidc-auth'; // --------------------------------------------------------------------------- // env-OR-app_config fallback middleware (D-02 / D-03 / Recommendation a) // // OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_AUTH_EXTERNAL_URL are non-secret config // (D-01) that the wizard writes to app_config. When the env var is absent, // this middleware reads the app_config value and sets it into process.env so // that the downstream oidcAuthMiddleware() picks it up from its env(c) read. // // Env floor vars that stay in env only (never in app_config): // OIDC_AUTH_SECRET, OIDC_CLIENT_SECRET — secrets; must be in Docker env (D-01) // // Called once per /api/* request that reaches the OIDC guard. // Reading 3 app_config rows adds negligible overhead for a 2-person household app. // --------------------------------------------------------------------------- export async function oidcConfigFallbackMiddleware(c: Context, next: Next): Promise { const needsIssuer = !process.env.OIDC_ISSUER; const needsClientId = !process.env.OIDC_CLIENT_ID; const needsExternalUrl = !process.env.OIDC_AUTH_EXTERNAL_URL; if (needsIssuer || needsClientId || needsExternalUrl) { // Build a minimal list of keys to read — only the absent ones const keysToRead: string[] = []; if (needsIssuer) keysToRead.push('oidc_issuer'); if (needsClientId) keysToRead.push('oidc_client_id'); if (needsExternalUrl) keysToRead.push('app_external_url'); // Read from app_config (written by POST /api/setup/config) for (const key of keysToRead) { const [row] = await db .select({ value: appConfig.value }) .from(appConfig) .where(eq(appConfig.key, key)) .limit(1); if (row?.value) { // Inject into process.env so oidcAuthMiddleware()'s env(c) picks it up. // This is safe: these are non-secret, app-config-owned values (D-01). // // WR-03 — single-write semantics: once written, process.env is NOT re-read // from DB on subsequent requests (the needsXxx guard above is false once set). // Consequence: if the operator changes these values via the wizard after the // container is already running, the in-process value is stale until restart. // A container restart is required to pick up any changed OIDC config values. if (key === 'oidc_issuer') { console.info( '[oidcFallback] Writing OIDC_ISSUER from app_config — container restart required to update this value', ); process.env.OIDC_ISSUER = row.value; } if (key === 'oidc_client_id') { console.info( '[oidcFallback] Writing OIDC_CLIENT_ID from app_config — container restart required to update this value', ); process.env.OIDC_CLIENT_ID = row.value; } if (key === 'app_external_url') { console.info( '[oidcFallback] Writing OIDC_AUTH_EXTERNAL_URL from app_config — container restart required to update this value', ); process.env.OIDC_AUTH_EXTERNAL_URL = row.value; } } } } await next(); }