fix(jwt+mcp): tighten issuer-scoped claim type handling, RFC-quote authorization_uri, surface MCP upstream auth errors, defense-in-depth on decode options

- handle_jwt: when an issuer-scoped _litellm_team_ids claim exists but
  has an unexpected type, return [] instead of falling through to the
  global team_ids_jwt_field path (different claim semantically).
- handle_jwt: _get_decode_options/_decode_jwt_with_public_key now take
  an explicit disable_audience_validation flag; passing audience=None
  without it raises, so audience checks can't silently disappear if the
  model validator is ever bypassed. _auth_jwt_with_issuer forwards the
  flag from JWTIssuerConfig.
- mcp_server: quote the authorization_uri WWW-Authenticate parameter
  value (RFC 6750 / 9728 auth-param must be quoted-string), matching
  the pass-through path.
- mcp_server: in _fetch_and_filter_server_tools, re-raise
  MCPUpstreamAuthError so the outer streamable-HTTP handler can surface
  a proper 401 + WWW-Authenticate challenge instead of returning an
  empty tool list.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
Cursor Agent 2026-05-26 19:53:54 +00:00
parent 9469ededc9
commit 61c9a2f580
No known key found for this signature in database
2 changed files with 31 additions and 2 deletions

View file

@ -1456,6 +1456,13 @@ if MCP_AVAILABLE:
f"Successfully fetched {len(tools)} tools from server {server.name}, {len(filtered_tools)} after filtering"
)
return filtered_tools
except MCPUpstreamAuthError:
# Surface upstream 401/403 to the outer handler so the
# client receives a proper WWW-Authenticate challenge
# instead of a silently empty tool list. Without this
# re-raise the broad ``except Exception`` below would
# swallow the auth error.
raise
except Exception as e:
verbose_logger.exception(
f"Error getting tools from server {server.name}: {str(e)}"
@ -2960,7 +2967,7 @@ if MCP_AVAILABLE:
_as_url = f"{base_url}/.well-known/oauth-authorization-server/mcp/{server_name}"
else:
_as_url = f"{base_url}/.well-known/oauth-authorization-server/{server_name}"
authorization_uri = f"Bearer authorization_uri={_as_url}"
authorization_uri = f'Bearer authorization_uri="{_as_url}"'
raise HTTPException(
status_code=401,

View file

@ -255,6 +255,11 @@ class JWTHandler:
return issuer_team_ids
if isinstance(issuer_team_ids, str):
return [issuer_team_ids]
# Issuer-scoped claim exists but has an unexpected type
# (e.g. int/dict from an unusual upstream mapping). Don't silently
# fall through to the global ``team_ids_jwt_field`` path — that
# would read a semantically unrelated claim on the same token.
return []
if self.litellm_jwtauth.team_ids_jwt_field is not None:
team_ids: Optional[List[str]] = get_nested_value(
@ -971,7 +976,18 @@ class JWTHandler:
self,
audience: Optional[Union[str, List[str]]],
issuer: Optional[str] = None,
disable_audience_validation: bool = False,
) -> Optional[dict]:
# Disabling audience verification must be an explicit choice — never
# an implicit consequence of ``audience`` being None. Otherwise a
# caller that accidentally constructs a config with ``audience=None``
# (bypassing the model validator) would silently lose audience
# validation. Require callers to opt in via
# ``disable_audience_validation=True``.
if audience is None and not disable_audience_validation:
raise ValueError(
"audience must be provided unless disable_audience_validation=True"
)
options: dict = {}
if audience is None:
options["verify_aud"] = False
@ -986,11 +1002,16 @@ class JWTHandler:
audience: Optional[Union[str, List[str]]],
issuer: Optional[str] = None,
options: Optional[dict] = None,
disable_audience_validation: bool = False,
) -> dict:
decode_options = (
options
if options is not None
else self._get_decode_options(audience=audience, issuer=issuer)
else self._get_decode_options(
audience=audience,
issuer=issuer,
disable_audience_validation=disable_audience_validation,
)
)
if isinstance(public_key, dict):
@ -1035,6 +1056,7 @@ class JWTHandler:
public_key=public_key,
audience=issuer_config.audience,
issuer=issuer_config.issuer,
disable_audience_validation=issuer_config.disable_audience_validation,
)
except jwt.ExpiredSignatureError:
raise Exception("Token Expired")