Skip to content

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

Closed
wants to merge 11 commits into from

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Jul 23, 2025

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

  • Centralized Logic: Moved claims challenge logic from get_token and get_token_info methods to the shared _get_token_base method
  • Eliminated Duplication: Removed identical claims checking code that existed in both methods
  • Consistent Flow: Claims now pass through TokenRequestOptions for uniform handling across sync and async implementations

Enhanced Error Messages

The error messages now include tenant information when available, providing complete az login commands:

from azure.identity import AzureCliCredential, CredentialUnavailableError

credential = AzureCliCredential()

# Complete command with tenant and scope
try:
    credential.get_token_info("https://management.azure.com/.default", 
                             options={"claims": "example-claims", "tenant_id": "my-tenant"})
except CredentialUnavailableError as e:
    print(e)  # "Failed to get token. Run az login --claims-challenge example-claims --tenant my-tenant --scope https://management.azure.com/.default"

# Command with scope only
try:
    credential.get_token("https://management.azure.com/.default", claims="example-claims")
except CredentialUnavailableError as e:
    print(e)  # "Failed to get token. Run az login --claims-challenge example-claims --scope https://management.azure.com/.default"

# Basic command
try:
    credential.get_token(claims="example-claims")
except CredentialUnavailableError as e:
    print(e)  # "Failed to get token. Run az login --claims-challenge example-claims"

Error Message Format

The enhanced format follows the pattern:

az login --claims-challenge {claims} [--tenant {tenant_id}] [--scope {scope}]

Benefits

  • DRY Principle: Single implementation eliminates maintenance overhead
  • Complete Commands: Users receive actionable commands with all necessary parameters
  • Backward Compatibility: Existing behavior preserved for empty/whitespace claims
  • Consistent Experience: Identical logic across sync and async implementations

Testing

Added comprehensive tests covering:

  • Claims with tenant and scope combinations
  • Claims with tenant but no scope
  • Both get_token and get_token_info methods
  • Sync and async implementations

Fixes #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.

Copilot AI and others added 2 commits July 23, 2025 21:32
… 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>
@Copilot Copilot AI changed the title [WIP] Raise error "Fail to get token, please run az login --claims-challenge" in AzureCliCredential when get_token or get_token_info methods are called and claims challenge parameter is provided Raise error for claims challenge in AzureCliCredential get_token and get_token_info methods Jul 23, 2025
@Copilot Copilot AI requested a review from xiangyan99 July 23, 2025 21:40
Copilot finished work on behalf of xiangyan99 July 23, 2025 21:40
@xiangyan99
Copy link
Member

azure/identity/_credentials/azure_cli.py:147: error: Item "None" of "Optional[str]" has no attribute "strip" [union-attr]
azure/identity/aio/_credentials/azure_cli.py:142: error: Item "None" of "Optional[str]" has no attribute "strip" [union-attr]

Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jul 23, 2025

azure/identity/_credentials/azure_cli.py:147: error: Item "None" of "Optional[str]" has no attribute "strip" [union-attr]
azure/identity/aio/_credentials/azure_cli.py:142: error: Item "None" of "Opt...

Fixed the MyPy errors by properly handling the Optional[str] return type from options.get("claims"). Changed the problematic code from calling .strip() directly on the result to using a null-safe intermediate variable. The fix maintains exact same functionality while resolving the type checking issues. Commit eb3eb89.

Copilot finished work on behalf of xiangyan99 July 23, 2025 22:21
@xiangyan99
Copy link
Member

2025-07-23T22:39:07.1483541Z azure/identity/_credentials/azure_cli.py:105: [C0301(line-too-long), ] Line too long (121/120)
2025-07-23T22:39:07.1484255Z azure/identity/_credentials/azure_cli.py:149: [C0301(line-too-long), ] Line too long (121/120)
2025-07-23T22:39:07.1484818Z azure/identity/_credentials/azure_cli.py:150: [C0303(trailing-whitespace), ] Trailing whitespace
2025-07-23T22:39:07.1485330Z ************* Module azure.identity.aio._credentials.azure_cli
2025-07-23T22:39:07.1485844Z azure/identity/aio/_credentials/azure_cli.py:97: [C0301(line-too-long), ] Line too long (121/120)
2025-07-23T22:39:07.1486383Z azure/identity/aio/_credentials/azure_cli.py:109: [C0303(trailing-whitespace), ] Trailing whitespace
2025-07-23T22:39:07.1486908Z azure/identity/aio/_credentials/azure_cli.py:144: [C0301(line-too-long), ] Line too long (121/120)
2025-07-23T22:39:07.1487421Z azure/identity/aio/_credentials/azure_cli.py:145: [C0303(trailing-whitespace), ] Trailing whitespace

…edential

Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jul 23, 2025

2025-07-23T22:39:07.1483541Z azure/identity/_credentials/azure_cli.py:105: [C0301(line-too-long), ] Line too long (121/120)
2025-07-23T22:39:07.1484255Z azure/identity/_credentials/azure_cli.py:149: ...

Fixed all pylint line-too-long and trailing-whitespace errors by breaking long lines appropriately and removing trailing spaces. Commit 1ced477.

Copilot finished work on behalf of xiangyan99 July 23, 2025 22:51
@xiangyan99
Copy link
Member

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>
Copy link
Contributor Author

Copilot AI commented Jul 23, 2025

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', ...

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.

