Files
OmniRoute/src/shared/utils/clientApiRouteAuth.ts
Alex 53f8284842 fix(api): enforce image generation API key auth (#8306)
* fix(api): enforce image generation API key auth

* fix(api): align image route auth guard with clientApiPolicy

The route-level guard added for image generation was stricter than the
authz middleware that already fronts /api/v1/* (src/proxy.ts →
clientApiPolicy), so requests the pipeline admits were 401'd by the
handler:

- A cookie-authenticated dashboard session was rejected under
  REQUIRE_API_KEY=true. The dashboard Media page
  (dashboard/cache/media) and the Playground call these routes with a
  session and no Bearer — the same mismatch already fixed for
  /api/playground/presets.
- A presented invalid key was rejected even with REQUIRE_API_KEY=false,
  where clientApiPolicy (#2257) and the sibling /v1/embeddings and
  /v1/web/fetch routes degrade a stale CLI key to anonymous instead.

Extract the shared guard into shared/utils/clientApiRouteAuth so both
image routes (and future /v1 handlers) mirror the middleware contract
instead of re-deriving it, and drop the now-dead auth imports.

Also switch the call-log attribution fallback back to `||`: with `??`,
an empty-string apiKeyId/apiKeyName would be persisted verbatim and
would block the request-scoped context, which the previous
`entry.apiKeyId || null` never did.

Tests: cover the dashboard-session and keyless-mode-invalid-key
branches, and split the auth/attribution cases into
image-generation-route-auth.test.ts to stay under the 800-line
new-test-file cap.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: alexey.nazarov@softmg.ru <alexey.nazarov@softmg.ru>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 22:38:25 -03:00

51 lines
2.0 KiB
TypeScript

/**
* Route-level authentication guard for CLIENT_API (`/api/v1/*`) handlers.
*
* Defence-in-depth companion to `clientApiPolicy`
* (src/server/authz/policies/clientApi.ts), which already fronts these routes
* through `src/proxy.ts`. The handler-level check must never be *stricter*
* than the middleware, otherwise callers the pipeline admits get a 401 from
* the route instead:
*
* - a presented key must be valid, but is only rejected while
* `REQUIRE_API_KEY=true`; with enforcement off a stale CLI key degrades to
* anonymous instead of failing the whole request (#2257);
* - a cookie-authenticated dashboard session is accepted in place of a key —
* the dashboard Media page and Playground call these routes with a session
* only (same fix as the preset auth mismatch in
* `src/app/api/playground/presets/route.ts`);
* - anonymous traffic stays allowed while `REQUIRE_API_KEY=false`.
*
* @module shared/utils/clientApiRouteAuth
*/
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags";
import { isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth";
/**
* Authenticate a client-API request at the route level.
*
* @param request - The incoming request.
* @returns A 401 `Response` the handler must return, or `null` when the
* request may proceed to policy enforcement.
*/
export async function enforceClientApiRouteAuth(request: Request): Promise<Response | null> {
const apiKeyRaw = extractApiKey(request);
if (apiKeyRaw) {
if (await isValidApiKey(apiKeyRaw)) return null;
return isRequireApiKeyEnabled()
? errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key")
: null;
}
if (await isDashboardSessionAuthenticated(request)) return null;
return isRequireApiKeyEnabled()
? errorResponse(HTTP_STATUS.UNAUTHORIZED, "Authentication required")
: null;
}