From 3c5256bbb4cfa9a18e5df896a6dcd001d64d5959 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Sun, 10 May 2026 02:53:06 +0200 Subject: [PATCH] fix: avoid h3 setCookie, use Set-Cookie header on Response directly h3 v2's setCookie modifies event response headers but doesn't carry them when handler returns a raw Response object. Build Set-Cookie header manually in redirect helpers instead. --- .../service-admin-http/html-response.ts | 31 +++++++++++++++++++ .../plugins/service-admin-http/routes-auth.ts | 23 ++++---------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/server/src/plugins/service-admin-http/html-response.ts b/server/src/plugins/service-admin-http/html-response.ts index 8c69276..116aad5 100644 --- a/server/src/plugins/service-admin-http/html-response.ts +++ b/server/src/plugins/service-admin-http/html-response.ts @@ -10,3 +10,34 @@ export function htmlPage(markup: unknown): Response { headers: { "content-type": "text/html; charset=utf-8" }, }); } + +/** + * Build a redirect Response with optional Set-Cookie header. + * Avoids h3's setCookie which doesn't play well with returning + * a raw Response object. + */ +export function redirectWithCookie( + location: string, + cookie?: { name: string; value: string; maxAge: number }, + status = 302, +): Response { + const headers = new Headers({ location }); + if (cookie) { + headers.set( + "set-cookie", + `${cookie.name}=${cookie.value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${cookie.maxAge}`, + ); + } + return new Response(null, { status, headers }); +} + +/** Build a redirect that clears a cookie. */ +export function redirectClearCookie(location: string, cookieName: string): Response { + return new Response(null, { + status: 302, + headers: { + location, + "set-cookie": `${cookieName}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`, + }, + }); +} diff --git a/server/src/plugins/service-admin-http/routes-auth.ts b/server/src/plugins/service-admin-http/routes-auth.ts index 7e25644..91a6ac5 100644 --- a/server/src/plugins/service-admin-http/routes-auth.ts +++ b/server/src/plugins/service-admin-http/routes-auth.ts @@ -1,17 +1,11 @@ /** * Authentication routes: login, TOTP, recovery, logout. */ -import { type H3, readBody, getCookie, setCookie, deleteCookie, getQuery, getRequestHeader } from "h3"; -import { htmlPage } from "./html-response.js"; +import { type H3, readBody, getCookie, getQuery, getRequestHeader } from "h3"; +import { htmlPage, redirectWithCookie, redirectClearCookie } from "./html-response.js"; import type { AdminDeps } from "./index.js"; import { LoginPage, TotpPage, RecoveryPage } from "../../web-templates/auth-pages.js"; -const COOKIE_OPTS = { - httpOnly: true, - secure: true, - sameSite: "lax" as const, - path: "/", -}; export function registerAuthRoutes(app: H3, deps: AdminDeps): void { // ---- Login ---------------------------------------------------------------- @@ -70,15 +64,11 @@ export function registerAuthRoutes(app: H3, deps: AdminDeps): void { totpPending, }); - setCookie(event, deps.cookieName, cookieValue, { - ...COOKIE_OPTS, - maxAge: deps.auth.config.sessionMaxSeconds, - }); - + const cookie = { name: deps.cookieName, value: cookieValue, maxAge: deps.auth.config.sessionMaxSeconds }; if (totpPending) { - return new Response(null, { status: 302, headers: { location: "/auth/totp" } }); + return redirectWithCookie("/auth/totp", cookie); } - return new Response(null, { status: 302, headers: { location: "/admin/" } }); + return redirectWithCookie("/admin/", cookie); }); // ---- TOTP ----------------------------------------------------------------- @@ -184,7 +174,6 @@ export function registerAuthRoutes(app: H3, deps: AdminDeps): void { deps.auth.revokeSession(resolved.session.id); } } - deleteCookie(event, deps.cookieName, { path: "/" }); - return new Response(null, { status: 302, headers: { location: "/auth/login" } }); + return redirectClearCookie("/auth/login", deps.cookieName); }); }