from __future__ import annotations import hmac import time from dataclasses import dataclass from typing import Annotated import jwt from fastapi import Header, HTTPException, status from agent_platform.config import get_settings @dataclass(frozen=True, slots=True) class UserIdentity: user_id: str email: str name: str role: str def issue_internal_identity(identity: UserIdentity, secret: str, ttl_seconds: int = 300) -> str: """Mint a fresh, short-lived identity for a trusted internal service hop.""" if not secret: raise ValueError("Identity signing secret is not configured") if ttl_seconds < 1: raise ValueError("Identity lifetime must be positive") now = int(time.time()) return jwt.encode( { "sub": identity.user_id, "email": identity.email, "name": identity.name, "role": identity.role, "iss": "open-webui", "iat": now, "exp": now + ttl_seconds, }, secret, algorithm="HS256", ) def verify_service_bearer(authorization: str | None, expected: str) -> None: if not expected: raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Service secret is not configured") scheme, _, token = (authorization or "").partition(" ") if scheme.lower() != "bearer" or not hmac.compare_digest(token, expected): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid service credentials") def decode_openwebui_identity(token: str | None, secret: str) -> UserIdentity: if not secret: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Identity verification is unavailable", ) if not token: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing signed user identity") try: claims = jwt.decode( token, secret, algorithms=["HS256"], issuer="open-webui", options={"require": ["sub", "iss", "iat", "exp"]}, ) except jwt.PyJWTError as exc: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid signed user identity") from exc user_id = str(claims.get("sub", "")).strip() if not user_id: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Signed identity has no subject") return UserIdentity( user_id=user_id, email=str(claims.get("email", "")), name=str(claims.get("name", "")), role=str(claims.get("role", "user")), ) def require_gateway_identity( authorization: Annotated[ str | None, Header(alias="Authorization", include_in_schema=False), ] = None, user_jwt: Annotated[ str | None, Header(alias="X-OpenWebUI-User-Jwt", include_in_schema=False), ] = None, ) -> UserIdentity: settings = get_settings() verify_service_bearer(authorization, settings.internal_gateway_key) return decode_openwebui_identity(user_jwt, settings.openwebui_forward_jwt_secret)