Files

136 lines
4.9 KiB
TypeScript

import { expect, test } from "bun:test";
import { createContext } from "@wrnexus/core";
import {
getOriginalRequestMethod,
getOriginalRequestOrigin,
getOriginalRequestPath,
getOriginalRequestUrl,
redirectToLogin,
} from "../src/index.ts";
function context(url: string, headers: HeadersInit = {}) {
const parsed = new URL(url);
return createContext(new Request(parsed, { headers }), parsed);
}
test("uses the direct context URL when no gateway headers exist", () => {
const ctx = context("https://app.example.test/account?tab=security");
expect(getOriginalRequestUrl(ctx).href).toBe("https://app.example.test/account?tab=security");
expect(getOriginalRequestOrigin(ctx)).toBe("https://app.example.test");
expect(getOriginalRequestPath(ctx)).toBe("/account?tab=security");
expect(getOriginalRequestMethod(ctx)).toBe("GET");
});
test("reconstructs an allowed original gateway URL", () => {
const ctx = context("http://sso.localhost:3000/api/verify", {
"x-forwarded-host": "admin.localhost:3000",
"x-forwarded-proto": "http",
"x-original-method": "GET",
"x-original-uri": "/users?page=2",
});
const url = getOriginalRequestUrl(ctx, { allowedHosts: ["admin.localhost:3000"] });
expect(url.href).toBe("http://admin.localhost:3000/users?page=2");
expect(getOriginalRequestMethod(ctx)).toBe("GET");
});
test("keeps the protected app URL when the SSO verifier crosses the gateway again", () => {
const ctx = context("http://sso.localhost:3000/api/verify", {
"x-forwarded-host": "sso.localhost:3000",
"x-forwarded-proto": "http",
"x-original-host": "admin.localhost:3000",
"x-original-proto": "http",
"x-original-method": "GET",
"x-original-uri": "/settings?tab=security",
});
const response = redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000"],
});
const location = new URL(response.headers.get("location")!);
expect(location.origin).toBe("http://sso.localhost:3000");
expect(location.pathname).toBe("/login");
expect(location.searchParams.get("returnTo")).toBe(
"http://admin.localhost:3000/settings?tab=security",
);
});
test("rejects untrusted hosts and unsafe request paths", () => {
const untrusted = context("http://sso.localhost/api/verify", {
"x-forwarded-host": "evil.example",
"x-original-uri": "/",
});
const unsafePath = context("http://sso.localhost/api/verify", {
"x-forwarded-host": "admin.localhost",
"x-original-uri": "//evil.example/steal",
});
expect(() => getOriginalRequestUrl(untrusted)).toThrow("Untrusted forwarded host");
expect(() => getOriginalRequestUrl(unsafePath, { allowedHosts: ["admin.localhost"] })).toThrow(
"absolute request path",
);
});
test("creates a safe login redirect with an encoded returnTo URL", () => {
const ctx = context("http://sso.localhost:3000/api/verify", {
"x-forwarded-host": "admin.localhost:3000",
"x-forwarded-proto": "http",
"x-original-uri": "/reports?range=week",
});
const response = redirectToLogin(ctx, "/login", {
allowedHosts: new Set(["admin.localhost:3000"]),
});
const location = new URL(response.headers.get("location")!);
expect(response.status).toBe(302);
expect(location.origin).toBe("http://sso.localhost:3000");
expect(location.pathname).toBe("/login");
expect(location.searchParams.get("returnTo")).toBe(
"http://admin.localhost:3000/reports?range=week",
);
});
test("relative login redirects use the configured public app origin", () => {
const previous = process.env.WRNEXUS_APP_ORIGIN;
process.env.WRNEXUS_APP_ORIGIN = "https://sso.example.test";
try {
const ctx = context("http://127.0.0.1:3002/api/verify", {
"x-original-host": "admin.example.test",
"x-original-proto": "https",
"x-original-uri": "/settings",
});
const response = redirectToLogin(ctx, "/sign-in", {
allowedHosts: ["admin.example.test"],
});
const location = new URL(response.headers.get("location")!);
expect(location.origin).toBe("https://sso.example.test");
expect(location.pathname).toBe("/sign-in");
expect(location.searchParams.get("returnTo")).toBe("https://admin.example.test/settings");
} finally {
if (previous === undefined) delete process.env.WRNEXUS_APP_ORIGIN;
else process.env.WRNEXUS_APP_ORIGIN = previous;
}
});
test("supports an allowed-host callback and custom response options", () => {
const ctx = context("https://login.example.test/api/verify", {
"x-forwarded-host": "reports.example.test",
"x-forwarded-proto": "https",
"x-original-uri": "/",
});
const response = redirectToLogin(ctx, "https://login.example.test/sign-in?tenant=acme", {
allowedHosts: (host) => host.endsWith(".example.test"),
returnToParam: "next",
status: 303,
});
const location = new URL(response.headers.get("location")!);
expect(response.status).toBe(303);
expect(location.searchParams.get("tenant")).toBe("acme");
expect(location.searchParams.get("next")).toBe("https://reports.example.test/");
});