From a0105bbea2012ad2e75b825d66393fa344d90f9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:18:42 +0000 Subject: [PATCH 01/11] Initial plan From 1541bfd31810c19bdd18d7808f5e0623e1552a1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:32:25 +0000 Subject: [PATCH 02/11] Implement claims challenge error for AzureCliCredential get_token and get_token_info methods Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- .../azure/identity/_credentials/azure_cli.py | 16 +++-- .../identity/aio/_credentials/azure_cli.py | 17 ++++-- .../tests/test_cli_credential.py | 53 ++++++++++++++++ .../tests/test_cli_credential_async.py | 61 +++++++++++++++++++ 4 files changed, 139 insertions(+), 8 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 0c05f9ba95e9..aa467607d7ed 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -90,7 +90,7 @@ def close(self) -> None: def get_token( self, *scopes: str, - claims: Optional[str] = None, # pylint:disable=unused-argument + claims: Optional[str] = None, tenant_id: Optional[str] = None, **kwargs: Any, ) -> AccessToken: @@ -102,16 +102,19 @@ def get_token( :param str scopes: desired scope for the access token. This credential allows only one scope per request. For more information about scopes, see https://learn.microsoft.com/entra/identity-platform/scopes-oidc. - :keyword str claims: not used by this credential; any value provided will be ignored. + :keyword str claims: additional claims required in the token. This credential does not support claims challenges. :keyword str tenant_id: optional tenant to include in the token request. :return: An access token with the desired scopes. :rtype: ~azure.core.credentials.AccessToken - :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI. + :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, + or when claims challenge is provided. :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ + if claims: + raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") options: TokenRequestOptions = {} if tenant_id: @@ -136,10 +139,15 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = :rtype: ~azure.core.credentials.AccessTokenInfo :return: An AccessTokenInfo instance containing information about the token. - :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI. + :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, + or when claims challenge is provided. :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ + if options and options.get("claims"): + claims = options["claims"] + raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + return self._get_token_base(*scopes, options=options) def _get_token_base( diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 90fa6af8442c..7efda6176773 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -82,7 +82,7 @@ def __init__( async def get_token( self, *scopes: str, - claims: Optional[str] = None, # pylint:disable=unused-argument + claims: Optional[str] = None, tenant_id: Optional[str] = None, **kwargs: Any, ) -> AccessToken: @@ -94,15 +94,19 @@ async def get_token( :param str scopes: desired scope for the access token. This credential allows only one scope per request. For more information about scopes, see https://learn.microsoft.com/entra/identity-platform/scopes-oidc. - :keyword str claims: not used by this credential; any value provided will be ignored. + :keyword str claims: additional claims required in the token. This credential does not support claims challenges. :keyword str tenant_id: optional tenant to include in the token request. :return: An access token with the desired scopes. :rtype: ~azure.core.credentials.AccessToken - :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI. + :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, + or when claims challenge is provided. :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ + if claims: + raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): return _SyncAzureCliCredential().get_token(*scopes, tenant_id=tenant_id, **kwargs) @@ -130,10 +134,15 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio :rtype: ~azure.core.credentials.AccessTokenInfo :return: An AccessTokenInfo instance containing information about the token. - :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI. + :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, + or when claims challenge is provided. :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ + if options and options.get("claims"): + claims = options["claims"] + raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): return _SyncAzureCliCredential().get_token_info(*scopes, options=options) diff --git a/sdk/identity/azure-identity/tests/test_cli_credential.py b/sdk/identity/azure-identity/tests/test_cli_credential.py index 039e7be199e0..dcbe97cec2c8 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential.py @@ -395,3 +395,56 @@ def fake_check_output(command_line, **_): kwargs = {"options": kwargs} token = getattr(credential, get_token_method)("scope", **kwargs) assert token.token == expected_token + + +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +def test_claims_challenge_raises_error(get_token_method): + """The credential should raise CredentialUnavailableError when claims challenge is provided""" + + claims = "test-claims-challenge" + expected_message = f"Fail to get token, please run az login --claims-challenge {claims}" + + if get_token_method == "get_token": + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + AzureCliCredential().get_token("scope", claims=claims) + else: # get_token_info + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + AzureCliCredential().get_token_info("scope", options={"claims": claims}) + + +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +def test_empty_claims_does_not_raise_error(get_token_method): + """The credential should not raise error when claims parameter is empty or None""" + + # Mock the CLI to avoid actual invocation + with mock.patch("shutil.which", return_value="az"): + with mock.patch(CHECK_OUTPUT, mock.Mock(return_value='{"accessToken": "token", "expiresOn": "2021-10-07 12:00:00.000000"}')): + + if get_token_method == "get_token": + # Test with None (default) + token = AzureCliCredential().get_token("scope") + assert token.token == "token" + + # Test with empty string + token = AzureCliCredential().get_token("scope", claims="") + assert token.token == "token" + + # Test with None explicitly + token = AzureCliCredential().get_token("scope", claims=None) + assert token.token == "token" + else: # get_token_info + # Test with None options + token = AzureCliCredential().get_token_info("scope") + assert token.token == "token" + + # Test with empty options + token = AzureCliCredential().get_token_info("scope", options={}) + assert token.token == "token" + + # Test with None claims in options + token = AzureCliCredential().get_token_info("scope", options={"claims": None}) + assert token.token == "token" + + # Test with empty string claims in options + token = AzureCliCredential().get_token_info("scope", options={"claims": ""}) + assert token.token == "token" diff --git a/sdk/identity/azure-identity/tests/test_cli_credential_async.py b/sdk/identity/azure-identity/tests/test_cli_credential_async.py index b5b727c6a238..aaee6aa80aeb 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential_async.py @@ -389,3 +389,64 @@ async def fake_exec(*args, **_): kwargs = {"options": kwargs} token = await getattr(credential, get_token_method)("scope", **kwargs) assert token.token == expected_token + + +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +async def test_claims_challenge_raises_error(get_token_method): + """The credential should raise CredentialUnavailableError when claims challenge is provided""" + + claims = "test-claims-challenge" + expected_message = f"Fail to get token, please run az login --claims-challenge {claims}" + + if get_token_method == "get_token": + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + await AzureCliCredential().get_token("scope", claims=claims) + else: # get_token_info + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + await AzureCliCredential().get_token_info("scope", options={"claims": claims}) + + +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +async def test_empty_claims_does_not_raise_error(get_token_method): + """The credential should not raise error when claims parameter is empty or None""" + + successful_output = json.dumps({ + "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), + "accessToken": "access-token", + "subscription": "subscription", + "tenant": "tenant", + "tokenType": "Bearer", + }) + + # Mock the CLI to avoid actual invocation + with mock.patch("shutil.which", return_value="az"): + with mock.patch(SUBPROCESS_EXEC, mock_exec(successful_output)): + + if get_token_method == "get_token": + # Test with None (default) + token = await AzureCliCredential().get_token("scope") + assert token.token == "access-token" + + # Test with empty string + token = await AzureCliCredential().get_token("scope", claims="") + assert token.token == "access-token" + + # Test with None explicitly + token = await AzureCliCredential().get_token("scope", claims=None) + assert token.token == "access-token" + else: # get_token_info + # Test with None options + token = await AzureCliCredential().get_token_info("scope") + assert token.token == "access-token" + + # Test with empty options + token = await AzureCliCredential().get_token_info("scope", options={}) + assert token.token == "access-token" + + # Test with None claims in options + token = await AzureCliCredential().get_token_info("scope", options={"claims": None}) + assert token.token == "access-token" + + # Test with empty string claims in options + token = await AzureCliCredential().get_token_info("scope", options={"claims": ""}) + assert token.token == "access-token" From 37b031ee7b324048caa0b78f2b1a8ec0e4b54b2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:38:03 +0000 Subject: [PATCH 03/11] Refine claims challenge handling to ignore whitespace-only claims and add comprehensive tests Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- .../azure/identity/_credentials/azure_cli.py | 4 ++-- .../azure/identity/aio/_credentials/azure_cli.py | 4 ++-- sdk/identity/azure-identity/tests/test_cli_credential.py | 8 ++++++++ .../azure-identity/tests/test_cli_credential_async.py | 8 ++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index aa467607d7ed..4c44ad4ec66b 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -113,7 +113,7 @@ def get_token( :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - if claims: + if claims and claims.strip(): raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") options: TokenRequestOptions = {} @@ -144,7 +144,7 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - if options and options.get("claims"): + if options and options.get("claims") and options.get("claims").strip(): claims = options["claims"] raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 7efda6176773..5279f934c3f4 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -104,7 +104,7 @@ async def get_token( :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - if claims: + if claims and claims.strip(): raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) @@ -139,7 +139,7 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - if options and options.get("claims"): + if options and options.get("claims") and options.get("claims").strip(): claims = options["claims"] raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") diff --git a/sdk/identity/azure-identity/tests/test_cli_credential.py b/sdk/identity/azure-identity/tests/test_cli_credential.py index dcbe97cec2c8..6619eb5a533f 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential.py @@ -432,6 +432,10 @@ def test_empty_claims_does_not_raise_error(get_token_method): # Test with None explicitly token = AzureCliCredential().get_token("scope", claims=None) assert token.token == "token" + + # Test with whitespace-only string + token = AzureCliCredential().get_token("scope", claims=" ") + assert token.token == "token" else: # get_token_info # Test with None options token = AzureCliCredential().get_token_info("scope") @@ -448,3 +452,7 @@ def test_empty_claims_does_not_raise_error(get_token_method): # Test with empty string claims in options token = AzureCliCredential().get_token_info("scope", options={"claims": ""}) assert token.token == "token" + + # Test with whitespace-only claims in options + token = AzureCliCredential().get_token_info("scope", options={"claims": " "}) + assert token.token == "token" diff --git a/sdk/identity/azure-identity/tests/test_cli_credential_async.py b/sdk/identity/azure-identity/tests/test_cli_credential_async.py index aaee6aa80aeb..2fd0f707185c 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential_async.py @@ -434,6 +434,10 @@ async def test_empty_claims_does_not_raise_error(get_token_method): # Test with None explicitly token = await AzureCliCredential().get_token("scope", claims=None) assert token.token == "access-token" + + # Test with whitespace-only string + token = await AzureCliCredential().get_token("scope", claims=" ") + assert token.token == "access-token" else: # get_token_info # Test with None options token = await AzureCliCredential().get_token_info("scope") @@ -450,3 +454,7 @@ async def test_empty_claims_does_not_raise_error(get_token_method): # Test with empty string claims in options token = await AzureCliCredential().get_token_info("scope", options={"claims": ""}) assert token.token == "access-token" + + # Test with whitespace-only claims in options + token = await AzureCliCredential().get_token_info("scope", options={"claims": " "}) + assert token.token == "access-token" From eb3eb892bb298aa8910405dbdde82090bbbf3c1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:20:47 +0000 Subject: [PATCH 04/11] Fix MyPy errors in AzureCliCredential claims challenge handling Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- .../azure-identity/azure/identity/_credentials/azure_cli.py | 6 +++--- .../azure/identity/aio/_credentials/azure_cli.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 4c44ad4ec66b..5ebda712c3b2 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -144,9 +144,9 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - if options and options.get("claims") and options.get("claims").strip(): - claims = options["claims"] - raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + claims_value = options.get("claims") if options else None + if claims_value and claims_value.strip(): + raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims_value}") return self._get_token_base(*scopes, options=options) diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 5279f934c3f4..e106dbdbf7ec 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -139,9 +139,9 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - if options and options.get("claims") and options.get("claims").strip(): - claims = options["claims"] - raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + claims_value = options.get("claims") if options else None + if claims_value and claims_value.strip(): + raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims_value}") # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): From 1ced477638ea1cd522d8d61d954395c115f6e973 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:50:13 +0000 Subject: [PATCH 05/11] Fix pylint line-too-long and trailing-whitespace errors in AzureCliCredential Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- .../azure/identity/_credentials/azure_cli.py | 8 +++++--- .../azure/identity/aio/_credentials/azure_cli.py | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 5ebda712c3b2..33338935b83e 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -102,7 +102,8 @@ def get_token( :param str scopes: desired scope for the access token. This credential allows only one scope per request. For more information about scopes, see https://learn.microsoft.com/entra/identity-platform/scopes-oidc. - :keyword str claims: additional claims required in the token. This credential does not support claims challenges. + :keyword str claims: additional claims required in the token. This credential does not support claims + challenges. :keyword str tenant_id: optional tenant to include in the token request. :return: An access token with the desired scopes. @@ -146,8 +147,9 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = """ claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): - raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims_value}") - + raise CredentialUnavailableError( + f"Fail to get token, please run az login --claims-challenge {claims_value}" + ) return self._get_token_base(*scopes, options=options) def _get_token_base( diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index e106dbdbf7ec..4f2d6183a59d 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -94,7 +94,8 @@ async def get_token( :param str scopes: desired scope for the access token. This credential allows only one scope per request. For more information about scopes, see https://learn.microsoft.com/entra/identity-platform/scopes-oidc. - :keyword str claims: additional claims required in the token. This credential does not support claims challenges. + :keyword str claims: additional claims required in the token. This credential does not support claims + challenges. :keyword str tenant_id: optional tenant to include in the token request. :return: An access token with the desired scopes. @@ -106,7 +107,7 @@ async def get_token( """ if claims and claims.strip(): raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") - + # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): return _SyncAzureCliCredential().get_token(*scopes, tenant_id=tenant_id, **kwargs) @@ -141,8 +142,9 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio """ claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): - raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims_value}") - + raise CredentialUnavailableError( + f"Fail to get token, please run az login --claims-challenge {claims_value}" + ) # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): return _SyncAzureCliCredential().get_token_info(*scopes, options=options) From 3f45fb21c77b8e1a85ac37d06b04b49bbc8366d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 23 Jul 2025 23:18:44 +0000 Subject: [PATCH 06/11] Apply black formatting to AzureCliCredential files Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- .../azure/identity/_credentials/azure_cli.py | 27 ++++++++++++---- .../identity/aio/_credentials/azure_cli.py | 32 ++++++++++++++----- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 33338935b83e..73c34ad9e6c8 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -115,7 +115,9 @@ def get_token( receive an access token. """ if claims and claims.strip(): - raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + raise CredentialUnavailableError( + f"Fail to get token, please run az login --claims-challenge {claims}" + ) options: TokenRequestOptions = {} if tenant_id: @@ -125,7 +127,9 @@ def get_token( return AccessToken(token_info.token, token_info.expires_on) @log_get_token - def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = None) -> AccessTokenInfo: + def get_token_info( + self, *scopes: str, options: Optional[TokenRequestOptions] = None + ) -> AccessTokenInfo: """Request an access token for `scopes`. This is an alternative to `get_token` to enable certain scenarios that require additional properties @@ -226,7 +230,9 @@ def get_safe_working_dir() -> str: if sys.platform.startswith("win"): path = os.environ.get("SYSTEMROOT") if not path: - raise CredentialUnavailableError(message="Environment variable 'SYSTEMROOT' has no value") + raise CredentialUnavailableError( + message="Environment variable 'SYSTEMROOT' has no value" + ) return path return "/bin" @@ -246,7 +252,9 @@ def _run_command(command_args: List[str], timeout: int) -> str: # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway. if sys.platform.startswith("win"): # On Windows, the expected executable is az.cmd, so we check for that first, falling back to 'az' in case. - az_path = shutil.which(EXECUTABLE_NAME + ".cmd") or shutil.which(EXECUTABLE_NAME) + az_path = shutil.which(EXECUTABLE_NAME + ".cmd") or shutil.which( + EXECUTABLE_NAME + ) else: az_path = shutil.which(EXECUTABLE_NAME) if not az_path: @@ -269,10 +277,13 @@ def _run_command(command_args: List[str], timeout: int) -> str: except subprocess.CalledProcessError as ex: # non-zero return from shell # Fallback check in case the executable is not found while executing subprocess. - if ex.returncode == 127 or (ex.stderr is not None and ex.stderr.startswith("'az' is not recognized")): + if ex.returncode == 127 or ( + ex.stderr is not None and ex.stderr.startswith("'az' is not recognized") + ): raise CredentialUnavailableError(message=CLI_NOT_FOUND) from ex if ex.stderr is not None and ( - ("az login" in ex.stderr or "az account set" in ex.stderr) and "AADSTS" not in ex.stderr + ("az login" in ex.stderr or "az account set" in ex.stderr) + and "AADSTS" not in ex.stderr ): raise CredentialUnavailableError(message=NOT_LOGGED_IN) from ex @@ -286,7 +297,9 @@ def _run_command(command_args: List[str], timeout: int) -> str: raise ClientAuthenticationError(message=message) from ex except OSError as ex: # failed to execute 'cmd' or '/bin/sh' - error = CredentialUnavailableError(message="Failed to execute '{}'".format(args[0])) + error = CredentialUnavailableError( + message="Failed to execute '{}'".format(args[0]) + ) raise error from ex except Exception as ex: # could be a timeout, for example diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 4f2d6183a59d..43e60dfb7d75 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -106,11 +106,17 @@ async def get_token( receive an access token. """ if claims and claims.strip(): - raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + raise CredentialUnavailableError( + f"Fail to get token, please run az login --claims-challenge {claims}" + ) # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) - if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): - return _SyncAzureCliCredential().get_token(*scopes, tenant_id=tenant_id, **kwargs) + if sys.platform.startswith("win") and not isinstance( + asyncio.get_event_loop(), asyncio.ProactorEventLoop + ): + return _SyncAzureCliCredential().get_token( + *scopes, tenant_id=tenant_id, **kwargs + ) options: TokenRequestOptions = {} if tenant_id: @@ -120,7 +126,9 @@ async def get_token( return AccessToken(token_info.token, token_info.expires_on) @log_get_token_async - async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = None) -> AccessTokenInfo: + async def get_token_info( + self, *scopes: str, options: Optional[TokenRequestOptions] = None + ) -> AccessTokenInfo: """Request an access token for `scopes`. This is an alternative to `get_token` to enable certain scenarios that require additional properties @@ -146,7 +154,9 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio f"Fail to get token, please run az login --claims-challenge {claims_value}" ) # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) - if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): + if sys.platform.startswith("win") and not isinstance( + asyncio.get_event_loop(), asyncio.ProactorEventLoop + ): return _SyncAzureCliCredential().get_token_info(*scopes, options=options) return await self._get_token_base(*scopes, options=options) @@ -197,7 +207,9 @@ async def _run_command(command_args: List[str], timeout: int) -> str: # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway. if sys.platform.startswith("win"): # On Windows, the expected executable is az.cmd, so we check for that first, falling back to 'az' in case. - az_path = shutil.which(EXECUTABLE_NAME + ".cmd") or shutil.which(EXECUTABLE_NAME) + az_path = shutil.which(EXECUTABLE_NAME + ".cmd") or shutil.which( + EXECUTABLE_NAME + ) else: az_path = shutil.which(EXECUTABLE_NAME) if not az_path: @@ -220,10 +232,14 @@ async def _run_command(command_args: List[str], timeout: int) -> str: stderr = stderr_b.decode() except asyncio.TimeoutError as ex: proc.kill() - raise CredentialUnavailableError(message="Timed out waiting for Azure CLI") from ex + raise CredentialUnavailableError( + message="Timed out waiting for Azure CLI" + ) from ex except OSError as ex: # failed to execute 'cmd' or '/bin/sh' - error = CredentialUnavailableError(message="Failed to execute '{}'".format(args[0])) + error = CredentialUnavailableError( + message="Failed to execute '{}'".format(args[0]) + ) raise error from ex if proc.returncode == 0: From ae796fc888c781ca28d243d819d5d0f3a2d1ca8a Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Wed, 23 Jul 2025 16:57:33 -0700 Subject: [PATCH 07/11] black --- .../azure/identity/_credentials/azure_cli.py | 27 ++++-------- .../identity/aio/_credentials/azure_cli.py | 32 ++++---------- .../tests/test_cli_credential.py | 28 +++++++------ .../tests/test_cli_credential_async.py | 42 ++++++++++--------- 4 files changed, 52 insertions(+), 77 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 73c34ad9e6c8..33338935b83e 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -115,9 +115,7 @@ def get_token( receive an access token. """ if claims and claims.strip(): - raise CredentialUnavailableError( - f"Fail to get token, please run az login --claims-challenge {claims}" - ) + raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") options: TokenRequestOptions = {} if tenant_id: @@ -127,9 +125,7 @@ def get_token( return AccessToken(token_info.token, token_info.expires_on) @log_get_token - def get_token_info( - self, *scopes: str, options: Optional[TokenRequestOptions] = None - ) -> AccessTokenInfo: + def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = None) -> AccessTokenInfo: """Request an access token for `scopes`. This is an alternative to `get_token` to enable certain scenarios that require additional properties @@ -230,9 +226,7 @@ def get_safe_working_dir() -> str: if sys.platform.startswith("win"): path = os.environ.get("SYSTEMROOT") if not path: - raise CredentialUnavailableError( - message="Environment variable 'SYSTEMROOT' has no value" - ) + raise CredentialUnavailableError(message="Environment variable 'SYSTEMROOT' has no value") return path return "/bin" @@ -252,9 +246,7 @@ def _run_command(command_args: List[str], timeout: int) -> str: # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway. if sys.platform.startswith("win"): # On Windows, the expected executable is az.cmd, so we check for that first, falling back to 'az' in case. - az_path = shutil.which(EXECUTABLE_NAME + ".cmd") or shutil.which( - EXECUTABLE_NAME - ) + az_path = shutil.which(EXECUTABLE_NAME + ".cmd") or shutil.which(EXECUTABLE_NAME) else: az_path = shutil.which(EXECUTABLE_NAME) if not az_path: @@ -277,13 +269,10 @@ def _run_command(command_args: List[str], timeout: int) -> str: except subprocess.CalledProcessError as ex: # non-zero return from shell # Fallback check in case the executable is not found while executing subprocess. - if ex.returncode == 127 or ( - ex.stderr is not None and ex.stderr.startswith("'az' is not recognized") - ): + if ex.returncode == 127 or (ex.stderr is not None and ex.stderr.startswith("'az' is not recognized")): raise CredentialUnavailableError(message=CLI_NOT_FOUND) from ex if ex.stderr is not None and ( - ("az login" in ex.stderr or "az account set" in ex.stderr) - and "AADSTS" not in ex.stderr + ("az login" in ex.stderr or "az account set" in ex.stderr) and "AADSTS" not in ex.stderr ): raise CredentialUnavailableError(message=NOT_LOGGED_IN) from ex @@ -297,9 +286,7 @@ def _run_command(command_args: List[str], timeout: int) -> str: raise ClientAuthenticationError(message=message) from ex except OSError as ex: # failed to execute 'cmd' or '/bin/sh' - error = CredentialUnavailableError( - message="Failed to execute '{}'".format(args[0]) - ) + error = CredentialUnavailableError(message="Failed to execute '{}'".format(args[0])) raise error from ex except Exception as ex: # could be a timeout, for example diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 43e60dfb7d75..4f2d6183a59d 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -106,17 +106,11 @@ async def get_token( receive an access token. """ if claims and claims.strip(): - raise CredentialUnavailableError( - f"Fail to get token, please run az login --claims-challenge {claims}" - ) + raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) - if sys.platform.startswith("win") and not isinstance( - asyncio.get_event_loop(), asyncio.ProactorEventLoop - ): - return _SyncAzureCliCredential().get_token( - *scopes, tenant_id=tenant_id, **kwargs - ) + if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): + return _SyncAzureCliCredential().get_token(*scopes, tenant_id=tenant_id, **kwargs) options: TokenRequestOptions = {} if tenant_id: @@ -126,9 +120,7 @@ async def get_token( return AccessToken(token_info.token, token_info.expires_on) @log_get_token_async - async def get_token_info( - self, *scopes: str, options: Optional[TokenRequestOptions] = None - ) -> AccessTokenInfo: + async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = None) -> AccessTokenInfo: """Request an access token for `scopes`. This is an alternative to `get_token` to enable certain scenarios that require additional properties @@ -154,9 +146,7 @@ async def get_token_info( f"Fail to get token, please run az login --claims-challenge {claims_value}" ) # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) - if sys.platform.startswith("win") and not isinstance( - asyncio.get_event_loop(), asyncio.ProactorEventLoop - ): + if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): return _SyncAzureCliCredential().get_token_info(*scopes, options=options) return await self._get_token_base(*scopes, options=options) @@ -207,9 +197,7 @@ async def _run_command(command_args: List[str], timeout: int) -> str: # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway. if sys.platform.startswith("win"): # On Windows, the expected executable is az.cmd, so we check for that first, falling back to 'az' in case. - az_path = shutil.which(EXECUTABLE_NAME + ".cmd") or shutil.which( - EXECUTABLE_NAME - ) + az_path = shutil.which(EXECUTABLE_NAME + ".cmd") or shutil.which(EXECUTABLE_NAME) else: az_path = shutil.which(EXECUTABLE_NAME) if not az_path: @@ -232,14 +220,10 @@ async def _run_command(command_args: List[str], timeout: int) -> str: stderr = stderr_b.decode() except asyncio.TimeoutError as ex: proc.kill() - raise CredentialUnavailableError( - message="Timed out waiting for Azure CLI" - ) from ex + raise CredentialUnavailableError(message="Timed out waiting for Azure CLI") from ex except OSError as ex: # failed to execute 'cmd' or '/bin/sh' - error = CredentialUnavailableError( - message="Failed to execute '{}'".format(args[0]) - ) + error = CredentialUnavailableError(message="Failed to execute '{}'".format(args[0])) raise error from ex if proc.returncode == 0: diff --git a/sdk/identity/azure-identity/tests/test_cli_credential.py b/sdk/identity/azure-identity/tests/test_cli_credential.py index 6619eb5a533f..719f3fc3b05b 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential.py @@ -400,10 +400,10 @@ def fake_check_output(command_line, **_): @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) def test_claims_challenge_raises_error(get_token_method): """The credential should raise CredentialUnavailableError when claims challenge is provided""" - + claims = "test-claims-challenge" expected_message = f"Fail to get token, please run az login --claims-challenge {claims}" - + if get_token_method == "get_token": with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): AzureCliCredential().get_token("scope", claims=claims) @@ -415,24 +415,26 @@ def test_claims_challenge_raises_error(get_token_method): @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) def test_empty_claims_does_not_raise_error(get_token_method): """The credential should not raise error when claims parameter is empty or None""" - + # Mock the CLI to avoid actual invocation with mock.patch("shutil.which", return_value="az"): - with mock.patch(CHECK_OUTPUT, mock.Mock(return_value='{"accessToken": "token", "expiresOn": "2021-10-07 12:00:00.000000"}')): - + with mock.patch( + CHECK_OUTPUT, mock.Mock(return_value='{"accessToken": "token", "expiresOn": "2021-10-07 12:00:00.000000"}') + ): + if get_token_method == "get_token": # Test with None (default) token = AzureCliCredential().get_token("scope") assert token.token == "token" - + # Test with empty string token = AzureCliCredential().get_token("scope", claims="") assert token.token == "token" - + # Test with None explicitly token = AzureCliCredential().get_token("scope", claims=None) assert token.token == "token" - + # Test with whitespace-only string token = AzureCliCredential().get_token("scope", claims=" ") assert token.token == "token" @@ -440,19 +442,19 @@ def test_empty_claims_does_not_raise_error(get_token_method): # Test with None options token = AzureCliCredential().get_token_info("scope") assert token.token == "token" - + # Test with empty options token = AzureCliCredential().get_token_info("scope", options={}) assert token.token == "token" - + # Test with None claims in options token = AzureCliCredential().get_token_info("scope", options={"claims": None}) assert token.token == "token" - - # Test with empty string claims in options + + # Test with empty string claims in options token = AzureCliCredential().get_token_info("scope", options={"claims": ""}) assert token.token == "token" - + # Test with whitespace-only claims in options token = AzureCliCredential().get_token_info("scope", options={"claims": " "}) assert token.token == "token" diff --git a/sdk/identity/azure-identity/tests/test_cli_credential_async.py b/sdk/identity/azure-identity/tests/test_cli_credential_async.py index 2fd0f707185c..4881c61b5f6b 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential_async.py @@ -394,10 +394,10 @@ async def fake_exec(*args, **_): @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) async def test_claims_challenge_raises_error(get_token_method): """The credential should raise CredentialUnavailableError when claims challenge is provided""" - + claims = "test-claims-challenge" expected_message = f"Fail to get token, please run az login --claims-challenge {claims}" - + if get_token_method == "get_token": with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): await AzureCliCredential().get_token("scope", claims=claims) @@ -409,32 +409,34 @@ async def test_claims_challenge_raises_error(get_token_method): @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) async def test_empty_claims_does_not_raise_error(get_token_method): """The credential should not raise error when claims parameter is empty or None""" - - successful_output = json.dumps({ - "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), - "accessToken": "access-token", - "subscription": "subscription", - "tenant": "tenant", - "tokenType": "Bearer", - }) - + + successful_output = json.dumps( + { + "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), + "accessToken": "access-token", + "subscription": "subscription", + "tenant": "tenant", + "tokenType": "Bearer", + } + ) + # Mock the CLI to avoid actual invocation with mock.patch("shutil.which", return_value="az"): with mock.patch(SUBPROCESS_EXEC, mock_exec(successful_output)): - + if get_token_method == "get_token": # Test with None (default) token = await AzureCliCredential().get_token("scope") assert token.token == "access-token" - + # Test with empty string token = await AzureCliCredential().get_token("scope", claims="") assert token.token == "access-token" - + # Test with None explicitly token = await AzureCliCredential().get_token("scope", claims=None) assert token.token == "access-token" - + # Test with whitespace-only string token = await AzureCliCredential().get_token("scope", claims=" ") assert token.token == "access-token" @@ -442,19 +444,19 @@ async def test_empty_claims_does_not_raise_error(get_token_method): # Test with None options token = await AzureCliCredential().get_token_info("scope") assert token.token == "access-token" - + # Test with empty options token = await AzureCliCredential().get_token_info("scope", options={}) assert token.token == "access-token" - + # Test with None claims in options token = await AzureCliCredential().get_token_info("scope", options={"claims": None}) assert token.token == "access-token" - - # Test with empty string claims in options + + # Test with empty string claims in options token = await AzureCliCredential().get_token_info("scope", options={"claims": ""}) assert token.token == "access-token" - + # Test with whitespace-only claims in options token = await AzureCliCredential().get_token_info("scope", options={"claims": " "}) assert token.token == "access-token" From e778f33848f75a871c2de4a0850cea3c86d1c60a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:21:44 +0000 Subject: [PATCH 08/11] Update error message format and docstrings for claims challenge handling - Change error message from "Fail to get token, please run" to "Failed to get token. Run" - Update docstrings to clarify error conditions as requested in code review - Update test expectations to match new error message format Co-authored-by: scottaddie <10702007+scottaddie@users.noreply.github.com> --- .../azure/identity/_credentials/azure_cli.py | 12 ++++++------ .../azure/identity/aio/_credentials/azure_cli.py | 12 ++++++------ .../azure-identity/tests/test_cli_credential.py | 2 +- .../tests/test_cli_credential_async.py | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 33338935b83e..9588906dfcb0 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -109,13 +109,13 @@ def get_token( :return: An access token with the desired scopes. :rtype: ~azure.core.credentials.AccessToken - :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, - or when claims challenge is provided. + :raises ~azure.identity.CredentialUnavailableError: the credential was either unable to invoke the Azure CLI + or a claims challenge was provided. :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ if claims and claims.strip(): - raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims}") options: TokenRequestOptions = {} if tenant_id: @@ -140,15 +140,15 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = :rtype: ~azure.core.credentials.AccessTokenInfo :return: An AccessTokenInfo instance containing information about the token. - :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, - or when claims challenge is provided. + :raises ~azure.identity.CredentialUnavailableError: the credential was either unable to invoke the Azure CLI + or a claims challenge was provided. :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): raise CredentialUnavailableError( - f"Fail to get token, please run az login --claims-challenge {claims_value}" + f"Failed to get token. Run az login --claims-challenge {claims_value}" ) return self._get_token_base(*scopes, options=options) diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 4f2d6183a59d..cffcb6f944ce 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -100,13 +100,13 @@ async def get_token( :return: An access token with the desired scopes. :rtype: ~azure.core.credentials.AccessToken - :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, - or when claims challenge is provided. + :raises ~azure.identity.CredentialUnavailableError: the credential was either unable to invoke the Azure CLI + or a claims challenge was provided. :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ if claims and claims.strip(): - raise CredentialUnavailableError(f"Fail to get token, please run az login --claims-challenge {claims}") + raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims}") # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): @@ -135,15 +135,15 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio :rtype: ~azure.core.credentials.AccessTokenInfo :return: An AccessTokenInfo instance containing information about the token. - :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, - or when claims challenge is provided. + :raises ~azure.identity.CredentialUnavailableError: the credential was either unable to invoke the Azure CLI + or a claims challenge was provided. :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): raise CredentialUnavailableError( - f"Fail to get token, please run az login --claims-challenge {claims_value}" + f"Failed to get token. Run az login --claims-challenge {claims_value}" ) # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): diff --git a/sdk/identity/azure-identity/tests/test_cli_credential.py b/sdk/identity/azure-identity/tests/test_cli_credential.py index 719f3fc3b05b..5a6eb751aeb6 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential.py @@ -402,7 +402,7 @@ def test_claims_challenge_raises_error(get_token_method): """The credential should raise CredentialUnavailableError when claims challenge is provided""" claims = "test-claims-challenge" - expected_message = f"Fail to get token, please run az login --claims-challenge {claims}" + expected_message = f"Failed to get token. Run az login --claims-challenge {claims}" if get_token_method == "get_token": with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): diff --git a/sdk/identity/azure-identity/tests/test_cli_credential_async.py b/sdk/identity/azure-identity/tests/test_cli_credential_async.py index 4881c61b5f6b..46475001c84f 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential_async.py @@ -396,7 +396,7 @@ async def test_claims_challenge_raises_error(get_token_method): """The credential should raise CredentialUnavailableError when claims challenge is provided""" claims = "test-claims-challenge" - expected_message = f"Fail to get token, please run az login --claims-challenge {claims}" + expected_message = f"Failed to get token. Run az login --claims-challenge {claims}" if get_token_method == "get_token": with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): From a9bd2a7480bd02dd2eb956b192c74f40c692d10b Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Thu, 24 Jul 2025 08:55:41 -0700 Subject: [PATCH 09/11] black --- .../azure-identity/azure/identity/_credentials/azure_cli.py | 4 +--- .../azure/identity/aio/_credentials/azure_cli.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 9588906dfcb0..265a6cf3fe16 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -147,9 +147,7 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = """ claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): - raise CredentialUnavailableError( - f"Failed to get token. Run az login --claims-challenge {claims_value}" - ) + raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims_value}") return self._get_token_base(*scopes, options=options) def _get_token_base( diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index cffcb6f944ce..5558ac321810 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -142,9 +142,7 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio """ claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): - raise CredentialUnavailableError( - f"Failed to get token. Run az login --claims-challenge {claims_value}" - ) + raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims_value}") # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): return _SyncAzureCliCredential().get_token_info(*scopes, options=options) From 2af955e34e4363c2e9d7cd19062571c4d681a9a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:37:19 +0000 Subject: [PATCH 10/11] Include scopes in az login command for claims challenge error messages Co-authored-by: weikanglim <2322434+weikanglim@users.noreply.github.com> --- .../azure/identity/_credentials/azure_cli.py | 10 ++++++++-- .../identity/aio/_credentials/azure_cli.py | 10 ++++++++-- .../azure-identity/tests/test_cli_credential.py | 17 ++++++++++++++++- .../tests/test_cli_credential_async.py | 17 ++++++++++++++++- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 265a6cf3fe16..8a4643857593 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -115,7 +115,10 @@ def get_token( receive an access token. """ if claims and claims.strip(): - raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims}") + login_cmd = f"az login --claims-challenge {claims}" + if scopes: + login_cmd += f" --scope {scopes[0]}" + raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") options: TokenRequestOptions = {} if tenant_id: @@ -147,7 +150,10 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = """ claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): - raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims_value}") + login_cmd = f"az login --claims-challenge {claims_value}" + if scopes: + login_cmd += f" --scope {scopes[0]}" + raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") return self._get_token_base(*scopes, options=options) def _get_token_base( diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 5558ac321810..553040fb069f 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -106,7 +106,10 @@ async def get_token( receive an access token. """ if claims and claims.strip(): - raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims}") + login_cmd = f"az login --claims-challenge {claims}" + if scopes: + login_cmd += f" --scope {scopes[0]}" + raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): @@ -142,7 +145,10 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio """ claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): - raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims_value}") + login_cmd = f"az login --claims-challenge {claims_value}" + if scopes: + login_cmd += f" --scope {scopes[0]}" + raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): return _SyncAzureCliCredential().get_token_info(*scopes, options=options) diff --git a/sdk/identity/azure-identity/tests/test_cli_credential.py b/sdk/identity/azure-identity/tests/test_cli_credential.py index 5a6eb751aeb6..4499c173a3f5 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential.py @@ -402,7 +402,7 @@ def test_claims_challenge_raises_error(get_token_method): """The credential should raise CredentialUnavailableError when claims challenge is provided""" claims = "test-claims-challenge" - expected_message = f"Failed to get token. Run az login --claims-challenge {claims}" + expected_message = f"Failed to get token. Run az login --claims-challenge {claims} --scope scope" if get_token_method == "get_token": with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): @@ -412,6 +412,21 @@ def test_claims_challenge_raises_error(get_token_method): AzureCliCredential().get_token_info("scope", options={"claims": claims}) +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +def test_claims_challenge_without_scopes(get_token_method): + """The credential should raise CredentialUnavailableError with appropriate message even when no scopes provided""" + + claims = "test-claims-challenge" + expected_message = f"Failed to get token. Run az login --claims-challenge {claims}" + + if get_token_method == "get_token": + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + AzureCliCredential().get_token(claims=claims) # No scopes provided + else: # get_token_info + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + AzureCliCredential().get_token_info(options={"claims": claims}) # No scopes provided + + @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) def test_empty_claims_does_not_raise_error(get_token_method): """The credential should not raise error when claims parameter is empty or None""" diff --git a/sdk/identity/azure-identity/tests/test_cli_credential_async.py b/sdk/identity/azure-identity/tests/test_cli_credential_async.py index 46475001c84f..3f40382f6121 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential_async.py @@ -396,7 +396,7 @@ async def test_claims_challenge_raises_error(get_token_method): """The credential should raise CredentialUnavailableError when claims challenge is provided""" claims = "test-claims-challenge" - expected_message = f"Failed to get token. Run az login --claims-challenge {claims}" + expected_message = f"Failed to get token. Run az login --claims-challenge {claims} --scope scope" if get_token_method == "get_token": with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): @@ -406,6 +406,21 @@ async def test_claims_challenge_raises_error(get_token_method): await AzureCliCredential().get_token_info("scope", options={"claims": claims}) +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +async def test_claims_challenge_without_scopes(get_token_method): + """The credential should raise CredentialUnavailableError with appropriate message even when no scopes provided""" + + claims = "test-claims-challenge" + expected_message = f"Failed to get token. Run az login --claims-challenge {claims}" + + if get_token_method == "get_token": + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + await AzureCliCredential().get_token(claims=claims) # No scopes provided + else: # get_token_info + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + await AzureCliCredential().get_token_info(options={"claims": claims}) # No scopes provided + + @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) async def test_empty_claims_does_not_raise_error(get_token_method): """The credential should not raise error when claims parameter is empty or None""" From 30832b6b09535a98333c1a818aebbd2b85438836 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:05:32 +0000 Subject: [PATCH 11/11] Refactor claims challenge logic to _get_token_base and add tenant support - Move claims challenge logic from get_token and get_token_info methods to _get_token_base method to eliminate code duplication - Pass claims through TokenRequestOptions instead of checking directly in individual methods - Add tenant_id support in error messages when provided in options - Update error message format to include tenant: "az login --claims-challenge {claims} [--tenant {tenant}] [--scope {scope}]" - Add comprehensive tests for tenant functionality in both sync and async versions - Maintains backward compatibility while providing more complete actionable commands Co-authored-by: pvaneck <1868861+pvaneck@users.noreply.github.com> --- .../azure/identity/_credentials/azure_cli.py | 28 ++++++++------ .../identity/aio/_credentials/azure_cli.py | 32 +++++++++------- .../tests/test_cli_credential.py | 35 ++++++++++++++++++ .../tests/test_cli_credential_async.py | 37 +++++++++++++++++++ 4 files changed, 108 insertions(+), 24 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index 8a4643857593..715ef07fef8f 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -114,15 +114,11 @@ def get_token( :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - if claims and claims.strip(): - login_cmd = f"az login --claims-challenge {claims}" - if scopes: - login_cmd += f" --scope {scopes[0]}" - raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") - options: TokenRequestOptions = {} if tenant_id: options["tenant_id"] = tenant_id + if claims: + options["claims"] = claims token_info = self._get_token_base(*scopes, options=options, **kwargs) return AccessToken(token_info.token, token_info.expires_on) @@ -148,17 +144,27 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ + return self._get_token_base(*scopes, options=options) + + def _get_token_base( + self, *scopes: str, options: Optional[TokenRequestOptions] = None, **kwargs: Any + ) -> AccessTokenInfo: + + # Check for claims challenge first claims_value = options.get("claims") if options else None if claims_value and claims_value.strip(): login_cmd = f"az login --claims-challenge {claims_value}" + + # Add tenant if provided in options + tenant_id_from_options = options.get("tenant_id") if options else None + if tenant_id_from_options: + login_cmd += f" --tenant {tenant_id_from_options}" + + # Add scope if provided if scopes: login_cmd += f" --scope {scopes[0]}" + raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") - return self._get_token_base(*scopes, options=options) - - def _get_token_base( - self, *scopes: str, options: Optional[TokenRequestOptions] = None, **kwargs: Any - ) -> AccessTokenInfo: tenant_id = options.get("tenant_id") if options else None if tenant_id: diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 553040fb069f..0b08cb85a879 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -105,19 +105,15 @@ async def get_token( :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - if claims and claims.strip(): - login_cmd = f"az login --claims-challenge {claims}" - if scopes: - login_cmd += f" --scope {scopes[0]}" - raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") - # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): - return _SyncAzureCliCredential().get_token(*scopes, tenant_id=tenant_id, **kwargs) + return _SyncAzureCliCredential().get_token(*scopes, claims=claims, tenant_id=tenant_id, **kwargs) options: TokenRequestOptions = {} if tenant_id: options["tenant_id"] = tenant_id + if claims: + options["claims"] = claims token_info = await self._get_token_base(*scopes, options=options, **kwargs) return AccessToken(token_info.token, token_info.expires_on) @@ -143,12 +139,6 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't receive an access token. """ - claims_value = options.get("claims") if options else None - if claims_value and claims_value.strip(): - login_cmd = f"az login --claims-challenge {claims_value}" - if scopes: - login_cmd += f" --scope {scopes[0]}" - raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") # only ProactorEventLoop supports subprocesses on Windows (and it isn't the default loop on Python < 3.8) if sys.platform.startswith("win") and not isinstance(asyncio.get_event_loop(), asyncio.ProactorEventLoop): return _SyncAzureCliCredential().get_token_info(*scopes, options=options) @@ -157,6 +147,22 @@ async def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptio async def _get_token_base( self, *scopes: str, options: Optional[TokenRequestOptions] = None, **kwargs: Any ) -> AccessTokenInfo: + # Check for claims challenge first + claims_value = options.get("claims") if options else None + if claims_value and claims_value.strip(): + login_cmd = f"az login --claims-challenge {claims_value}" + + # Add tenant if provided in options + tenant_id_from_options = options.get("tenant_id") if options else None + if tenant_id_from_options: + login_cmd += f" --tenant {tenant_id_from_options}" + + # Add scope if provided + if scopes: + login_cmd += f" --scope {scopes[0]}" + + raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") + tenant_id = options.get("tenant_id") if options else None if tenant_id: validate_tenant_id(tenant_id) diff --git a/sdk/identity/azure-identity/tests/test_cli_credential.py b/sdk/identity/azure-identity/tests/test_cli_credential.py index 4499c173a3f5..4a566a408e01 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential.py @@ -473,3 +473,38 @@ def test_empty_claims_does_not_raise_error(get_token_method): # Test with whitespace-only claims in options token = AzureCliCredential().get_token_info("scope", options={"claims": " "}) assert token.token == "token" + + +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +def test_claims_challenge_with_tenant(get_token_method): + """The credential should include tenant in the error message when claims and tenant are provided""" + + claims = "test-claims-challenge" + tenant_id = "test-tenant-id" + + if get_token_method == "get_token": + # Test with get_token - tenant passed via get_token parameter (should appear in options) + expected_message = f"Failed to get token. Run az login --claims-challenge {claims} --tenant {tenant_id} --scope scope" + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + AzureCliCredential().get_token("scope", claims=claims, tenant_id=tenant_id) + else: # get_token_info + # Test with get_token_info - tenant passed via options + expected_message = f"Failed to get token. Run az login --claims-challenge {claims} --tenant {tenant_id} --scope scope" + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + AzureCliCredential().get_token_info("scope", options={"claims": claims, "tenant_id": tenant_id}) + + +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +def test_claims_challenge_with_tenant_without_scopes(get_token_method): + """The credential should include tenant in error message when claims and tenant are provided but no scopes""" + + claims = "test-claims-challenge" + tenant_id = "test-tenant-id" + expected_message = f"Failed to get token. Run az login --claims-challenge {claims} --tenant {tenant_id}" + + if get_token_method == "get_token": + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + AzureCliCredential().get_token(claims=claims, tenant_id=tenant_id) # No scopes provided + else: # get_token_info + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + AzureCliCredential().get_token_info(options={"claims": claims, "tenant_id": tenant_id}) # No scopes provided diff --git a/sdk/identity/azure-identity/tests/test_cli_credential_async.py b/sdk/identity/azure-identity/tests/test_cli_credential_async.py index 3f40382f6121..b41a936e7593 100644 --- a/sdk/identity/azure-identity/tests/test_cli_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_cli_credential_async.py @@ -475,3 +475,40 @@ async def test_empty_claims_does_not_raise_error(get_token_method): # Test with whitespace-only claims in options token = await AzureCliCredential().get_token_info("scope", options={"claims": " "}) assert token.token == "access-token" + + +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +@pytest.mark.asyncio +async def test_claims_challenge_with_tenant(get_token_method): + """The credential should include tenant in the error message when claims and tenant are provided""" + + claims = "test-claims-challenge" + tenant_id = "test-tenant-id" + + if get_token_method == "get_token": + # Test with get_token - tenant passed via get_token parameter (should appear in options) + expected_message = f"Failed to get token. Run az login --claims-challenge {claims} --tenant {tenant_id} --scope scope" + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + await AzureCliCredential().get_token("scope", claims=claims, tenant_id=tenant_id) + else: # get_token_info + # Test with get_token_info - tenant passed via options + expected_message = f"Failed to get token. Run az login --claims-challenge {claims} --tenant {tenant_id} --scope scope" + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + await AzureCliCredential().get_token_info("scope", options={"claims": claims, "tenant_id": tenant_id}) + + +@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) +@pytest.mark.asyncio +async def test_claims_challenge_with_tenant_without_scopes(get_token_method): + """The credential should include tenant in error message when claims and tenant are provided but no scopes""" + + claims = "test-claims-challenge" + tenant_id = "test-tenant-id" + expected_message = f"Failed to get token. Run az login --claims-challenge {claims} --tenant {tenant_id}" + + if get_token_method == "get_token": + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + await AzureCliCredential().get_token(claims=claims, tenant_id=tenant_id) # No scopes provided + else: # get_token_info + with pytest.raises(CredentialUnavailableError, match=re.escape(expected_message)): + await AzureCliCredential().get_token_info(options={"claims": claims, "tenant_id": tenant_id}) # No scopes provided