From a40c463a879a2666d54f91fda7f8aefc0a48b6db Mon Sep 17 00:00:00 2001 From: nyatoru Date: Mon, 23 Feb 2026 03:46:09 +0700 Subject: [PATCH 1/2] feat(api): add JWT session auth fallback for models endpoint --- src/app/api/v1/models/route.ts | 51 ++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/app/api/v1/models/route.ts b/src/app/api/v1/models/route.ts index ad39b6df06..613a0a8d68 100644 --- a/src/app/api/v1/models/route.ts +++ b/src/app/api/v1/models/route.ts @@ -3,6 +3,8 @@ import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models import { AI_PROVIDERS } from "@/shared/constants/providers"; import { getProviderConnections, getCombos, getAllCustomModels, getSettings } from "@/lib/localDb"; import { extractApiKey, isValidApiKey } from "@/sse/services/auth"; +import { jwtVerify } from "jose"; +import { cookies } from "next/headers"; import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry.ts"; import { getAllImageModels } from "@omniroute/open-sse/config/imageRegistry.ts"; import { getAllRerankModels } from "@omniroute/open-sse/config/rerankRegistry.ts"; @@ -104,9 +106,54 @@ export async function GET(request: Request) { settings = await getSettings(); } catch {} if (settings.requireAuthForModels === true) { + // Check authentication: API key OR dashboard session (JWT cookie) + let isAuthenticated = false; + + // 1. Check API key (for external clients) const apiKey = extractApiKey(request); - if (!apiKey || !(await isValidApiKey(apiKey))) { - return new Response("Not Found", { status: 404 }); + if (apiKey && (await isValidApiKey(apiKey))) { + isAuthenticated = true; + } + + // 2. Check JWT cookie (ONLY for dashboard requests - same origin) + // External API clients must use API key authentication + if (!isAuthenticated && process.env.JWT_SECRET) { + const origin = request.headers.get("origin"); + const referer = request.headers.get("referer"); + const host = request.headers.get("host"); + + // Check if request is from dashboard (same origin) + const isDashboardRequest = + !origin || // No origin header = same-origin request (browser navigation) + (host && origin.includes(host)) || // Origin matches host + (referer && host && referer.includes(host)); // Referer matches host + + if (isDashboardRequest) { + try { + const cookieStore = await cookies(); + const token = cookieStore.get("auth_token")?.value; + if (token) { + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + await jwtVerify(token, secret); + isAuthenticated = true; + } + } catch { + // Invalid/expired token or cookies not available — not authenticated + } + } + } + + if (!isAuthenticated) { + return Response.json( + { + error: { + message: "Authentication required", + type: "invalid_request_error", + code: "invalid_api_key", + }, + }, + { status: 401 } + ); } } From 02dc8ea0f3b4fef023e30e015208a6e58829963e Mon Sep 17 00:00:00 2001 From: nyatoru Date: Mon, 23 Feb 2026 04:00:22 +0700 Subject: [PATCH 2/2] fix(api): enhance authentication for /models endpoint with stricter checks --- src/app/api/v1/models/route.ts | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/app/api/v1/models/route.ts b/src/app/api/v1/models/route.ts index 613a0a8d68..3db7c4d0d6 100644 --- a/src/app/api/v1/models/route.ts +++ b/src/app/api/v1/models/route.ts @@ -99,8 +99,9 @@ export async function OPTIONS() { */ export async function GET(request: Request) { try { - // Issue #100: Optionally require API key for /models (security hardening) - // When enabled, unauthenticated requests get 404 to hide endpoint existence + // Issue #100: Optionally require authentication for /models (security hardening) + // When enabled, unauthenticated requests get 401 with proper error response. + // Supports API key (Bearer token) for external clients and JWT cookie for dashboard. let settings: Record = {}; try { settings = await getSettings(); @@ -121,12 +122,53 @@ export async function GET(request: Request) { const origin = request.headers.get("origin"); const referer = request.headers.get("referer"); const host = request.headers.get("host"); + const secFetchSite = request.headers.get("sec-fetch-site"); // Check if request is from dashboard (same origin) - const isDashboardRequest = - !origin || // No origin header = same-origin request (browser navigation) - (host && origin.includes(host)) || // Origin matches host - (referer && host && referer.includes(host)); // Referer matches host + // Security: Use strict matching instead of loose includes() + let isDashboardRequest = false; + + // Check sec-fetch-site header (set by browser, harder to spoof) + // "same-origin" = same origin request, "none" = same origin navigation + if (secFetchSite === "same-origin" || secFetchSite === "none") { + isDashboardRequest = true; + } + + // Fallback: Check origin/referer against host with proper URL parsing + if (!isDashboardRequest && host) { + const normalizeHost = (h: string) => { + // Remove port if present for comparison + const colonIndex = h.lastIndexOf(":"); + return colonIndex > 0 ? h.slice(0, colonIndex) : h; + }; + const normalizedHost = normalizeHost(host); + + if (origin) { + try { + const originUrl = new URL(origin); + const normalizedOrigin = normalizeHost(originUrl.host); + // Exact match only + if (normalizedOrigin === normalizedHost) { + isDashboardRequest = true; + } + } catch { + // Invalid URL, not a dashboard request + } + } + + if (!isDashboardRequest && referer) { + try { + const refererUrl = new URL(referer); + const normalizedReferer = normalizeHost(refererUrl.host); + // Exact match only + if (normalizedReferer === normalizedHost) { + isDashboardRequest = true; + } + } catch { + // Invalid URL, not a dashboard request + } + } + } if (isDashboardRequest) { try {