mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(mcp,jwt): drop unneeded async helper + suppress misleading unscoped JWT warning
- _build_oauth_authorization_server_response: revert to sync (no awaits in body). The function only does dict construction and synchronous registry lookups; async added coroutine creation overhead per discovery call without need. - _build_decode_kwargs: accept has_issuer_config so the global path's 'JWT auth is unscoped' warning is suppressed when LiteLLM_JWTAuth.issuers provides per-issuer scoping. Previously the warning fired spuriously for admins who intentionally use only the new issuers config.
This commit is contained in:
parent
31f8c56cb7
commit
7905e996bd
4 changed files with 46 additions and 10 deletions
|
|
@ -971,11 +971,16 @@ async def oauth_protected_resource_mcp(
|
|||
)
|
||||
|
||||
|
||||
async def _build_oauth_authorization_server_response(
|
||||
def _build_oauth_authorization_server_response(
|
||||
request: Request,
|
||||
mcp_server_name: Optional[str],
|
||||
) -> dict:
|
||||
"""Build OAuth authorization server metadata response (gateway-as-AS shape)."""
|
||||
"""Build OAuth authorization server metadata response (gateway-as-AS shape).
|
||||
|
||||
Synchronous because the body only does dict construction and synchronous
|
||||
registry lookups; unlike :func:`_build_oauth_protected_resource_response`
|
||||
it does not need to await any upstream IO.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
|
@ -1039,7 +1044,7 @@ async def oauth_authorization_server_mcp_standard(
|
|||
Standard pattern: /mcp/{server_name}
|
||||
Discovery path: /.well-known/oauth-authorization-server/mcp/{server_name}
|
||||
"""
|
||||
return await _build_oauth_authorization_server_response(
|
||||
return _build_oauth_authorization_server_response(
|
||||
request=request,
|
||||
mcp_server_name=mcp_server_name,
|
||||
)
|
||||
|
|
@ -1058,7 +1063,7 @@ async def oauth_authorization_server_mcp(
|
|||
|
||||
Supports both legacy pattern (/{server_name}) and root endpoint.
|
||||
"""
|
||||
return await _build_oauth_authorization_server_response(
|
||||
return _build_oauth_authorization_server_response(
|
||||
request=request,
|
||||
mcp_server_name=mcp_server_name,
|
||||
)
|
||||
|
|
@ -1129,7 +1134,7 @@ async def oauth_authorization_server_legacy(request: Request, mcp_server_name: s
|
|||
"""
|
||||
OAuth authorization server discovery for legacy /{server_name}/mcp pattern.
|
||||
"""
|
||||
return await _build_oauth_authorization_server_response(
|
||||
return _build_oauth_authorization_server_response(
|
||||
request=request,
|
||||
mcp_server_name=mcp_server_name,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -835,7 +835,7 @@ class JWTHandler:
|
|||
_unscoped_jwt_warning_emitted = False
|
||||
|
||||
@classmethod
|
||||
def _build_decode_kwargs(cls) -> dict:
|
||||
def _build_decode_kwargs(cls, has_issuer_config: bool = False) -> dict:
|
||||
"""Build the audience/issuer/options kwargs for ``jwt.decode``.
|
||||
|
||||
Setting ``JWT_AUDIENCE`` (and optionally ``JWT_ISSUER``) turns on the
|
||||
|
|
@ -843,6 +843,11 @@ class JWTHandler:
|
|||
minted by other applications that share the same IdP signing keys.
|
||||
When both are unset PyJWT only checks the signature and expiry, which
|
||||
is preserved for backward compatibility but logged once as a warning.
|
||||
|
||||
``has_issuer_config`` suppresses the warning when the caller has
|
||||
configured per-issuer scoping via ``LiteLLM_JWTAuth.issuers``: this
|
||||
global path is then just the fallback for tokens whose ``iss`` did not
|
||||
match any configured issuer, not the only scoping mechanism.
|
||||
"""
|
||||
audience = os.getenv("JWT_AUDIENCE")
|
||||
issuer = os.getenv("JWT_ISSUER")
|
||||
|
|
@ -850,6 +855,7 @@ class JWTHandler:
|
|||
if (
|
||||
audience is None
|
||||
and issuer is None
|
||||
and not has_issuer_config
|
||||
and not cls._unscoped_jwt_warning_emitted
|
||||
):
|
||||
verbose_proxy_logger.warning(
|
||||
|
|
@ -1051,7 +1057,9 @@ class JWTHandler:
|
|||
kid=kid,
|
||||
)
|
||||
|
||||
decode_kwargs = self._build_decode_kwargs()
|
||||
decode_kwargs = self._build_decode_kwargs(
|
||||
has_issuer_config=bool(getattr(self.litellm_jwtauth, "issuers", None))
|
||||
)
|
||||
|
||||
public_key = await self.get_public_key(kid=kid)
|
||||
|
||||
|
|
|
|||
|
|
@ -1592,7 +1592,7 @@ async def test_oauth_authorization_server_returns_empty_scopes_when_none():
|
|||
mock_request.headers = {}
|
||||
|
||||
try:
|
||||
response = await _build_oauth_authorization_server_response(
|
||||
response = _build_oauth_authorization_server_response(
|
||||
request=mock_request,
|
||||
mcp_server_name="atlassian_mcp",
|
||||
)
|
||||
|
|
@ -1981,7 +1981,7 @@ async def test_discovery_root_includes_server_name_prefix():
|
|||
|
||||
try:
|
||||
# Call with mcp_server_name=None (root discovery)
|
||||
response = await _build_oauth_authorization_server_response(
|
||||
response = _build_oauth_authorization_server_response(
|
||||
request=mock_request,
|
||||
mcp_server_name=None,
|
||||
)
|
||||
|
|
@ -2024,7 +2024,7 @@ async def test_discovery_root_does_not_expose_private_server_for_external_client
|
|||
"litellm.proxy._experimental.mcp_server.discoverable_endpoints.IPAddressUtils.get_mcp_client_ip",
|
||||
return_value="198.51.100.10",
|
||||
):
|
||||
authorization_response = await _build_oauth_authorization_server_response(
|
||||
authorization_response = _build_oauth_authorization_server_response(
|
||||
request=mock_request,
|
||||
mcp_server_name=None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2754,3 +2754,26 @@ def test_build_decode_kwargs_no_warning_when_scoped(
|
|||
if "neither JWT_AUDIENCE nor JWT_ISSUER" in r.getMessage()
|
||||
]
|
||||
assert matching == []
|
||||
|
||||
|
||||
def test_build_decode_kwargs_no_warning_when_issuer_config_scoped(
|
||||
monkeypatch, _reset_unscoped_warning_flag, caplog
|
||||
):
|
||||
"""When per-issuer config (``LiteLLM_JWTAuth.issuers``) scopes the proxy,
|
||||
the global path's unscoped-fallback warning must not fire — admins who
|
||||
intentionally use issuer config without env-var scoping otherwise see a
|
||||
misleading warning implying their JWT auth is unscoped."""
|
||||
import logging
|
||||
|
||||
monkeypatch.delenv("JWT_AUDIENCE", raising=False)
|
||||
monkeypatch.delenv("JWT_ISSUER", raising=False)
|
||||
caplog.set_level(logging.WARNING)
|
||||
|
||||
JWTHandler._build_decode_kwargs(has_issuer_config=True)
|
||||
|
||||
matching = [
|
||||
r
|
||||
for r in caplog.records
|
||||
if "neither JWT_AUDIENCE nor JWT_ISSUER" in r.getMessage()
|
||||
]
|
||||
assert matching == []
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue