Files
coreci-chat/apps/control-plane/lib/auth.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

82 lines
2.8 KiB
TypeScript

/**
* control-plane lib/auth — Next.js adapter for @coreci/auth middleware.
*
* Wraps the framework-agnostic `createAuthMiddleware` so a Route Handler can
* call `authenticateRequest(request)` and get back a typed AuthResult that
* either yields the user (authorized) or a ready-made Next.js Response (401 /
* 403) the handler can return directly.
*
* REQ-005 critical-path: every /api/* route calls this BEFORE doing work.
*/
import type { NextRequest } from "next/server";
import { authenticate, type AuthResult, type AuthRequest } from "@coreci/auth";
import { getDb } from "./db.js";
/** SESSION_SIGNING_KEY — infra bootstrap cred (G-010 tier a). Set via env. */
function signingKey(): string {
const k = process.env.SESSION_SIGNING_KEY;
if (!k) throw new Error("SESSION_SIGNING_KEY not configured");
return k;
}
/** Convert a NextRequest into the framework-agnostic AuthRequest shape. */
function toAuthRequest(req: NextRequest): AuthRequest {
const url = req.nextUrl;
return {
method: req.method,
path: url.pathname,
getCookie(name: string): string | undefined {
const c = req.cookies.get(name);
return c?.value;
},
};
}
/**
* Authenticate + enforce RBAC for a Next.js Route Handler.
* Returns the AuthResult; the caller branches on `.status`.
*/
export async function authenticateRequest(req: NextRequest): Promise<AuthResult> {
const db = await getDb();
return authenticate(db, signingKey(), toAuthRequest(req));
}
/** Helper: turn an unauthorized/forbidden AuthResult into a Next.js Response. */
export function authFailureResponse(result: AuthResult): Response {
if (result.status === "unauthorized") {
return new Response(
JSON.stringify({ error: "unauthorized", reason: result.reason }),
{ status: 401, headers: { "content-type": "application/json" } },
);
}
return new Response(
JSON.stringify({
error: "forbidden",
required: result.decision.required,
unknownRole: result.decision.unknownRole,
}),
{ status: 403, headers: { "content-type": "application/json" } },
);
}
/**
* Require authorization for a route. Returns the user on success, or a Response
* (already-serialized 401/403) on failure. The canonical route handler shape:
*
* export async function GET(req) {
* const auth = await requireAuth(req, "GET", "/api/me");
* if (auth instanceof Response) return auth;
* const user = auth.user; // {id, tenantId, role}
* ...
* }
*/
export async function requireAuth(
req: NextRequest,
): Promise<{ user: { id: string; tenantId: string; role: "admin" | "operator" | "viewer" } } | Response> {
const result = await authenticateRequest(req);
if (result.status === "authorized") {
return { user: { id: result.user.userId, tenantId: result.user.tenantId, role: result.user.role } };
}
return authFailureResponse(result);
}