Files
coreci-chat/packages/auth/tests/middleware.test.ts
T
CIAgent bcca7686ee feat(P2): Wave B identity & RBAC — WorkOS SSO, tenant provisioning, RBAC at gateway
REQ-001: SSO session via WorkOS (getAuthorizationUrl + exchangeCodeForSession)
REQ-002: tenant provisioning on first signup (admin role)
REQ-003: invitations via single-use acceptance link
REQ-004: RBAC role assignment (admin/operator/viewer)
REQ-005: RBAC enforcement at API gateway from first endpoint (/api/me)

---ci---
phase: 2
milestone: v0.1
status: execute
---/ci---
2026-08-25 01:47:56 +00:00

145 lines
5.8 KiB
TypeScript

/**
* Auth middleware tests — REQ-005.
*
* - extracts the user from the `coreci_session` cookie on a valid session.
* - returns `unauthorized` when no cookie is present.
* - returns `unauthorized` when the cookie is invalid/tampered.
* - returns `forbidden` when the role lacks the required permission.
* - returns `authorized` for a viewer hitting a read route, `forbidden` for
* a viewer hitting an admin route.
*/
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import type { DbClient } from "@coreci/db";
import { bootTestDb, seedTenantAndUser, type TestDb } from "./helpers.js";
import { createSession } from "../src/sessions.js";
import { createAuthMiddleware, authenticate, SESSION_COOKIE, type AuthRequest } from "../src/middleware.js";
const SIGNING_KEY = "mw-test-session-signing-key";
function reqFor(method: string, path: string, cookie: string | undefined): AuthRequest {
return {
method,
path,
getCookie(name: string) {
return name === SESSION_COOKIE ? cookie : undefined;
},
};
}
describe("auth middleware (REQ-005)", () => {
let testDb: TestDb;
let db: DbClient;
let tenantId: string;
let adminUserId: string;
beforeAll(async () => {
testDb = await bootTestDb();
db = testDb.db;
const seeded = await seedTenantAndUser(db, "org_mw", "mw-admin@example.com");
tenantId = seeded.tenantId;
adminUserId = seeded.userId;
});
afterAll(async () => {
await testDb.teardown();
});
it("returns unauthorized when no cookie is present", async () => {
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
const r = await mw(reqFor("GET", "/api/me", undefined));
expect(r.status).toBe("unauthorized");
expect(r).toMatchObject({ status: "unauthorized", reason: "no_cookie" });
});
it("returns unauthorized for an invalid/tampered cookie", async () => {
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
const r = await mw(reqFor("GET", "/api/me", "garbage.token.value"));
expect(r.status).toBe("unauthorized");
expect(r).toMatchObject({ status: "unauthorized", reason: "invalid_session" });
});
it("extracts user + authorizes a viewer on a read route", async () => {
// Create a viewer session.
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "viewer", {
lifetimeSeconds: 60,
});
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
const r = await mw(reqFor("GET", "/api/me", token));
expect(r.status).toBe("authorized");
if (r.status === "authorized") {
expect(r.user.userId).toBe(adminUserId);
expect(r.user.tenantId).toBe(tenantId);
expect(r.user.role).toBe("viewer");
expect(r.decision.required).toBe("read");
}
});
it("forbids a viewer on an admin route (POST /api/byom)", async () => {
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "viewer", {
lifetimeSeconds: 60,
});
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
const r = await mw(reqFor("POST", "/api/byom", token));
expect(r.status).toBe("forbidden");
if (r.status === "forbidden") {
expect(r.decision.required).toBe("admin");
expect(r.user.role).toBe("viewer");
}
});
it("authorizes an operator on a read route but forbids admin routes", async () => {
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "operator", {
lifetimeSeconds: 60,
});
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
expect((await mw(reqFor("GET", "/api/me", token))).status).toBe("authorized");
expect((await mw(reqFor("GET", "/api/targets", token))).status).toBe("authorized");
expect((await mw(reqFor("POST", "/api/byom", token))).status).toBe("forbidden");
expect((await mw(reqFor("POST", "/api/relay/issue-token", token))).status).toBe("forbidden");
});
it("authorizes an admin on every listed route", async () => {
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "admin", {
lifetimeSeconds: 60,
});
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
for (const [method, path] of [
["GET", "/api/me"],
["POST", "/api/byom"],
["GET", "/api/team"],
["POST", "/api/team"],
["PATCH", "/api/team/00000000-0000-0000-0000-0000000000aa"],
["POST", "/api/invitations"],
["GET", "/api/invitations"],
["POST", "/api/relay/issue-token"],
["GET", "/api/targets"],
] as const) {
const r = await mw(reqFor(method, path, token));
expect(r.status, `${method} ${path}`).toBe("authorized");
}
});
it("role change is enforced on the next API call (REQ-004)", async () => {
// Start as admin, hit an admin route (allowed).
const { token, sessionId } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "admin", {
lifetimeSeconds: 60,
});
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
expect((await mw(reqFor("POST", "/api/byom", token))).status).toBe("authorized");
// Simulate a role change: update the sessions row role to viewer.
await db.query("UPDATE sessions SET role = 'viewer' WHERE id = $1", [sessionId]);
// The very next call is now forbidden — RBAC enforced from the gateway.
const r = await mw(reqFor("POST", "/api/byom", token));
expect(r.status).toBe("forbidden");
});
it("authenticate() standalone helper matches createAuthMiddleware", async () => {
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "admin", {
lifetimeSeconds: 60,
});
const r = await authenticate(db, SIGNING_KEY, reqFor("GET", "/api/me", token));
expect(r.status).toBe("authorized");
});
});