fix(oauth2): send introspection body via httpx content= not data=

_prepare_introspection_request returns a pre-encoded form body (Authlib
ClientAuth may append client auth to it). The call site passed it through
client.post(data=<str>), which httpx deprecates for strings (it warns
'Use content=<...> to upload raw bytes/text content') and would re-encode
a mapping. Send it as raw content with the form Content-Type already set.
Adds a regression test asserting the introspection POST uses content=
carrying the urlencoded token (Greptile review finding).
This commit is contained in:
Yassin Kortam 2026-06-08 18:10:42 -07:00
parent e08b6c18d6
commit 7b1d607129
2 changed files with 50 additions and 5 deletions

View file

@ -143,14 +143,18 @@ class Oauth2Handler:
# OAuth2 Token Introspection (RFC 7662) - requires POST with form data
verbose_proxy_logger.debug("Using OAuth2 introspection endpoint (POST)")
headers, data = Oauth2Handler._prepare_introspection_request(
token=token,
oauth_client_id=oauth_client_id,
oauth_client_secret=oauth_client_secret,
headers, introspection_body = (
Oauth2Handler._prepare_introspection_request(
token=token,
oauth_client_id=oauth_client_id,
oauth_client_secret=oauth_client_secret,
)
)
response = await client.post(
token_info_endpoint, headers=headers, data=data
token_info_endpoint,
headers=headers,
content=introspection_body,
)
else:
# Generic token info endpoint - uses GET with Bearer token

View file

@ -1,8 +1,11 @@
import base64
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
from urllib.parse import parse_qs
import pytest
sys.path.insert(0, os.path.abspath("../../../.."))
from litellm.proxy.auth.oauth2_check import Oauth2Handler
@ -47,3 +50,41 @@ def test_introspection_request_without_credentials_only_sends_token():
assert "Authorization" not in headers
parsed = parse_qs(body)
assert parsed == {"token": ["opaque-token"]}
@pytest.mark.asyncio
async def test_check_oauth2_token_sends_introspection_body_as_raw_content(monkeypatch):
"""The introspection POST must send the pre-encoded form body via httpx
``content=``. ``_prepare_introspection_request`` already form-encodes the
body (and Authlib may append client auth), so passing it through ``data=``
would re-encode it and is deprecated for strings in httpx.
"""
monkeypatch.setenv(
"OAUTH_TOKEN_INFO_ENDPOINT", "https://idp.example.com/introspect"
)
monkeypatch.setenv("OAUTH_CLIENT_ID", "client-id")
monkeypatch.setenv("OAUTH_CLIENT_SECRET", "client-secret")
response = MagicMock()
response.raise_for_status = MagicMock()
response.json = MagicMock(
return_value={"active": True, "sub": "u1", "role": "internal_user"}
)
client = MagicMock()
client.post = AsyncMock(return_value=response)
with (
patch("litellm.proxy.proxy_server.premium_user", True),
patch(
"litellm.proxy.auth.oauth2_check.get_async_httpx_client",
return_value=client,
),
):
await Oauth2Handler.check_oauth2_token("opaque-token")
assert client.post.await_count == 1
kwargs = client.post.await_args.kwargs
assert "data" not in kwargs
assert isinstance(kwargs["content"], str)
assert "token=opaque-token" in kwargs["content"]