// @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<string, unknown>,
algorithm = "HS256",
secret = "ai-control-center-local-development-secret-2026",
): Promise<string> {
const claims: Record<string, unknown> = {
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));
}