mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-10-07 16:18:53 +00:00
Replace Bearer as the documented session path with Cognito hosted UI plus __Host-ap_* cookies so the SPA can call /api with credentials include.
138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
ACCESS_MAX_AGE_SEC,
|
|
COOKIE_ACCESS,
|
|
COOKIE_ID,
|
|
COOKIE_OAUTH,
|
|
COOKIE_REFRESH,
|
|
COOKIE_SESSION_HINT,
|
|
REFRESH_MAX_AGE_SEC,
|
|
clearCookie,
|
|
clearedSessionCookies,
|
|
cookieNames,
|
|
parseCookies,
|
|
serializeCookie,
|
|
serializeOauthCookie,
|
|
sessionCookieValue,
|
|
tokenCookies,
|
|
} from "./cookies.js";
|
|
|
|
const host = cookieNames("dev");
|
|
const local = cookieNames("local");
|
|
|
|
describe("auth cookies", () => {
|
|
it("prefixes deployed cookies with __Host- and keeps local names unprefixed", () => {
|
|
expect(host).toEqual({
|
|
access: `__Host-${COOKIE_ACCESS}`,
|
|
id: `__Host-${COOKIE_ID}`,
|
|
refresh: `__Host-${COOKIE_REFRESH}`,
|
|
oauth: `__Host-${COOKIE_OAUTH}`,
|
|
hint: `__Host-${COOKIE_SESSION_HINT}`,
|
|
});
|
|
expect(local).toEqual({
|
|
access: COOKIE_ACCESS,
|
|
id: COOKIE_ID,
|
|
refresh: COOKIE_REFRESH,
|
|
oauth: COOKIE_OAUTH,
|
|
hint: COOKIE_SESSION_HINT,
|
|
});
|
|
});
|
|
|
|
it("sets HttpOnly, SameSite=Lax, Path=/, no Domain, and Secure outside local", () => {
|
|
const cookie = serializeCookie(host.access, "tok", {
|
|
maxAge: ACCESS_MAX_AGE_SEC,
|
|
stage: "dev",
|
|
});
|
|
expect(cookie.startsWith(`${host.access}=`)).toBe(true);
|
|
expect(cookie).toContain("HttpOnly");
|
|
expect(cookie).toContain("SameSite=Lax");
|
|
expect(cookie).toContain("Path=/");
|
|
expect(cookie).toMatch(/(?:^|; )Secure(?:;|$)/);
|
|
expect(cookie).not.toMatch(/Domain=/i);
|
|
expect(cookie).toContain(`Max-Age=${ACCESS_MAX_AGE_SEC}`);
|
|
});
|
|
|
|
it("omits Secure on local and scopes refresh to /api/auth", () => {
|
|
const cookie = serializeCookie(local.access, "tok", {
|
|
maxAge: ACCESS_MAX_AGE_SEC,
|
|
stage: "local",
|
|
});
|
|
expect(cookie).toContain("HttpOnly");
|
|
expect(cookie).not.toMatch(/(?:^|; )Secure(?:;|$)/);
|
|
expect(cookie).not.toMatch(/Domain=/i);
|
|
|
|
const refresh = tokenCookies(
|
|
{ accessToken: "a", idToken: "i", refreshToken: "r" },
|
|
"local",
|
|
).find((item) => item.startsWith(`${local.refresh}=`));
|
|
expect(refresh).toContain("Path=/api/auth");
|
|
expect(refresh).not.toMatch(/(?:^|; )Secure(?:;|$)/);
|
|
});
|
|
|
|
it("sets a non-HttpOnly session hint for 8h", () => {
|
|
const cookies = tokenCookies({ accessToken: "a", idToken: "i", refreshToken: "r" }, "dev");
|
|
const hint = cookies.find((item) => item.startsWith(`${host.hint}=`));
|
|
expect(hint).toContain(`${host.hint}=1`);
|
|
expect(hint).toContain(`Max-Age=${REFRESH_MAX_AGE_SEC}`);
|
|
expect(hint).not.toContain("HttpOnly");
|
|
expect(hint).toContain("SameSite=Lax");
|
|
expect(hint).toMatch(/(?:^|; )Secure(?:;|$)/);
|
|
});
|
|
|
|
it("ignores unprefixed session cookies on deployed stages", () => {
|
|
expect(
|
|
sessionCookieValue({ headers: { cookie: `${COOKIE_ACCESS}=legacy` } }, "access", "dev"),
|
|
).toBeUndefined();
|
|
expect(
|
|
sessionCookieValue(
|
|
{ headers: { cookie: `${host.access}=host; ${COOKIE_ACCESS}=legacy` } },
|
|
"access",
|
|
"dev",
|
|
),
|
|
).toBe("host");
|
|
});
|
|
|
|
it("reads unprefixed session cookies only on local", () => {
|
|
expect(
|
|
sessionCookieValue({ headers: { cookie: `${COOKIE_ACCESS}=local` } }, "access", "local"),
|
|
).toBe("local");
|
|
});
|
|
|
|
it("parses Cookie headers and keeps the first duplicate", () => {
|
|
expect(
|
|
parseCookies({
|
|
headers: { cookie: `${host.access}=first; ${host.access}=second` },
|
|
}),
|
|
).toEqual({ [host.access]: "first" });
|
|
});
|
|
|
|
it("clears cookies with Max-Age=0 and the same host-only attributes", () => {
|
|
const cookie = clearCookie(host.access, "dev");
|
|
expect(cookie).toContain("Max-Age=0");
|
|
expect(cookie).toContain("HttpOnly");
|
|
expect(cookie).not.toMatch(/Domain=/i);
|
|
expect(cookie).toMatch(/(?:^|; )Secure(?:;|$)/);
|
|
});
|
|
|
|
it("clears both __Host- and legacy ap_* cookies on deployed stages", () => {
|
|
const cookies = clearedSessionCookies("dev");
|
|
expect(
|
|
cookies.some((item) => item.startsWith(`${host.refresh}=`) && item.includes("Max-Age=0")),
|
|
).toBe(true);
|
|
expect(
|
|
cookies.some(
|
|
(item) =>
|
|
item.startsWith(`${COOKIE_REFRESH}=`) &&
|
|
item.includes("Max-Age=0") &&
|
|
item.includes("Path=/"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("encodes oauth state without a Domain attribute", () => {
|
|
const cookie = serializeOauthCookie({ state: "st", verifier: "ver", returnTo: "/" }, "dev");
|
|
expect(cookie.startsWith(`${host.oauth}=`)).toBe(true);
|
|
expect(cookie).toContain("HttpOnly");
|
|
expect(cookie).not.toMatch(/Domain=/i);
|
|
});
|
|
});
|