mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(jwt): validate issuer audience at config load + dedicated key-miss exception
- Move JWTIssuerConfig audience-required guard into a Pydantic model_validator so misconfiguration fails at startup instead of on the first request. - Replace the string-match `No matching public key found` filter in get_public_key's multi-URL fallback with a dedicated NoMatchingJWTPublicKeyError; only that specific exception triggers continuation, every other error still surfaces.
This commit is contained in:
parent
123f0fd482
commit
0fada28153
4 changed files with 39 additions and 52 deletions
|
|
@ -4470,6 +4470,14 @@ class JWTIssuerConfig(BaseModel):
|
|||
"extra": "forbid",
|
||||
}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_audience_configured(self) -> "JWTIssuerConfig":
|
||||
if self.audience is None and not self.disable_audience_validation:
|
||||
raise ValueError(
|
||||
f"JWT issuer {self.issuer} must configure audience or set disable_audience_validation=True"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ from .auth_checks import (
|
|||
)
|
||||
|
||||
|
||||
class NoMatchingJWTPublicKeyError(Exception):
|
||||
"""Raised when a JWKS endpoint returns no key matching the requested ``kid``."""
|
||||
|
||||
|
||||
class JWTHandler:
|
||||
"""
|
||||
- treat the sub id passed in as the user id
|
||||
|
|
@ -704,7 +708,7 @@ class JWTHandler:
|
|||
if public_key is not None:
|
||||
return cast(dict, public_key)
|
||||
|
||||
raise Exception(
|
||||
raise NoMatchingJWTPublicKeyError(
|
||||
f"No matching public key found. keys={resolved_jwks_url}, kid={kid}"
|
||||
)
|
||||
|
||||
|
|
@ -721,14 +725,12 @@ class JWTHandler:
|
|||
return await self._get_public_key_from_jwks_url(
|
||||
jwks_url=key_url, kid=kid
|
||||
)
|
||||
except Exception as e:
|
||||
if "No matching public key found" not in str(e):
|
||||
raise
|
||||
except NoMatchingJWTPublicKeyError as e:
|
||||
verbose_proxy_logger.debug(
|
||||
"JWT Auth: No matching public key found at %s: %s", key_url, e
|
||||
)
|
||||
|
||||
raise Exception(
|
||||
raise NoMatchingJWTPublicKeyError(
|
||||
f"No matching public key found. keys={keys_url_list}, kid={kid}"
|
||||
)
|
||||
|
||||
|
|
@ -1013,14 +1015,6 @@ class JWTHandler:
|
|||
async def _auth_jwt_with_issuer(
|
||||
self, token: str, issuer_config: JWTIssuerConfig, kid: Optional[str]
|
||||
) -> dict:
|
||||
if (
|
||||
issuer_config.audience is None
|
||||
and not issuer_config.disable_audience_validation
|
||||
):
|
||||
raise Exception(
|
||||
f"JWT issuer {issuer_config.issuer} must configure audience or set disable_audience_validation=True"
|
||||
)
|
||||
|
||||
public_key = await self._get_public_key_from_jwks_url(
|
||||
jwks_url=self._get_jwks_url_for_issuer(issuer_config=issuer_config),
|
||||
kid=kid,
|
||||
|
|
|
|||
|
|
@ -1947,8 +1947,7 @@ async def test_multi_issuer_jwt_missing_mapped_claim_is_optional(monkeypatch):
|
|||
assert JWTHandler.LITELLM_USER_ID_CLAIM not in claims
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_issuer_jwt_requires_audience_unless_explicitly_disabled(
|
||||
def test_multi_issuer_jwt_requires_audience_unless_explicitly_disabled(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.delenv("JWT_AUDIENCE", raising=False)
|
||||
|
|
@ -1956,25 +1955,16 @@ async def test_multi_issuer_jwt_requires_audience_unless_explicitly_disabled(
|
|||
|
||||
issuer = "https://issuer.example.com"
|
||||
jwks_url = f"{issuer}/keys"
|
||||
private_key, jwk = _get_rsa_key_and_jwk(kid="issuer-key")
|
||||
jwt_handler = _get_jwt_handler_with_issuer_keys(
|
||||
issuers=[
|
||||
{
|
||||
"issuer": issuer,
|
||||
"jwks_url": jwks_url,
|
||||
}
|
||||
],
|
||||
keys_by_url={jwks_url: [jwk]},
|
||||
)
|
||||
token = _encode_rsa_jwt(
|
||||
private_key=private_key,
|
||||
issuer=issuer,
|
||||
audience="some-other-client",
|
||||
kid="issuer-key",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
await jwt_handler.auth_jwt(token=token)
|
||||
LiteLLM_JWTAuth(
|
||||
issuers=[
|
||||
{
|
||||
"issuer": issuer,
|
||||
"jwks_url": jwks_url,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert "must configure audience" in str(exc.value)
|
||||
|
||||
|
|
|
|||
|
|
@ -2892,7 +2892,12 @@ async def test_get_public_key_tries_next_jwks_url_when_kid_missing(monkeypatch):
|
|||
def test_get_jwks_url_for_issuer_falls_back_to_discovery_document():
|
||||
jwt_handler = JWTHandler()
|
||||
issuer_config = LiteLLM_JWTAuth(
|
||||
issuers=[{"issuer": "https://issuer.example.com/tenant/"}]
|
||||
issuers=[
|
||||
{
|
||||
"issuer": "https://issuer.example.com/tenant/",
|
||||
"disable_audience_validation": True,
|
||||
}
|
||||
]
|
||||
).issuers[0]
|
||||
|
||||
jwks_url = jwt_handler._get_jwks_url_for_issuer(issuer_config=issuer_config)
|
||||
|
|
@ -3146,8 +3151,7 @@ async def test_multi_issuer_jwt_missing_mapped_claim_leaves_user_id_unset(
|
|||
assert jwt_handler.LITELLM_USER_ID_CLAIM not in claims
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_issuer_jwt_requires_audience_unless_explicitly_disabled(
|
||||
def test_multi_issuer_jwt_requires_audience_unless_explicitly_disabled(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.delenv("JWT_AUDIENCE", raising=False)
|
||||
|
|
@ -3155,25 +3159,16 @@ async def test_multi_issuer_jwt_requires_audience_unless_explicitly_disabled(
|
|||
|
||||
issuer = "https://issuer.example.com"
|
||||
jwks_url = f"{issuer}/keys"
|
||||
private_key, jwk = _get_rsa_key_and_jwk(kid="issuer-key")
|
||||
jwt_handler = _get_jwt_handler_with_issuer_keys(
|
||||
issuers=[
|
||||
{
|
||||
"issuer": issuer,
|
||||
"jwks_url": jwks_url,
|
||||
}
|
||||
],
|
||||
keys_by_url={jwks_url: [jwk]},
|
||||
)
|
||||
token = _encode_rsa_jwt(
|
||||
private_key=private_key,
|
||||
issuer=issuer,
|
||||
audience="some-other-client",
|
||||
kid="issuer-key",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc:
|
||||
await jwt_handler.auth_jwt(token=token)
|
||||
LiteLLM_JWTAuth(
|
||||
issuers=[
|
||||
{
|
||||
"issuer": issuer,
|
||||
"jwks_url": jwks_url,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert "must configure audience" in str(exc.value)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue