From 7a91d80f9adbb214b6ab2cde67e9a09cdda21e3b Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Fri, 24 Apr 2026 17:04:37 -0700 Subject: [PATCH] fix(auth): exempt auth=false pass-through endpoints from centralized gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-configured pass-through endpoints with ``auth: false`` are explicitly unauthenticated — the builder short-circuits at check_api_key_for_custom_headers_or_pass_through_endpoints and returns a fresh empty UserAPIKeyAuth() without an api_key, user_id, or role. Pre-refactor, that empty token never reached common_checks. After the centralization, it does — and common_checks rejects it as admin-only, breaking every Langfuse / custom unauthenticated pass-through. This is the same regression class as the public-routes one: a builder fast-path whose return value cannot survive common_checks. Honor the same contract here — when the matched endpoint config has auth != True, skip the centralized gate. auth=True endpoints still run the full gate (covered by a companion test). No security regression: ``auth: false`` is the operator's explicit opt-out from LiteLLM auth on this path. The original commit closed seven authenticated bypasses; this exemption applies only to a path the operator has already declared unauthenticated. --- litellm/proxy/auth/user_api_key_auth.py | 16 ++++ .../proxy/auth/test_user_api_key_auth.py | 90 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index ccb8fcddd26..c23c8993321 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -1638,6 +1638,22 @@ async def _run_centralized_common_checks( ): return + # User-configured pass-through endpoints with ``auth: false`` are + # explicitly unauthenticated — the builder returns an empty + # UserAPIKeyAuth() and the request is forwarded as-is. Running + # common_checks on the empty token would reject the request as + # admin-only. The "auth" flag on the endpoint config is the + # contract; honor it. + pass_through_endpoints = general_settings.get("pass_through_endpoints", None) + if pass_through_endpoints is not None: + for endpoint in pass_through_endpoints: + if ( + isinstance(endpoint, dict) + and endpoint.get("path", "") == route + and endpoint.get("auth") is not True + ): + return + # No-auth dev mode: master_key unset AND no JWT/OAuth2 auth # configured. The builder returns an INTERNAL_USER token for any # api_key; the proxy is unauthenticated by configuration. diff --git a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py index 0105432d071..8130e0a942a 100644 --- a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py +++ b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py @@ -2090,6 +2090,96 @@ async def test_centralized_common_checks_skips_public_routes(): setattr(_proxy_server_mod, k, v) +@pytest.mark.asyncio +async def test_centralized_common_checks_skips_passthrough_endpoint_with_auth_false(): + """Regression: user-configured pass-through endpoints with + ``auth: false`` are explicitly unauthenticated. The builder + short-circuits and returns a fresh empty UserAPIKeyAuth(); running + common_checks on that empty token would reject the request as + admin-only. The "auth" flag on the endpoint config is the contract + — when it's anything other than True, skip the gate.""" + import litellm.proxy.proxy_server as _proxy_server_mod + from fastapi import Request + from starlette.datastructures import URL + + token = UserAPIKeyAuth() + request = Request(scope={"type": "http"}) + request._url = URL(url="/api/public/ingestion") + + attrs = _proxy_attrs_for_centralized_checks(user_custom_auth=None) + attrs["general_settings"] = { + "pass_through_endpoints": [ + { + "path": "/api/public/ingestion", + "target": "https://us.cloud.langfuse.com/api/public/ingestion", + "auth": False, + } + ] + } + originals = {a: getattr(_proxy_server_mod, a, None) for a in attrs} + try: + for k, v in attrs.items(): + setattr(_proxy_server_mod, k, v) + with patch( + "litellm.proxy.auth.user_api_key_auth.common_checks", + new_callable=AsyncMock, + ) as mock_checks: + await _run_centralized_common_checks( + user_api_key_auth_obj=token, + request=request, + request_data={}, + route="/api/public/ingestion", + ) + mock_checks.assert_not_awaited() + finally: + for k, v in originals.items(): + setattr(_proxy_server_mod, k, v) + + +@pytest.mark.asyncio +async def test_centralized_common_checks_runs_for_passthrough_endpoint_with_auth_true(): + """Companion to the auth=False test: when a pass-through endpoint + has ``auth: true``, the builder runs full authentication and the + centralized gate must run too. Skipping based on path-match alone + would re-open every ``auth: true`` pass-through endpoint.""" + import litellm.proxy.proxy_server as _proxy_server_mod + from fastapi import Request + from starlette.datastructures import URL + + token = UserAPIKeyAuth(api_key="sk-test", user_id="u1") + request = Request(scope={"type": "http"}) + request._url = URL(url="/api/public/ingestion") + + attrs = _proxy_attrs_for_centralized_checks(user_custom_auth=None) + attrs["general_settings"] = { + "pass_through_endpoints": [ + { + "path": "/api/public/ingestion", + "target": "https://us.cloud.langfuse.com/api/public/ingestion", + "auth": True, + } + ] + } + originals = {a: getattr(_proxy_server_mod, a, None) for a in attrs} + try: + for k, v in attrs.items(): + setattr(_proxy_server_mod, k, v) + with patch( + "litellm.proxy.auth.user_api_key_auth.common_checks", + new_callable=AsyncMock, + ) as mock_checks: + await _run_centralized_common_checks( + user_api_key_auth_obj=token, + request=request, + request_data={}, + route="/api/public/ingestion", + ) + mock_checks.assert_awaited_once() + finally: + for k, v in originals.items(): + setattr(_proxy_server_mod, k, v) + + @pytest.mark.asyncio async def test_centralized_common_checks_master_key_admin_overrides_db_user_role(): """Regression: master_key tokens have user_id=litellm_proxy_admin_name