feat(12-02): OIDC boot env-OR-app_config fallback + pre-auth mount verification
- A2 CONFIRMED: @hono/oidc-auth reads OIDC_ISSUER/OIDC_CLIENT_ID/OIDC_AUTH_EXTERNAL_URL at per-request call time (env(c) → process.env), NOT at import time — fresh instance boots cleanly without OIDC env vars - Implement oidcConfigFallbackMiddleware in auth/middleware.ts: reads OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_AUTH_EXTERNAL_URL from app_config when process.env is absent, injects into process.env before oidcAuthMiddleware() reads it (D-02/D-03/Recommendation a) - Mount oidcConfigFallbackMiddleware before oidcAuthMiddleware() in index.ts so wizard-configured instances work before a container restart - Verify /api/setup mount order: line 49 < devAuthBypass line 54 (T-12-09/Pitfall 1) - Fix push.test.ts vi.doMock for middleware.js: add oidcConfigFallbackMiddleware stub - 394 tests pass | 5 todo (D-08 RED scaffolds); typecheck clean
This commit is contained in:
@@ -11,6 +11,20 @@
|
||||
* OIDC_REDIRECT_URI — https://familysync.<domain>/callback
|
||||
* OIDC_AUTH_EXTERNAL_URL — https://familysync.<domain> — 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
|
||||
@@ -23,4 +37,57 @@
|
||||
* 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<void> {
|
||||
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)
|
||||
if (key === 'oidc_issuer') process.env.OIDC_ISSUER = row.value;
|
||||
if (key === 'oidc_client_id') process.env.OIDC_CLIENT_ID = row.value;
|
||||
if (key === 'app_external_url') process.env.OIDC_AUTH_EXTERNAL_URL = row.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user