fix: address potential bugs in auth_utils, mcp discoverable endpoints, and mcp auth

- auth_utils.get_request_route: return '/' instead of empty string when
  raw_path exactly equals root_path so downstream route allowlist checks
  still see a leading slash
- discoverable_endpoints.fetch_upstream_oauth_protected_resource: also
  cache negative results (no upstream metadata) for a shorter TTL so we
  don't re-fetch on every discovery request and so the per-key fetch
  lock can be pruned
- user_api_key_auth_mcp: guard the oauth2_headers 401 cold-start
  passthrough bypass with _has_client_supplied_mcp_auth, matching the
  parallel bypass in the no-Authorization branch so MCP-auth-bearing
  requests don't silently downgrade to anonymous admission

Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
Cursor Agent 2026-05-21 20:22:29 +00:00
parent 77206df140
commit c949c54c4d
No known key found for this signature in database
3 changed files with 19 additions and 2 deletions

View file

@ -268,6 +268,10 @@ class MCPRequestHandler:
)
if (
mcp_servers_from_path is not None
and not _has_client_supplied_mcp_auth(
mcp_auth_header,
mcp_server_auth_headers,
)
and _is_mcp_passthrough_cold_start(
mcp_servers_from_path, client_ip=client_ip
)

View file

@ -31,8 +31,11 @@ from litellm.types.mcp_server.mcp_server_manager import MCPServer
# TTL cache for upstream OAuth metadata fetched from pass-through MCP servers.
# Keeps us from hammering the upstream IdP on each discovery request.
# Keyed by (server_id, resource_url) → (expires_at_epoch, payload).
_OAUTH_METADATA_CACHE: Dict[Tuple[str, str], Tuple[float, dict]] = {}
# A payload of ``None`` is a negative-result entry that prevents repeated
# upstream fetches when the IdP consistently has no metadata to serve.
_OAUTH_METADATA_CACHE: Dict[Tuple[str, str], Tuple[float, Optional[dict]]] = {}
_OAUTH_METADATA_CACHE_TTL_SECONDS = 300
_OAUTH_METADATA_NEGATIVE_CACHE_TTL_SECONDS = 60
_OAUTH_METADATA_CACHE_MAX_SIZE = 128
# Per-(server_id, resource_url) async locks so concurrent discovery requests
# coalesce onto a single upstream fetch instead of issuing N parallel calls.
@ -830,6 +833,15 @@ async def fetch_upstream_oauth_protected_resource(
if len(network_errors) == len(candidates):
raise network_errors[-1]
# Negative-result caching: when no candidate yielded a usable payload,
# remember that for a shorter TTL so we don't re-fetch on every
# subsequent discovery request (and so the per-key lock can be pruned).
now = time.time()
_OAUTH_METADATA_CACHE[cache_key] = (
now + _OAUTH_METADATA_NEGATIVE_CACHE_TTL_SECONDS,
None,
)
_prune_oauth_metadata_cache(now)
return None

View file

@ -518,7 +518,8 @@ def get_request_route(request: Request) -> str:
if root_path and (
raw_path == root_path or raw_path.startswith(root_path + "/")
):
return raw_path[len(root_path) :]
stripped = raw_path[len(root_path) :]
return stripped or "/"
return raw_path
except Exception as e:
verbose_proxy_logger.debug(