Files
OmniRoute/src/app/api/a2a/_auth.ts
Diego Rodrigues de Sa e Souza 9cb91dee74 fix(security): close round-3 advisories (v7g9, x7vm, j7j4, jcm5) + exposure warning (wmgv) (#11261)
Validated on a 2-PR combined board: routeGuard 36/36 (within the 68/68 focused-file total), a2a-task-owner-idor 7/7, a2a-tasks-auth, search-baseurl-ssrf-guard, cli-serve-hostname, spawn-capable-prefixes-client-safe all green, typecheck:core + dashboard-typecheck clean, gates within baseline. Five real High-severity advisories fixed with TDD (each failing-then-passing): settings export/import-json ALWAYS_PROTECTED completion, MITM route LOCAL_ONLY+SPAWN_CAPABLE gating, search baseUrl SSRF/IMDS guard, A2A REST task auth+ownership (previously none at all), and the loud boot exposure warning. GHSA-cjv9 confirmed already closed on this base (verified). Round 3 of the advisory sweep.
2026-08-23 20:32:42 -03:00

52 lines
2.2 KiB
TypeScript

/**
* Shared authorization for the REST A2A task routes (GHSA-jcm5-6wpp-wjj8).
*
* Dual audience: the dashboard calls these routes with a management session,
* A2A clients with an inference API key. Posture matrix:
*
* - REQUIRE_API_KEY=true: a valid OmniRoute key is mandatory (the same
* posture the /v1 inference plane enforces); a management session also
* passes (dashboard), via alwaysRequireAuth so requireLogin=false cannot
* bypass it.
* - otherwise + requireLogin=true: management session, or a valid key.
* - otherwise + requireLogin=false (local-first default): open, by design.
*
* Callers authenticated by key are owner-scoped — another principal's tasks
* answer as if they did not exist. Management/operator view sees all tasks.
*/
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags";
import { resolveA2AOwner } from "@/lib/a2a/authenticate";
export interface A2ARestAuth {
/** Owner scope for task reads/mutations; undefined = operator view (all tasks). */
owner: string | undefined;
}
/**
* NOTE: the failure branch is whatever requireManagementAuth returns — today a
* plain `Response` from createErrorResponse(), NOT a NextResponse. Callers must
* test with `instanceof Response` (NextResponse extends Response), never
* `instanceof NextResponse`, or the 401 silently falls through to the handler.
*/
export async function authorizeA2ATaskRoute(request: Request): Promise<A2ARestAuth | Response> {
const apiKey = extractApiKey(request);
if (isRequireApiKeyEnabled()) {
if (apiKey && (await isValidApiKey(apiKey))) return { owner: resolveA2AOwner(request) };
const managementError = await requireManagementAuth(request, {
invalidApiKeyStatus: 401,
alwaysRequireAuth: true,
});
if (managementError === null) return { owner: undefined };
return managementError;
}
const managementError = await requireManagementAuth(request, { invalidApiKeyStatus: 401 });
if (managementError === null) return { owner: undefined };
if (apiKey && (await isValidApiKey(apiKey))) return { owner: resolveA2AOwner(request) };
return managementError;
}