mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
refactor(oauth2): use Authlib for token introspection
This commit is contained in:
parent
70b5ea878e
commit
33bf08a6e1
4 changed files with 94 additions and 21 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import base64
|
||||
import os
|
||||
from typing import Dict, Optional, Tuple
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
from authlib.oauth2.auth import ClientAuth
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.identity import build_user_api_key_auth_from_oauth2_response
|
||||
|
|
@ -46,33 +47,28 @@ class Oauth2Handler:
|
|||
token: str,
|
||||
oauth_client_id: Optional[str],
|
||||
oauth_client_secret: Optional[str],
|
||||
) -> Tuple[Dict[str, str], Dict[str, str]]:
|
||||
) -> Tuple[Dict[str, str], str]:
|
||||
"""
|
||||
Prepare headers and data for OAuth2 introspection endpoint (RFC 7662).
|
||||
Prepare headers and form body for OAuth2 introspection (RFC 7662).
|
||||
|
||||
Args:
|
||||
token: The OAuth2 token to validate
|
||||
oauth_client_id: OAuth2 client ID
|
||||
oauth_client_secret: OAuth2 client secret
|
||||
Confidential clients (id + secret) authenticate via HTTP Basic;
|
||||
public clients (id only) carry ``client_id`` in the body. Both are
|
||||
delegated to Authlib's RFC 6749 client-auth encoders.
|
||||
|
||||
Returns:
|
||||
Tuple of (headers, data) for the introspection request
|
||||
Tuple of (headers, form-encoded body string).
|
||||
"""
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
data = {"token": token}
|
||||
body = urlencode({"token": token})
|
||||
|
||||
# Add client authentication if credentials are provided
|
||||
if oauth_client_id and oauth_client_secret:
|
||||
# Use HTTP Basic authentication for client credentials
|
||||
credentials = base64.b64encode(
|
||||
f"{oauth_client_id}:{oauth_client_secret}".encode()
|
||||
).decode()
|
||||
headers["Authorization"] = f"Basic {credentials}"
|
||||
elif oauth_client_id:
|
||||
# For public clients, include client_id in the request body
|
||||
data["client_id"] = oauth_client_id
|
||||
if not oauth_client_id:
|
||||
return headers, body
|
||||
|
||||
return headers, data
|
||||
auth_method = "client_secret_basic" if oauth_client_secret else "none"
|
||||
_, headers, body = ClientAuth(
|
||||
oauth_client_id, oauth_client_secret, auth_method
|
||||
).prepare("POST", "", headers, body)
|
||||
return headers, body
|
||||
|
||||
@staticmethod
|
||||
def _prepare_token_info_request(token: str) -> Dict[str, str]:
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ proxy = [
|
|||
"rq>=2.7.0,<3.0",
|
||||
"orjson>=3.11.6,<4.0",
|
||||
"apscheduler>=3.11.2,<4.0",
|
||||
"authlib>=1.3.0,<2.0",
|
||||
"fastapi-sso>=0.19.0,<1.0",
|
||||
"PyJWT>=2.12.0,<3.0",
|
||||
"python-multipart>=0.0.27,<1.0",
|
||||
|
|
|
|||
49
tests/test_litellm/proxy/auth/test_oauth2_check.py
Normal file
49
tests/test_litellm/proxy/auth/test_oauth2_check.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import base64
|
||||
import os
|
||||
import sys
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../.."))
|
||||
|
||||
from litellm.proxy.auth.oauth2_check import Oauth2Handler
|
||||
|
||||
|
||||
def test_introspection_request_confidential_client_uses_basic_auth():
|
||||
headers, body = Oauth2Handler._prepare_introspection_request(
|
||||
token="opaque-token",
|
||||
oauth_client_id="client-id",
|
||||
oauth_client_secret="client-secret",
|
||||
)
|
||||
|
||||
assert headers["Content-Type"] == "application/x-www-form-urlencoded"
|
||||
expected = base64.b64encode(b"client-id:client-secret").decode()
|
||||
assert headers["Authorization"] == f"Basic {expected}"
|
||||
|
||||
parsed = parse_qs(body)
|
||||
assert parsed["token"] == ["opaque-token"]
|
||||
assert "client_secret" not in parsed
|
||||
|
||||
|
||||
def test_introspection_request_public_client_puts_client_id_in_body():
|
||||
headers, body = Oauth2Handler._prepare_introspection_request(
|
||||
token="opaque-token",
|
||||
oauth_client_id="client-id",
|
||||
oauth_client_secret=None,
|
||||
)
|
||||
|
||||
assert "Authorization" not in headers
|
||||
parsed = parse_qs(body)
|
||||
assert parsed["token"] == ["opaque-token"]
|
||||
assert parsed["client_id"] == ["client-id"]
|
||||
|
||||
|
||||
def test_introspection_request_without_credentials_only_sends_token():
|
||||
headers, body = Oauth2Handler._prepare_introspection_request(
|
||||
token="opaque-token",
|
||||
oauth_client_id=None,
|
||||
oauth_client_secret=None,
|
||||
)
|
||||
|
||||
assert "Authorization" not in headers
|
||||
parsed = parse_qs(body)
|
||||
assert parsed == {"token": ["opaque-token"]}
|
||||
29
uv.lock
generated
29
uv.lock
generated
|
|
@ -9,7 +9,7 @@ resolution-markers = [
|
|||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-06-03T21:40:52.018333Z"
|
||||
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[manifest]
|
||||
|
|
@ -423,6 +423,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a2/1f/aa74b23b6eea4cf9b79ace914df59123c4c8e7e4bd32dd22d09c126422d9/aurelio_sdk-0.0.19-py3-none-any.whl", hash = "sha256:390c0212b59ce99116df8722d3badced88c5ef0bb742a6222d479ceed0ed3948", size = 17322, upload-time = "2025-03-24T14:37:31.305Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "joserfc" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-ai-contentsafety"
|
||||
version = "1.0.0"
|
||||
|
|
@ -2735,6 +2748,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "joserfc"
|
||||
version = "1.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/c3/2f590052b55cbdd0ace470ee7ee1f685f6882051be93a9374891005623e2/joserfc-1.7.0.tar.gz", hash = "sha256:4aced6ab0c47846f0a531402aec2419a874b91e918df9c4c9da8a82fb559d6c4", size = 232967, upload-time = "2026-06-02T09:59:34.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/83/b6b62a66a06ce872d9429a5eb5ee20b2002fd9c331b953c94381c1f7c9f9/joserfc-1.7.0-py3-none-any.whl", hash = "sha256:17e5d7a5a35e65442b05efc435a3d5d46696ffa2c8a2ed0eea6f63fc268e3224", size = 70387, upload-time = "2026-06-02T09:59:33.264Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonlines"
|
||||
version = "4.0.0"
|
||||
|
|
@ -3318,6 +3343,7 @@ mlflow = [
|
|||
]
|
||||
proxy = [
|
||||
{ name = "apscheduler" },
|
||||
{ name = "authlib" },
|
||||
{ name = "azure-identity" },
|
||||
{ name = "azure-storage-blob" },
|
||||
{ name = "backoff" },
|
||||
|
|
@ -3472,6 +3498,7 @@ requires-dist = [
|
|||
{ name = "apscheduler", marker = "extra == 'proxy'", specifier = ">=3.11.2,<4.0" },
|
||||
{ name = "audioread", marker = "extra == 'stt-nvidia-riva'", specifier = ">=3.0.1" },
|
||||
{ name = "aurelio-sdk", marker = "python_full_version < '3.14' and extra == 'semantic-router'", specifier = ">=0.0.19,<1.0" },
|
||||
{ name = "authlib", marker = "extra == 'proxy'", specifier = ">=1.3.0,<2.0" },
|
||||
{ name = "azure-ai-contentsafety", marker = "extra == 'proxy-runtime'", specifier = ">=1.0.0,<2.0" },
|
||||
{ name = "azure-identity", marker = "extra == 'extra-proxy'", specifier = ">=1.25.2,<2.0" },
|
||||
{ name = "azure-identity", marker = "extra == 'proxy'", specifier = ">=1.25.2,<2.0" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue