-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
The current wording ("Defaults to the value of the environment variable AZURE_CLIENT_ID, if any. If not specified,
a system-assigned identity will be used.") is ambiguous. Users migrating from ManagedIdentityCredential often
write code like:
client_id = config.get("managed_identity_client_id") # May be None
credential = DefaultAzureCredential(managed_identity_client_id=client_id)This silently breaks the AZURE_CLIENT_ID fallback because kwargs.pop returns None (the explicit value)
instead of falling back to os.environ.get(EnvironmentVariables.AZURE_CLIENT_ID).
Related: #36365
File 1: Source docstring (generates API reference)
File: sdk/identity/azure-identity/azure/identity/_credentials/default.py
Current text (lines ~110-111):
:keyword str managed_identity_client_id: The client ID of a user-assigned managed identity. Defaults to the value
of the environment variable AZURE_CLIENT_ID, if any. If not specified, a system-assigned identity will be used.Replace with:
:keyword str managed_identity_client_id: The client ID of a user-assigned managed identity. Defaults to the value
of the environment variable AZURE_CLIENT_ID, if any. If not specified, a system-assigned identity will be used.
.. note::
The AZURE_CLIENT_ID environment variable fallback only applies when this parameter is **not passed at all**
(i.e., omitted from the constructor call). Explicitly passing ``managed_identity_client_id=None`` will
**not** fall back to AZURE_CLIENT_ID and will instead behave as if no client ID was provided, using a
system-assigned managed identity. If you are passing a variable that may be ``None``, use the following
pattern to preserve the environment variable fallback::
credential = DefaultAzureCredential(
**({"managed_identity_client_id": client_id} if client_id else {})
)This also affects the async version - verify and apply the same change to:
sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py
(The async DefaultAzureCredential inherits from the sync version, so the docstring may already be shared. Verify before duplicating.)
File 2: Conceptual guide (credential chains)
Page: https://learn.microsoft.com/azure/developer/python/sdk/authentication/credential-chains
Repo: This page is sourced from a Microsoft Docs repo (likely MicrosoftDocs/azure-dev-docs).
The source file path would be something like:
articles/python/sdk/authentication/credential-chains.md
Where to add: In the "Usage guidance for DefaultAzureCredential" section, under "Unpredictable behavior", add a new bullet or a new subsection.
Proposed addition (after the "Unpredictable behavior" bullet):
> [!IMPORTANT]
> **Migration pitfall with `managed_identity_client_id=None`:** When migrating from
> `ManagedIdentityCredential(client_id=client_id)` to
> `DefaultAzureCredential(managed_identity_client_id=client_id)`, be aware that these behave
> differently when `client_id` is `None`. With `ManagedIdentityCredential`, `client_id=None`
> simply means "use system-assigned identity." With `DefaultAzureCredential`, the
> `managed_identity_client_id` parameter defaults to `AZURE_CLIENT_ID` only when the parameter
> is **omitted entirely** - explicitly passing `None` bypasses this fallback. If your
> `client_id` variable might be `None`, either omit the parameter or use a conditional:
>
> ```python
> # Safe pattern when client_id might be None:
> kwargs = {}
> if client_id is not None:
> kwargs["managed_identity_client_id"] = client_id
> credential = DefaultAzureCredential(**kwargs)
> ```File 3: SDK README (optional, lower priority)
File: sdk/identity/azure-identity/README.md
Where: In the DefaultAzureCredential section, after the existing usage examples.
Proposed addition:
> **Note:** The `managed_identity_client_id` parameter defaults to the `AZURE_CLIENT_ID`
> environment variable only when the parameter is omitted. Explicitly passing `None` does
> not trigger this fallback. See the
> [API reference](https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential)
> for details.Proof of the behavior
Source code evidence
default.py constructor (link):
managed_identity_client_id = kwargs.pop(
"managed_identity_client_id", os.environ.get(EnvironmentVariables.AZURE_CLIENT_ID)
)Python's dict.pop(key, default) returns the default only when key is absent.
When key is present with value None, it returns None.
Reproduction
import os
os.environ["AZURE_CLIENT_ID"] = "my-user-assigned-identity-id"
# Scenario 1: parameter omitted -> env var IS used
kwargs1 = {}
result1 = kwargs1.pop("managed_identity_client_id", os.environ.get("AZURE_CLIENT_ID"))
print(f"Omitted: {result1}")
# Output: "my-user-assigned-identity-id"
# Scenario 2: parameter explicitly None -> env var NOT used
kwargs2 = {"managed_identity_client_id": None}
result2 = kwargs2.pop("managed_identity_client_id", os.environ.get("AZURE_CLIENT_ID"))
print(f"Explicit None: {result2}")
# Output: None (env var skipped!)Real-world failure (from issue #36365)
When deployed to a VM with multiple user-assigned managed identities and AZURE_CLIENT_ID set,
DefaultAzureCredential(managed_identity_client_id=None) results in:
ManagedIdentityCredential authentication unavailable. No identity has been assigned to this resource.
Error: Unexpected response "{'error': 'invalid_request', 'error_description':
'Multiple user assigned identities exist, please specify the clientId / resourceId
of the identity in the token request'}"
Metadata
Metadata
Assignees
Labels
Type
Projects
Status