Skip to content

[Identity] Update AZURE_TOKEN_CREDENTIALS to allow specific creds #41709

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

Merged
merged 4 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion sdk/identity/azure-identity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Release History

## 1.23.1 (Unreleased)
## 1.24.0b1 (Unreleased)

### Features Added

- Expanded the set of acceptable values for environment variable `AZURE_TOKEN_CREDENTIALS` to allow for selection of a specific credential in the `DefaultAzureCredential` chain. At runtime, only the specified credential will be used when acquiring tokens with `DefaultAzureCredential`. For example, setting `AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential` will make `DefaultAzureCredential` use only `WorkloadIdentityCredential`.
- Valid values are `EnvironmentCredential`, `WorkloadIdentityCredential`, `ManagedIdentityCredential`, `AzureCliCredential`, `AzurePowershellCredential`, `AzureDeveloperCliCredential`, and `InteractiveBrowserCredential`. ([#41709](https://github.com/Azure/azure-sdk-for-python/pull/41709))

### Breaking Changes

### Bugs Fixed
Expand Down
25 changes: 25 additions & 0 deletions sdk/identity/azure-identity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,31 @@ variables:

Configuration is attempted in the preceding order. For example, if values for a client secret and certificate are both present, the client secret is used.

### Configuring DefaultAzureCredential

`DefaultAzureCredential` supports an additional environment variable to customize its behavior:

|Variable name|Value
|-|-
|`AZURE_TOKEN_CREDENTIALS`|Credential name or group

#### AZURE_TOKEN_CREDENTIALS values

You can set `AZURE_TOKEN_CREDENTIALS` to one of the following values to control which credentials `DefaultAzureCredential` attempts to use:

**Individual Credentials:**
- `EnvironmentCredential` - Only use environment variables for service principal authentication
- `WorkloadIdentityCredential` - Only use workload identity for Kubernetes authentication
- `ManagedIdentityCredential` - Only use managed identity authentication
- `AzureCliCredential` - Only use Azure CLI authentication
- `AzurePowerShellCredential` - Only use Azure PowerShell authentication
- `AzureDeveloperCliCredential` - Only use Azure Developer CLI authentication
- `InteractiveBrowserCredential` - Only use interactive browser authentication

**Credential Groups:**
- `prod` - Use deployed service credentials: `EnvironmentCredential`, `WorkloadIdentityCredential`, and `ManagedIdentityCredential`
- `dev` - Use developer credentials: `SharedTokenCacheCredential`, `AzureCliCredential`, `AzurePowerShellCredential`, and `AzureDeveloperCliCredential`

## Continuous Access Evaluation

As of version 1.14.0, accessing resources protected by [Continuous Access Evaluation (CAE)][cae] is possible on a per-request basis. This behavior can be enabled by setting the `enable_cae` keyword argument to `True` in the credential's `get_token` method. CAE isn't supported for developer and managed identity credentials.
Expand Down
100 changes: 69 additions & 31 deletions sdk/identity/azure-identity/azure/identity/_credentials/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions, SupportsTokenInfo, TokenCredential
from .._constants import EnvironmentVariables
from .._internal import get_default_authority, normalize_authority, within_dac
from .._internal import get_default_authority, normalize_authority, within_dac, process_credential_exclusions
from .azure_powershell import AzurePowerShellCredential
from .browser import InteractiveBrowserCredential
from .chained import ChainedTokenCredential
Expand Down Expand Up @@ -133,36 +133,74 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement

process_timeout = kwargs.pop("process_timeout", 10)

token_credentials_env = os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS, "").strip().lower()
exclude_workload_identity_credential = kwargs.pop("exclude_workload_identity_credential", False)
exclude_environment_credential = kwargs.pop("exclude_environment_credential", False)
exclude_managed_identity_credential = kwargs.pop("exclude_managed_identity_credential", False)
exclude_shared_token_cache_credential = kwargs.pop("exclude_shared_token_cache_credential", False)
exclude_visual_studio_code_credential = kwargs.pop("exclude_visual_studio_code_credential", True)
exclude_developer_cli_credential = kwargs.pop("exclude_developer_cli_credential", False)
exclude_cli_credential = kwargs.pop("exclude_cli_credential", False)
exclude_interactive_browser_credential = kwargs.pop("exclude_interactive_browser_credential", True)
exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False)

