mirror of
https://github.com/tiennm99/litellm.git
synced 2026-06-18 09:32:08 +00:00
fix(sso): replace httpx.AsyncClient() with get_async_httpx_client
Use the cached SSO_HANDLER client instead of creating a new httpx.AsyncClient per request in PKCE token exchange and userinfo fetch. Converts httpx.BasicAuth to a manual Authorization header since AsyncHTTPHandler.post() does not accept an auth param. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,6 @@ import secrets
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast
|
||||
|
||||
import httpx
|
||||
import jwt
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
@@ -2801,20 +2800,19 @@ class SSOAuthenticationHandler:
|
||||
if redirect_url:
|
||||
token_data["redirect_uri"] = redirect_url
|
||||
|
||||
post_kwargs: Dict[str, Any] = {
|
||||
"data": token_data,
|
||||
"headers": {
|
||||
**additional_headers,
|
||||
"Content-Type": "application/x-www-form-urlencoded", # must not be overridden
|
||||
"Accept": "application/json",
|
||||
},
|
||||
"timeout": 30.0,
|
||||
request_headers = {
|
||||
**additional_headers,
|
||||
"Content-Type": "application/x-www-form-urlencoded", # must not be overridden
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
if not include_client_id:
|
||||
# Use Basic Auth only when a secret is available; public PKCE clients omit it.
|
||||
if client_secret:
|
||||
post_kwargs["auth"] = httpx.BasicAuth(client_id, client_secret)
|
||||
credentials = base64.b64encode(
|
||||
f"{client_id}:{client_secret}".encode()
|
||||
).decode()
|
||||
request_headers["Authorization"] = f"Basic {credentials}"
|
||||
else:
|
||||
token_data["client_id"] = client_id
|
||||
else:
|
||||
@@ -2822,27 +2820,27 @@ class SSOAuthenticationHandler:
|
||||
if client_secret:
|
||||
token_data["client_secret"] = client_secret
|
||||
|
||||
# The try/except is INSIDE the async with so that TLS teardown exceptions
|
||||
# from __aexit__ propagate as-is and are NOT mis-labelled as "Token endpoint
|
||||
# request failed". httpx buffers the full response body before __aexit__,
|
||||
# so status_code / text / json() remain valid after the context exits.
|
||||
async with httpx.AsyncClient() as http_client:
|
||||
try:
|
||||
response = await http_client.post(token_endpoint, **post_kwargs)
|
||||
except Exception as exc:
|
||||
# Catch network-level errors (SSL, DNS, TCP, timeout, etc.) and
|
||||
# wrap them as a clean ProxyException rather than leaking raw
|
||||
# httpx or OS exceptions to callers.
|
||||
verbose_proxy_logger.error("PKCE token endpoint unreachable: %s", exc)
|
||||
raise ProxyException(
|
||||
message=f"Token endpoint request failed: {exc}",
|
||||
type=ProxyErrorTypes.auth_error,
|
||||
param="token_exchange",
|
||||
code=status.HTTP_401_UNAUTHORIZED,
|
||||
) from exc
|
||||
|
||||
# Response processing outside the async with — httpx buffers the full
|
||||
# response body so status_code / text / json() remain valid after __aexit__.
|
||||
http_client = get_async_httpx_client(
|
||||
llm_provider=httpxSpecialProvider.SSO_HANDLER
|
||||
)
|
||||
try:
|
||||
response = await http_client.post(
|
||||
url=token_endpoint,
|
||||
data=token_data,
|
||||
headers=request_headers,
|
||||
timeout=30.0,
|
||||
)
|
||||
except Exception as exc:
|
||||
# Catch network-level errors (SSL, DNS, TCP, timeout, etc.) and
|
||||
# wrap them as a clean ProxyException rather than leaking raw
|
||||
# httpx or OS exceptions to callers.
|
||||
verbose_proxy_logger.error("PKCE token endpoint unreachable: %s", exc)
|
||||
raise ProxyException(
|
||||
message=f"Token endpoint request failed: {exc}",
|
||||
type=ProxyErrorTypes.auth_error,
|
||||
param="token_exchange",
|
||||
code=status.HTTP_401_UNAUTHORIZED,
|
||||
) from exc
|
||||
if response.status_code != 200:
|
||||
verbose_proxy_logger.error(
|
||||
"PKCE token exchange failed. status=%s body=%s",
|
||||
@@ -2970,41 +2968,43 @@ class SSOAuthenticationHandler:
|
||||
|
||||
if userinfo_endpoint:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
userinfo_endpoint,
|
||||
headers={
|
||||
**additional_headers,
|
||||
"Authorization": f"Bearer {access_token}", # must not be overridden
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
try:
|
||||
userinfo_raw = resp.json()
|
||||
if not userinfo_raw:
|
||||
# JSON null (None) or empty dict ({}) — no identity claims.
|
||||
# Treat as failure so id_token fallback can be attempted.
|
||||
verbose_proxy_logger.warning(
|
||||
"Userinfo endpoint returned an empty or null response "
|
||||
"(type=%s); treating as failure and attempting id_token fallback. "
|
||||
"Check your provider's userinfo endpoint configuration.",
|
||||
type(userinfo_raw).__name__,
|
||||
)
|
||||
userinfo = None
|
||||
else:
|
||||
userinfo = userinfo_raw
|
||||
except Exception as json_err:
|
||||
client = get_async_httpx_client(
|
||||
llm_provider=httpxSpecialProvider.SSO_HANDLER
|
||||
)
|
||||
resp = await client.get(
|
||||
url=userinfo_endpoint,
|
||||
headers={
|
||||
**additional_headers,
|
||||
"Authorization": f"Bearer {access_token}", # must not be overridden
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
try:
|
||||
userinfo_raw = resp.json()
|
||||
if not userinfo_raw:
|
||||
# JSON null (None) or empty dict ({}) — no identity claims.
|
||||
# Treat as failure so id_token fallback can be attempted.
|
||||
verbose_proxy_logger.warning(
|
||||
"Userinfo endpoint returned non-JSON response (status 200): %s",
|
||||
json_err,
|
||||
"Userinfo endpoint returned an empty or null response "
|
||||
"(type=%s); treating as failure and attempting id_token fallback. "
|
||||
"Check your provider's userinfo endpoint configuration.",
|
||||
type(userinfo_raw).__name__,
|
||||
)
|
||||
else:
|
||||
userinfo = None
|
||||
else:
|
||||
userinfo = userinfo_raw
|
||||
except Exception as json_err:
|
||||
verbose_proxy_logger.warning(
|
||||
"Userinfo endpoint returned %s (body: %s), falling back to id_token",
|
||||
resp.status_code,
|
||||
resp.text[:500],
|
||||
"Userinfo endpoint returned non-JSON response (status 200): %s",
|
||||
json_err,
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.warning(
|
||||
"Userinfo endpoint returned %s (body: %s), falling back to id_token",
|
||||
resp.status_code,
|
||||
resp.text[:500],
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
"Userinfo endpoint error: %s, falling back to id_token", e
|
||||
|
||||
Reference in New Issue
Block a user