-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Refactor AzureCliCredential claims challenge logic and add tenant support in error messages #42195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
… get_token_info methods Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com>
… add comprehensive tests Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com>
azure/identity/_credentials/azure_cli.py:147: error: Item "None" of "Optional[str]" has no attribute "strip" [union-attr] |
Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com>
Fixed the MyPy errors by properly handling the Optional[str] return type from |
2025-07-23T22:39:07.1483541Z azure/identity/_credentials/azure_cli.py:105: [C0301(line-too-long), ] Line too long (121/120) |
…edential Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com>
Fixed all pylint line-too-long and trailing-whitespace errors by breaking long lines appropriately and removing trailing spaces. Commit 1ced477. |
run black Command '['/opt/hostedtoolcache/Python/3.9.23/x64/bin/python', '-m', 'tox', 'run', '--root', '.', '-c', '/mnt/vss/_work/1/s/eng/tox/tox.ini', '-e', 'black', '--', '--cov=azure.identity', '--cov-append', '--ignore', 'azure_identity.egg-info']' returned non-zero exit status 1. |
Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com>
Applied black formatting to fix all code style issues. The files now comply with black formatting standards by properly breaking long lines and formatting function signatures. Commit 3f45fb2. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements claims challenge validation in AzureCliCredential to raise specific errors when claims parameters are provided. The Azure CLI credential doesn't support claims challenges, so this change ensures appropriate error handling with clear user guidance.
- Added claims validation logic to both sync and async AzureCliCredential methods
- Updated error handling to raise CredentialUnavailableError with specific guidance message
- Maintained backward compatibility by only validating non-empty, non-whitespace claims
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
File | Description |
---|---|
azure/identity/_credentials/azure_cli.py | Added claims validation in sync get_token and get_token_info methods |
azure/identity/aio/_credentials/azure_cli.py | Added claims validation in async get_token and get_token_info methods |
tests/test_cli_credential.py | Added comprehensive test coverage for sync claims validation scenarios |
tests/test_cli_credential_async.py | Added comprehensive test coverage for async claims validation scenarios |
# 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"}') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded datetime string '2021-10-07 12:00:00.000000' in the mock response should use a dynamic date or a more recent date to avoid potential issues with token expiration validation.
CHECK_OUTPUT, mock.Mock(return_value='{"accessToken": "token", "expiresOn": "2021-10-07 12:00:00.000000"}') | |
CHECK_OUTPUT, mock.Mock(return_value=json.dumps({"accessToken": "token", "expiresOn": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")})) |
Copilot uses AI. Check for mistakes.
|
||
successful_output = json.dumps( | ||
{ | ||
"expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using datetime.now() without importing datetime module from the datetime package. Should be 'from datetime import datetime' or use a fixed future date for consistency.
"expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), | |
"expiresOn": "2023-01-01 00:00:00.000000", |
Copilot uses AI. Check for mistakes.
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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the error message format as suggested. Commit e778f33.
: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}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the error message format as suggested. Commit e778f33.
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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the error message format as suggested. Commit e778f33.
"""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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the expected message format in test as suggested. Commit e778f33.
"""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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the expected message format in test as suggested. Commit e778f33.
:raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI, | ||
or when claims challenge is provided. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be clearer if this is rewritten as follows.
: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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the docstring wording to be clearer as suggested. Commit e778f33.
@@ -136,10 +140,16 @@ 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See rewrite suggestion above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the docstring wording to be clearer as suggested. Commit e778f33.
: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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See rewrite suggestion above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the docstring wording to be clearer as suggested. Commit e778f33.
@@ -130,10 +135,16 @@ 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See rewrite suggestion above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the docstring wording to be clearer as suggested. Commit e778f33.
- 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>
if claims and claims.strip(): | ||
raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't fully form the required login command, for example --scopes
could also be provided that should be included in the final login command.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the error message to include scopes in the suggested az login
command. Now when scopes are provided, the error message will suggest: az login --claims-challenge {claims} --scope {first_scope}
. When no scopes are provided, it will suggest: az login --claims-challenge {claims}
. This provides users with a more complete and actionable command. Commit 2af955e.
: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"Failed to get token. Run az login --claims-challenge {claims_value}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What should be the format of encoding here? Right now, the credential receives the claims
as a JSON string, and we end up passing this unquoted, which might not work for passing the argument through the shell environment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
User just needs to run something like
az login --claims-challenge eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlcyI6WyJwMSJdfX19
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation is correct. Azure claims challenges are typically JWT tokens that are base64-encoded (as shown in xiangyan99's example), which only contain alphanumeric characters plus +, /, and = - these don't require shell escaping or quotes. The format follows standard Azure CLI usage patterns.
Co-authored-by: weikanglim <2322434+weikanglim@users.noreply.github.com>
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addressesI tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py
Outdated
Show resolved
Hide resolved
sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py
Outdated
Show resolved
Hide resolved
…port - 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>
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addressesI tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
if scopes: | ||
login_cmd += f" --scope {scopes[0]}" | ||
|
||
raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot Extract the base error message into a constant that can be used in both azure_cli.py files, and make the error message more descriptive. Something like: "Claims challenge received, but not supported. To authenticate with the required claims, please run the following command: ". Then you can append the login_cmd to the message.
/azp run python - pullrequest |
Azure Pipelines successfully started running 1 pipeline(s). |
Closed and all merged into #42568 |
This PR refactors the claims challenge handling in AzureCliCredential to eliminate code duplication and enhances error messages to include tenant information for more complete and actionable
az login
commands.Key Changes
Refactored Architecture
get_token
andget_token_info
methods to the shared_get_token_base
methodTokenRequestOptions
for uniform handling across sync and async implementationsEnhanced Error Messages
The error messages now include tenant information when available, providing complete
az login
commands:Error Message Format
The enhanced format follows the pattern:
Benefits
Testing
Added comprehensive tests covering:
get_token
andget_token_info
methodsFixes #42194.
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.