Skip to content
4 changes: 3 additions & 1 deletion sdk/identity/azure-identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
### Features Added

- Credential HTTP pipeline policies can now be overridden via the `headers_policy`, `logging_policy`, `http_logging_policy`, `proxy_policy`, `user_agent_policy`, `custom_hook_policy`, and `retry_policy` keyword arguments when constructing credentials. The `per_retry_policies` and `per_call_policies` are also now supported. This allows users to inject custom policies or override settings of built-in policies. ([#46072](https://github.com/Azure/azure-sdk-for-python/pull/46072))
- `ManagedIdentityCredential` now supports user-assigned managed identities on Azure Arc-enabled servers. An identity can be selected by client ID, object ID, or resource ID. Token responses that do not confirm the requested identity are rejected.

### Breaking Changes

### Bugs Fixed

- Fixed `AzureDeveloperCliCredential` to correctly parse error messages from Azure Developer CLI v1.23.7 and later, which previously caused raw JSON to surface in `ClientAuthenticationError` instead of the underlying error text.
- Fixed synchronous Service Fabric managed identity authentication with MSAL 1.38.0 and later. Service Fabric now uses a `requests.Session`; a supplied `transport` is ignored with a warning.

### Other Changes

- Added `RequestIdPolicy` to the default pipeline policies to ensure a unique `x-ms-client-request-id` header is sent with each request. ([#46070](https://github.com/Azure/azure-sdk-for-python/pull/46070))
- `CertificateCredential` now passes the PEM private_key to MSAL as a str rather than bytes, matching MSAL's documented `client_credential` contract. ([#46801](https://github.com/Azure/azure-sdk-for-python/pull/46801))
- Temporarily constrained `msal` to `<1.38.0` because MSAL 1.38 is incompatible with the Azure Core-backed transport used by synchronous Service Fabric managed identity authentication.
- Bumped the minimum dependency on `msal` to `>=1.38.0`.

## 1.25.3 (2026-03-12)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,10 @@ def get_unavailable_message(self, desc: str = "") -> str:


def _get_request(url: str, scope: str, identity_config: Dict) -> HttpRequest:
if identity_config:
raise ClientAuthenticationError(
message="User assigned managed identities are not supported by Azure Arc. To authenticate with the system "
"assigned identity omit the client id when constructing the credential, and if authenticating with "
"DefaultAzureCredential ensure the AZURE_CLIENT_ID environment variable is not set."
)

return HttpRequest("GET", url, params=dict({"api-version": "2020-06-01", "resource": scope}, **identity_config))
params = {"api-version": "2020-06-01", "resource": scope}
# Azure Arc requires the IMDS msi_res_id spelling for resource ID requests
params.update({"msi_res_id" if name == "resource_id" else name: value for name, value in identity_config.items()})
return HttpRequest("GET", url, params=params)
Comment thread
kashifkhan marked this conversation as resolved.


def _get_secret_key(response: PipelineResponse) -> str:
Expand Down Expand Up @@ -78,6 +74,32 @@ def _get_key_file_path() -> str:
raise ValueError(f"Azure Arc MSI is not supported on this platform {sys.platform}")


def _validate_user_assigned_identity(identity_config: Dict, content: Dict) -> None:
"""Validates that Azure Arc returned the requested user-assigned identity token.

:param dict identity_config: The configuration of the requested user-assigned identity.
:param dict content: The deserialized response content.
:raises ClientAuthenticationError: If the response content is invalid.
"""
if not identity_config:
return

response_fields = {"client_id": "client_id", "object_id": "object_id", "resource_id": "msi_res_id"}

for identity_type, response_field in response_fields.items():
if identity_type not in identity_config:
continue
returned_id = content.get(response_field)
if identity_type == "resource_id":
returned_id = returned_id or content.get("mi_res_id")
if not returned_id or str(identity_config[identity_type]).lower() != returned_id.lower():
raise ClientAuthenticationError(
message="Azure Arc did not confirm the requested user-assigned managed identity "
"in the token response. The agent likely does not support user-assigned "
"managed identities and returned the system-assigned identity."
)


def _validate_key_file(file_path: str) -> None:
"""Validates that a given Azure Arc MSI file path is valid for use.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# ------------------------------------
import functools
import os
import warnings
from typing import Dict, Optional, Any

from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions
Expand All @@ -25,8 +26,36 @@ class ServiceFabricCredential(MsalManagedIdentityClient):
def get_unavailable_message(self, desc: str = "") -> str:
return f"Service Fabric managed identity configuration not found in environment. {desc}"

def _create_http_client(self, **kwargs: Any) -> Any:
import requests

ignored_options = [
name
for name in (
"transport",
"raw_request_hook",
"raw_response_hook",
"retry_policy",
"proxy_policy",
)
if kwargs.get(name) is not None
]
if ignored_options:
warnings.warn(
"The following arguments are ignored for synchronous Service Fabric managed identity credential "
"because MSAL >= 1.38.0 requires a requests.Session and does not support Azure Core pipeline "
"customization: {}.".format(", ".join(ignored_options)),
UserWarning,
stacklevel=3,
)
return requests.Session() # Service Fabric requires requests.Session for MSAL >= 1.38.0, temporary workaround
Comment thread
JennyPng marked this conversation as resolved.
Comment thread
JennyPng marked this conversation as resolved.

def get_token(
self, *scopes: str, claims: Optional[str] = None, tenant_id: Optional[str] = None, **kwargs: Any
self,
*scopes: str,
claims: Optional[str] = None,
tenant_id: Optional[str] = None,
**kwargs: Any,
) -> AccessToken:
if self._settings.get("client_id") or self._settings.get("identity_config"):
raise ClientAuthenticationError(message=SERVICE_FABRIC_ERROR_MESSAGE)
Expand Down Expand Up @@ -56,5 +85,7 @@ def _get_client_args(**kwargs: Any) -> Optional[Dict]:

def _get_request(url: str, scope: str, identity_config: Dict) -> HttpRequest:
return HttpRequest(
"GET", url, params=dict({"api-version": "2019-07-01-preview", "resource": scope}, **identity_config)
"GET",
url,
params=dict({"api-version": "2019-07-01-preview", "resource": scope}, **identity_config),
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(
self, *, client_id: Optional[str] = None, identity_config: Optional[Mapping[str, str]] = None, **kwargs: Any
) -> None:
self._settings = {"client_id": client_id, "identity_config": identity_config or {}}
self._client = MsalClient(**kwargs)
self._client = self._create_http_client(**kwargs)
managed_identity = self.get_managed_identity()
self._msal_client = msal.ManagedIdentityClient(managed_identity, http_client=self._client)

Expand All @@ -45,6 +45,9 @@ def get_unavailable_message(self, desc: str = "") -> str:
def close(self) -> None:
self.__exit__()

def _create_http_client(self, **kwargs: Any) -> Any:
return MsalClient(**kwargs)

def _request_token(self, *scopes: str, **kwargs: Any) -> AccessTokenInfo:
if not scopes:
raise ValueError('"get_token" requires at least one scope')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@
from .._internal.managed_identity_base import AsyncManagedIdentityBase
from .._internal.managed_identity_client import AsyncManagedIdentityClient
from ..._constants import EnvironmentVariables
from ..._credentials.azure_arc import _get_request, _get_secret_key
from ..._credentials.azure_arc import _get_request, _get_secret_key, _validate_user_assigned_identity


class AzureArcCredential(AsyncManagedIdentityBase):
def get_client(self, **kwargs: Any) -> Optional[AsyncManagedIdentityClient]:
url = os.environ.get(EnvironmentVariables.IDENTITY_ENDPOINT)
imds = os.environ.get(EnvironmentVariables.IMDS_ENDPOINT)
identity_config = dict(kwargs.pop("identity_config", None) or {})
client_id = kwargs.pop("client_id", None)
if client_id:
identity_config["client_id"] = client_id
if url and imds:
return AsyncManagedIdentityClient(
per_retry_policies=[ArcChallengeAuthPolicy()],
request_factory=functools.partial(_get_request, url),
identity_config=identity_config,
_content_callback=functools.partial(_validate_user_assigned_identity, identity_config),
**kwargs,
)
return None
Expand Down
2 changes: 1 addition & 1 deletion sdk/identity/azure-identity/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ classifiers = [
dependencies = [
"azure-core>=1.31.0",
"cryptography>=2.5",
"msal>=1.35.1,<1.38.0",
"msal>=1.38.0",
Comment thread
JennyPng marked this conversation as resolved.
"msal-extensions>=1.2.0",
"typing-extensions>=4.0.0",
]
Expand Down
Loading