Skip to content

docs: clarify DefaultAzureCredential behavior when managed_identity_client_id=None #45897

@georgekosmidis

Description

@georgekosmidis

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

Labels

Azure.Identitycustomer-reportedIssues that are reported by GitHub users external to the Azure organization.needs-team-attentionWorkflow: This issue needs attention from Azure service team or SDK teamquestionThe issue doesn't require a change to the product in order to be resolved. Most issues start as that

Type

No type

Projects

Status

Untriaged

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions