Skip to content

Commit eb0ecce

Browse files
authored
[Identity] Adjust cache credential error behavior in DAC (#42934)
SharedTokenCacheCredential will now follow the similar pattern as other developer tool credentials and raise CredentialUnavailableError when within DAC for most exceptions. Signed-off-by: Paul Van Eck <[email protected]>
1 parent 95c0373 commit eb0ecce

File tree

6 files changed

+99
-15
lines changed

6 files changed

+99
-15
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
# Release History
22

3-
## 1.24.1 (Unreleased)
3+
## 1.25.0 (2025-09-11)
44

55
### Features Added
66

77
- `AzureDeveloperCliCredential` now supports `claims` in `get_token` and `get_token_info`. ([#42568](https://github.com/Azure/azure-sdk-for-python/pull/42568))
88
- Added new keyword argument `require_envvar` to `DefaultAzureCredential` to enforce the presence of the `AZURE_TOKEN_CREDENTIALS` environment variable. ([#42660](https://github.com/Azure/azure-sdk-for-python/pull/42660))
99

10-
### Breaking Changes
11-
1210
### Bugs Fixed
1311

1412
- Fixed an issue where `AzureDeveloperCliCredential` would time out during token requests when `azd` prompts for user interaction. This issue commonly occurred in environments where the `AZD_DEBUG` environment variable was set, causing the Azure Developer CLI to display additional prompts that interfered with automated token acquisition. ([#42535](https://github.com/Azure/azure-sdk-for-python/pull/42535))
1513
- Fixed an issue where credentials configured with a default tenant ID of "organizations" (such as `InteractiveBrowserCredential` and `DeviceCodeCredential`) would fail authentication when a specific tenant ID was provided in `get_token` or `get_token_info` method calls. ([#42721](https://github.com/Azure/azure-sdk-for-python/pull/42721))
1614

1715
### Other Changes
1816

17+
- Updated `SharedTokenCacheCredential` to raise `CredentialUnavailableError` instead of `ClientAuthenticationError` during token refresh failures when within the context of `DefaultAzureCredential`. ([#42934](https://github.com/Azure/azure-sdk-for-python/pull/42934))
18+
1919
## 1.24.0 (2025-08-07)
2020

2121
### Bugs Fixed

sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .silent import SilentAuthenticationCredential
99
from .. import CredentialUnavailableError
1010
from .._constants import DEVELOPER_SIGN_ON_CLIENT_ID
11-
from .._internal import AadClient, AadClientBase
11+
from .._internal import AadClient, AadClientBase, within_dac
1212
from .._internal.decorators import log_get_token
1313
from .._internal.shared_token_cache import NO_TOKEN, SharedTokenCacheBase
1414

@@ -191,10 +191,17 @@ def _get_token_base(
191191

192192
# try each refresh token, returning the first access token acquired
193193
for refresh_token in self._get_refresh_tokens(account, is_cae=is_cae):
194-
token = cast(AadClient, self._client).obtain_token_by_refresh_token(
195-
scopes, refresh_token, claims=claims, tenant_id=tenant_id, enable_cae=is_cae, **kwargs
196-
)
197-
return token
194+
try:
195+
token = cast(AadClient, self._client).obtain_token_by_refresh_token(
196+
scopes, refresh_token, claims=claims, tenant_id=tenant_id, enable_cae=is_cae, **kwargs
197+
)
198+
return token
199+
except Exception as e: # pylint: disable=broad-except
200+
if within_dac.get():
201+
raise CredentialUnavailableError( # pylint: disable=raise-missing-from
202+
message=getattr(e, "message", str(e)), response=getattr(e, "response", None)
203+
)
204+
raise
198205

199206
raise CredentialUnavailableError(message=NO_TOKEN.format(account.get("username")))
200207

sdk/identity/azure-identity/azure/identity/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
# ------------------------------------
5-
VERSION = "1.24.1"
5+
VERSION = "1.25.0"

sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ... import CredentialUnavailableError
99
from ..._constants import DEVELOPER_SIGN_ON_CLIENT_ID
1010
from ..._internal.shared_token_cache import NO_TOKEN, SharedTokenCacheBase
11+
from ..._internal.utils import within_dac
1112
from .._internal import AsyncContextManager
1213
from .._internal.aad_client import AadClient
1314
from .._internal.decorators import log_get_token_async
@@ -143,11 +144,17 @@ async def _get_token_base(
143144

144145
# try each refresh token, returning the first access token acquired
145146
for refresh_token in self._get_refresh_tokens(account, is_cae=is_cae):
146-
token = await cast(AadClient, self._client).obtain_token_by_refresh_token(
147-
scopes, refresh_token, claims=claims, tenant_id=tenant_id, enable_cae=is_cae, **kwargs
148-
)
149-
return token
150-
147+
try:
148+
token = await cast(AadClient, self._client).obtain_token_by_refresh_token(
149+
scopes, refresh_token, claims=claims, tenant_id=tenant_id, enable_cae=is_cae, **kwargs
150+
)
151+
return token
152+
except Exception as e: # pylint: disable=broad-except
153+
if within_dac.get():
154+
raise CredentialUnavailableError( # pylint: disable=raise-missing-from
155+
message=getattr(e, "message", str(e)), response=getattr(e, "response", None)
156+
)
157+
raise
151158
raise CredentialUnavailableError(message=NO_TOKEN.format(account.get("username")))
152159

153160
def _get_auth_client(self, **kwargs: Any) -> AadClientBase:

sdk/identity/azure-identity/tests/test_shared_cache_credential.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,36 @@ def send(request, **kwargs):
917917
within_dac.set(False)
918918

919919

920+
@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS)
921+
def test_within_dac_refresh_token_error(get_token_method):
922+
"""When within DAC context and refresh token fails, should raise CredentialUnavailableError"""
923+
924+
925+
refresh_token = "invalid-refresh-token"
926+
scope = "scope"
927+
account = get_account_event(uid="uid_a", utid="utid", username=upn, refresh_token=refresh_token)
928+
cache = populated_cache(account)
929+
930+
def send(request, **kwargs):
931+
# Mock a token request that fails with invalid_grant (401/400 error)
932+
if "refresh_token" in (request.data or {}):
933+
return mock_response(
934+
status_code=400, json_payload={"error": "invalid_grant", "error_description": "Refresh token expired"}
935+
)
936+
# Allow discovery requests to succeed
937+
return get_discovery_response("https://localhost/tenant")
938+
939+
transport = Mock(send=send)
940+
credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username=upn)
941+
942+
within_dac.set(True)
943+
try:
944+
with pytest.raises(CredentialUnavailableError):
945+
getattr(credential, get_token_method)(scope)
946+
finally:
947+
within_dac.set(False)
948+
949+
920950
@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS)
921951
def test_claims_challenge(get_token_method):
922952
"""get_token should pass any claims challenge to MSAL token acquisition APIs"""

sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,19 @@
1717
NO_ACCOUNTS,
1818
NO_MATCHING_ACCOUNTS,
1919
)
20+
from azure.identity._internal import within_dac
2021
from azure.identity._internal.user_agent import USER_AGENT
2122
from msal import TokenCache
2223
import pytest
2324

24-
from helpers import build_aad_response, id_token_claims, mock_response, Request, GET_TOKEN_METHODS
25+
from helpers import (
26+
build_aad_response,
27+
id_token_claims,
28+
mock_response,
29+
get_discovery_response,
30+
Request,
31+
GET_TOKEN_METHODS,
32+
)
2533
from helpers_async import async_validating_transport, AsyncMockTransport
2634
from test_shared_cache_credential import get_account_event, populated_cache
2735

@@ -390,6 +398,38 @@ async def test_no_refresh_token(get_token_method):
390398
await getattr(credential, get_token_method)("scope")
391399

392400

401+
@pytest.mark.asyncio
402+
@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS)
403+
async def test_within_dac_refresh_token_error(get_token_method):
404+
"""When within DAC context and refresh token fails, should raise CredentialUnavailableError"""
405+
406+
407+
refresh_token = "invalid-refresh-token"
408+
scope = "scope"
409+
account = get_account_event(uid="uid_a", utid="utid", username=upn, refresh_token=refresh_token)
410+
cache = populated_cache(account)
411+
412+
async def send(request, **kwargs):
413+
# Mock a token request that fails with invalid_grant (401/400 error)
414+
if "refresh_token" in (request.data or {}):
415+
return mock_response(
416+
status_code=400, json_payload={"error": "invalid_grant", "error_description": "Refresh token expired"}
417+
)
418+
return get_discovery_response("https://localhost/tenant")
419+
420+
transport = AsyncMockTransport(send=send)
421+
credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username=upn)
422+
423+
# Set within_dac context to True
424+
within_dac.set(True)
425+
try:
426+
# When within DAC context, should raise CredentialUnavailableError instead of ClientAuthenticationError
427+
with pytest.raises(CredentialUnavailableError):
428+
await getattr(credential, get_token_method)(scope)
429+
finally:
430+
within_dac.set(False)
431+
432+
393433
@pytest.mark.asyncio
394434
@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS)
395435
async def test_two_accounts_no_username_or_tenant(get_token_method):

0 commit comments

Comments
 (0)