Skip to content

workload_identity_client_id silently inherits from managed_identity_client_id instead of independently reading AZURE_CLIENT_ID #45900

@georgekosmidis

Description

@georgekosmidis
  • Package Name: azure-identity
  • Package Version: 1.25.1 (also present on main at commit b21e2fa)
  • Operating System: Any (bug is in SDK logic, not platform-specific)
  • Python Version: 3.9+

Describe the bug

In DefaultAzureCredential.__init__, the workload_identity_client_id parameter silently inherits its default value from the already-resolved managed_identity_client_id instead of independently reading the AZURE_CLIENT_ID environment variable. This contradicts the documentation and creates a hidden coupling between two unrelated credentials.

The documented behavior (API reference, docstring):

workload_identity_client_id (str) - The client ID of an identity assigned to the pod. Defaults to the value of the environment variable AZURE_CLIENT_ID, if any.

The actual code at default.py line 157:

managed_identity_client_id = kwargs.pop(
    "managed_identity_client_id", os.environ.get(EnvironmentVariables.AZURE_CLIENT_ID)
)
workload_identity_client_id = kwargs.pop("workload_identity_client_id", managed_identity_client_id)
#                                                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^
#                                              BUG: defaults to managed_identity_client_id
#                                              DOCS SAY: defaults to os.environ.get("AZURE_CLIENT_ID")

The default for workload_identity_client_id is managed_identity_client_id (the already-resolved variable), not os.environ.get(EnvironmentVariables.AZURE_CLIENT_ID) as the documentation states.

To Reproduce

  1. Set the AZURE_CLIENT_ID environment variable to a known value (e.g., "env-client-id")
  2. Create a DefaultAzureCredential with an explicit managed_identity_client_id but without specifying workload_identity_client_id:
import os
os.environ["AZURE_CLIENT_ID"] = "env-client-id"

from azure.identity import DefaultAzureCredential

# Pass a DIFFERENT managed_identity_client_id than what's in AZURE_CLIENT_ID
credential = DefaultAzureCredential(managed_identity_client_id="custom-mi-id")
  1. Observe that workload_identity_client_id internally resolves to "custom-mi-id" (inherited from managed_identity_client_id) instead of "env-client-id" (from AZURE_CLIENT_ID).

This can be verified by stepping through the constructor or by examining the WorkloadIdentityCredential that gets added to the chain - it will receive client_id="custom-mi-id" instead of client_id="env-client-id".

The issue is even worse when managed_identity_client_id=None is explicitly passed (see "Additional context" below):

# This makes BOTH credentials lose their AZURE_CLIENT_ID fallback
credential = DefaultAzureCredential(managed_identity_client_id=None)

# managed_identity_client_id = None  (kwargs.pop returns None, skips env var)
# workload_identity_client_id = None (inherited None, also skips env var)

Minimal standalone reproduction (no Azure environment needed):

import os

os.environ["AZURE_CLIENT_ID"] = "env-client-id"

# Simulates the exact kwargs.pop logic from DefaultAzureCredential.__init__

# --- Scenario 1: managed_identity_client_id explicitly set ---
kwargs = {"managed_identity_client_id": "custom-mi-id"}

managed_identity_client_id = kwargs.pop(
    "managed_identity_client_id", os.environ.get("AZURE_CLIENT_ID")
)
workload_identity_client_id = kwargs.pop(
    "workload_identity_client_id", managed_identity_client_id  # <-- current (buggy) code
)

print(f"managed_identity_client_id:  {managed_identity_client_id}")
# Output: "custom-mi-id" (correct)
print(f"workload_identity_client_id: {workload_identity_client_id}")
# Output: "custom-mi-id" (BUG - docs say this should be "env-client-id")

# --- Scenario 2: managed_identity_client_id=None cascades ---
kwargs2 = {"managed_identity_client_id": None}

managed_identity_client_id2 = kwargs2.pop(
    "managed_identity_client_id", os.environ.get("AZURE_CLIENT_ID")
)
workload_identity_client_id2 = kwargs2.pop(
    "workload_identity_client_id", managed_identity_client_id2
)

print(f"\nmanaged_identity_client_id:  {managed_identity_client_id2}")
# Output: None (separate kwargs.pop issue)
print(f"workload_identity_client_id: {workload_identity_client_id2}")
# Output: None (BUG - cascading None, should be "env-client-id")

Expected behavior

workload_identity_client_id should independently default to os.environ.get("AZURE_CLIENT_ID") as the documentation states, regardless of what value managed_identity_client_id was set to.

The fix is a one-line change in default.py line 157:

# Before (buggy):
workload_identity_client_id = kwargs.pop("workload_identity_client_id", managed_identity_client_id)

# After (fixed):
workload_identity_client_id = kwargs.pop(
    "workload_identity_client_id", os.environ.get(EnvironmentVariables.AZURE_CLIENT_ID)
)

Screenshots

N/A - code-level bug.

Additional context

This bug compounds with a related issue where DefaultAzureCredential(managed_identity_client_id=None) does not fall back to AZURE_CLIENT_ID either (because kwargs.pop returns the explicit None instead of the default). When combined, explicitly passing managed_identity_client_id=None causes both managed_identity_client_id AND workload_identity_client_id to become None, losing the AZURE_CLIENT_ID fallback for the entire credential chain.

This is especially problematic during migration from ManagedIdentityCredential(client_id=client_id) to DefaultAzureCredential(managed_identity_client_id=client_id) where client_id may be None at runtime.

Related issue: #36365 (ManagedIdentityCredential class doesn't use AZURE_CLIENT_ID environment variable)

Documentation sources confirming the expected behavior:

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