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:
parent
06681ddfcc
commit
2a997993d4
@ -17,7 +17,6 @@ import secrets
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast
|
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast
|
||||||
|
|
||||||
import httpx
|
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
@ -2801,20 +2800,19 @@ class SSOAuthenticationHandler:
|
|||||||
if redirect_url:
|
if redirect_url:
|
||||||
token_data["redirect_uri"] = redirect_url
|
token_data["redirect_uri"] = redirect_url
|
||||||
|
|
||||||
post_kwargs: Dict[str, Any] = {
|
request_headers = {
|
||||||
"data": token_data,
|
**additional_headers,
|
||||||
"headers": {
|
"Content-Type": "application/x-www-form-urlencoded", # must not be overridden
|
||||||
**additional_headers,
|
"Accept": "application/json",
|
||||||
"Content-Type": "application/x-www-form-urlencoded", # must not be overridden
|
|
||||||
"Accept": "application/json",
|
|
||||||
},
|
|
||||||
"timeout": 30.0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if not include_client_id:
|
if not include_client_id:
|
||||||
# Use Basic Auth only when a secret is available; public PKCE clients omit it.
|
# Use Basic Auth only when a secret is available; public PKCE clients omit it.
|
||||||
if client_secret:
|
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:
|
else:
|
||||||
token_data["client_id"] = client_id
|
token_data["client_id"] = client_id
|
||||||
else:
|
else:
|
||||||
@ -2822,27 +2820,27 @@ class SSOAuthenticationHandler:
|
|||||||
if client_secret:
|
if client_secret:
|
||||||
token_data["client_secret"] = client_secret
|
token_data["client_secret"] = client_secret
|
||||||
|
|
||||||
# The try/except is INSIDE the async with so that TLS teardown exceptions
|
http_client = get_async_httpx_client(
|
||||||
# from __aexit__ propagate as-is and are NOT mis-labelled as "Token endpoint
|
llm_provider=httpxSpecialProvider.SSO_HANDLER
|
||||||
# request failed". httpx buffers the full response body before __aexit__,
|
)
|
||||||
# so status_code / text / json() remain valid after the context exits.
|
try:
|
||||||
async with httpx.AsyncClient() as http_client:
|
response = await http_client.post(
|
||||||
try:
|
url=token_endpoint,
|
||||||
response = await http_client.post(token_endpoint, **post_kwargs)
|
data=token_data,
|
||||||
except Exception as exc:
|
headers=request_headers,
|
||||||
# Catch network-level errors (SSL, DNS, TCP, timeout, etc.) and
|
timeout=30.0,
|
||||||
# wrap them as a clean ProxyException rather than leaking raw
|
)
|
||||||
# httpx or OS exceptions to callers.
|
except Exception as exc:
|
||||||
verbose_proxy_logger.error("PKCE token endpoint unreachable: %s", exc)
|
# Catch network-level errors (SSL, DNS, TCP, timeout, etc.) and
|
||||||
raise ProxyException(
|
# wrap them as a clean ProxyException rather than leaking raw
|
||||||
message=f"Token endpoint request failed: {exc}",
|
# httpx or OS exceptions to callers.
|
||||||
type=ProxyErrorTypes.auth_error,
|
verbose_proxy_logger.error("PKCE token endpoint unreachable: %s", exc)
|
||||||
param="token_exchange",
|
raise ProxyException(
|
||||||
code=status.HTTP_401_UNAUTHORIZED,
|
message=f"Token endpoint request failed: {exc}",
|
||||||
) from exc
|
type=ProxyErrorTypes.auth_error,
|
||||||
|
param="token_exchange",
|
||||||
# Response processing outside the async with — httpx buffers the full
|
code=status.HTTP_401_UNAUTHORIZED,
|
||||||
# response body so status_code / text / json() remain valid after __aexit__.
|
) from exc
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
verbose_proxy_logger.error(
|
verbose_proxy_logger.error(
|
||||||
"PKCE token exchange failed. status=%s body=%s",
|
"PKCE token exchange failed. status=%s body=%s",
|
||||||
@ -2970,41 +2968,43 @@ class SSOAuthenticationHandler:
|
|||||||
|
|
||||||
if userinfo_endpoint:
|
if userinfo_endpoint:
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
client = get_async_httpx_client(
|
||||||
resp = await client.get(
|
llm_provider=httpxSpecialProvider.SSO_HANDLER
|
||||||
userinfo_endpoint,
|
)
|
||||||
headers={
|
resp = await client.get(
|
||||||
**additional_headers,
|
url=userinfo_endpoint,
|
||||||
"Authorization": f"Bearer {access_token}", # must not be overridden
|
headers={
|
||||||
},
|
**additional_headers,
|
||||||
timeout=30.0,
|
"Authorization": f"Bearer {access_token}", # must not be overridden
|
||||||
)
|
},
|
||||||
if resp.status_code == 200:
|
timeout=30.0,
|
||||||
try:
|
)
|
||||||
userinfo_raw = resp.json()
|
if resp.status_code == 200:
|
||||||
if not userinfo_raw:
|
try:
|
||||||
# JSON null (None) or empty dict ({}) — no identity claims.
|
userinfo_raw = resp.json()
|
||||||
# Treat as failure so id_token fallback can be attempted.
|
if not userinfo_raw:
|
||||||
verbose_proxy_logger.warning(
|
# JSON null (None) or empty dict ({}) — no identity claims.
|
||||||
"Userinfo endpoint returned an empty or null response "
|
# Treat as failure so id_token fallback can be attempted.
|
||||||
"(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:
|
|
||||||
verbose_proxy_logger.warning(
|
verbose_proxy_logger.warning(
|
||||||
"Userinfo endpoint returned non-JSON response (status 200): %s",
|
"Userinfo endpoint returned an empty or null response "
|
||||||
json_err,
|
"(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(
|
verbose_proxy_logger.warning(
|
||||||
"Userinfo endpoint returned %s (body: %s), falling back to id_token",
|
"Userinfo endpoint returned non-JSON response (status 200): %s",
|
||||||
resp.status_code,
|
json_err,
|
||||||
resp.text[:500],
|
|
||||||
)
|
)
|
||||||
|
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:
|
except Exception as e:
|
||||||
verbose_proxy_logger.warning(
|
verbose_proxy_logger.warning(
|
||||||
"Userinfo endpoint error: %s, falling back to id_token", e
|
"Userinfo endpoint error: %s, falling back to id_token", e
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user