mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(mcp): never promote caller oauth2 headers to the resolved credential on the v1 arm
This commit is contained in:
parent
48572c9516
commit
040aa9d896
2 changed files with 89 additions and 4 deletions
|
|
@ -4755,12 +4755,19 @@ class MCPServerManager:
|
|||
back with any header the resolver claimed already dropped. Unmigrated (v1) servers resolve
|
||||
through the stored-token lookup instead, and a missing per-user credential raises the same
|
||||
discovery challenge the MCPClient path serves, rather than egressing unauthenticated.
|
||||
|
||||
The resolved headers carry only credentials the gateway itself resolved (a stored per-user
|
||||
token, a minted or exchanged token). Caller-supplied ``oauth2_headers`` are never promoted
|
||||
into them: on the v2 arm they feed only subject-token extraction (the designed RFC 8693
|
||||
input), and on the v1 arm their presence disables the stored lookup entirely, so a
|
||||
caller's gateway credential can never displace a per-server BYOK header or leak upstream
|
||||
as the resolved credential.
|
||||
"""
|
||||
spec = to_server_spec(mcp_server)
|
||||
if spec is None:
|
||||
stored_headers = await self._resolve_oauth2_headers_for_tool_call(
|
||||
mcp_server, oauth2_headers, user_api_key_auth
|
||||
)
|
||||
if oauth2_headers:
|
||||
return None, forwarded_headers
|
||||
stored_headers = await self._resolve_oauth2_headers_for_tool_call(mcp_server, None, user_api_key_auth)
|
||||
return stored_headers, forwarded_headers
|
||||
|
||||
subject_token: str | None = None
|
||||
|
|
@ -4872,6 +4879,7 @@ class MCPServerManager:
|
|||
)
|
||||
tasks.append(during_hook_task)
|
||||
|
||||
caller_oauth2_headers = oauth2_headers
|
||||
oauth2_headers = await self._resolve_oauth2_headers_for_tool_call(mcp_server, oauth2_headers, user_api_key_auth)
|
||||
|
||||
# For OpenAPI servers, call the tool handler directly instead of via MCP client
|
||||
|
|
@ -4891,7 +4899,7 @@ class MCPServerManager:
|
|||
)
|
||||
resolved_auth_headers, forwarded_headers = await self.resolve_openapi_upstream_auth(
|
||||
mcp_server=mcp_server,
|
||||
oauth2_headers=oauth2_headers,
|
||||
oauth2_headers=caller_oauth2_headers,
|
||||
raw_headers=raw_headers,
|
||||
mcp_auth_header=mcp_auth_header,
|
||||
user_api_key_auth=user_api_key_auth,
|
||||
|
|
|
|||
|
|
@ -1119,3 +1119,80 @@ class TestOpenApiResolvedUpstreamAuth:
|
|||
)
|
||||
|
||||
called.assert_not_awaited()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_caller_oauth2_headers_never_become_resolved_for_byok_server(self):
|
||||
"""Greptile P1 regression: BYOK servers defer to v1 (to_server_spec None), and the v1 arm
|
||||
must never promote caller-supplied oauth2 headers into the resolved-auth slot, where they
|
||||
would override the per-server BYOK credential and leak the caller's gateway Authorization
|
||||
upstream."""
|
||||
manager = MCPServerManager()
|
||||
server = MCPServer(
|
||||
server_id="byok-spec",
|
||||
name="byok_spec",
|
||||
server_name="byok_spec",
|
||||
url=None,
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.api_key,
|
||||
spec_path="https://example.com/openapi.yaml",
|
||||
is_byok=True,
|
||||
)
|
||||
|
||||
resolved, forwarded = await manager.resolve_openapi_upstream_auth(
|
||||
mcp_server=server,
|
||||
oauth2_headers={"Authorization": "Bearer sk-litellm-gateway-key"},
|
||||
raw_headers=None,
|
||||
mcp_auth_header="user-byok-key",
|
||||
user_api_key_auth=UserAPIKeyAuth(user_id="alice", api_key="sk-user"),
|
||||
forwarded_headers=None,
|
||||
)
|
||||
|
||||
assert resolved is None
|
||||
assert forwarded is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v1_server_threads_stored_headers_only_without_caller_headers(self):
|
||||
"""The v1 (unmigrated) arm resolves the stored per-user token only when the caller sent no
|
||||
oauth2 headers of their own; with caller headers present the stored lookup is skipped and
|
||||
nothing is promoted to resolved."""
|
||||
manager = MCPServerManager()
|
||||
server = MCPServer(
|
||||
server_id="v1-spec",
|
||||
name="v1_spec",
|
||||
server_name="v1_spec",
|
||||
url=None,
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
spec_path="https://example.com/openapi.yaml",
|
||||
delegate_auth_to_upstream=True,
|
||||
)
|
||||
stored = {"Authorization": "Bearer stored-v1-token"}
|
||||
user_auth = UserAPIKeyAuth(user_id="alice", api_key="sk-user")
|
||||
|
||||
with patch.object(
|
||||
manager, "_resolve_oauth2_headers_for_tool_call", new=AsyncMock(return_value=stored)
|
||||
) as lookup:
|
||||
resolved, _ = await manager.resolve_openapi_upstream_auth(
|
||||
mcp_server=server,
|
||||
oauth2_headers=None,
|
||||
raw_headers=None,
|
||||
mcp_auth_header=None,
|
||||
user_api_key_auth=user_auth,
|
||||
forwarded_headers=None,
|
||||
)
|
||||
assert resolved == stored
|
||||
lookup.assert_awaited_once_with(server, None, user_auth)
|
||||
|
||||
with patch.object(
|
||||
manager, "_resolve_oauth2_headers_for_tool_call", new=AsyncMock(return_value=stored)
|
||||
) as lookup:
|
||||
resolved, _ = await manager.resolve_openapi_upstream_auth(
|
||||
mcp_server=server,
|
||||
oauth2_headers={"Authorization": "Bearer caller-supplied"},
|
||||
raw_headers=None,
|
||||
mcp_auth_header=None,
|
||||
user_api_key_auth=user_auth,
|
||||
forwarded_headers=None,
|
||||
)
|
||||
assert resolved is None
|
||||
lookup.assert_not_awaited()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue