import { afterEach, describe, expect, it } from "vitest"; import { buildApp } from "../src/app.js"; import { loadEnv } from "../src/config/env.js"; import { canAccessProject } from "../src/security/authStore.js"; import { parseAutonomyClaimInput } from "../src/routes/autonomy.js"; import { isOwnerRequest, parseWorkGraphLeaseInput } from "../src/routes/operatingSystem.js"; import { hashPassword, verifyPassword } from "../src/security/password.js"; import { apiPaths, authSessionDataSchema, successEnvelopeSchema } from "@projectbrain/contracts"; import { PassThrough } from "node:stream"; const ownerPassword = "correct-horse-battery-staple"; const env = () => loadEnv({ NODE_ENV: "test", LOG_LEVEL: "silent", PROJECTBRAIN_DATA_SOURCE: "fixture", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: ownerPassword, PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters", PROJECTBRAIN_SESSION_TTL_HOURS: "1", PROJECTBRAIN_TRUST_PROXY: "false", }); const apps: Awaited>[] = []; const makeApp = async () => { const app = await buildApp({ env: env() }); apps.push(app); return app; }; afterEach(async () => { await Promise.all(apps.splice(0).map((app) => app.close())); }); const login = async (app: Awaited>) => { const response = await app.inject({ method: "POST", url: apiPaths.auth.login, payload: { username: "owner", password: ownerPassword } }); const cookie = response.cookies.find((item) => item.name === "projectbrain_session"); return { response, cookie: cookie ? `${cookie.name}=${cookie.value}` : "", csrfToken: response.json().data?.csrfToken as string }; }; describe("authentication boundary", () => { it("rejects known insecure production bootstrap credentials", () => { expect(() => loadEnv({ NODE_ENV: "production", DATABASE_URL: "postgresql://example.invalid/projectbrain", PROJECTBRAIN_DATA_SOURCE: "fixture", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: "adminadminadmin", PROJECTBRAIN_AUTH_SECRET: "production-secret-with-at-least-thirty-two-characters" })).toThrow(/insecure default/); }); it("requires a complete, database-backed Entra configuration", () => { const base = { NODE_ENV: "test", PROJECTBRAIN_DATA_SOURCE: "fixture", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: ownerPassword, PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters" }; expect(() => loadEnv({ ...base, PROJECTBRAIN_ENTRA_TENANT_ID: "11111111-1111-4111-8111-111111111111" })).toThrow(/must be configured together/); expect(() => loadEnv({ ...base, PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com", PROJECTBRAIN_ENTRA_TENANT_ID: "11111111-1111-4111-8111-111111111111", PROJECTBRAIN_ENTRA_CLIENT_ID: "22222222-2222-4222-8222-222222222222", PROJECTBRAIN_ENTRA_CLIENT_SECRET: "a-test-secret-value" })).toThrow(/database/); const blank = loadEnv({ ...base, PROJECTBRAIN_ENTRA_TENANT_ID: "", PROJECTBRAIN_ENTRA_CLIENT_ID: "", PROJECTBRAIN_ENTRA_CLIENT_SECRET: "" }); expect(blank.PROJECTBRAIN_ENTRA_TENANT_ID).toBeUndefined(); }); it("marks the production session cookie Secure", async () => { const productionEnv = loadEnv({ NODE_ENV: "production", DATABASE_URL: "postgresql://example.invalid/projectbrain", LOG_LEVEL: "silent", PROJECTBRAIN_DATA_SOURCE: "fixture", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: ownerPassword, PROJECTBRAIN_AUTH_SECRET: "production-secret-with-at-least-thirty-two-characters", PROJECTBRAIN_SESSION_TTL_HOURS: "1", PROJECTBRAIN_TRUST_PROXY: "false" }); const app = await buildApp({ env: productionEnv }); apps.push(app); const response = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "owner", password: ownerPassword } }); expect(response.headers["set-cookie"]).toContain("Secure"); }, 10_000); it("hashes passwords with a random salt and rejects malformed hashes", async () => { const first = await hashPassword(ownerPassword); const second = await hashPassword(ownerPassword); expect(first).not.toBe(second); expect(await verifyPassword(ownerPassword, first)).toBe(true); expect(await verifyPassword("incorrect-password-value", first)).toBe(false); expect(await verifyPassword(ownerPassword, "not-a-supported-hash")).toBe(false); }); it("denies fixture project access to a non-owner identity", async () => { expect(await canAccessProject(env(), { id: "member-1", username: "member", role: "member", credentialVersion: 1 }, "widefrog", false)).toBe(false); }); it("keeps portfolio-wide operating-system access owner-only", () => { expect(isOwnerRequest({ authUser: { id: "owner-1", username: "owner", role: "owner", credentialVersion: 1 } } as never)).toBe(true); expect(isOwnerRequest({ authUser: { id: "member-1", username: "member", role: "member", credentialVersion: 1 } } as never)).toBe(false); }); it("keeps only deliberate health, readiness and login bootstrap routes public", async () => { const app = await makeApp(); expect((await app.inject({ method: "GET", url: "/api/health" })).statusCode).toBe(200); expect((await app.inject({ method: "GET", url: "/api/system/readiness" })).statusCode).toBe(200); const metrics = await app.inject({ method: "GET", url: "/api/metrics" }); expect(metrics.statusCode).toBe(200); expect(metrics.body).toContain("projectbrain_http_requests_total"); expect(metrics.body).toContain("projectbrain_database_ready 1"); const entraConfig = await app.inject({ method: "GET", url: apiPaths.auth.entraConfig }); expect(entraConfig.statusCode).toBe(200); expect(entraConfig.json().data).toEqual({ enabled: false, provider: "microsoft-entra" }); expect((await app.inject({ method: "GET", url: apiPaths.auth.entraStart })).statusCode).toBe(503); expect((await app.inject({ method: "POST", url: apiPaths.auth.entraLinkStart })).statusCode).toBe(401); const protectedResponse = await app.inject({ method: "GET", url: "/api/projects" }); expect(protectedResponse.statusCode).toBe(401); expect(protectedResponse.json().error.code).toBe("AUTHENTICATION_REQUIRED"); }); it("rejects unsafe incoming correlation IDs and emits a safe replacement", async () => { const app = await makeApp(); const response = await app.inject({ method: "GET", url: "/api/health", headers: { "x-request-id": "unsafe\nrequest" } }); expect(response.headers["x-request-id"]).toMatch(/^req-[0-9a-f-]+$/); }); it("redacts credentials and project content from standard logs", async () => { const chunks: string[] = []; const logStream = new PassThrough(); logStream.on("data", (chunk) => chunks.push(chunk.toString())); const loggedEnv = { ...env(), LOG_LEVEL: "info" as const }; const app = await buildApp({ env: loggedEnv, logStream }); apps.push(app); await app.inject({ method: "POST", url: apiPaths.auth.login, payload: { username: "owner", password: "secret-project-content" } }); await new Promise((resolve) => setImmediate(resolve)); const output = chunks.join(""); expect(output).not.toContain("secret-project-content"); expect(output).not.toContain(ownerPassword); }); it("creates a secure server-side session and exposes no password", async () => { const app = await makeApp(); const { response, cookie, csrfToken } = await login(app); expect(response.statusCode).toBe(200); expect(cookie).toContain("projectbrain_session="); expect(response.headers["set-cookie"]).toContain("HttpOnly"); expect(response.headers["set-cookie"]).toContain("SameSite=Lax"); expect(csrfToken.length).toBeGreaterThan(20); expect(JSON.stringify(response.json())).not.toContain(ownerPassword); expect(successEnvelopeSchema(authSessionDataSchema).safeParse(response.json()).success).toBe(true); const session = await app.inject({ method: "GET", url: apiPaths.auth.session, headers: { cookie } }); expect(session.statusCode).toBe(200); expect(session.headers["set-cookie"]).toContain("Max-Age=3600"); expect(session.json().data.user.username).toBe("owner"); expect(successEnvelopeSchema(authSessionDataSchema).safeParse(session.json()).success).toBe(true); }); it("rejects invalid credentials and enforces login rate limiting", async () => { const app = await makeApp(); const attempts = []; for (let index = 0; index < 6; index += 1) attempts.push(await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "owner", password: "incorrect-password-value" } })); expect(attempts[0].statusCode).toBe(401); expect(attempts[5].statusCode).toBe(429); }); it("rejects malformed login and an incorrect current password", async () => { const app = await makeApp(); expect((await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "x", password: "short" } })).statusCode).toBe(400); const { cookie, csrfToken } = await login(app); const response = await app.inject({ method: "POST", url: "/api/auth/rotate-password", headers: { cookie, "x-csrf-token": csrfToken }, payload: { currentPassword: "incorrect-password-value", newPassword: "a-different-secure-owner-password" } }); expect(response.statusCode).toBe(403); }); it("requires CSRF for mutations and accepts the session token", async () => { const app = await makeApp(); const { cookie, csrfToken } = await login(app); const missing = await app.inject({ method: "POST", url: "/api/projects", headers: { cookie }, payload: { name: "Secure Project", description: "A securely created project." } }); expect(missing.statusCode).toBe(403); expect(missing.json().error.code).toBe("CSRF_TOKEN_INVALID"); const accepted = await app.inject({ method: "POST", url: "/api/projects", headers: { cookie, "x-csrf-token": csrfToken }, payload: { name: "Secure Project", description: "A securely created project." } }); expect(accepted.statusCode).toBe(201); }); it("does not accept a caller-supplied actor for durable work-graph commands", async () => { expect(parseWorkGraphLeaseInput({ actor: "system:owner", leaseMinutes: 10 })).toEqual({ leaseMinutes: 10 }); expect(parseAutonomyClaimInput({ actor: "system:owner", runId: "scheduled-run" })).toEqual({ runId: "scheduled-run" }); }); it("revokes a session on logout", async () => { const app = await makeApp(); const { cookie, csrfToken } = await login(app); expect((await app.inject({ method: "POST", url: "/api/auth/logout", headers: { cookie, "x-csrf-token": csrfToken } })).statusCode).toBe(200); expect((await app.inject({ method: "GET", url: "/api/projects", headers: { cookie } })).statusCode).toBe(401); }); it("rotates credentials and revokes every existing session", async () => { const app = await makeApp(); const { cookie, csrfToken } = await login(app); const rotated = await app.inject({ method: "POST", url: "/api/auth/rotate-password", headers: { cookie, "x-csrf-token": csrfToken }, payload: { currentPassword: ownerPassword, newPassword: "a-different-secure-owner-password" } }); expect(rotated.statusCode).toBe(200); expect((await app.inject({ method: "GET", url: "/api/projects", headers: { cookie } })).statusCode).toBe(401); expect((await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "owner", password: ownerPassword } })).statusCode).toBe(401); expect((await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "owner", password: "a-different-secure-owner-password" } })).statusCode).toBe(200); }); it("emits baseline security headers", async () => { const app = await makeApp(); const response = await app.inject({ method: "GET", url: "/api/health" }); expect(response.headers["content-security-policy"]).toContain("default-src 'self'"); expect(response.headers["x-content-type-options"]).toBe("nosniff"); expect(response.headers["x-frame-options"]).toBe("SAMEORIGIN"); }); });