- /callback: reads signed state, extracts linkUserId, calls linkOidcToUser after OIDC session set; OidcLinkConflictError redirects to /?error=oidc-link-conflict - OIDC guard: oidcAuthMiddleware() factory called once at construction, handler invoked per-request inside skip-when-user-set wrapper (D-03) - middleware.ts: de-Authelia-ize comments — generic OIDC identity provider language (D-06, AUTH-LOCAL-18) - localAuthMiddleware.ts: cast to typeof DEV_USER for ContextVariableMap type compatibility - All 446 tests pass; typecheck clean
115 lines
5.5 KiB
TypeScript
115 lines
5.5 KiB
TypeScript
/**
|
|
* 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.<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
|
|
* 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<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).
|
|
//
|
|
// 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();
|
|
}
|