231 lines
8.1 KiB
TypeScript
231 lines
8.1 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
import { extractTokenFromCookie, extractTokenFromHeader, generateToken } from "./auth";
|
|
import { getConfig, updateAppearanceConfig, type Config } from "./config";
|
|
import { generateTotpCode } from "./mfa";
|
|
import { getBrandingImage } from "./db";
|
|
import {
|
|
createUser,
|
|
deleteUser,
|
|
enableUserMfa,
|
|
getUserById,
|
|
setUserMfaSecret,
|
|
} from "./users";
|
|
import { handleAuthRoutes } from "./server/routes/auth-routes";
|
|
import { handleMiscRoutes } from "./server/routes/misc-routes";
|
|
|
|
const PASSWORD = "Aa1!AuthMfaTest_2026";
|
|
const mockServer = {
|
|
requestIP: () => ({ address: "127.0.0.1" }),
|
|
};
|
|
let originalAppearance: Config["appearance"];
|
|
const uploadPublicRoot = ".";
|
|
|
|
beforeAll(() => {
|
|
originalAppearance = JSON.parse(JSON.stringify(getConfig().appearance));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (originalAppearance) {
|
|
await updateAppearanceConfig(originalAppearance.customCSS, originalAppearance.loginBranding);
|
|
}
|
|
});
|
|
|
|
describe("auth token extraction", () => {
|
|
test("extractTokenFromHeader returns bearer token", () => {
|
|
expect(extractTokenFromHeader("Bearer abc123")).toBe("abc123");
|
|
expect(extractTokenFromHeader("Basic abc123")).toBeNull();
|
|
});
|
|
|
|
test("extractTokenFromCookie finds overlord_token", () => {
|
|
const cookie = "other=1; overlord_token=token123; theme=dark";
|
|
expect(extractTokenFromCookie(cookie)).toBe("token123");
|
|
});
|
|
|
|
test("extractTokenFromCookie returns null when missing", () => {
|
|
expect(extractTokenFromCookie("foo=bar")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("account onboarding", () => {
|
|
test("is pending for a new account and remains completed at the account level", async () => {
|
|
const username = `onboarding_${Date.now().toString(36)}`;
|
|
const created = await createUser(username, PASSWORD, "viewer", "test");
|
|
expect(created.success).toBe(true);
|
|
|
|
try {
|
|
const user = getUserById(created.userId!);
|
|
expect(user).toBeTruthy();
|
|
const token = await generateToken(user!);
|
|
const headers = { Cookie: `overlord_token=${token}` };
|
|
|
|
const meUrl = new URL("https://localhost/api/auth/me");
|
|
const before = await handleAuthRoutes(new Request(meUrl, { headers }), meUrl, mockServer);
|
|
expect(before?.status).toBe(200);
|
|
expect(before?.headers.get("cache-control")).toContain("no-store");
|
|
expect((await before!.json()).needsOnboarding).toBe(true);
|
|
|
|
const completeUrl = new URL("https://localhost/api/auth/onboarding/complete");
|
|
const completed = await handleAuthRoutes(
|
|
new Request(completeUrl, { method: "POST", headers }),
|
|
completeUrl,
|
|
mockServer,
|
|
);
|
|
expect(completed?.status).toBe(200);
|
|
expect(completed?.headers.get("cache-control")).toContain("no-store");
|
|
|
|
const after = await handleAuthRoutes(new Request(meUrl, { headers }), meUrl, mockServer);
|
|
expect(after?.status).toBe(200);
|
|
expect((await after!.json()).needsOnboarding).toBe(false);
|
|
|
|
const repeated = await handleAuthRoutes(
|
|
new Request(completeUrl, { method: "POST", headers }),
|
|
completeUrl,
|
|
mockServer,
|
|
);
|
|
expect(repeated?.status).toBe(200);
|
|
expect((await repeated!.json()).needsOnboarding).toBe(false);
|
|
} finally {
|
|
deleteUser(created.userId!);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("login branding", () => {
|
|
test("returns configured public login branding", async () => {
|
|
const url = new URL("https://localhost/api/login/branding");
|
|
const res = await handleAuthRoutes(new Request(url), url, mockServer);
|
|
expect(res?.status).toBe(200);
|
|
|
|
const body = (await res!.json()) as any;
|
|
expect(body).toEqual(originalAppearance.loginBranding);
|
|
});
|
|
|
|
test("returns sanitized enterprise branding fields", async () => {
|
|
await updateAppearanceConfig("", {
|
|
productName: "Acme SOC",
|
|
navName: "Acme Console",
|
|
title: "Welcome to Acme",
|
|
subtitle: "Use your Acme identity",
|
|
iconClass: "fa-solid fa-shield-halved<script>",
|
|
logoUrl: "javascript:alert(1)",
|
|
navLogoUrl: "/assets/acme-nav.png",
|
|
heroImageUrl: "https://cdn.example.test/login.jpg",
|
|
accentColor: "#14B8A6",
|
|
footerText: "Authorized access only",
|
|
supportText: "Need help?",
|
|
supportUrl: "https://help.example.test",
|
|
});
|
|
|
|
const url = new URL("https://localhost/api/login/branding");
|
|
const res = await handleAuthRoutes(new Request(url), url, mockServer);
|
|
expect(res?.status).toBe(200);
|
|
|
|
const body = (await res!.json()) as any;
|
|
expect(body.productName).toBe("Acme SOC");
|
|
expect(body.navName).toBe("Acme Console");
|
|
expect(body.iconClass).toBe("fa-solid fa-shield-halvedscript");
|
|
expect(body.logoUrl).toBe("");
|
|
expect(body.navLogoUrl).toBe("/assets/acme-nav.png");
|
|
expect(body.heroImageUrl).toBe("https://cdn.example.test/login.jpg");
|
|
expect(body.accentColor).toBe("#14b8a6");
|
|
expect(body.footerText).toBe("Authorized access only");
|
|
expect(body.supportUrl).toBe("https://help.example.test/");
|
|
});
|
|
});
|
|
|
|
describe("branding uploads", () => {
|
|
test("accepts admin image upload and returns a branding asset URL", async () => {
|
|
const username = `branding_upload_${Date.now().toString(36)}`;
|
|
const created = await createUser(username, PASSWORD, "admin", "test");
|
|
expect(created.success).toBe(true);
|
|
|
|
try {
|
|
const user = getUserById(created.userId!);
|
|
expect(user).toBeTruthy();
|
|
const token = await generateToken(user!);
|
|
const pngBytes = new Uint8Array([
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
0x00, 0x00, 0x00, 0x0d,
|
|
]);
|
|
const form = new FormData();
|
|
form.append("kind", "nav-logo");
|
|
form.append("file", new File([pngBytes], "logo.png", { type: "image/png" }));
|
|
|
|
const url = new URL("https://localhost/api/settings/appearance/image");
|
|
const res = await handleMiscRoutes(
|
|
new Request(url, {
|
|
method: "POST",
|
|
headers: { Cookie: `overlord_token=${token}` },
|
|
body: form,
|
|
}),
|
|
url,
|
|
{
|
|
CORS_HEADERS: {},
|
|
SERVER_VERSION: "test",
|
|
PUBLIC_ROOT: uploadPublicRoot,
|
|
requestIP: () => ({ address: "127.0.0.1" }),
|
|
getConsoleSessionCount: () => 0,
|
|
getRdSessionCount: () => 0,
|
|
getFileBrowserSessionCount: () => 0,
|
|
getProcessSessionCount: () => 0,
|
|
},
|
|
);
|
|
|
|
expect(res?.status).toBe(200);
|
|
const body = (await res!.json()) as any;
|
|
expect(body.url).toBe("/api/branding/image/nav-logo");
|
|
const stored = getBrandingImage("nav-logo");
|
|
expect(stored?.contentType).toBe("image/png");
|
|
expect(Array.from(stored?.bytes || [])).toEqual(Array.from(pngBytes));
|
|
} finally {
|
|
deleteUser(created.userId!);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("auth MFA login", () => {
|
|
test("MFA-enabled user gets a challenge and can login with TOTP", async () => {
|
|
const username = `mfa_user_${Date.now().toString(36)}`;
|
|
const created = await createUser(username, PASSWORD, "operator", "test");
|
|
expect(created.success).toBe(true);
|
|
|
|
try {
|
|
const secret = "JBSWY3DPEHPK3PXP";
|
|
expect(setUserMfaSecret(created.userId!, secret).success).toBe(true);
|
|
expect(enableUserMfa(created.userId!).success).toBe(true);
|
|
|
|
const url = new URL("https://localhost/api/login");
|
|
const challenge = await handleAuthRoutes(
|
|
new Request(url, {
|
|
method: "POST",
|
|
body: JSON.stringify({ user: username, pass: PASSWORD }),
|
|
}),
|
|
url,
|
|
mockServer,
|
|
);
|
|
expect(challenge?.status).toBe(202);
|
|
expect((await challenge!.json()).mfaRequired).toBe(true);
|
|
|
|
const login = await handleAuthRoutes(
|
|
new Request(url, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
user: username,
|
|
pass: PASSWORD,
|
|
mfaCode: generateTotpCode(secret),
|
|
}),
|
|
}),
|
|
url,
|
|
mockServer,
|
|
);
|
|
expect(login?.status).toBe(200);
|
|
const body = (await login!.json()) as any;
|
|
expect(body.ok).toBe(true);
|
|
expect(body.token).toBeTruthy();
|
|
expect(getUserById(created.userId!)).not.toBeNull();
|
|
} finally {
|
|
deleteUser(created.userId!);
|
|
}
|
|
});
|
|
});
|