fix(jwt,mcp): clarify issuers fallthrough + add TTL on mcp permission cache

- LiteLLM_JWTAuth.issuers docs now state explicitly that unlisted
  issuers fall back to the global JWT_AUDIENCE/JWT_ISSUER path; the
  field is additive routing, not an allow-list. Matches actual
  control flow in handle_jwt.auth_jwt and the regression tests
  asserting backwards compatibility with the global JWKS path.
- MCPRequestHandler._get_{org,agent}_object_permission now pass
  ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL on async_set_cache,
  mirroring the auth_checks.py pattern so the cache TTL is explicit
  on both DualCache layers.
This commit is contained in:
mateo-berri 2026-05-21 03:48:17 +00:00 • committed by Claude
parent 7905e996bd
commit 91f44f3de9
No known key found for this signature in database
2 changed files with 15 additions and 5 deletions

View file

@ -7,6 +7,7 @@ from starlette.requests import Request
from starlette.types import Scope
from litellm._logging import verbose_logger
from litellm.constants import DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL
from litellm.proxy._types import (
LiteLLM_TeamTable,
ProxyException,
@ -1155,6 +1156,7 @@ class MCPRequestHandler:
await user_api_key_cache.async_set_cache(
key=cache_key,
value=MCPRequestHandler._ORG_NO_PERMISSION_SENTINEL,
ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL,
)
return None
@ -1164,7 +1166,9 @@ class MCPRequestHandler:
# get_end_user_object / get_team_object in auth_checks.py).
obj_perm = LiteLLM_ObjectPermissionTable(**org_row.object_permission.dict())
await user_api_key_cache.async_set_cache(
key=cache_key, value=obj_perm.dict()
key=cache_key,
value=obj_perm.dict(),
ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL,
)
return obj_perm
except Exception as e:
@ -1334,6 +1338,7 @@ class MCPRequestHandler:
await user_api_key_cache.async_set_cache(
key=cache_key,
value=MCPRequestHandler._AGENT_NO_PERMISSION_SENTINEL,
ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL,
)
return None
@ -1341,7 +1346,9 @@ class MCPRequestHandler:
**agent_row.object_permission.dict()
)
await user_api_key_cache.async_set_cache(
key=cache_key, value=obj_perm.dict()
key=cache_key,
value=obj_perm.dict(),
ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL,
)
return obj_perm
except Exception as e:

View file

@ -4397,8 +4397,11 @@ class JWTIssuerConfig(BaseModel):
"""
Issuer-bound JWT validation configuration.
When configured, LiteLLM selects this issuer by the token's unverified `iss`
claim, then validates the token only against this issuer's JWKS and audience.
When a token's unverified `iss` claim matches an entry in
``LiteLLM_JWTAuth.issuers``, LiteLLM validates it only against that
issuer's JWKS and audience. Tokens whose `iss` does not match any
configured issuer fall back to the global JWT_AUDIENCE/JWT_ISSUER
validation path; `issuers` is additive routing, not an allow-list.
"""
issuer: str = Field(description="Exact expected JWT issuer (`iss`) value.")
@ -4550,7 +4553,7 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase):
)
issuers: Optional[List[JWTIssuerConfig]] = Field(
default=None,
description="Optional issuer-bound JWT validation rules. When set, tokens must match one configured issuer by exact `iss` claim before JWKS lookup.",
description="Optional issuer-bound JWT validation rules. When a token's `iss` matches a configured issuer, validation uses that issuer's JWKS, audience, and claim mappings. Tokens with an unlisted `iss` fall back to the global JWT_AUDIENCE/JWT_ISSUER validation path — this is additive routing, not an allow-list.",
)
#########################################################