Phase 12: Initial Setup Wizard #22
@@ -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();
|
||||
}
|
||||
|
||||
+12
-1
@@ -11,7 +11,11 @@ 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 { oidcAuthMiddleware, processOAuthCallback } from './auth/middleware.js';
|
||||
import {
|
||||
oidcAuthMiddleware,
|
||||
processOAuthCallback,
|
||||
oidcConfigFallbackMiddleware,
|
||||
} from './auth/middleware.js';
|
||||
import { devAuthBypass } from './auth/devBypass.js';
|
||||
import { persistSessionCookie } from './auth/persistSessionCookie.js';
|
||||
import { startBrokerPoller } from './broker/poller.js';
|
||||
@@ -56,6 +60,13 @@ app.use('/api/*', devAuthBypass());
|
||||
// OIDC_AUTH_EXTERNAL_URL is MANDATORY behind Pangolin to construct the correct
|
||||
// redirect_uri (Pitfall 1). Set it to https://familysync.<domain>.
|
||||
if (!devBypassActive) {
|
||||
// Phase 12 / D-02 / D-03: env-OR-app_config fallback for non-secret OIDC config.
|
||||
// Reads OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_AUTH_EXTERNAL_URL from app_config when
|
||||
// absent from process.env — so a fresh unconfigured instance does not crash at boot
|
||||
// and a wizard-configured instance reads the DB values before a container restart.
|
||||
// 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());
|
||||
// Re-issues the session-scoped oidc-auth cookie as persistent so PWA sessions survive close (AUTH-02).
|
||||
app.use('/api/*', persistSessionCookie());
|
||||
|
||||
@@ -117,6 +117,7 @@ describe('POST /api/push/subscription', () => {
|
||||
oidcAuthMiddleware: () => async (_c: unknown, next: () => Promise<void>) => next(),
|
||||
processOAuthCallback: () => async (c: { json: (v: unknown) => unknown }) =>
|
||||
c.json({ ok: true }),
|
||||
oidcConfigFallbackMiddleware: async (_c: unknown, next: () => Promise<void>) => next(),
|
||||
}));
|
||||
|
||||
const { app: freshApp } = await import('../../src/index.js?v=unauth');
|
||||
|
||||
Reference in New Issue
Block a user