// @vitest-environment node import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { decodeProtectedHeader, jwtVerify, SignJWT } from "jose"; vi.mock("server-only", () => ({})); import { authenticateRequest, signAuthToken, toAuthIdentity, verifyAuthToken, } from "@/lib/auth/server"; beforeEach(() => { vi.stubEnv("JWT_SECRET", "ai-control-center-local-development-secret-2026"); vi.stubEnv("JWT_ISSUER", "corp-ui"); vi.stubEnv("JWT_AUDIENCE", "aegida-gate"); vi.stubEnv("AEGIDA_TENANT_ID", "tenant-1"); }); afterEach(() => vi.unstubAllEnvs()); describe("server authentication", () => { it("issues the exact gate-verifiable HS256 identity for 24 hours", async () => { const token = await signAuthToken({ id: "11111111-1111-4111-8111-111111111111", email: "user@example.corp", externalUserId: "42", }); const user = await verifyAuthToken(token); const { payload } = await jwtVerify( token, new TextEncoder().encode("ai-control-center-local-development-secret-2026"), { algorithms: ["HS256"], issuer: "corp-ui", audience: "aegida-gate" }, ); expect(user).toEqual({ id: "11111111-1111-4111-8111-111111111111", email: "user@example.corp", externalUserId: "42", tenantId: "tenant-1", iat: expect.any(Number), exp: expect.any(Number), }); expect(user.exp - user.iat).toBe(86_400); expect(payload).toEqual({ sub: "11111111-1111-4111-8111-111111111111", email: "user@example.corp", tenant_id: "tenant-1", external_user_id: "42", iss: "corp-ui", aud: "aegida-gate", iat: expect.any(Number), exp: expect.any(Number), }); expect(decodeProtectedHeader(token)).toEqual({ alg: "HS256", typ: "JWT" }); }); it.each([ ["wrong issuer", { iss: "other-ui" }], ["missing issuer", { iss: undefined }], ["wrong audience", { aud: "other-gate" }], ["missing audience", { aud: undefined }], ["additional audience", { aud: ["aegida-gate", "other-gate"] }], ["wrong tenant", { tenant_id: "tenant-2" }], ["missing tenant", { tenant_id: undefined }], ["invalid external ID", { external_user_id: "042" }], ["missing external ID", { external_user_id: undefined }], ["expired", { exp: 1 }], ])("rejects a token with %s", async (_name, patch) => { await expect(verifyAuthToken(await testToken(patch))).rejects.toMatchObject({ status: 401, }); }); it("rejects a wrong algorithm and signature", async () => { await expect( verifyAuthToken(await testToken({}, "HS384")), ).rejects.toMatchObject({ status: 401 }); await expect( verifyAuthToken(await testToken({}, "HS256", "different-secret-that-is-still-at-least-32-bytes")), ).rejects.toMatchObject({ status: 401 }); }); it("requires a JWT secret of at least 32 UTF-8 bytes", async () => { vi.stubEnv("JWT_SECRET", "short-secret"); await expect( signAuthToken({ id: "11111111-1111-4111-8111-111111111111", email: "user@example.corp", externalUserId: "42", }), ).rejects.toThrow("JWT_SECRET"); }); it.each(["tenant/unsafe", "tenant-нет", "t".repeat(129)])( "rejects a Gate-incompatible configured tenant %j", async (tenantId) => { vi.stubEnv("AEGIDA_TENANT_ID", tenantId); await expect( signAuthToken({ id: "11111111-1111-4111-8111-111111111111", email: "user@example.corp", externalUserId: "42", }), ).rejects.toThrow("AEGIDA_TENANT_ID"); }, ); it("rejects missing and malformed bearer authorization", async () => { await expect( authenticateRequest(new Request("http://localhost/api/chat")), ).rejects.toMatchObject({ status: 401 }); await expect( authenticateRequest( new Request("http://localhost/api/chat", { headers: { Authorization: "Token invalid" }, }), ), ).rejects.toMatchObject({ status: 401 }); }); it("accepts the bearer scheme without regard to letter case", async () => { const token = await signAuthToken({ id: "00000000-0000-4000-8000-000000000001", email: "demo@ai-control.local", externalUserId: "42", }); await expect( authenticateRequest( new Request("http://localhost/api/chat", { headers: { Authorization: `bEaReR ${token}` }, }), ), ).resolves.toMatchObject({ id: "00000000-0000-4000-8000-000000000001" }); }); it("rejects a signed token with a non-UUID subject", async () => { const token = await signAuthToken({ id: "legacy-user", email: "demo@ai-control.local", externalUserId: "42", }); await expect(verifyAuthToken(token)).rejects.toMatchObject({ status: 401 }); }); it("projects synchronized external profiles into complete app identities", () => { expect(toAuthIdentity({ id: "00000000-0000-4000-8000-000000000001", externalUserId: "42", login: "alice", fullName: "Alice Smith", position: "Engineer", email: "alice@example.com", firstName: "Alice", lastName: "Smith", middleName: "Jane", roles: ["admin", "employee"], lastModelId: "chatgpt", passwordHash: "", createdAt: "2026-07-01T12:00:00.000Z", updatedAt: "2026-07-02T12:00:00.000Z", })).toEqual({ id: "00000000-0000-4000-8000-000000000001", externalUserId: "42", login: "alice", fullName: "Alice Smith", position: "Engineer", email: "alice@example.com", firstName: "Alice", lastName: "Smith", middleName: "Jane", roles: ["admin", "employee"], lastModelId: "chatgpt", }); }); }); async function testToken( changes: Record, algorithm = "HS256", secret = "ai-control-center-local-development-secret-2026", ): Promise { const claims: Record = { email: "user@example.corp", tenant_id: "tenant-1", external_user_id: "42", iss: "corp-ui", aud: "aegida-gate", sub: "11111111-1111-4111-8111-111111111111", iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600, ...changes, }; for (const [claim, value] of Object.entries(claims)) { if (value === undefined) delete claims[claim]; } return new SignJWT(claims) .setProtectedHeader({ alg: algorithm, typ: "JWT" }) .sign(new TextEncoder().encode(secret)); }