import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { getConfig, updateOidcConfig, updateSecurityConfig, type Config } from "../../config"; import { createUser } from "../../users"; import { createOidcLoginRedirect, resolveOidcUser } from "../oidc"; import { getOidcLocalMfaBlockReason, handleOidcRoutes } from "./oidc-routes"; const server = { requestIP: () => ({ address: "127.0.0.1" }), }; let originalOidc: Config["oidc"]; let originalSecurity: Config["security"]; const originalFetch = globalThis.fetch; beforeAll(() => { originalOidc = { ...getConfig().oidc, scopes: [...getConfig().oidc.scopes] }; originalSecurity = { ...getConfig().security }; }); afterAll(async () => { globalThis.fetch = originalFetch; await updateOidcConfig(originalOidc); await updateSecurityConfig(originalSecurity); }); describe("OIDC routes", () => { test("status reports disabled when OIDC is not configured", async () => { const req = new Request("https://localhost/api/oidc/status"); const url = new URL(req.url); const res = await handleOidcRoutes(req, url, server); expect(res?.status).toBe(200); const body = await res!.json(); expect(body.enabled).toBe(false); expect(body.loginUrl).toBeNull(); }); test("login redirect stores a browser-bound OIDC state cookie", async () => { const issuer = `https://oidc-state-${Date.now()}.example.test`; await configureOidc(issuer); mockDiscovery(issuer); const req = new Request("https://localhost/api/oidc/login?returnTo=/settings.html"); const res = await createOidcLoginRedirect(req, new URL(req.url)); expect(res.status).toBe(302); expect(res.headers.get("Set-Cookie")).toContain("overlord_oidc_state="); expect(res.headers.get("Set-Cookie")).toContain("HttpOnly"); expect(res.headers.get("Set-Cookie")).toContain("SameSite=Lax"); const redirect = new URL(res.headers.get("Location")!); expect(redirect.searchParams.get("state")).toBeTruthy(); expect(redirect.searchParams.get("code_challenge_method")).toBe("S256"); }); test("callback rejects state values that are not bound to the browser cookie", async () => { const issuer = `https://oidc-csrf-${Date.now()}.example.test`; await configureOidc(issuer); mockDiscovery(issuer); const loginReq = new Request("https://localhost/api/oidc/login"); const loginRes = await createOidcLoginRedirect(loginReq, new URL(loginReq.url)); const state = new URL(loginRes.headers.get("Location")!).searchParams.get("state")!; const callbackReq = new Request(`https://localhost/api/oidc/callback?code=fake-code&state=${state}`); const callbackRes = await handleOidcRoutes(callbackReq, new URL(callbackReq.url), server); expect(callbackRes?.status).toBe(302); const location = new URL(callbackRes!.headers.get("Location")!); expect(location.pathname).toBe("/login.html"); expect(location.searchParams.get("oidc_error")).toContain("state cookie mismatch"); expect(callbackRes!.headers.get("Set-Cookie")).toContain("overlord_oidc_state=;"); }); test("login redirect rejects non-HTTPS issuers before discovery", async () => { await configureOidc("http://oidc-insecure.example.test"); setMockFetch(async () => { throw new Error("fetch should not be called for insecure issuer"); }); const req = new Request("https://localhost/api/oidc/login"); await expect(createOidcLoginRedirect(req, new URL(req.url))).rejects.toThrow("HTTPS"); }); test("email linking does not fall back to local usernames", async () => { const username = `oidc_link_${Date.now().toString(36)}`; const created = await createUser(username, "Aa1!VeryLongTestPassword_2026", "operator", "test"); expect(created.success).toBe(true); const oidc: Config["oidc"] = { ...getConfig().oidc, enabled: true, issuer: "https://issuer.example.test", clientId: "client", allowEmailLink: true, autoProvision: false, allowedEmails: [], allowedDomains: [], }; await expect(resolveOidcUser(oidc, "https://issuer.example.test", { sub: `subject-${Date.now()}`, email: `${username}@example.test`, email_verified: true, preferred_username: "not-local", } as any)).rejects.toThrow("No linked Overlord account exists"); }); test("OIDC session issuance is blocked when local MFA is enabled or required", async () => { await updateSecurityConfig({ ...getConfig().security, mfaRequiredForAdmins: true, mfaRequiredForNonAdmins: false, }); expect(getOidcLocalMfaBlockReason({ role: "operator", mfa_enabled: 1, mfa_secret: "secret", })).toContain("Local MFA is enabled"); expect(getOidcLocalMfaBlockReason({ role: "admin", mfa_enabled: 0, mfa_secret: null, })).toContain("Local MFA is required"); expect(getOidcLocalMfaBlockReason({ role: "operator", mfa_enabled: 0, mfa_secret: null, })).toBeNull(); }); }); async function configureOidc(issuer: string): Promise { await updateOidcConfig({ enabled: true, issuer, clientId: "overlord-test", clientSecret: "secret", redirectUri: "https://localhost/api/oidc/callback", scopes: ["openid", "profile", "email"], clientAuthMethod: "client_secret_post", autoProvision: false, allowEmailLink: false, defaultRole: "viewer", allowedEmails: [], allowedDomains: [], groupClaim: "groups", adminGroups: [], operatorGroups: [], viewerGroups: [], }); } function mockDiscovery(issuer: string): void { setMockFetch(async (input: RequestInfo | URL) => { const url = String(input); if (url === `${issuer}/.well-known/openid-configuration`) { return Response.json({ issuer, authorization_endpoint: `${issuer}/authorize`, token_endpoint: `${issuer}/token`, jwks_uri: `${issuer}/jwks`, }); } throw new Error(`Unexpected OIDC fetch: ${url}`); }); } function setMockFetch(handler: (input: RequestInfo | URL, init?: RequestInit) => Promise): void { globalThis.fetch = Object.assign(handler, { preconnect: originalFetch.preconnect, }) as typeof fetch; }