fix(cookie): dynamically calculate parent wildcard domain (.atyg.org) to prevent host-scoping redirect loop

This commit is contained in:
Tyler Gillispie 2026-08-24 13:11:17 -07:00
parent ce29459f78
commit 7420401147
4 changed files with 55 additions and 9 deletions

View File

@ -9,6 +9,26 @@ export interface AuthenticatedUser {
username: string;
}
/**
* Calculates the wildcard parent cookie domain (e.g. auth.atyg.org -> .atyg.org)
* to ensure cookies are sent to all subdomains (ed-droid.atyg.org, grafana.atyg.org, etc.).
*/
export function getCookieDomain(customRpId?: string): string | undefined {
const envDomain = Deno.env.get("COOKIE_DOMAIN");
if (envDomain) {
return envDomain.startsWith(".") ? envDomain : `.${envDomain}`;
}
const targetId = customRpId || Deno.env.get("RP_ID") || "";
if (!targetId || !targetId.includes(".") || targetId === "localhost") {
return undefined;
}
const parts = targetId.split(".").filter(Boolean);
if (parts.length >= 2) {
return `.${parts.slice(-2).join(".")}`;
}
return `.${targetId}`;
}
/**
* Helper to get authenticated user from session cookie.
* Checks Valkey cache first, with automatic PostgreSQL sessions table fallback.

View File

@ -418,3 +418,13 @@ Deno.test("WebAuthn - /api/login/challenge handles username for PRF", async () =
sqlWrapper.sql = originalSql;
}
});
Deno.test("Cookie Domain Scoping - getCookieDomain derives wildcard parent domain", async () => {
const { getCookieDomain } = await import("./auth-session.ts");
assertEquals(getCookieDomain("auth.atyg.org"), ".atyg.org");
assertEquals(getCookieDomain("ed-droid.atyg.org"), ".atyg.org");
assertEquals(getCookieDomain("atyg.org"), ".atyg.org");
assertEquals(getCookieDomain("localhost"), undefined);
assertEquals(getCookieDomain(""), undefined);
});

View File

@ -48,6 +48,22 @@ const origin = Deno.env.get("ORIGIN") ||
(import.meta.main ? undefined : "http://localhost");
const requireHardwareToken = Deno.env.get("REQUIRE_HARDWARE_TOKEN") === "true";
export function getCookieDomain(customRpId?: string): string | undefined {
const envDomain = Deno.env.get("COOKIE_DOMAIN");
if (envDomain) {
return envDomain.startsWith(".") ? envDomain : `.${envDomain}`;
}
const targetId = customRpId || Deno.env.get("RP_ID") || "";
if (!targetId || !targetId.includes(".") || targetId === "localhost") {
return undefined;
}
const parts = targetId.split(".").filter(Boolean);
if (parts.length >= 2) {
return `.${parts.slice(-2).join(".")}`;
}
return `.${targetId}`;
}
if (!rpID || !origin) {
throw new Error(
"Missing critical environment variables: RP_ID and ORIGIN must be set.",
@ -544,8 +560,7 @@ app.post("/api/register/verify", async (c) => {
maxAge: 0,
});
const cookieDomain = Deno.env.get("COOKIE_DOMAIN") ||
(rpID && rpID.includes(".") ? `.${rpID}` : undefined);
const cookieDomain = getCookieDomain(rpID);
if (cookieDomain) {
deleteCookie(c, "session_id", { domain: cookieDomain, path: "/" });
}
@ -735,8 +750,7 @@ app.post("/api/login/verify", async (c) => {
}
}
const cookieDomain = Deno.env.get("COOKIE_DOMAIN") ||
(rpID && rpID.includes(".") ? `.${rpID}` : undefined);
const cookieDomain = getCookieDomain(rpID);
// Clear any existing host-scoped cookie to prevent domain duplication
deleteCookie(c, "session_id", { path: "/" });
@ -2080,8 +2094,7 @@ app.post("/api/revoke", async (c) => {
// ignore
}
const cookieDomain = Deno.env.get("COOKIE_DOMAIN") ||
(rpID && rpID.includes(".") ? `.${rpID}` : undefined);
const cookieDomain = getCookieDomain(rpID);
if (cookieDomain) {
deleteCookie(c, "session_id", {

View File

@ -3,7 +3,11 @@ import { serveStatic } from "jsr:@hono/hono@4/deno";
import { deleteCookie, getCookie } from "jsr:@hono/hono@4/cookie";
import { sql } from "../server/db.ts";
import { valkey } from "../server/valkey.ts";
import { getAuthenticatedUser, isGlobalAdmin } from "../server/auth-session.ts";
import {
getAuthenticatedUser,
getCookieDomain,
isGlobalAdmin,
} from "../server/auth-session.ts";
import { LoginPage } from "./components/LoginPage.tsx";
import { RegisterPage } from "./components/RegisterPage.tsx";
import { SessionsPage } from "./components/SessionsPage.tsx";
@ -37,8 +41,7 @@ uiApp.get("/logout", async (c) => {
}
const rpID = Deno.env.get("RP_ID") || "";
const cookieDomain = Deno.env.get("COOKIE_DOMAIN") ||
(rpID.includes(".") ? `.${rpID}` : undefined);
const cookieDomain = getCookieDomain(rpID);
if (cookieDomain) {
deleteCookie(c, "session_id", {