fix(cookie): dynamically calculate parent wildcard domain (.atyg.org) to prevent host-scoping redirect loop
This commit is contained in:
parent
ce29459f78
commit
7420401147
@ -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.
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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", {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user