Support OpenWebUI: verify X-OpenWebUI-User-Jwt (HS256) or fall back to X-User-Email + portal secret

This commit is contained in:
inference-bot committed 2026-09-06 21:51:25 -06:00
1 parent 57991cd9f7
commit 80ac982055
2 files changed
+35 -7

No files matched your search

+34 -7
View File
@@ -20,6 +20,7 @@ import sqlite3
import secrets import secrets
import httpx import httpx
import jwt
from fastapi import FastAPI, Request, Response, HTTPException from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@@ -35,11 +36,18 @@ LITELLM_BASE = os.environ.get("LITELLM_BASE", "https://gateway.inference.coop")
LITELLM_MASTER_KEY = os.environ.get("LITELLM_MASTER_KEY", "") LITELLM_MASTER_KEY = os.environ.get("LITELLM_MASTER_KEY", "")
OPENCOLLECTIVE_SECRET = os.environ.get("OPENCOLLECTIVE_WEBHOOK_SECRET", "") OPENCOLLECTIVE_SECRET = os.environ.get("OPENCOLLECTIVE_WEBHOOK_SECRET", "")
# Shared secret that LibreChat sends as a header on every request, so the # Shared secret that the chat frontend uses to authenticate requests to the
# portal can verify the request genuinely came through LibreChat (which is # portal. LibreChat sends it as an X-Portal-Secret header; OpenWebUI signs the
# behind Cloudron SSO) rather than a direct, spoofed request. # user's email as a JWT (see FORWARD_USER_INFO_HEADER_JWT_SECRET below).
PORTAL_SECRET = os.environ.get("PORTAL_SECRET", "") PORTAL_SECRET = os.environ.get("PORTAL_SECRET", "")
# OpenWebUI can sign the forwarded user identity as a JWT using this shared
# secret. When set, the portal verifies the JWT (HS256) to confirm the email
# genuinely came from OpenWebUI (behind Cloudron SSO) rather than a spoofed
# header. This is stronger than the plain X-Portal-Secret header because the
# email itself is tamper-proof.
OWUI_JWT_SECRET = os.environ.get("OWUI_JWT_SECRET", "")
# Secret token required in the Open Collective webhook URL path. Open # Secret token required in the Open Collective webhook URL path. Open
# Collective's generic webhooks are not HMAC-signed, so a secret in the URL # Collective's generic webhooks are not HMAC-signed, so a secret in the URL
# is the standard way to authenticate them. # is the standard way to authenticate them.
@@ -497,13 +505,32 @@ async def list_models():
@app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) @app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def inject_key(request: Request, path: str): async def inject_key(request: Request, path: str):
"""Read the member's email from a header, inject their key, forward to LiteLLM.""" """Read the member's email from a header, inject their key, forward to LiteLLM."""
# Verify the request came through LibreChat (shared secret), so a direct # Verify the request came through the chat frontend, so a direct caller
# caller can't spoof the X-User-Email header and use another member's key. # can't spoof the user-email header and use another member's key.
#
# Two supported auth paths:
# 1. OpenWebUI signs the user identity as a JWT (X-OpenWebUI-User-Jwt)
# with a shared secret — the email is tamper-proof, so no separate
# secret header is needed.
# 2. LibreChat sends a plain X-User-Email header plus an X-Portal-Secret
# shared-secret header.
email = ""
jwt_header = request.headers.get("x-openwebui-user-jwt", "")
if jwt_header and OWUI_JWT_SECRET:
try:
claims = jwt.decode(jwt_header, OWUI_JWT_SECRET, algorithms=["HS256"])
email = claims.get("email", "")
except jwt.PyJWTError:
raise HTTPException(401, "Invalid user identity token")
else:
_verify_portal_secret(request) _verify_portal_secret(request)
email = (
request.headers.get("x-user-email", "")
or request.headers.get("x-openwebui-user-email", "")
)
email = request.headers.get("x-user-email", "")
if not email: if not email:
raise HTTPException(401, "No member identity (x-user-email header)") raise HTTPException(401, "No member identity (user-email header)")
# Look up the member's key from the persistent store # Look up the member's key from the persistent store
member_key = get_member_key(email) member_key = get_member_key(email)
+1
View File
@@ -2,3 +2,4 @@ fastapi==0.115.0
uvicorn[standard]==0.30.6 uvicorn[standard]==0.30.6
httpx==0.27.2 httpx==0.27.2
pydantic==2.9.2 pydantic==2.9.2
PyJWT==2.9.0