mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(mcp): forward Authorization in pass-through when x-litellm-api-key is admission
Commit 3753970cc9 widened the Authorization strip to cover all
is_oauth_passthrough servers — protecting against the LiteLLM admission
key leaking upstream when the caller used Authorization for admission,
but also silently stripping legitimate upstream OAuth bearers when the
caller used x-litellm-api-key for admission.
That broke transparent OAuth pass-through (EAI-506 V5/V6): standards-
compliant MCP clients (OpenCode, Claude Code, mcp-inspector) complete
PKCE against the upstream IdP and send the resulting token as plain
Authorization: Bearer per the MCP spec — with the wider strip in place,
that token never reaches the upstream and tools/list returns empty.
Narrow the strip: skip Authorization for pass-through servers only when
the caller did NOT supply x-litellm-api-key. When x-litellm-api-key is
present, admission is unambiguous and Authorization is free to carry
the upstream OAuth bearer.
The original security guarantee is preserved — a client that sends only
Authorization (no x-litellm-api-key) still has it stripped, so the
LiteLLM key cannot leak upstream via that path.
Tests:
- new: forwards Authorization when x-litellm-api-key is present
- new: still strips Authorization when only Authorization is present
- existing pass-through + M2M tests unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3990165b26
commit
f6359cbf04
2 changed files with 96 additions and 9 deletions
|
|
@ -1168,18 +1168,30 @@ if MCP_AVAILABLE:
|
|||
str(k).lower(): v for k, v in raw_headers.items() if isinstance(k, str)
|
||||
}
|
||||
|
||||
has_explicit_litellm_admission_header = (
|
||||
normalized_raw_headers.get("x-litellm-api-key") is not None
|
||||
)
|
||||
|
||||
for header in server.extra_headers:
|
||||
if not isinstance(header, str):
|
||||
continue
|
||||
# Never forward the inbound Authorization header that was used for
|
||||
# LiteLLM API key authentication:
|
||||
# - skip if server has client_credentials (fetch upstream token via M2M flow)
|
||||
# - skip if server is oauth_passthrough (upstream token must come from
|
||||
# a server-specific header, not the LiteLLM API key header)
|
||||
if header.lower() == "authorization" and (
|
||||
server.has_client_credentials or server.is_oauth_passthrough
|
||||
):
|
||||
continue
|
||||
if header.lower() == "authorization":
|
||||
# M2M servers fetch their own upstream token via the
|
||||
# client_credentials flow — never forward the caller's
|
||||
# Authorization header.
|
||||
if server.has_client_credentials:
|
||||
continue
|
||||
# Transparent OAuth pass-through: forward the caller's
|
||||
# Authorization header only when LiteLLM admission used
|
||||
# a different header (`x-litellm-api-key`). Without an
|
||||
# explicit admission header, `Authorization` may itself
|
||||
# be the LiteLLM key — strip it to avoid leaking the
|
||||
# gateway credential upstream.
|
||||
if (
|
||||
server.is_oauth_passthrough
|
||||
and not has_explicit_litellm_admission_header
|
||||
):
|
||||
continue
|
||||
header_value = normalized_raw_headers.get(header.lower())
|
||||
if header_value is None:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -177,6 +177,81 @@ def test_prepare_mcp_server_headers_passthrough_forwards_other_headers():
|
|||
assert extra_headers == {"x-request-id": "req-123", "x-trace-id": "trace-456"}
|
||||
|
||||
|
||||
def test_prepare_mcp_server_headers_passthrough_forwards_authorization_with_explicit_admission():
|
||||
"""Transparent OAuth pass-through: when LiteLLM admission used the explicit
|
||||
`x-litellm-api-key` header, the inbound `Authorization` header is
|
||||
unambiguously the upstream OAuth bearer and MUST be forwarded.
|
||||
|
||||
Regression for EAI-506 V5/V6 — a standards-compliant MCP client (e.g.
|
||||
OpenCode) completes PKCE against the upstream IdP and sends the resulting
|
||||
token as plain `Authorization: Bearer <token>` per the MCP spec.
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy._experimental.mcp_server.server import (
|
||||
_prepare_mcp_server_headers,
|
||||
)
|
||||
except ImportError:
|
||||
pytest.skip("MCP server not available")
|
||||
|
||||
server = MCPServer(
|
||||
server_id="server-passthrough-explicit-admission",
|
||||
name="server",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.none,
|
||||
extra_headers=["Authorization"],
|
||||
)
|
||||
|
||||
server_auth_header, extra_headers = _prepare_mcp_server_headers(
|
||||
server=server,
|
||||
mcp_server_auth_headers=None,
|
||||
mcp_auth_header=None,
|
||||
oauth2_headers=None,
|
||||
raw_headers={
|
||||
"x-litellm-api-key": "Bearer sk-litellm-key",
|
||||
"authorization": "Bearer upstream-okta-token",
|
||||
},
|
||||
)
|
||||
|
||||
assert server_auth_header is None
|
||||
assert extra_headers == {"Authorization": "Bearer upstream-okta-token"}
|
||||
|
||||
|
||||
def test_prepare_mcp_server_headers_passthrough_strips_authorization_without_admission_header():
|
||||
"""Counterpart to the explicit-admission test: without `x-litellm-api-key`,
|
||||
the inbound `Authorization` may itself be the LiteLLM admission key, so we
|
||||
strip it to avoid leaking the gateway credential upstream. This preserves
|
||||
the security guarantee introduced in commit 3753970cc9.
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy._experimental.mcp_server.server import (
|
||||
_prepare_mcp_server_headers,
|
||||
)
|
||||
except ImportError:
|
||||
pytest.skip("MCP server not available")
|
||||
|
||||
server = MCPServer(
|
||||
server_id="server-passthrough-no-admission",
|
||||
name="server",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.none,
|
||||
extra_headers=["Authorization", "x-request-id"],
|
||||
)
|
||||
|
||||
server_auth_header, extra_headers = _prepare_mcp_server_headers(
|
||||
server=server,
|
||||
mcp_server_auth_headers=None,
|
||||
mcp_auth_header=None,
|
||||
oauth2_headers=None,
|
||||
raw_headers={
|
||||
"authorization": "Bearer sk-litellm-key",
|
||||
"x-request-id": "req-789",
|
||||
},
|
||||
)
|
||||
|
||||
assert server_auth_header is None
|
||||
assert extra_headers == {"x-request-id": "req-789"}
|
||||
|
||||
|
||||
def test_prepare_mcp_server_headers_oauth2_m2m_omits_litellm_caller_authorization():
|
||||
"""M2M OAuth must not put caller Bearer (LiteLLM API key) into extra_headers (#23652)."""
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue