From af6ebf07eadc00144ac77fdd4367fb95c9527946 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:41:17 +0000 Subject: [PATCH] fix(oidc): guard naive STS expiration and wrap AWS ClientError as ValueError --- litellm/secret_managers/main.py | 16 ++++-- .../test_secret_managers_main.py | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/litellm/secret_managers/main.py b/litellm/secret_managers/main.py index 4dcfb70766c..77deffc0524 100644 --- a/litellm/secret_managers/main.py +++ b/litellm/secret_managers/main.py @@ -126,7 +126,8 @@ def _get_aws_sts_client(region: str) -> "STSClient": def _aws_oidc_cache_ttl(expiration: datetime | None) -> int | None: if expiration is None: return None - remaining = int((expiration - datetime.now(timezone.utc)).total_seconds()) - 60 + aware_expiration = expiration if expiration.tzinfo is not None else expiration.replace(tzinfo=timezone.utc) + remaining = int((aware_expiration - datetime.now(timezone.utc)).total_seconds()) - 60 return remaining if remaining > 0 else None @@ -145,12 +146,17 @@ def _get_aws_oidc_token( if isinstance(cached_token, str): return cached_token + from botocore.exceptions import ClientError + build_client = sts_client_factory or _get_aws_sts_client sts_client = build_client(_resolve_aws_region()) - response = sts_client.get_web_identity_token( - Audience=[oidc_aud], - SigningAlgorithm="RS256", - ) + try: + response = sts_client.get_web_identity_token( + Audience=[oidc_aud], + SigningAlgorithm="RS256", + ) + except ClientError as e: + raise ValueError(f"AWS OIDC provider failed: {e}") from e oidc_token = response["WebIdentityToken"] ttl = _aws_oidc_cache_ttl(response.get("Expiration")) diff --git a/tests/test_litellm/secret_managers/test_secret_managers_main.py b/tests/test_litellm/secret_managers/test_secret_managers_main.py index 47fd5a3efd7..ce9b62f233f 100644 --- a/tests/test_litellm/secret_managers/test_secret_managers_main.py +++ b/tests/test_litellm/secret_managers/test_secret_managers_main.py @@ -339,6 +339,56 @@ def test_oidc_aws_no_expiration_skips_caching(): mock_cache.set_cache.assert_not_called() +def test_oidc_aws_handles_naive_expiration(): + """A naive Expiration from boto3 must not crash ttl computation.""" + from datetime import datetime, timedelta, timezone + + from litellm.secret_managers.main import _get_aws_oidc_token + + naive_expiration = (datetime.now(timezone.utc) + timedelta(seconds=3600)).replace(tzinfo=None) + fake_client = _FakeSTSClient(token="aws_jwt", expiration=naive_expiration) + mock_cache = Mock() + mock_cache.get_cache.return_value = None + + with patch("litellm.secret_managers.main.oidc_cache", mock_cache): + with patch("litellm.secret_managers.main._resolve_aws_region", return_value="us-east-1"): + token = _get_aws_oidc_token( + oidc_aud="aud", + cache_key="oidc/aws/aud", + sts_client_factory=Mock(return_value=fake_client), + ) + + assert token == "aws_jwt" + (cache_kwargs,) = mock_cache.set_cache.call_args_list + assert 3500 <= cache_kwargs.kwargs["ttl"] <= 3540 + + +def test_oidc_aws_wraps_client_error(): + """A boto3 ClientError is surfaced as a descriptive ValueError.""" + from botocore.exceptions import ClientError + + from litellm.secret_managers.main import _get_aws_oidc_token + + class _RaisingSTSClient: + def get_web_identity_token(self, **kwargs): + raise ClientError( + {"Error": {"Code": "AccessDenied", "Message": "not authorized"}}, + "GetWebIdentityToken", + ) + + mock_cache = Mock() + mock_cache.get_cache.return_value = None + + with patch("litellm.secret_managers.main.oidc_cache", mock_cache): + with patch("litellm.secret_managers.main._resolve_aws_region", return_value="us-east-1"): + with pytest.raises(ValueError, match="AWS OIDC provider failed"): + _get_aws_oidc_token( + oidc_aud="aud", + cache_key="oidc/aws/aud", + sts_client_factory=Mock(return_value=_RaisingSTSClient()), + ) + + def test_get_aws_sts_client_pins_regional_endpoint(): from litellm.secret_managers.main import _get_aws_sts_client