fix(02): dev-auth bypass no longer blocked by oidcAuthMiddleware
- index.ts: compute devBypassActive at startup; skip app.use(oidcAuthMiddleware)
entirely when active so the OIDC guard never runs in local dev
- routes/me.ts: read c.get('user') first; return dev identity directly when
devAuthBypass injected it, bypassing getAuth() and the DB upsert
- auth/devBypass.ts: add ContextVariableMap augmentation for 'user' key;
correct stale comment that claimed getAuth/401 path was still active
This commit is contained in:
@@ -9,12 +9,12 @@
|
|||||||
* When the bypass is inactive (wrong env, or NODE_ENV=production) the middleware is a
|
* When the bypass is inactive (wrong env, or NODE_ENV=production) the middleware is a
|
||||||
* pure no-op passthrough — production behaviour is unchanged.
|
* pure no-op passthrough — production behaviour is unchanged.
|
||||||
*
|
*
|
||||||
* Context key: 'user' — matches the key read by downstream consumers (e.g. routes/me.ts
|
* Context key: 'user' — matches the key read by downstream consumers.
|
||||||
* calls getAuth(c) from @hono/oidc-auth; the events route will read c.get('user') directly).
|
* In dev bypass mode, c.get('user') returns DEV_USER. index.ts does NOT mount
|
||||||
* In dev bypass mode, c.get('user') returns DEV_USER. getAuth(c) is still called by me.ts
|
* oidcAuthMiddleware when devBypassActive is true, so getAuth(c) is never called.
|
||||||
* but will return null because no OIDC session cookie is present; me.ts guards this with
|
* routes/me.ts reads c.get('user') first and returns the dev identity directly,
|
||||||
* `if (!auth) return 401`. When using the bypass, consume c.get('user') directly in routes
|
* skipping the DB upsert and getAuth path entirely. Other routes (e.g. events)
|
||||||
* that need the user object (events route pattern in Plan 02).
|
* also read c.get('user') directly — same pattern, no change needed there.
|
||||||
*
|
*
|
||||||
* Security:
|
* Security:
|
||||||
* - The FIRST conditional is always `NODE_ENV === 'production'` — checked before reading
|
* - The FIRST conditional is always `NODE_ENV === 'production'` — checked before reading
|
||||||
@@ -35,6 +35,18 @@ export const DEV_USER = {
|
|||||||
color: COLOR_PALETTE[0], // '#4A90D9' — first palette slot
|
color: COLOR_PALETTE[0], // '#4A90D9' — first palette slot
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend Hono's ContextVariableMap so that c.get('user') / c.set('user', ...)
|
||||||
|
* are statically typed throughout the app. The value type is the DEV_USER shape,
|
||||||
|
* which is compatible with both the bypass path and any future app-level user object
|
||||||
|
* stored on context (they share the same id/displayName/color subset).
|
||||||
|
*/
|
||||||
|
declare module 'hono' {
|
||||||
|
interface ContextVariableMap {
|
||||||
|
user: typeof DEV_USER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Hono MiddlewareHandler that injects DEV_USER into the request context
|
* Returns a Hono MiddlewareHandler that injects DEV_USER into the request context
|
||||||
* when the dev-auth bypass is active, or a pure passthrough when inactive.
|
* when the dev-auth bypass is active, or a pure passthrough when inactive.
|
||||||
|
|||||||
+16
-1
@@ -11,6 +11,17 @@ import { startBrokerPoller } from './broker/poller.js'
|
|||||||
|
|
||||||
export const app = new Hono()
|
export const app = new Hono()
|
||||||
|
|
||||||
|
// Compute once at startup: bypass is active only in non-production with explicit opt-in.
|
||||||
|
// In production NODE_ENV='production' → devBypassActive=false → OIDC is always mounted.
|
||||||
|
const devBypassActive =
|
||||||
|
process.env.NODE_ENV !== 'production' && process.env.DEV_AUTH_BYPASS === 'true'
|
||||||
|
|
||||||
|
if (devBypassActive) {
|
||||||
|
console.warn(
|
||||||
|
'⚠ DEV_AUTH_BYPASS active — OIDC guard DISABLED. Never use in production.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// OIDC callback — must be registered BEFORE oidcAuthMiddleware so the
|
// OIDC callback — must be registered BEFORE oidcAuthMiddleware so the
|
||||||
// authorization-code exchange is not itself intercepted by the auth check (T-02-02)
|
// authorization-code exchange is not itself intercepted by the auth check (T-02-02)
|
||||||
app.get('/callback', (c) => processOAuthCallback(c))
|
app.get('/callback', (c) => processOAuthCallback(c))
|
||||||
@@ -19,15 +30,19 @@ app.get('/callback', (c) => processOAuthCallback(c))
|
|||||||
app.route('/health', healthRouter)
|
app.route('/health', healthRouter)
|
||||||
|
|
||||||
// Dev-auth bypass — no-op passthrough unless DEV_AUTH_BYPASS=true AND NODE_ENV!='production'.
|
// Dev-auth bypass — no-op passthrough unless DEV_AUTH_BYPASS=true AND NODE_ENV!='production'.
|
||||||
// When active, injects a fixed dev user so the OIDC guard below is not required for local dev.
|
// 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).
|
// Must be mounted BEFORE oidcAuthMiddleware (T-02-01 mitigation; see auth/devBypass.ts).
|
||||||
app.use('/api/*', devAuthBypass())
|
app.use('/api/*', devAuthBypass())
|
||||||
|
|
||||||
// Protect all /api/* routes with OIDC session middleware (AUTH-01, T-02-05).
|
// 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 Authelia's authorize endpoint.
|
||||||
// OIDC_AUTH_EXTERNAL_URL is MANDATORY behind Pangolin to construct the correct
|
// OIDC_AUTH_EXTERNAL_URL is MANDATORY behind Pangolin to construct the correct
|
||||||
// redirect_uri (Pitfall 1). Set it to https://familysync.<domain>.
|
// redirect_uri (Pitfall 1). Set it to https://familysync.<domain>.
|
||||||
|
if (!devBypassActive) {
|
||||||
app.use('/api/*', oidcAuthMiddleware())
|
app.use('/api/*', oidcAuthMiddleware())
|
||||||
|
}
|
||||||
|
|
||||||
// Protected API routes (behind oidcAuthMiddleware)
|
// Protected API routes (behind oidcAuthMiddleware)
|
||||||
app.route('/api/me', meRouter)
|
app.route('/api/me', meRouter)
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* GET /api/me — returns the authenticated user's identity and assigned color.
|
* GET /api/me — returns the authenticated user's identity and assigned color.
|
||||||
*
|
*
|
||||||
* Flow:
|
* Flow (normal — OIDC active):
|
||||||
* 1. getAuth(c) reads iss + sub from the OIDC session JWT cookie
|
* 1. getAuth(c) reads iss + sub from the OIDC session JWT cookie
|
||||||
* (validated and refreshed by oidcAuthMiddleware — never reaches here unauthenticated)
|
* (validated and refreshed by oidcAuthMiddleware — never reaches here unauthenticated)
|
||||||
* 2. upsertUser(iss, sub, email) writes the user row on first visit, returns
|
* 2. upsertUser(iss, sub, email) writes the user row on first visit, returns
|
||||||
* the existing row on subsequent visits (idempotent, keyed on iss+sub, D-10)
|
* the existing row on subsequent visits (idempotent, keyed on iss+sub, D-10)
|
||||||
* 3. Returns { user: { id, displayName, color } }
|
* 3. Returns { user: { id, displayName, color } }
|
||||||
*
|
*
|
||||||
|
* Flow (dev bypass — DEV_AUTH_BYPASS=true, non-production):
|
||||||
|
* devAuthBypass() injects DEV_USER via c.set('user', DEV_USER). oidcAuthMiddleware
|
||||||
|
* is NOT mounted in index.ts when the bypass is active, so getAuth(c) is never called.
|
||||||
|
* This handler reads c.get('user') first and short-circuits to return the dev identity
|
||||||
|
* directly, skipping the DB upsert.
|
||||||
|
*
|
||||||
* The OIDC session cookie is httpOnly + Secure + SameSite (T-02-03).
|
* The OIDC session cookie is httpOnly + Secure + SameSite (T-02-03).
|
||||||
* No credential or refresh-token data is included in the response (T-02-04).
|
* No credential or refresh-token data is included in the response (T-02-04).
|
||||||
*/
|
*/
|
||||||
@@ -15,12 +21,28 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { getAuth } from '../auth/middleware.js'
|
import { getAuth } from '../auth/middleware.js'
|
||||||
import { upsertUser } from '../auth/user.js'
|
import { upsertUser } from '../auth/user.js'
|
||||||
|
// Side-effect import: brings in the ContextVariableMap augmentation for c.get('user')
|
||||||
|
import '../auth/devBypass.js'
|
||||||
|
|
||||||
export const meRouter = new Hono()
|
export const meRouter = new Hono()
|
||||||
|
|
||||||
meRouter.get('/', async (c) => {
|
meRouter.get('/', async (c) => {
|
||||||
// getAuth returns null only if the session is invalid — oidcAuthMiddleware on
|
// Dev-auth bypass path: devAuthBypass() sets c.get('user') to DEV_USER when active.
|
||||||
// /api/* redirects unauthenticated requests before this handler is reached.
|
// Return the injected dev identity directly — no DB round-trip, no OIDC session needed.
|
||||||
|
const devUser = c.get('user')
|
||||||
|
if (devUser) {
|
||||||
|
return c.json({
|
||||||
|
user: {
|
||||||
|
id: devUser.id,
|
||||||
|
displayName: devUser.displayName,
|
||||||
|
color: devUser.color,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal OIDC path: getAuth returns null only if the session is invalid.
|
||||||
|
// oidcAuthMiddleware on /api/* redirects unauthenticated requests before this handler
|
||||||
|
// is reached, so null here indicates a genuine session error.
|
||||||
const auth = await getAuth(c)
|
const auth = await getAuth(c)
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return c.json({ error: 'Unauthorized' }, 401)
|
return c.json({ error: 'Unauthorized' }, 401)
|
||||||
|
|||||||
Reference in New Issue
Block a user