diff --git a/litellm/proxy/auth/v2/authenticators.py b/litellm/proxy/auth/v2/authenticators.py index 2e305555852..5dea06b0ef4 100644 --- a/litellm/proxy/auth/v2/authenticators.py +++ b/litellm/proxy/auth/v2/authenticators.py @@ -101,15 +101,28 @@ def _load_jwt_settings() -> Any: ) +def _jwt_is_configured() -> bool: + import os + + from litellm.proxy.proxy_server import general_settings + + cfg = (general_settings or {}).get("auth_v2_jwt") or {} + return bool(cfg.get("jwks_uri") or os.getenv("AUTH_V2_JWKS_URI")) + + class JWTAuthenticator: """Verifies a bearer JWT with authlib and maps its claims to an identity.""" def can_handle(self, api_key: Optional[str]) -> bool: - return ( + # Only claim JWT-shaped tokens when JWT auth is actually configured, so an + # unconfigured deployment falls through to a clean 401 instead of a 500. + if not ( isinstance(api_key, str) and not api_key.startswith("sk-") and api_key.count(".") == 2 - ) + ): + return False + return _jwt_is_configured() async def authenticate(self, api_key: str, ctx: AuthContext) -> Any: from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth diff --git a/tests/test_litellm/proxy/auth/v2/test_authenticators.py b/tests/test_litellm/proxy/auth/v2/test_authenticators.py index 4747dde5296..45401cc534d 100644 --- a/tests/test_litellm/proxy/auth/v2/test_authenticators.py +++ b/tests/test_litellm/proxy/auth/v2/test_authenticators.py @@ -1,10 +1,13 @@ import pytest from litellm.proxy.auth.v2.authenticators import ( + JWTAuthenticator, MasterKeyAuthenticator, VirtualKeyAuthenticator, ) +JWT_SHAPED = "header.payload.signature" + MASTER = "sk-master-secret-123" @@ -40,3 +43,27 @@ def test_virtual_key_handles_sk_prefix_but_not_master_first(): assert vk.can_handle("sk-abc123") is True assert vk.can_handle("not-a-key") is False assert vk.can_handle(None) is False + + +def test_jwt_authenticator_inert_when_unconfigured(monkeypatch): + # No auth_v2_jwt config and no env var: a JWT-shaped token must fall through + # (can_handle False) so the chain ends in a clean 401, not a 500. + monkeypatch.setattr( + "litellm.proxy.proxy_server.general_settings", {}, raising=False + ) + monkeypatch.delenv("AUTH_V2_JWKS_URI", raising=False) + assert JWTAuthenticator().can_handle(JWT_SHAPED) is False + + +def test_jwt_authenticator_handles_jwt_shape_when_configured(monkeypatch): + monkeypatch.setattr( + "litellm.proxy.proxy_server.general_settings", + {"auth_v2_jwt": {"jwks_uri": "https://idp.example/jwks"}}, + raising=False, + ) + auth = JWTAuthenticator() + assert auth.can_handle(JWT_SHAPED) is True + # Still only claims JWT-shaped tokens, never virtual keys or non-3-part values. + assert auth.can_handle("sk-abc.def.ghi") is False + assert auth.can_handle("not.a.jwt.token") is False + assert auth.can_handle(None) is False