mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
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:
parent
9469ededc9
commit
61c9a2f580
2 changed files with 31 additions and 2 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue