feat(260610-k1z-01): add persistSessionCookie() middleware (AUTH-02)
- Re-issues oidc-auth cookie with maxAge so PWA sessions survive close/reopen
- Guards on c.get('oidcAuthJwt'): only runs when @hono/oidc-auth set a valid session
- Falsy oidcAuthJwt falls straight through — no resurrection of deleted/absent cookies
- Cookie attrs mirror the library: httpOnly, secure, sameSite=Lax, conditional domain
- maxAge reads OIDC_AUTH_EXPIRES (default 86400s)
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* persistSessionCookie — AUTH-02: upgrade the OIDC session cookie to a persistent one.
|
||||||
|
*
|
||||||
|
* WHY THIS EXISTS
|
||||||
|
* ---------------
|
||||||
|
* @hono/oidc-auth 1.8.3 sets its `oidc-auth` session cookie with
|
||||||
|
* { path, httpOnly: true, secure: true [, domain] }
|
||||||
|
* and NO maxAge/expires. That makes it a SESSION-SCOPED cookie: the browser discards
|
||||||
|
* it when the PWA or browser tab is closed. On reopen there is no cookie, so the OIDC
|
||||||
|
* guard 302-redirects to Authelia and forces a full re-login — even though the
|
||||||
|
* server-side JWT is still valid for OIDC_AUTH_EXPIRES seconds (default 86 400 / 1 day).
|
||||||
|
*
|
||||||
|
* The library has no option to set maxAge. This middleware wraps around it and
|
||||||
|
* re-issues the same signed JWT as a persistent cookie (with maxAge + sameSite:'Lax').
|
||||||
|
*
|
||||||
|
* WHEN THE RE-ISSUE FIRES
|
||||||
|
* -----------------------
|
||||||
|
* @hono/oidc-auth calls `c.set('oidcAuthJwt', session_jwt)` ONLY on requests where a
|
||||||
|
* valid session is created or refreshed. Logged-out / no-session / unauthenticated
|
||||||
|
* requests do NOT set `oidcAuthJwt`. This is the guard signal: re-issue the persistent
|
||||||
|
* cookie only when the library produced a valid session this request.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MiddlewareHandler } from 'hono'
|
||||||
|
import { setCookie } from 'hono/cookie'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Hono MiddlewareHandler that upgrades the session-scoped oidc-auth cookie
|
||||||
|
* (set by @hono/oidc-auth) to a persistent cookie with a Max-Age.
|
||||||
|
*
|
||||||
|
* Mount immediately after oidcAuthMiddleware() on '/api/*' inside the !devBypassActive
|
||||||
|
* block in index.ts. Under dev bypass there is no oidc-auth cookie so it must not run.
|
||||||
|
*/
|
||||||
|
export function persistSessionCookie(): MiddlewareHandler {
|
||||||
|
return async (c, next) => {
|
||||||
|
// Read the freshly-signed session JWT placed on context by @hono/oidc-auth.
|
||||||
|
// The 'as never' cast is required because 'oidcAuthJwt' is a library-internal key
|
||||||
|
// that is not declared in Hono's ContextVariableMap — mirrors the loose-context
|
||||||
|
// convention used elsewhere in this codebase (e.g. resolveUserId).
|
||||||
|
const jwt = c.get('oidcAuthJwt' as never) as string | undefined
|
||||||
|
|
||||||
|
// CRITICAL CORRECTNESS GUARD: if no valid session JWT is on context (logged-out,
|
||||||
|
// deleted, or never-set request), do NOT touch cookies and fall straight through.
|
||||||
|
// This property prevents resurrecting a deleted or absent cookie. Without it a
|
||||||
|
// logged-out user would receive a new oidc-auth Set-Cookie with no value, which
|
||||||
|
// could re-authenticate them or produce confusing browser state.
|
||||||
|
if (!jwt) {
|
||||||
|
await next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// A valid session JWT is present — re-issue the cookie BEFORE next() so that if
|
||||||
|
// any future downstream handler deletes the cookie, that delete Set-Cookie header
|
||||||
|
// comes last and wins (safe ordering even without a current logout/revoke route).
|
||||||
|
const name = process.env.OIDC_COOKIE_NAME ?? 'oidc-auth'
|
||||||
|
const path = process.env.OIDC_COOKIE_PATH ?? '/'
|
||||||
|
const maxAge = Number(process.env.OIDC_AUTH_EXPIRES ?? 86400)
|
||||||
|
|
||||||
|
// Build the options object. domain is included ONLY when OIDC_COOKIE_DOMAIN is set —
|
||||||
|
// mirroring the library's own conditional-domain logic so the two Set-Cookie headers
|
||||||
|
// carry identical scope attributes (avoids a split-cookie situation where the library
|
||||||
|
// sets domain=X but we set no domain, or vice-versa).
|
||||||
|
const options: Parameters<typeof setCookie>[3] = {
|
||||||
|
path,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'Lax',
|
||||||
|
maxAge, // seconds — Hono's setCookie maxAge unit matches OIDC_AUTH_EXPIRES unit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.OIDC_COOKIE_DOMAIN) {
|
||||||
|
options.domain = process.env.OIDC_COOKIE_DOMAIN
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-issue the same JWT the library signed. Do NOT re-sign or modify the payload.
|
||||||
|
setCookie(c, name, jwt, options)
|
||||||
|
|
||||||
|
await next()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user