From 4e43b19d4e144e2852c63f5743cc8872a829fd58 Mon Sep 17 00:00:00 2001 From: Narendranath Gogineni Date: Tue, 19 May 2026 16:17:31 +0530 Subject: [PATCH 1/5] add auth validation for public routes --- CLAUDE.md | 3 + conf.example.json | 1 + conf.json | 3 +- src/middlewares/adminAuth/index.ts | 161 ++++++++++++++++++++++++ src/public/index.html | 188 ++++++++++++++++++++++++++++- src/start-server.ts | 9 +- 6 files changed, 359 insertions(+), 6 deletions(-) create mode 100644 src/middlewares/adminAuth/index.ts diff --git a/CLAUDE.md b/CLAUDE.md index 9d16a34fc..c9346bfe1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,7 @@ This is the **Portkey AI Gateway** - a fast, reliable AI gateway that routes req - `hooks` - Pre/post request hooks - `memoryCache` - Response caching - `logger` - Request/response logging +- `adminAuth` - Node-only local UI session auth for `/public/*` login endpoints and `/log/stream` - `portkey` - Core Portkey-specific middleware for routing, guardrails, etc. **Plugin System (`plugins/`)** @@ -89,4 +90,6 @@ Tests are organized by component: The gateway uses `conf.json` for runtime configuration. Sample config available in `conf_sample.json`. +For local UI hardening, set `admin_token` in `conf.json`. The `/public` UI uses this token to establish an in-memory admin session, and `/log/stream` requires that session. + Key environment variables and configuration handled through Hono's adapter system for multi-environment deployment. \ No newline at end of file diff --git a/conf.example.json b/conf.example.json index e4c72f33a..865c8113a 100644 --- a/conf.example.json +++ b/conf.example.json @@ -1,4 +1,5 @@ { + "admin_token": "set-a-strong-local-admin-token", "plugins_enabled": [ "default", "portkey", diff --git a/conf.json b/conf.json index 940c8bdd6..75aa9b680 100644 --- a/conf.json +++ b/conf.json @@ -17,5 +17,6 @@ "apiKey": "..." } }, - "cache": false + "cache": false, + "admin_token": "test-token" } diff --git a/src/middlewares/adminAuth/index.ts b/src/middlewares/adminAuth/index.ts new file mode 100644 index 000000000..8855b79f2 --- /dev/null +++ b/src/middlewares/adminAuth/index.ts @@ -0,0 +1,161 @@ +import { Context, Next } from 'hono'; +import conf from '../../../conf.json'; + +const SESSION_COOKIE_NAME = 'portkey_admin_session'; +const SESSION_MAX_AGE_SECONDS = 60 * 60 * 12; // 12 hours +const adminSessions = new Map(); + +const getConfiguredAdminToken = (): string => { + const adminToken = conf?.admin_token; + if ( + !adminToken || + typeof adminToken !== 'string' || + adminToken.trim() === '' + ) { + throw new Error( + 'Admin UI auth requires conf.json.admin_token. Set admin_token or start the gateway with --headless.' + ); + } + return adminToken; +}; + +const parseCookies = (cookieHeader?: string): Record => { + if (!cookieHeader) return {}; + + return cookieHeader.split(';').reduce( + (acc, cookiePart) => { + const [rawKey, ...valueParts] = cookiePart.trim().split('='); + if (!rawKey) return acc; + acc[rawKey] = decodeURIComponent(valueParts.join('=')); + return acc; + }, + {} as Record + ); +}; + +const getSessionId = (c: Context): string | undefined => { + const cookies = parseCookies(c.req.header('cookie')); + return cookies[SESSION_COOKIE_NAME]; +}; + +const getBearerToken = (c: Context): string | undefined => { + const authHeader = c.req.header('authorization'); + if (!authHeader) return undefined; + + const [scheme, token] = authHeader.trim().split(/\s+/, 2); + if (!scheme || !token) return undefined; + if (scheme.toLowerCase() !== 'bearer') return undefined; + + return token; +}; + +const isSessionActive = (sessionId?: string): boolean => { + if (!sessionId) return false; + + const expiresAt = adminSessions.get(sessionId); + if (!expiresAt) return false; + + if (expiresAt < Date.now()) { + adminSessions.delete(sessionId); + return false; + } + + return true; +}; + +const createSession = (): string => { + const sessionId = crypto.randomUUID(); + const expiresAt = Date.now() + SESSION_MAX_AGE_SECONDS * 1000; + adminSessions.set(sessionId, expiresAt); + return sessionId; +}; + +const setSessionCookie = (c: Context, sessionId: string) => { + c.header( + 'Set-Cookie', + `${SESSION_COOKIE_NAME}=${encodeURIComponent(sessionId)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_MAX_AGE_SECONDS}` + ); +}; + +export const adminAuthMiddleware = async (c: Context, next: Next) => { + let configuredToken: string; + try { + configuredToken = getConfiguredAdminToken(); + } catch (error) { + const message = + error instanceof Error + ? error.message + : 'Admin UI auth is misconfigured.'; + return c.json({ status: 'failure', message }, 500); + } + + const hasValidSession = isSessionActive(getSessionId(c)); + const hasValidBearerToken = getBearerToken(c) === configuredToken; + + if (!hasValidSession && !hasValidBearerToken) { + return c.json( + { + status: 'failure', + message: + 'Admin authentication required. Use a valid admin session cookie or Authorization: Bearer .', + }, + 401 + ); + } + + await next(); +}; + +export const adminAuthSessionStatusHandler = (c: Context) => { + try { + getConfiguredAdminToken(); + } catch (error) { + const message = + error instanceof Error + ? error.message + : 'Admin UI auth is misconfigured.'; + return c.json({ status: 'failure', message }, 500); + } + + return c.json({ authenticated: isSessionActive(getSessionId(c)) }); +}; + +export const adminAuthLoginHandler = async (c: Context) => { + let configuredToken: string; + try { + configuredToken = getConfiguredAdminToken(); + } catch (error) { + const message = + error instanceof Error + ? error.message + : 'Admin UI auth is misconfigured.'; + return c.json({ status: 'failure', message }, 500); + } + + let body: { admin_token?: string } = {}; + try { + body = await c.req.json(); + } catch { + return c.json( + { + status: 'failure', + message: 'Invalid request body. Expected JSON with admin_token.', + }, + 400 + ); + } + + if (!body.admin_token || body.admin_token !== configuredToken) { + return c.json( + { + status: 'failure', + message: 'Invalid admin token.', + }, + 401 + ); + } + + const sessionId = createSession(); + setSessionCookie(c, sessionId); + return c.json({ authenticated: true }); +}; diff --git a/src/public/index.html b/src/public/index.html index 9bd7e77e2..014ff42b1 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -851,6 +851,54 @@ border-bottom-color: #3b82f6; font-weight: bold; } + + body.admin-auth-pending header, + body.admin-auth-pending .main-content { + display: none; + } + + .admin-auth-modal { + position: fixed; + inset: 0; + z-index: 3000; + display: none; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.6); + padding: 1rem; + } + + body.admin-auth-pending .admin-auth-modal { + display: flex; + } + + .admin-auth-card { + width: 100%; + max-width: 420px; + background: white; + border-radius: 0.75rem; + padding: 1.25rem; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25); + } + + .admin-auth-title { + margin: 0 0 0.4rem; + font-size: 1.2rem; + font-weight: 700; + } + + .admin-auth-subtitle { + margin: 0 0 0.9rem; + font-size: 0.875rem; + color: #6b7280; + } + + .admin-auth-error { + margin-top: 0.6rem; + color: #dc2626; + font-size: 0.8rem; + min-height: 1.1rem; + } @@ -867,11 +915,29 @@ - + +
+
+

Admin token required

+

Enter your admin_token stored in conf.json to access the local gateway UI. see github discussion for more details.

+
+ + +
+
+
+