if token_credentials_env == "dev":
# In dev mode, use only developer credentials
exclude_environment_credential = True
exclude_managed_identity_credential = True
exclude_workload_identity_credential = True
elif token_credentials_env == "prod":
# In prod mode, use only production credentials
exclude_shared_token_cache_credential = True
exclude_visual_studio_code_credential = True
exclude_cli_credential = True
exclude_developer_cli_credential = True
exclude_powershell_credential = True
exclude_interactive_browser_credential = True
elif token_credentials_env != "":
# If the environment variable is set to something other than dev or prod, raise an error
raise ValueError(
f"Invalid value for {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS}: {token_credentials_env}. "
"Valid values are 'dev' or 'prod'."
)
# Define credential configuration mapping
credential_config = {
"environment": {
"exclude_param": "exclude_environment_credential",
"env_name": "environmentcredential",
"default_exclude": False,
},
"workload_identity": {
"exclude_param": "exclude_workload_identity_credential",
"env_name": "workloadidentitycredential",
"default_exclude": False,
},
"managed_identity": {
"exclude_param": "exclude_managed_identity_credential",
"env_name": "managedidentitycredential",
"default_exclude": False,
},
"shared_token_cache": {
"exclude_param": "exclude_shared_token_cache_credential",
"default_exclude": False,
},
"visual_studio_code": {
"exclude_param": "exclude_visual_studio_code_credential",
"default_exclude": True,
},
"cli": {
"exclude_param": "exclude_cli_credential",
"env_name": "azureclicredential",
"default_exclude": False,
},
"developer_cli": {
"exclude_param": "exclude_developer_cli_credential",
"env_name": "azuredeveloperclicredential",
"default_exclude": False,
},
"powershell": {
"exclude_param": "exclude_powershell_credential",
"env_name": "azurepowershellcredential",
"default_exclude": False,
},
"interactive_browser": {
"exclude_param": "exclude_interactive_browser_credential",
"env_name": "interactivebrowsercredential",
"default_exclude": True,
},
}

# Extract user-provided exclude flags and set defaults
exclude_flags = {}
user_excludes = {}
for cred_key, config in credential_config.items():
param_name = cast(str, config["exclude_param"])
user_excludes[cred_key] = kwargs.pop(param_name, None)
exclude_flags[cred_key] = config["default_exclude"]

# Process AZURE_TOKEN_CREDENTIALS environment variable and apply user overrides
exclude_flags = process_credential_exclusions(credential_config, exclude_flags, user_excludes)

# Extract individual exclude flags for backward compatibility
exclude_environment_credential = exclude_flags["environment"]
exclude_workload_identity_credential = exclude_flags["workload_identity"]
exclude_managed_identity_credential = exclude_flags["managed_identity"]
exclude_shared_token_cache_credential = exclude_flags["shared_token_cache"]
exclude_visual_studio_code_credential = exclude_flags["visual_studio_code"]
exclude_cli_credential = exclude_flags["cli"]
exclude_developer_cli_credential = exclude_flags["developer_cli"]
exclude_powershell_credential = exclude_flags["powershell"]
exclude_interactive_browser_credential = exclude_flags["interactive_browser"]

