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;
|
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.
|
* Helper to get authenticated user from session cookie.
|
||||||
* Checks Valkey cache first, with automatic PostgreSQL sessions table fallback.
|
* 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;
|
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");
|
(import.meta.main ? undefined : "http://localhost");
|
||||||
const requireHardwareToken = Deno.env.get("REQUIRE_HARDWARE_TOKEN") === "true";
|
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) {
|
if (!rpID || !origin) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Missing critical environment variables: RP_ID and ORIGIN must be set.",
|
"Missing critical environment variables: RP_ID and ORIGIN must be set.",
|
||||||
@ -544,8 +560,7 @@ app.post("/api/register/verify", async (c) => {
|
|||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cookieDomain = Deno.env.get("COOKIE_DOMAIN") ||
|
const cookieDomain = getCookieDomain(rpID);
|
||||||
(rpID && rpID.includes(".") ? `.${rpID}` : undefined);
|
|
||||||
if (cookieDomain) {
|
if (cookieDomain) {
|
||||||
deleteCookie(c, "session_id", { domain: cookieDomain, path: "/" });
|
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") ||
|
const cookieDomain = getCookieDomain(rpID);
|
||||||
(rpID && rpID.includes(".") ? `.${rpID}` : undefined);
|
|
||||||
|
|
||||||
// Clear any existing host-scoped cookie to prevent domain duplication
|
// Clear any existing host-scoped cookie to prevent domain duplication
|
||||||
deleteCookie(c, "session_id", { path: "/" });
|
deleteCookie(c, "session_id", { path: "/" });
|
||||||
@ -2080,8 +2094,7 @@ app.post("/api/revoke", async (c) => {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookieDomain = Deno.env.get("COOKIE_DOMAIN") ||
|
const cookieDomain = getCookieDomain(rpID);
|
||||||
(rpID && rpID.includes(".") ? `.${rpID}` : undefined);
|
|
||||||
|
|
||||||
if (cookieDomain) {
|
if (cookieDomain) {
|
||||||
deleteCookie(c, "session_id", {
|
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 { deleteCookie, getCookie } from "jsr:@hono/hono@4/cookie";
|
||||||
import { sql } from "../server/db.ts";
|
import { sql } from "../server/db.ts";
|
||||||
import { valkey } from "../server/valkey.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 { LoginPage } from "./components/LoginPage.tsx";
|
||||||
import { RegisterPage } from "./components/RegisterPage.tsx";
|
import { RegisterPage } from "./components/RegisterPage.tsx";
|
||||||
import { SessionsPage } from "./components/SessionsPage.tsx";
|
import { SessionsPage } from "./components/SessionsPage.tsx";
|
||||||
@ -37,8 +41,7 @@ uiApp.get("/logout", async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rpID = Deno.env.get("RP_ID") || "";
|
const rpID = Deno.env.get("RP_ID") || "";
|
||||||
const cookieDomain = Deno.env.get("COOKIE_DOMAIN") ||
|
const cookieDomain = getCookieDomain(rpID);
|
||||||
(rpID.includes(".") ? `.${rpID}` : undefined);
|
|
||||||
|
|
||||||
if (cookieDomain) {
|
if (cookieDomain) {
|
||||||
deleteCookie(c, "session_id", {
|
deleteCookie(c, "session_id", {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user