Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/_experimental/mcp_server/auth/token_endpoint_auth.py: 39%
25 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""Client authentication for OAuth 2.0 token-endpoint requests (RFC 6749 section 2.3.1).
3A confidential MCP upstream may require ``client_secret_basic`` (HTTP Basic, the OIDC
4default) or ``client_secret_post`` (credentials in the form body). Every token-endpoint
5POST in the MCP gateway builds its client authentication here so the two methods are
6applied identically across the inbound exchange, the refresh grants, the M2M
7client_credentials fetch, and RFC 8693 token exchange. The default is
8``client_secret_post`` so servers that never set ``token_endpoint_auth_method`` keep
9their current behavior.
10"""
12from __future__ import annotations
14import base64
15from dataclasses import dataclass
16from typing import Final
17from urllib.parse import quote_plus
19from litellm.types.mcp_server.mcp_server_manager import MCPTokenEndpointAuthMethod
22@dataclass(frozen=True, slots=True)
23class TokenEndpointClientAuth:
24 headers: dict[str, str]
25 body: dict[str, str]
28class TokenEndpointAuthConfigError(ValueError):
29 """``client_secret_basic`` is configured but the client credentials needed for it are missing.
31 Subclasses ``ValueError`` so existing call sites that already guard missing credentials with
32 ``except ValueError`` / ``except Exception`` keep mapping it to their own failure contract.
33 """
36def normalize_token_endpoint_auth_method(
37 value: object,
38) -> MCPTokenEndpointAuthMethod | None:
39 """Narrow an untyped (DB/JSON-sourced) value to the auth-method literal, else ``None``."""
40 if value == "client_secret_basic":
41 return "client_secret_basic"
42 if value == "client_secret_post":
43 return "client_secret_post"
44 return None
47def build_token_endpoint_client_auth(
48 *,
49 auth_method: MCPTokenEndpointAuthMethod | None,
50 client_id: str | None,
51 client_secret: str | None,
52) -> TokenEndpointClientAuth:
53 """Return the headers and body fields that authenticate the client to the token endpoint.
55 ``client_secret_basic`` is a confidential-client method, so it requires both ``client_id`` and
56 ``client_secret`` and raises ``TokenEndpointAuthConfigError`` when either is missing rather than
57 silently degrading to a weaker request (RFC 6749 section 2.3.1; matches the "absent credential
58 must surface, never fall sideways" rule). It sends an HTTP Basic ``Authorization`` header and
59 keeps the credentials out of the body. Any other method (including ``None``, the default) is the
60 ``client_secret_post`` path: it places whichever of ``client_id`` / ``client_secret`` are present
61 into the body, so a secretless client_id (a public client authenticating with PKCE) stays valid.
62 """
63 if auth_method == "client_secret_basic":
64 if not client_id or not client_secret:
65 raise TokenEndpointAuthConfigError(
66 "token_endpoint_auth_method=client_secret_basic requires both client_id and client_secret"
67 )
68 # RFC 6749 section 2.3.1: form-urlencode each value before joining with ':' so a
69 # client_id/secret containing reserved characters (':', '+', '%', ...) is transmitted intact.
70 userpass: Final = f"{quote_plus(client_id)}:{quote_plus(client_secret)}"
71 encoded: Final = base64.b64encode(userpass.encode()).decode()
72 return TokenEndpointClientAuth(headers={"Authorization": f"Basic {encoded}"}, body={})
73 return TokenEndpointClientAuth(
74 headers={},
75 body={
76 **({"client_id": client_id} if client_id else {}),
77 **({"client_secret": client_secret} if client_secret else {}),
78 },
79 )