Copilot finished work on behalf of xiangyan99 July 23, 2025 23:19
@xiangyan99 xiangyan99 marked this pull request as ready for review July 24, 2025 00:26
@Copilot Copilot AI review requested due to automatic review settings July 24, 2025 00:26
@xiangyan99 xiangyan99 requested review from pvaneck and a team as code owners July 24, 2025 00:26
@xiangyan99 xiangyan99 requested a review from maorleger July 24, 2025 00:26
@xiangyan99 xiangyan99 requested a review from minhanh-phan July 24, 2025 00:26
Copy link
Contributor

@Copilot Copilot AI left a 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"}')
Copy link
Preview

Copilot AI Jul 24, 2025

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.

Suggested change
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"),
Copy link
Preview

Copilot AI Jul 24, 2025

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.

Suggested change
"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}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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}"

Copy link
Contributor Author

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}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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}")

Copy link
Contributor Author

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}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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}"

Copy link
Contributor Author

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}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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}"

Copy link
Contributor Author

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}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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}"

Copy link
Contributor Author

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.

Comment on lines 112 to 113
:raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI,
or when claims challenge is provided.
Copy link
Member

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.

Suggested change
: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.

Copy link
Contributor Author

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See rewrite suggestion above

Copy link
Contributor Author

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See rewrite suggestion above

Copy link
Contributor Author

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See rewrite suggestion above

Copy link
Contributor Author

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>
Copilot finished work on behalf of scottaddie July 24, 2025 14:24
@Copilot Copilot AI requested a review from scottaddie July 24, 2025 14:24
Comment on lines 117 to 118
if claims and claims.strip():
raise CredentialUnavailableError(f"Failed to get token. Run az login --claims-challenge {claims}")
Copy link
Contributor

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.

Copy link
Contributor Author

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}")
Copy link
Contributor

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.

Copy link
Member

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

Copy link
Contributor Author

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>
Copy link
Contributor Author

Copilot AI commented Jul 29, 2025

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 addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

  • app.aladdin.microsoft.com
    • Triggering command: /usr/bin/../../opt/az/bin/python3 -Im azure.cli login --claims-challenge (dns block)
  • dc.services.visualstudio.com
    • Triggering command: /opt/az/bin/python3 /opt/az/lib/python3.12/site-packages/azure/cli/telemetry/__init__.py /home/REDACTED/.azure /home/REDACTED/.azure/telemetry/20250729192632298 (dns block)
    • Triggering command: /opt/az/bin/python3 /opt/az/lib/python3.12/site-packages/azure/cli/telemetry/__init__.py /home/REDACTED/.azure /home/REDACTED/.azure/telemetry/20250729192643745 (dns block)
    • Triggering command: /opt/az/bin/python3 /opt/az/lib/python3.12/site-packages/azure/cli/telemetry/__init__.py /home/REDACTED/.azure /home/REDACTED/.azure/telemetry/20250729192655377 (dns block)
  • pypi.org
    • Triggering command: /home/REDACTED/work/azure-sdk-for-python/azure-sdk-for-python/.venv/bin/python /home/REDACTED/work/azure-sdk-for-python/azure-sdk-for-python/.venv/lib/python3.9/site-packages/pip/__pip-REDACTED__.py install --ignore-installed --no-user --prefix /tmp/pip-build-env-9gypziuv/overlay --no-warn-script-location --no-binary :none: --only-binary :none: -i REDACTED -- setuptools&gt;=40.8.0 (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Copilot Copilot AI changed the title Raise error for claims challenge in AzureCliCredential get_token and get_token_info methods Include scopes in az login command for claims challenge error messages in AzureCliCredential Jul 29, 2025
@Copilot Copilot AI requested a review from weikanglim July 29, 2025 19:38
Copilot finished work on behalf of weikanglim July 29, 2025 19:38
Copilot finished work on behalf of xiangyan99 July 29, 2025 19:46
…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>
Copy link
Contributor Author

Copilot AI commented Jul 29, 2025

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 addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

  • pypi.org
    • Triggering command: /home/REDACTED/work/azure-sdk-for-python/azure-sdk-for-python/.venv/bin/python /home/REDACTED/work/azure-sdk-for-python/azure-sdk-for-python/.venv/lib/python3.9/site-packages/pip/__pip-REDACTED__.py install --ignore-installed --no-user --prefix /tmp/pip-build-env-5unggf75/overlay --no-warn-script-location --no-binary :none: --only-binary :none: -i REDACTED -- setuptools&gt;=40.8.0 (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Copilot Copilot AI changed the title Include scopes in az login command for claims challenge error messages in AzureCliCredential Refactor AzureCliCredential claims challenge logic and add tenant support in error messages Jul 29, 2025
Copilot finished work on behalf of pvaneck July 29, 2025 23:07
@Copilot Copilot AI requested a review from pvaneck July 29, 2025 23:07
if scopes:
login_cmd += f" --scope {scopes[0]}"

raise CredentialUnavailableError(f"Failed to get token. Run {login_cmd}")
Copy link
Member

@pvaneck pvaneck Aug 15, 2025

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.

@xiangyan99
Copy link
Member

/azp run python - pullrequest

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@xiangyan99
Copy link
Member

Closed and all merged into #42568

@xiangyan99 xiangyan99 closed this Aug 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Raise error "Fail to get token, please run az login --claims-challenge" in AzureCliCredential when get_token or get_token_info methods are called and claims challenge parameter is provided
5 participants