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
« 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
4from fastapi import Request
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
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)
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``.
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).
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
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 )
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)
71 if not oauth2_config_mappings:
72 raise ValueError("Oauth2 config mappings not found in general_settings")
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 )
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 }
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