From a17583d3fc797f465e9ee61f631d0caeffdc5ec2 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 22 Feb 2026 18:36:57 -0300 Subject: [PATCH] feat(api): JWT session auth for models endpoint + refactor (v1.2.0) - Merged PR #110 by @nyatoru: JWT session auth fallback for /v1/models - Refactored: removed ~60 lines of same-origin detection (sameSite:lax already prevents CSRF) - Changed auth failure response: 404 Not Found -> 401 Unauthorized (OpenAI format) - Version bumped to v1.2.0 Closes #110 --- CHANGELOG.md | 18 ++++++++ package.json | 2 +- src/app/api/v1/models/route.ts | 78 ++++++---------------------------- 3 files changed, 33 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b660cea7..017091d589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [1.2.0] — 2026-02-22 + +> ### ✨ Feature Release — Dashboard Session Auth for Models Endpoint +> +> Dashboard users can now access `/v1/models` via their existing session when API key auth is required. + +### ✨ New Features + +- **JWT Session Auth Fallback** — When `requireAuthForModels` is enabled, the `/v1/models` endpoint now accepts both API key (Bearer token) for external clients **and** the dashboard JWT session cookie (`auth_token`), allowing logged-in dashboard users to view models without needing an explicit API key ([PR #110](https://github.com/diegosouzapw/OmniRoute/pull/110) by [@nyatoru](https://github.com/nyatoru)) + +### 🔧 Improvements + +- **401 instead of 404** — Authentication failures on `/v1/models` now return `401 Unauthorized` with a structured JSON error body (OpenAI-compatible format) instead of a generic `404 Not Found`, improving debuggability for API clients +- **Simplified auth logic** — Refactored the JWT cookie verification to reuse the same pattern as `apiAuth.ts`, removing redundant same-origin detection (~60 lines) since the `sameSite:lax` + `httpOnly` cookie flags already provide equivalent CSRF protection + +--- + ## [1.1.1] — 2026-02-22 > ### 🐛 Bugfix Release — API Key Creation & Codex Team Plan Quotas @@ -450,6 +467,7 @@ New environment variables: --- +[1.2.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.2.0 [1.1.1]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.1.1 [1.0.7]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.7 [1.0.6]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.6 diff --git a/package.json b/package.json index bb682bd8c5..bc99aef827 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "1.1.1", + "version": "1.2.0", "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.", "type": "module", "bin": { diff --git a/src/app/api/v1/models/route.ts b/src/app/api/v1/models/route.ts index 3db7c4d0d6..34364811ac 100644 --- a/src/app/api/v1/models/route.ts +++ b/src/app/api/v1/models/route.ts @@ -108,6 +108,7 @@ export async function GET(request: Request) { } catch {} if (settings.requireAuthForModels === true) { // Check authentication: API key OR dashboard session (JWT cookie) + // Supports dual auth: Bearer token for external clients, cookie for dashboard. let isAuthenticated = false; // 1. Check API key (for external clients) @@ -116,72 +117,21 @@ export async function GET(request: Request) { isAuthenticated = true; } - // 2. Check JWT cookie (ONLY for dashboard requests - same origin) - // External API clients must use API key authentication + // 2. Check JWT cookie (for dashboard session) + // The auth_token cookie has sameSite:lax + httpOnly, which already + // prevents cross-origin abuse — no additional origin check needed. + // Same pattern as shared/utils/apiAuth.ts verifyAuth(). if (!isAuthenticated && process.env.JWT_SECRET) { - 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) - // 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 { - 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 + 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 } }