Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
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)


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,8 +4,11 @@
# ------------------------------------
import functools
import os
import warnings
from typing import Dict, Optional, Any

import requests

from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions
from azure.core.exceptions import ClientAuthenticationError
from azure.core.rest import HttpRequest
Expand All @@ -25,8 +28,28 @@ 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) -> requests.Session:
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.

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 +79,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