litellm/litellm/proxy_auth/credentials.py
mateo-berri f747bc6704 refactor(types): replace Any with real types across 29 more backend files
Second batch of the fifth basedpyright Any reduction round. Every change is
typing-only and leaves runtime behavior identical.

Guardrail hooks and file, vector store and usage endpoints move their payload,
header and response annotations from Any to object, Mapping[str, object] or the
concrete response model the call site already produces. Two private aggregation
helpers in the guardrail usage endpoints take a key accessor function instead of
an attribute name string, so the key they read is checked against the row type.
The verification token repository reaches its two tables through Protocols that
name the handles it calls, rather than reading them off an untyped prisma
client, and the Azure AD credential wrapper describes the azure-identity
credential it wraps the same way.
2026-09-08 10:14:07 +00:00

265 lines
8.2 KiB
Python

"""
Credential providers for proxy authentication.
This module provides a provider-agnostic interface for obtaining OAuth2/JWT tokens.
It follows the same TokenCredential protocol used by Azure SDK.
"""
import time
from dataclasses import dataclass
from typing import Final, Protocol, runtime_checkable
@dataclass
class AccessToken:
"""
Represents an OAuth2 access token with expiration.
This matches the structure used by azure.core.credentials.AccessToken.
Attributes:
token: The access token string (typically a JWT).
expires_on: Unix timestamp when the token expires.
"""
token: str
expires_on: int
@runtime_checkable
class TokenCredential(Protocol):
"""
Protocol for credential providers.
This matches the azure.core.credentials.TokenCredential interface,
allowing any Azure SDK credential to be used directly.
Any class implementing get_token(scope) -> AccessToken can be used.
"""
def get_token(self, scope: str) -> AccessToken:
"""
Get an access token for the specified scope.
Args:
scope: The OAuth2 scope to request (e.g., "api://my-app/.default")
Returns:
AccessToken with the token string and expiration timestamp.
"""
...
class _AzureAccessToken(Protocol):
"""The two attributes :class:`AzureADCredential` reads off an azure-identity token."""
@property
def token(self) -> str: ...
@property
def expires_on(self) -> int: ...
class _AzureTokenCredential(Protocol):
"""The single method :class:`AzureADCredential` calls on the credential it wraps."""
def get_token(self, *scopes: str) -> _AzureAccessToken: ...
class AzureADCredential:
"""
Wrapper for Azure Identity credentials.
This wraps any azure-identity credential (DefaultAzureCredential,
ClientSecretCredential, ManagedIdentityCredential, etc.) and converts
the token to our AccessToken format.
If no credential is provided, it will use DefaultAzureCredential
which tries multiple authentication methods automatically.
Example:
# Use default credential chain (env vars, managed identity, CLI, etc.)
cred = AzureADCredential()
# Or provide a specific credential
from azure.identity import ClientSecretCredential
azure_cred = ClientSecretCredential(tenant_id, client_id, client_secret)
cred = AzureADCredential(credential=azure_cred)
"""
def __init__(self, credential: _AzureTokenCredential | None = None):
"""
Initialize with an optional Azure credential.
Args:
credential: An azure-identity credential object. If None,
DefaultAzureCredential will be used on first token request.
"""
self._credential: _AzureTokenCredential | None = credential
self._initialized = credential is not None
def get_token(self, scope: str) -> AccessToken:
"""
Get an access token from Azure AD.
Args:
scope: The OAuth2 scope (e.g., "api://my-app/.default")
Returns:
AccessToken with the JWT and expiration.
Raises:
ImportError: If azure-identity is not installed.
"""
result: Final = self._resolve_credential().get_token(scope)
return AccessToken(token=result.token, expires_on=result.expires_on)
def _resolve_credential(self) -> _AzureTokenCredential:
"""Return the wrapped credential, building the Azure default chain on first use.
Raises:
ImportError: If azure-identity is not installed.
"""
existing: Final = self._credential
if existing is not None:
return existing
try:
from azure.identity import DefaultAzureCredential
created: Final = DefaultAzureCredential()
except ImportError:
raise ImportError(
"azure-identity is required for AzureADCredential. Install it with: pip install azure-identity"
)
self._credential = created
self._initialized = True
return created
class GenericOAuth2Credential:
"""
Generic OAuth2 client credentials flow.
This works with any OAuth2 provider (Okta, Auth0, Keycloak, etc.)
that supports the client_credentials grant type.
Example:
cred = GenericOAuth2Credential(
client_id="my-client-id",
client_secret="my-client-secret",
token_url="https://my-idp.com/oauth2/token"
)
"""
def __init__(self, client_id: str, client_secret: str, token_url: str):
"""
Initialize OAuth2 client credentials.
Args:
client_id: OAuth2 client ID
client_secret: OAuth2 client secret
token_url: Token endpoint URL (e.g., "https://idp.com/oauth2/token")
"""
self.client_id = client_id
self.client_secret = client_secret
self.token_url = token_url
self._cached_token: AccessToken | None = None
def get_token(self, scope: str) -> AccessToken:
"""
Get an access token using OAuth2 client credentials flow.
Tokens are cached and reused until they expire (with 60s buffer).
Args:
scope: The OAuth2 scope to request
Returns:
AccessToken with the token and expiration.
"""
# Return cached token if still valid (with 60s buffer)
if self._cached_token and self._cached_token.expires_on > time.time() + 60:
return self._cached_token
import httpx
response: Final = httpx.post(
self.token_url,
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": scope,
},
)
response.raise_for_status()
data: Final = response.json()
self._cached_token = AccessToken(
token=data["access_token"],
expires_on=int(time.time()) + data.get("expires_in", 3600),
)
return self._cached_token
class ProxyAuthHandler:
"""
Manages OAuth2/JWT token lifecycle for proxy authentication.
This handler:
- Obtains tokens from the configured credential provider
- Caches tokens to avoid unnecessary requests
- Automatically refreshes tokens before they expire (60s buffer)
- Generates Authorization headers for HTTP requests
Set this as litellm.proxy_auth to automatically inject auth headers
into all requests to your LiteLLM Proxy.
Example:
import litellm
from litellm.proxy_auth import AzureADCredential, ProxyAuthHandler
litellm.proxy_auth = ProxyAuthHandler(
credential=AzureADCredential(),
scope="api://my-litellm-proxy/.default"
)
litellm.api_base = "https://my-proxy.example.com"
# Auth headers are now automatically injected
response = litellm.completion(model="gpt-4", messages=[...])
"""
def __init__(self, credential: TokenCredential, scope: str):
"""
Initialize the proxy auth handler.
Args:
credential: A TokenCredential implementation (AzureADCredential,
GenericOAuth2Credential, or any custom implementation)
scope: The OAuth2 scope to request tokens for
"""
self.credential = credential
self.scope = scope
self._cached_token: AccessToken | None = None
def get_token(self) -> AccessToken:
"""
Get a valid access token, refreshing if necessary.
Returns:
AccessToken that is valid for at least 60 more seconds.
"""
# Refresh if no token or token expires within 60 seconds
if not self._cached_token or self._cached_token.expires_on <= time.time() + 60:
self._cached_token = self.credential.get_token(self.scope)
return self._cached_token
def get_auth_headers(self) -> dict[str, str]:
"""
Get HTTP headers for authentication.
Returns:
Dict with Authorization header containing Bearer token.
"""
token: Final = self.get_token()
return {"Authorization": f"Bearer {token.token}"}