fix(auth): exempt auth=false pass-through endpoints from centralized gate

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.
This commit is contained in:
Yuneng Jiang 2026-04-24 17:04:37 -07:00
parent 3560823196
commit 7a91d80f9a
No known key found for this signature in database
2 changed files with 106 additions and 0 deletions

View file

@ -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.

View file

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