Compare commits

...

4 Commits

Author SHA1 Message Date
diegosouzapw a17583d3fc 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
2026-02-22 18:36:57 -03:00
diegosouzapw 62c634ae78 Merge PR #110: feat(api): add JWT session auth fallback for models endpoint 2026-02-22 18:34:41 -03:00
nyatoru 02dc8ea0f3 fix(api): enhance authentication for /models endpoint with stricter checks 2026-02-23 04:00:22 +07:00
nyatoru a40c463a87 feat(api): add JWT session auth fallback for models endpoint 2026-02-23 03:46:09 +07:00
3 changed files with 62 additions and 5 deletions
+18
View File
@@ -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
+1 -1
View File
@@ -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": {
+43 -4
View File
@@ -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";
@@ -97,16 +99,53 @@ 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<string, any> = {};
try {
settings = await getSettings();
} 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)
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 (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) {
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 }
);
}
}