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

1"""Client authentication for OAuth 2.0 token-endpoint requests (RFC 6749 section 2.3.1). 

2 

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""" 

11 

12from __future__ import annotations 

13 

14import base64 

15from dataclasses import dataclass 

16from typing import Final 

17from urllib.parse import quote_plus 

18 

19from litellm.types.mcp_server.mcp_server_manager import MCPTokenEndpointAuthMethod 

20 

21 

22@dataclass(frozen=True, slots=True) 

23class TokenEndpointClientAuth: 

24 headers: dict[str, str] 

25 body: dict[str, str] 

26 

27 

28class TokenEndpointAuthConfigError(ValueError): 

29 """``client_secret_basic`` is configured but the client credentials needed for it are missing. 

30 

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 """ 

34 

35 

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 

45 

46 

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. 

54 

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 )