credentials: List[SupportsTokenInfo] = []
within_dac.set(True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .utils import (
get_default_authority,
normalize_authority,
process_credential_exclusions,
resolve_tenant,
validate_scope,
validate_subscription,
Expand Down Expand Up @@ -48,6 +49,7 @@ def _scopes_to_resource(*scopes) -> str:
"get_default_authority",
"InteractiveCredential",
"normalize_authority",
"process_credential_exclusions",
"resolve_tenant",
"validate_scope",
"validate_subscription",
Expand Down
62 changes: 62 additions & 0 deletions sdk/identity/azure-identity/azure/identity/_internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,65 @@ def resolve_tenant(
'when creating the credential, or add "*" to additionally_allowed_tenants to allow '
"acquiring tokens for any tenant.".format(tenant_id)
)


def process_credential_exclusions(credential_config: dict, exclude_flags: dict, user_excludes: dict) -> dict:
"""Process credential exclusions based on environment variable and user overrides.

This method handles the AZURE_TOKEN_CREDENTIALS environment variable to determine
which credentials should be excluded from the credential chain, and then applies
any user-provided exclude overrides which take precedence over environment settings.

:param credential_config: Configuration mapping for all available credentials, containing
exclude parameter names, environment names, and default exclude settings
:type credential_config: dict
:param exclude_flags: Dictionary of exclude flags for each credential (will be modified)
:type exclude_flags: dict
:param user_excludes: User-provided exclude overrides from constructor kwargs
:type user_excludes: dict

:return: Dictionary of final exclude flags for each credential
:rtype: dict

:raises ValueError: If token_credentials_env contains an invalid credential name
"""
# Handle AZURE_TOKEN_CREDENTIALS environment variable
token_credentials_env = os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS, "").strip().lower()

if token_credentials_env == "dev":
# In dev mode, use only developer credentials
dev_credentials = {"cli", "developer_cli", "powershell", "shared_token_cache"}
for cred_key in credential_config:
exclude_flags[cred_key] = cred_key not in dev_credentials
elif token_credentials_env == "prod":
# In prod mode, use only production credentials
prod_credentials = {"environment", "workload_identity", "managed_identity"}
for cred_key in credential_config:
exclude_flags[cred_key] = cred_key not in prod_credentials
elif token_credentials_env:
# If a specific credential is specified, exclude all others except the specified one
valid_credentials = {config["env_name"] for config in credential_config.values() if "env_name" in config}

if token_credentials_env not in valid_credentials:
valid_values = ["dev", "prod"] + sorted(valid_credentials)
raise ValueError(
f"Invalid value for {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS}: {token_credentials_env}. "
f"Valid values are: {', '.join(valid_values)}."
)

# Find which credential was selected and exclude all others
selected_cred_key = None
for cred_key, config in credential_config.items():
if config.get("env_name") == token_credentials_env:
selected_cred_key = cred_key
break

for cred_key in credential_config:
exclude_flags[cred_key] = cred_key != selected_cred_key

# Apply user-provided exclude flags (these override environment variable settings)
for cred_key, user_value in user_excludes.items():
if user_value is not None:
exclude_flags[cred_key] = user_value

return exclude_flags
2 changes: 1 addition & 1 deletion sdk/identity/azure-identity/azure/identity/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
VERSION = "1.23.1"
VERSION = "1.24.0b1"
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions
from azure.core.credentials_async import AsyncTokenCredential, AsyncSupportsTokenInfo
from ..._constants import EnvironmentVariables
from ..._internal import get_default_authority, normalize_authority, within_dac
from ..._internal import get_default_authority, normalize_authority, within_dac, process_credential_exclusions
from .azure_cli import AzureCliCredential
from .azd_cli import AzureDeveloperCliCredential
from .azure_powershell import AzurePowerShellCredential
Expand Down Expand Up @@ -89,7 +89,7 @@ class DefaultAzureCredential(ChainedTokenCredential):
:caption: Create a DefaultAzureCredential.
"""

def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statements
def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statements, too-many-locals
if "tenant_id" in kwargs:
raise TypeError("'tenant_id' is not supported in DefaultAzureCredential.")

Expand Down Expand Up @@ -125,34 +125,68 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement

process_timeout = kwargs.pop("process_timeout", 10)

token_credentials_env = os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS, "").strip().lower()
exclude_workload_identity_credential = kwargs.pop("exclude_workload_identity_credential", False)
exclude_visual_studio_code_credential = kwargs.pop("exclude_visual_studio_code_credential", True)
exclude_developer_cli_credential = kwargs.pop("exclude_developer_cli_credential", False)
exclude_cli_credential = kwargs.pop("exclude_cli_credential", False)
exclude_environment_credential = kwargs.pop("exclude_environment_credential", False)
exclude_managed_identity_credential = kwargs.pop("exclude_managed_identity_credential", False)
exclude_shared_token_cache_credential = kwargs.pop("exclude_shared_token_cache_credential", False)
exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False)

if token_credentials_env == "dev":
# In dev mode, use only developer credentials
exclude_environment_credential = True
exclude_managed_identity_credential = True
exclude_workload_identity_credential = True
elif token_credentials_env == "prod":
# In prod mode, use only production credentials
exclude_shared_token_cache_credential = True
exclude_visual_studio_code_credential = True
exclude_cli_credential = True
exclude_developer_cli_credential = True
exclude_powershell_credential = True
elif token_credentials_env != "":
# If the environment variable is set to something other than dev or prod, raise an error
raise ValueError(
f"Invalid value for {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS}: {token_credentials_env}. "
"Valid values are 'dev' or 'prod'."
)
# Define credential configuration mapping (async version)
credential_config = {
"environment": {
"exclude_param": "exclude_environment_credential",
"env_name": "environmentcredential",
"default_exclude": False,
},
"workload_identity": {
"exclude_param": "exclude_workload_identity_credential",
"env_name": "workloadidentitycredential",
"default_exclude": False,
},
"managed_identity": {
"exclude_param": "exclude_managed_identity_credential",
"env_name": "managedidentitycredential",
"default_exclude": False,
},
"shared_token_cache": {
"exclude_param": "exclude_shared_token_cache_credential",
"default_exclude": False,
},
"visual_studio_code": {
"exclude_param": "exclude_visual_studio_code_credential",
"default_exclude": True,
},
"cli": {
"exclude_param": "exclude_cli_credential",
"env_name": "azureclicredential",
"default_exclude": False,
},
"developer_cli": {
"exclude_param": "exclude_developer_cli_credential",
"env_name": "azuredeveloperclicredential",
"default_exclude": False,
},
"powershell": {
"exclude_param": "exclude_powershell_credential",
"env_name": "azurepowershellcredential",
"default_exclude": False,
},
}

# Extract user-provided exclude flags and set defaults
exclude_flags = {}
user_excludes = {}
for cred_key, config in credential_config.items():
param_name = cast(str, config["exclude_param"])
user_excludes[cred_key] = kwargs.pop(param_name, None)
exclude_flags[cred_key] = config["default_exclude"]

# Process AZURE_TOKEN_CREDENTIALS environment variable and apply user overrides
exclude_flags = process_credential_exclusions(credential_config, exclude_flags, user_excludes)

# Extract individual exclude flags for backward compatibility
exclude_environment_credential = exclude_flags["environment"]
exclude_workload_identity_credential = exclude_flags["workload_identity"]
exclude_managed_identity_credential = exclude_flags["managed_identity"]
exclude_shared_token_cache_credential = exclude_flags["shared_token_cache"]
exclude_visual_studio_code_credential = exclude_flags["visual_studio_code"]
exclude_cli_credential = exclude_flags["cli"]
exclude_developer_cli_credential = exclude_flags["developer_cli"]
exclude_powershell_credential = exclude_flags["powershell"]

credentials: List[AsyncSupportsTokenInfo] = []
within_dac.set(True)
Expand Down
2 changes: 1 addition & 1 deletion sdk/identity/azure-identity/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/identity/azure-identity",
keywords="azure, azure sdk",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3",
Expand Down
Loading