64 lines
3.6 KiB
TypeScript
64 lines
3.6 KiB
TypeScript
import { afterEach, beforeAll, beforeEach, expect, it, vi } from "vitest";
|
|
import { exportJWK, generateKeyPair, SignJWT } from "jose";
|
|
const auth = vi.hoisted(() => ({ session: vi.fn() }));
|
|
vi.mock("next-auth", () => ({ getServerSession: auth.session }));
|
|
vi.mock("@/lib/auth/admin-auth", () => ({ adminAuthOptions: {}, requiredAdminRole: "ops" }));
|
|
const issuer = "https://sso.example/realms/operators";
|
|
let signed: string;
|
|
const fetcher = vi.fn();
|
|
beforeAll(async () => {
|
|
const keys = await generateKeyPair("RS256");
|
|
const jwks = { keys: [{ ...await exportJWK(keys.publicKey), kid: "whoami-key", alg: "RS256" }] };
|
|
fetcher.mockImplementation(async () => Response.json(jwks));
|
|
signed = await new SignJWT({ resource_access: { portal: { roles: ["ops"] } }, name: "Not exposed", email: "private@example.test" })
|
|
.setProtectedHeader({ alg: "RS256", kid: "whoami-key" }).setIssuer(issuer).setAudience("portal")
|
|
.setSubject("machine-subject").setExpirationTime("5m").sign(keys.privateKey);
|
|
});
|
|
beforeEach(() => {
|
|
vi.stubEnv("KEYCLOAK_ISSUER_URL", issuer);
|
|
vi.stubEnv("KEYCLOAK_CLIENT_ID", "portal");
|
|
vi.stubGlobal("fetch", fetcher);
|
|
});
|
|
afterEach(() => { auth.session.mockReset(); vi.unstubAllEnvs(); vi.unstubAllGlobals(); });
|
|
|
|
import { assertResponse } from "@/test/openapi-contract";
|
|
import { GET } from "./route";
|
|
async function get(request: Request) {
|
|
const response = await GET(request);
|
|
await assertResponse("/api/admin/whoami", "get", response);
|
|
return response;
|
|
}
|
|
function request(authorization?: string) {
|
|
return new Request("https://portal.example/api/admin/whoami", { headers: authorization === undefined ? {} : { authorization } });
|
|
}
|
|
it("returns only a safe machine identity through real bearer verification", async () => {
|
|
auth.session.mockResolvedValue({ user: { roles: ["ops"] } });
|
|
const response = await get(request(`Bearer ${signed}`));
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("cache-control")).toBe("no-store");
|
|
expect(await response.json()).toEqual({ authenticationMethod: "bearer", subject: "machine-subject", name: null, email: null });
|
|
expect(auth.session).not.toHaveBeenCalled();
|
|
});
|
|
it("returns the existing browser identity without roles or session internals", async () => {
|
|
auth.session.mockResolvedValue({ user: { name: "Admin", email: "admin@example.test", roles: ["ops"], image: "private-image" }, expires: "private-expiry" });
|
|
const response = await get(request());
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("cache-control")).toBe("no-store");
|
|
expect(await response.json()).toEqual({ authenticationMethod: "session", subject: null, name: "Admin", email: "admin@example.test" });
|
|
});
|
|
it.each([401, 403, 503])("returns a safe %s problem for browser auth failures", async (status) => {
|
|
if (status === 503) auth.session.mockRejectedValue(new Error("private-session-error"));
|
|
else auth.session.mockResolvedValue(status === 401 ? null : { user: { roles: [] } });
|
|
const response = await get(request());
|
|
expect(response.status).toBe(status);
|
|
expect(response.headers.get("content-type")).toBe("application/problem+json");
|
|
expect(response.headers.get("www-authenticate")).toBe(status === 401 ? 'Bearer realm="admin-api"' : null);
|
|
expect(await response.json()).toMatchObject({ status, instance: "/api/admin/whoami" });
|
|
});
|
|
it("never falls back to browser auth for a supplied invalid token", async () => {
|
|
auth.session.mockResolvedValue({ user: { roles: ["ops"] } });
|
|
const response = await get(request("Bearer invalid"));
|
|
expect(response.status).toBe(401);
|
|
expect(auth.session).not.toHaveBeenCalled();
|
|
});
|