Support OpenWebUI: verify X-OpenWebUI-User-Jwt (HS256) or fall back to X-User-Email + portal secret
This commit is contained in:
1 parent
57991cd9f7
commit
80ac982055
2 files changed
+36
-8
No files matched your search
+35
-8
@@ -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.
|
||||||
_verify_portal_secret(request)
|
#
|
||||||
|
# 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)
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user