Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/auth/oauth2_proxy_hook.py: 30%

23 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1from collections.abc import Mapping 

2from typing import Final 

3 

4from fastapi import Request 

5 

6from litellm._logging import verbose_proxy_logger 

7from litellm.proxy._types import UserAPIKeyAuth 

8from litellm.proxy.auth.trusted_proxy_utils import require_trusted_proxy_request 

9 

10# OAuth2-proxy header trust is for **identity assertion** from a trusted 

11# upstream auth proxy (oauth2-proxy, Authelia, etc.). The allowlist below 

12# is the only safe surface — anything else (``user_role``, ``api_key``, 

13# ``permissions``, ``max_budget``, ``user_max_budget``, 

14# ``team_tpm_limit``, ``end_user_max_budget``, ``allowed_model_region``, 

15# and dozens of similar policy fields scattered across the 

16# ``LiteLLM_VerificationTokenView`` hierarchy) is a privilege grant that 

17# would let a caller forge their own enforcement parameters by sending 

18# the matching header. 

19# 

20# A denylist of "privileged fields" is unmaintainable in this codebase: 

21# the auth model has ~50 budget/spend/limit/permission fields and gains 

22# more with each release. An allowlist scoped to identity assertion is 

23# default-secure — new fields are blocked automatically. 

24# 

25# Operators who need a trusted upstream to assert anything beyond 

26# identity should switch to JWT authentication, which validates a 

27# signature on the assertion rather than blindly trusting headers. 

28ALLOWED_OAUTH2_PROXY_FIELDS: Final[frozenset[str]] = frozenset( 

29 { 

30 "user_id", 

31 "user_email", 

32 "team_id", 

33 "team_alias", 

34 "org_id", 

35 "models", 

36 } 

37) 

38 

39 

40async def handle_oauth2_proxy_request(request: Request) -> UserAPIKeyAuth: 

41 """ 

42 Resolve a ``UserAPIKeyAuth`` from request headers per the admin-set 

43 ``oauth2_config_mappings``. 

44 

45 The auth model assumes the proxy is deployed behind a trusted OAuth2 

46 reverse proxy that injects authenticated identity headers (e.g. 

47 oauth2-proxy, Authelia). 

48 

49 **Identity-only allowlist.** ``oauth2_config_mappings`` maps header 

50 names to ``UserAPIKeyAuth`` fields. Without an allowlist, an admin 

51 who maps the wrong header to ``user_role`` lets any caller send 

52 ``X-User-Role: proxy_admin`` and gain full admin privileges 

53 (Pydantic coerces the string into the enum). Only fields in 

54 ``ALLOWED_OAUTH2_PROXY_FIELDS`` (identity assertion only — see the 

55 constant's comment) may be mapped; any other mapping is rejected at 

56 request time so the misconfiguration surfaces loudly rather than as 

57 a silent privesc. 

58 """ 

59 from litellm.proxy.proxy_server import general_settings 

60 

61 verbose_proxy_logger.debug("Handling oauth2 proxy request") 

62 require_trusted_proxy_request( 

63 request=request, 

64 general_settings=general_settings, 

65 feature_name="OAuth2 proxy auth", 

66 ) 

67 

68 oauth2_config_mappings: Final[dict[str, str]] = general_settings.get("oauth2_config_mappings") or {} 

69 verbose_proxy_logger.debug("Oauth2 config mappings: %s", oauth2_config_mappings) 

70 

71 if not oauth2_config_mappings: 

72 raise ValueError("Oauth2 config mappings not found in general_settings") 

73 

74 disallowed: Final = sorted(set(oauth2_config_mappings.keys()) - ALLOWED_OAUTH2_PROXY_FIELDS) 

75 if disallowed: 

76 raise ValueError( 

77 "Oauth2 proxy auth refuses to map non-identity UserAPIKeyAuth " 

78 f"fields from request headers: {disallowed}. Only identity " 

79 f"fields are accepted ({sorted(ALLOWED_OAUTH2_PROXY_FIELDS)}); " 

80 "anything else (privileges, budgets, rate limits, metadata) " 

81 "would let a caller forge enforcement parameters by spoofing " 

82 "the matching header. If you need a trusted upstream to " 

83 "assert anything beyond identity, use JWT auth " 

84 "(signature-validated) instead of header-trust." 

85 ) 

86 

87 auth_data: Final[Mapping[str, str | list[str]]] = { 

88 key: [model.strip() for model in value.split(",")] if key == "models" else value 

89 for key, header in oauth2_config_mappings.items() 

90 if (value := request.headers.get(header)) 

91 } 

92 

93 verbose_proxy_logger.debug( 

94 "Auth data before creating UserAPIKeyAuth object: keys=%s", 

95 list(auth_data.keys()), 

96 ) 

97 user_api_key_auth: Final = UserAPIKeyAuth.model_validate(auth_data) 

98 verbose_proxy_logger.debug( 

99 "UserAPIKeyAuth object created with keys: %s", 

100 list(user_api_key_auth.__fields_set__), 

101 ) 

102 return user_api_key_auth