From 83847523361484ec4abf04af4a36ff3d1bc86e59 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:20:37 -0700 Subject: [PATCH 01/12] update msal ver --- sdk/identity/azure-identity/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/pyproject.toml b/sdk/identity/azure-identity/pyproject.toml index b11b428b2f45..67057eb96d38 100644 --- a/sdk/identity/azure-identity/pyproject.toml +++ b/sdk/identity/azure-identity/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "azure-core>=1.31.0", "cryptography>=2.5", - "msal>=1.35.1", + "msal>=1.38.0", "msal-extensions>=1.2.0", "typing-extensions>=4.0.0", ] From d1a42e5db318059a585e0c14a8b1063eb7d8adb5 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:35:32 -0700 Subject: [PATCH 02/12] thumbprint --- .../azure-identity/tests/test_managed_identity.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/identity/azure-identity/tests/test_managed_identity.py b/sdk/identity/azure-identity/tests/test_managed_identity.py index beda28ec5de4..5e197c16eced 100644 --- a/sdk/identity/azure-identity/tests/test_managed_identity.py +++ b/sdk/identity/azure-identity/tests/test_managed_identity.py @@ -828,9 +828,9 @@ def test_service_fabric(get_token_method): """Service Fabric 2019-07-01-preview""" access_token = "****" expires_on = 42 - endpoint = "http://localhost:42/token" + endpoint = "https://localhost:42/token" secret = "expected-secret" - thumbprint = "SHA1HEX" + thumbprint = "0123456789abcdef0123456789abcdef01234567" scope = "scope" def send(request, **kwargs): @@ -869,9 +869,9 @@ def send(request, **kwargs): def test_service_fabric_tenant_id(get_token_method): access_token = "****" expires_on = 42 - endpoint = "http://localhost:42/token" + endpoint = "https://localhost:42/token" secret = "expected-secret" - thumbprint = "SHA1HEX" + thumbprint = "0123456789abcdef0123456789abcdef01234567" scope = "scope" def send(request, **kwargs): @@ -912,7 +912,7 @@ def send(request, **kwargs): @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) def test_service_fabric_with_client_id_error(get_token_method): """ManagedIdentityCredential should raise an error if a user identity is provided.""" - endpoint = "http://localhost:42" + endpoint = "https://localhost:42" with mock.patch( "os.environ", { From 9d49df70130d0a70838e82e0c46e4cda9af523bb Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:10:16 -0700 Subject: [PATCH 03/12] 1.37 --- sdk/identity/azure-identity/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/pyproject.toml b/sdk/identity/azure-identity/pyproject.toml index 67057eb96d38..ad65a46ad98e 100644 --- a/sdk/identity/azure-identity/pyproject.toml +++ b/sdk/identity/azure-identity/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "azure-core>=1.31.0", "cryptography>=2.5", - "msal>=1.38.0", + "msal>=1.37.0", "msal-extensions>=1.2.0", "typing-extensions>=4.0.0", ] From 58430e9e70ec47f998900b4cfe3a2e814eb8b90a Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:13:42 -0700 Subject: [PATCH 04/12] bump 1.38.0 --- sdk/identity/azure-identity/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/pyproject.toml b/sdk/identity/azure-identity/pyproject.toml index 59c77c680fba..67057eb96d38 100644 --- a/sdk/identity/azure-identity/pyproject.toml +++ b/sdk/identity/azure-identity/pyproject.toml @@ -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", "msal-extensions>=1.2.0", "typing-extensions>=4.0.0", ] From 14a4dafa61979a5981323da205eacfcc302e1ede Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:16:10 -0700 Subject: [PATCH 05/12] pass request session to msal --- .../azure/identity/_credentials/service_fabric.py | 10 ++++++++++ .../identity/_internal/msal_managed_identity_client.py | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py index b79234ee4adf..78156f7f997f 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py @@ -4,6 +4,8 @@ # ------------------------------------ import functools import os +import requests +import warnings from typing import Dict, Optional, Any from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions @@ -25,6 +27,14 @@ 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: + if kwargs.get("transport") is not None: + warnings.warn( + "The transport argument is ignored for synchronous Service Fabric " + "managed identity credential because MSAL >= 1.38.0 requires a requests.Session for Service Fabric.", + UserWarning, stacklevel=3) + return requests.Session() # Service Fabric requires requests.Session for MSAL >= 1.38.0, temporary workaround + def get_token( self, *scopes: str, claims: Optional[str] = None, tenant_id: Optional[str] = None, **kwargs: Any ) -> AccessToken: diff --git a/sdk/identity/azure-identity/azure/identity/_internal/msal_managed_identity_client.py b/sdk/identity/azure-identity/azure/identity/_internal/msal_managed_identity_client.py index b17091c4141f..10f93ecd53c5 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/msal_managed_identity_client.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/msal_managed_identity_client.py @@ -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) @@ -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') From d581db49f07dd25e7eda01b106e312a48d889edc Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:52:48 -0700 Subject: [PATCH 06/12] update service fabric tests --- .../identity/_credentials/service_fabric.py | 24 +- .../tests/test_managed_identity.py | 361 +++++++++++++----- 2 files changed, 290 insertions(+), 95 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py index 78156f7f997f..2d3e7f19a370 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py @@ -32,17 +32,27 @@ def _create_http_client(self, **kwargs: Any) -> requests.Session: warnings.warn( "The transport argument is ignored for synchronous Service Fabric " "managed identity credential because MSAL >= 1.38.0 requires a requests.Session for Service Fabric.", - UserWarning, stacklevel=3) - return requests.Session() # Service Fabric requires requests.Session for MSAL >= 1.38.0, temporary workaround + UserWarning, + stacklevel=3, + ) + return ( + requests.Session() + ) # Service Fabric requires requests.Session for MSAL >= 1.38.0, temporary workaround 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) return super().get_token(*scopes, claims=claims, tenant_id=tenant_id, **kwargs) - def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = None) -> AccessTokenInfo: + def get_token_info( + self, *scopes: str, options: Optional[TokenRequestOptions] = None + ) -> AccessTokenInfo: if self._settings.get("client_id") or self._settings.get("identity_config"): raise ClientAuthenticationError(message=SERVICE_FABRIC_ERROR_MESSAGE) return super().get_token_info(*scopes, options=options) @@ -66,5 +76,9 @@ 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 + ), ) diff --git a/sdk/identity/azure-identity/tests/test_managed_identity.py b/sdk/identity/azure-identity/tests/test_managed_identity.py index 5e197c16eced..3592a0591b11 100644 --- a/sdk/identity/azure-identity/tests/test_managed_identity.py +++ b/sdk/identity/azure-identity/tests/test_managed_identity.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ from itertools import product +import json import time import logging from unittest import mock @@ -15,19 +16,33 @@ from azure.identity._internal.user_agent import USER_AGENT from azure.identity._internal import within_credential_chain import pytest - -from helpers import build_aad_response, validating_transport, mock_response, Request, GET_TOKEN_METHODS +import requests + +from helpers import ( + build_aad_response, + validating_transport, + mock_response, + Request, + GET_TOKEN_METHODS, +) MANAGED_IDENTITY_ENVIRON = "azure.identity._credentials.managed_identity.os.environ" +SERVICE_FABRIC_ENVIRON = { + EnvironmentVariables.IDENTITY_ENDPOINT: "https://localhost/token", + EnvironmentVariables.IDENTITY_HEADER: "...", + EnvironmentVariables.IDENTITY_SERVER_THUMBPRINT: "0123456789abcdef0123456789abcdef01234567", +} ALL_ENVIRONMENTS = ( - {EnvironmentVariables.IDENTITY_ENDPOINT: "...", EnvironmentVariables.IDENTITY_HEADER: "..."}, # App Service - {EnvironmentVariables.MSI_ENDPOINT: "..."}, # Cloud Shell - { # Service Fabric + { EnvironmentVariables.IDENTITY_ENDPOINT: "...", EnvironmentVariables.IDENTITY_HEADER: "...", - EnvironmentVariables.IDENTITY_SERVER_THUMBPRINT: "...", - }, - {EnvironmentVariables.IDENTITY_ENDPOINT: "...", EnvironmentVariables.IMDS_ENDPOINT: "..."}, # Arc + }, # App Service + {EnvironmentVariables.MSI_ENDPOINT: "..."}, # Cloud Shell + SERVICE_FABRIC_ENVIRON, + { + EnvironmentVariables.IDENTITY_ENDPOINT: "...", + EnvironmentVariables.IMDS_ENDPOINT: "...", + }, # Arc { # token exchange EnvironmentVariables.AZURE_AUTHORITY_HOST: "https://localhost", EnvironmentVariables.AZURE_CLIENT_ID: "...", @@ -35,23 +50,35 @@ EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE: __file__, }, {}, # IMDS - {EnvironmentVariables.MSI_ENDPOINT: "...", EnvironmentVariables.MSI_SECRET: "..."}, # Azure ML + { + EnvironmentVariables.MSI_ENDPOINT: "...", + EnvironmentVariables.MSI_SECRET: "...", + }, # Azure ML +) +# Workaround while Service Fabric requires requests.Session for MSAL >= 1.38.0 +AZURE_CORE_TRANSPORT_ENVIRONMENTS = tuple( + environ for environ in ALL_ENVIRONMENTS if environ is not SERVICE_FABRIC_ENVIRON ) # Environments where MSAL-based managed identity clients are used MSAL_MANAGED_IDENTITY_ENVIRON = ( - {EnvironmentVariables.IDENTITY_ENDPOINT: "...", EnvironmentVariables.IDENTITY_HEADER: "..."}, # App Service - { # Service Fabric + { EnvironmentVariables.IDENTITY_ENDPOINT: "...", EnvironmentVariables.IDENTITY_HEADER: "...", - EnvironmentVariables.IDENTITY_SERVER_THUMBPRINT: "...", - }, - {EnvironmentVariables.IDENTITY_ENDPOINT: "...", EnvironmentVariables.IMDS_ENDPOINT: "..."}, # Arc - {EnvironmentVariables.MSI_ENDPOINT: "...", EnvironmentVariables.MSI_SECRET: "..."}, # Azure ML + }, # App Service + SERVICE_FABRIC_ENVIRON, + { + EnvironmentVariables.IDENTITY_ENDPOINT: "...", + EnvironmentVariables.IMDS_ENDPOINT: "...", + }, # Arc + { + EnvironmentVariables.MSI_ENDPOINT: "...", + EnvironmentVariables.MSI_SECRET: "...", + }, # Azure ML {}, # IMDS ) -@pytest.mark.parametrize("environ", ALL_ENVIRONMENTS) +@pytest.mark.parametrize("environ", AZURE_CORE_TRANSPORT_ENVIRONMENTS) def test_close(environ): transport = mock.MagicMock() with mock.patch.dict("os.environ", environ, clear=True): @@ -62,7 +89,7 @@ def test_close(environ): assert transport.__exit__.call_count == 1 -@pytest.mark.parametrize("environ", ALL_ENVIRONMENTS) +@pytest.mark.parametrize("environ", AZURE_CORE_TRANSPORT_ENVIRONMENTS) def test_context_manager(environ): transport = mock.MagicMock() with mock.patch.dict("os.environ", environ, clear=True): @@ -76,6 +103,44 @@ def test_context_manager(environ): assert transport.__exit__.call_count == 1 +def test_service_fabric_close(): + session = requests.Session() + with ( + mock.patch.dict("os.environ", SERVICE_FABRIC_ENVIRON, clear=True), + mock.patch( + "azure.identity._credentials.service_fabric.requests.Session", + return_value=session, + ), + mock.patch.object(session, "close") as close, + ): + credential = ManagedIdentityCredential() + credential.close() + + close.assert_called_once_with() + + +def test_service_fabric_context_manager(): + session = requests.Session() + with ( + mock.patch.dict("os.environ", SERVICE_FABRIC_ENVIRON, clear=True), + mock.patch( + "azure.identity._credentials.service_fabric.requests.Session", + return_value=session, + ), + mock.patch.object(session, "close") as close, + ): + with ManagedIdentityCredential(): + close.assert_not_called() + + close.assert_called_once_with() + + +def test_service_fabric_warns_when_transport_is_ignored(): + with mock.patch.dict("os.environ", SERVICE_FABRIC_ENVIRON, clear=True): + with pytest.warns(UserWarning, match="transport argument is ignored"): + ManagedIdentityCredential(transport=mock.Mock()) + + def test_close_incomplete_configuration(): ManagedIdentityCredential().close() @@ -85,7 +150,10 @@ def test_context_manager_incomplete_configuration(): pass -@pytest.mark.parametrize("environ,get_token_method", product(ALL_ENVIRONMENTS, GET_TOKEN_METHODS)) +@pytest.mark.parametrize( + "environ,get_token_method", + product(AZURE_CORE_TRANSPORT_ENVIRONMENTS, GET_TOKEN_METHODS), +) def test_custom_hooks(environ, get_token_method): """The credential's pipeline should include azure-core's CustomHookPolicy""" @@ -105,11 +173,15 @@ def test_custom_hooks(environ, get_token_method): "token_type": "Bearer", } ) - transport = validating_transport(requests=[Request()] * 2, responses=[expected_response] * 2) + transport = validating_transport( + requests=[Request()] * 2, responses=[expected_response] * 2 + ) with mock.patch.dict(MANAGED_IDENTITY_ENVIRON, environ, clear=True): credential = ManagedIdentityCredential( - transport=transport, raw_request_hook=request_hook, raw_response_hook=response_hook + transport=transport, + raw_request_hook=request_hook, + raw_response_hook=response_hook, ) getattr(credential, get_token_method)(scope) @@ -120,7 +192,10 @@ def test_custom_hooks(environ, get_token_method): assert pipeline_response.http_response == expected_response -@pytest.mark.parametrize("environ,get_token_method", product(ALL_ENVIRONMENTS, GET_TOKEN_METHODS)) +@pytest.mark.parametrize( + "environ,get_token_method", + product(AZURE_CORE_TRANSPORT_ENVIRONMENTS, GET_TOKEN_METHODS), +) def test_tenant_id(environ, get_token_method): scope = "scope" expected_token = "***" @@ -138,11 +213,15 @@ def test_tenant_id(environ, get_token_method): "token_type": "Bearer", } ) - transport = validating_transport(requests=[Request()] * 2, responses=[expected_response] * 2) + transport = validating_transport( + requests=[Request()] * 2, responses=[expected_response] * 2 + ) with mock.patch.dict(MANAGED_IDENTITY_ENVIRON, environ, clear=True): credential = ManagedIdentityCredential( - transport=transport, raw_request_hook=request_hook, raw_response_hook=response_hook + transport=transport, + raw_request_hook=request_hook, + raw_response_hook=response_hook, ) getattr(credential, get_token_method)(scope) @@ -186,7 +265,9 @@ def test_cloud_shell(get_token_method): ) with mock.patch("os.environ", {EnvironmentVariables.MSI_ENDPOINT: endpoint}): - token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) + token = getattr( + ManagedIdentityCredential(transport=transport), get_token_method + )(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -225,7 +306,9 @@ def test_cloud_shell_tenant_id(get_token_method): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope, **kwargs) + token = getattr( + ManagedIdentityCredential(transport=transport), get_token_method + )(scope, **kwargs) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -253,7 +336,11 @@ def test_azure_ml(get_token_method): url, method="GET", required_headers={"secret": secret, "User-Agent": USER_AGENT}, - required_params={"api-version": "2017-09-01", "resource": scope, "clientid": client_id}, + required_params={ + "api-version": "2017-09-01", + "resource": scope, + "clientid": client_id, + }, ), ], responses=[ @@ -272,14 +359,22 @@ def test_azure_ml(get_token_method): with mock.patch.dict( MANAGED_IDENTITY_ENVIRON, - {EnvironmentVariables.MSI_ENDPOINT: url, EnvironmentVariables.MSI_SECRET: secret}, + { + EnvironmentVariables.MSI_ENDPOINT: url, + EnvironmentVariables.MSI_SECRET: secret, + }, clear=True, ): - token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) + token = getattr( + ManagedIdentityCredential(transport=transport), get_token_method + )(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 - token = getattr(ManagedIdentityCredential(transport=transport, client_id=client_id), get_token_method)(scope) + token = getattr( + ManagedIdentityCredential(transport=transport, client_id=client_id), + get_token_method, + )(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -305,7 +400,11 @@ def test_azure_ml_tenant_id(get_token_method): url, method="GET", required_headers={"secret": secret, "User-Agent": USER_AGENT}, - required_params={"api-version": "2017-09-01", "resource": scope, "clientid": client_id}, + required_params={ + "api-version": "2017-09-01", + "resource": scope, + "clientid": client_id, + }, ), ], responses=[ @@ -324,13 +423,18 @@ def test_azure_ml_tenant_id(get_token_method): with mock.patch.dict( MANAGED_IDENTITY_ENVIRON, - {EnvironmentVariables.MSI_ENDPOINT: url, EnvironmentVariables.MSI_SECRET: secret}, + { + EnvironmentVariables.MSI_ENDPOINT: url, + EnvironmentVariables.MSI_SECRET: secret, + }, clear=True, ): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope, **kwargs) + token = getattr( + ManagedIdentityCredential(transport=transport), get_token_method + )(scope, **kwargs) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -375,12 +479,20 @@ def test_cloud_shell_identity_config(get_token_method): * 2, ) - with mock.patch.dict(MANAGED_IDENTITY_ENVIRON, {EnvironmentVariables.MSI_ENDPOINT: endpoint}, clear=True): - token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) + with mock.patch.dict( + MANAGED_IDENTITY_ENVIRON, + {EnvironmentVariables.MSI_ENDPOINT: endpoint}, + clear=True, + ): + token = getattr( + ManagedIdentityCredential(transport=transport), get_token_method + )(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 - credential = ManagedIdentityCredential(transport=transport, identity_config={param_name: param_value}) + credential = ManagedIdentityCredential( + transport=transport, identity_config={param_name: param_value} + ) token = getattr(credential, get_token_method)(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -400,7 +512,10 @@ def test_prefers_app_service_2019_08_01(get_token_method): Request( base_url=endpoint, method="GET", - required_headers={"X-IDENTITY-HEADER": secret, "User-Agent": USER_AGENT}, + required_headers={ + "X-IDENTITY-HEADER": secret, + "User-Agent": USER_AGENT, + }, required_params={"api-version": "2019-08-01", "resource": scope}, ) ], @@ -423,7 +538,9 @@ def test_prefers_app_service_2019_08_01(get_token_method): EnvironmentVariables.MSI_SECRET: secret, } with mock.patch.dict("os.environ", environ, clear=True): - token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) + token = getattr( + ManagedIdentityCredential(transport=transport), get_token_method + )(scope) assert token.token == access_token assert abs(token.expires_on - expires_on) <= 1 @@ -471,7 +588,9 @@ def send(request, **kwargs): }, clear=True, ): - token = getattr(ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method)(scope) + token = getattr( + ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method + )(scope) assert token.token == access_token assert abs(token.expires_on - expires_on) <= 1 @@ -522,7 +641,9 @@ def send(request, **kwargs): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr(ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method)(scope, **kwargs) + token = getattr( + ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method + )(scope, **kwargs) assert token.token == access_token assert abs(token.expires_on - expires_on) <= 1 @@ -543,13 +664,23 @@ def test_app_service_user_assigned_identity(get_token_method): Request( base_url=endpoint, method="GET", - required_headers={"X-IDENTITY-HEADER": secret, "User-Agent": USER_AGENT}, - required_params={"api-version": "2019-08-01", "client_id": client_id, "resource": scope}, + required_headers={ + "X-IDENTITY-HEADER": secret, + "User-Agent": USER_AGENT, + }, + required_params={ + "api-version": "2019-08-01", + "client_id": client_id, + "resource": scope, + }, ), Request( base_url=endpoint, method="GET", - required_headers={"X-IDENTITY-HEADER": secret, "User-Agent": USER_AGENT}, + required_headers={ + "X-IDENTITY-HEADER": secret, + "User-Agent": USER_AGENT, + }, required_params={ "api-version": "2019-08-01", "client_id": client_id, @@ -572,10 +703,16 @@ def test_app_service_user_assigned_identity(get_token_method): with mock.patch.dict( MANAGED_IDENTITY_ENVIRON, - {EnvironmentVariables.IDENTITY_ENDPOINT: endpoint, EnvironmentVariables.IDENTITY_HEADER: secret}, + { + EnvironmentVariables.IDENTITY_ENDPOINT: endpoint, + EnvironmentVariables.IDENTITY_HEADER: secret, + }, clear=True, ): - token = getattr(ManagedIdentityCredential(client_id=client_id, transport=transport), get_token_method)(scope) + token = getattr( + ManagedIdentityCredential(client_id=client_id, transport=transport), + get_token_method, + )(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -617,7 +754,9 @@ def test_imds(get_token_method): # ensure e.g. $MSI_ENDPOINT isn't set, so we get ImdsCredential with mock.patch.dict("os.environ", clear=True): - token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) + token = getattr( + ManagedIdentityCredential(transport=transport), get_token_method + )(scope) assert token.token == expected_token @@ -656,7 +795,9 @@ def test_imds_tenant_id(get_token_method): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope, **kwargs) + token = getattr( + ManagedIdentityCredential(transport=transport), get_token_method + )(scope, **kwargs) assert token.token == expected_token @@ -767,19 +908,29 @@ def send(request, **kwargs): if request.data: assert "client_id" not in request.body # Cloud Shell return mock_response( - json_payload=(build_aad_response(access_token=expected_access_token, expires_on="42", resource=scope)) + json_payload=( + build_aad_response( + access_token=expected_access_token, expires_on="42", resource=scope + ) + ) ) # IMDS - credential = ManagedIdentityCredential(client_id=None, transport=mock.Mock(send=send)) + credential = ManagedIdentityCredential( + client_id=None, transport=mock.Mock(send=send) + ) token = getattr(credential, get_token_method)(scope) assert token.token == expected_access_token # Cloud Shell with mock.patch.dict( - MANAGED_IDENTITY_ENVIRON, {EnvironmentVariables.MSI_ENDPOINT: "https://localhost"}, clear=True + MANAGED_IDENTITY_ENVIRON, + {EnvironmentVariables.MSI_ENDPOINT: "https://localhost"}, + clear=True, ): - credential = ManagedIdentityCredential(client_id=None, transport=mock.Mock(send=send)) + credential = ManagedIdentityCredential( + client_id=None, transport=mock.Mock(send=send) + ) token = getattr(credential, get_token_method)(scope) assert token.token == expected_access_token @@ -798,7 +949,11 @@ def test_imds_user_assigned_identity(get_token_method): base_url=endpoint, method="GET", required_headers={"Metadata": "true", "User-Agent": USER_AGENT}, - required_params={"api-version": "2018-02-01", "client_id": client_id, "resource": scope}, + required_params={ + "api-version": "2018-02-01", + "client_id": client_id, + "resource": scope, + }, ), ], responses=[ @@ -819,12 +974,21 @@ def test_imds_user_assigned_identity(get_token_method): # ensure e.g. $MSI_ENDPOINT isn't set, so we get ImdsCredential with mock.patch.dict("os.environ", clear=True): - token = getattr(ManagedIdentityCredential(client_id=client_id, transport=transport), get_token_method)(scope) + token = getattr( + ManagedIdentityCredential(client_id=client_id, transport=transport), + get_token_method, + )(scope) assert token.token == expected_token +@pytest.fixture +def mock_service_fabric_request(): + with mock.patch.object(requests.Session, "get") as session_get: + yield session_get + + @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) -def test_service_fabric(get_token_method): +def test_service_fabric(get_token_method, mock_service_fabric_request): """Service Fabric 2019-07-01-preview""" access_token = "****" expires_on = 42 @@ -833,24 +997,18 @@ def test_service_fabric(get_token_method): thumbprint = "0123456789abcdef0123456789abcdef01234567" scope = "scope" - def send(request, **kwargs): - # ensure the `claims` and `tenant_id` keywords from credential's `get_token` method don't make it to transport - assert "claims" not in kwargs - assert "tenant_id" not in kwargs - assert request.url.startswith(endpoint) - assert request.method == "GET" - assert request.headers["Secret"] == secret - assert request.query["api-version"] == "2019-07-01-preview" - assert request.query["resource"] == scope - - return mock_response( - json_payload={ + mock_service_fabric_request.return_value = mock.Mock( + status_code=200, + headers={"content-type": "application/json"}, + text=json.dumps( + { "access_token": access_token, "expires_on": str(expires_on), "resource": scope, "token_type": "Bearer", } - ) + ), + ) with mock.patch( "os.environ", @@ -860,13 +1018,19 @@ def send(request, **kwargs): EnvironmentVariables.IDENTITY_SERVER_THUMBPRINT: thumbprint, }, ): - token = getattr(ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method)(scope) + token = getattr(ManagedIdentityCredential(), get_token_method)(scope) assert token.token == access_token assert abs(token.expires_on - expires_on) <= 1 + mock_service_fabric_request.assert_called_once_with( + endpoint, + params={"api-version": "2019-07-01-preview", "resource": scope}, + headers={"Secret": secret}, + ) + @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) -def test_service_fabric_tenant_id(get_token_method): +def test_service_fabric_tenant_id(get_token_method, mock_service_fabric_request): access_token = "****" expires_on = 42 endpoint = "https://localhost:42/token" @@ -874,24 +1038,18 @@ def test_service_fabric_tenant_id(get_token_method): thumbprint = "0123456789abcdef0123456789abcdef01234567" scope = "scope" - def send(request, **kwargs): - # ensure the `claims` and `tenant_id` keywords from credential's `get_token` method don't make it to transport - assert "claims" not in kwargs - assert "tenant_id" not in kwargs - assert request.url.startswith(endpoint) - assert request.method == "GET" - assert request.headers["Secret"] == secret - assert request.query["api-version"] == "2019-07-01-preview" - assert request.query["resource"] == scope - - return mock_response( - json_payload={ + mock_service_fabric_request.return_value = mock.Mock( + status_code=200, + headers={"content-type": "application/json"}, + text=json.dumps( + { "access_token": access_token, "expires_on": str(expires_on), "resource": scope, "token_type": "Bearer", } - ) + ), + ) with mock.patch( "os.environ", @@ -904,10 +1062,16 @@ def send(request, **kwargs): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr(ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method)(scope, **kwargs) + token = getattr(ManagedIdentityCredential(), get_token_method)(scope, **kwargs) assert token.token == access_token assert abs(token.expires_on - expires_on) <= 1 + mock_service_fabric_request.assert_called_once_with( + endpoint, + params={"api-version": "2019-07-01-preview", "resource": scope}, + headers={"Secret": secret}, + ) + @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) def test_service_fabric_with_client_id_error(get_token_method): @@ -1001,7 +1165,9 @@ def test_token_exchange(tmpdir, get_token_method): ) with mock.patch.dict("os.environ", mock_environ, clear=True): - credential = ManagedIdentityCredential(client_id=nondefault_client_id, transport=transport) + credential = ManagedIdentityCredential( + client_id=nondefault_client_id, transport=transport + ) token = getattr(credential, get_token_method)(scope) assert token.token == access_token @@ -1035,7 +1201,9 @@ def test_token_exchange(tmpdir, get_token_method): with pytest.raises(ValueError): ManagedIdentityCredential() - credential = ManagedIdentityCredential(client_id=nondefault_client_id, transport=transport) + credential = ManagedIdentityCredential( + client_id=nondefault_client_id, transport=transport + ) token = getattr(credential, get_token_method)(scope) assert token.token == access_token @@ -1108,11 +1276,17 @@ def test_validate_identity_config(): with pytest.raises(ValueError): ManagedIdentityCredential(identity_config={"object_id": "bar"}, client_id="bar") with pytest.raises(ValueError): - ManagedIdentityCredential(identity_config={"resource_id": "bar"}, client_id="bar") + ManagedIdentityCredential( + identity_config={"resource_id": "bar"}, client_id="bar" + ) with pytest.raises(ValueError): - ManagedIdentityCredential(identity_config={"object_id": "bar", "resource_id": "foo"}) + ManagedIdentityCredential( + identity_config={"object_id": "bar", "resource_id": "foo"} + ) with pytest.raises(ValueError): - ManagedIdentityCredential(identity_config={"object_id": "bar", "client_id": "foo"}) + ManagedIdentityCredential( + identity_config={"object_id": "bar", "client_id": "foo"} + ) def test_validate_identity_config_output(): @@ -1131,7 +1305,9 @@ def test_validate_identity_config_output(): def test_validate_cloud_shell_credential(): with mock.patch.dict( - MANAGED_IDENTITY_ENVIRON, {EnvironmentVariables.MSI_ENDPOINT: "https://localhost"}, clear=True + MANAGED_IDENTITY_ENVIRON, + {EnvironmentVariables.MSI_ENDPOINT: "https://localhost"}, + clear=True, ): ManagedIdentityCredential() with pytest.raises(ValueError): @@ -1145,7 +1321,9 @@ def test_validate_cloud_shell_credential(): def test_log(caplog): - with caplog.at_level(logging.INFO, logger="azure.identity._credentials.managed_identity"): + with caplog.at_level( + logging.INFO, logger="azure.identity._credentials.managed_identity" + ): ManagedIdentityCredential() assert "ManagedIdentityCredential will use IMDS" in caplog.text @@ -1182,7 +1360,10 @@ def test_log(caplog): assert "workload identity with client_id: foo" in caplog.text -@pytest.mark.parametrize("environ,get_token_method", product(MSAL_MANAGED_IDENTITY_ENVIRON, GET_TOKEN_METHODS)) +@pytest.mark.parametrize( + "environ,get_token_method", + product(MSAL_MANAGED_IDENTITY_ENVIRON, GET_TOKEN_METHODS), +) def test_claims_propagated(environ, get_token_method): """Test that claims passed are forwarded to MSAL's acquire_token_for_client.""" from azure.identity import ManagedIdentityCredential From 4106fd5ef390f5aa04b7249816a80b1e8c79a4da Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:59:08 -0700 Subject: [PATCH 07/12] black --- .../identity/_credentials/service_fabric.py | 12 +-- .../tests/test_managed_identity.py | 90 +++++-------------- 2 files changed, 25 insertions(+), 77 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py index 2d3e7f19a370..626ce304b0f3 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py @@ -35,9 +35,7 @@ def _create_http_client(self, **kwargs: Any) -> requests.Session: UserWarning, stacklevel=3, ) - return ( - requests.Session() - ) # Service Fabric requires requests.Session for MSAL >= 1.38.0, temporary workaround + return requests.Session() # Service Fabric requires requests.Session for MSAL >= 1.38.0, temporary workaround def get_token( self, @@ -50,9 +48,7 @@ def get_token( raise ClientAuthenticationError(message=SERVICE_FABRIC_ERROR_MESSAGE) return super().get_token(*scopes, claims=claims, tenant_id=tenant_id, **kwargs) - def get_token_info( - self, *scopes: str, options: Optional[TokenRequestOptions] = None - ) -> AccessTokenInfo: + def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = None) -> AccessTokenInfo: if self._settings.get("client_id") or self._settings.get("identity_config"): raise ClientAuthenticationError(message=SERVICE_FABRIC_ERROR_MESSAGE) return super().get_token_info(*scopes, options=options) @@ -78,7 +74,5 @@ 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 - ), + params=dict({"api-version": "2019-07-01-preview", "resource": scope}, **identity_config), ) diff --git a/sdk/identity/azure-identity/tests/test_managed_identity.py b/sdk/identity/azure-identity/tests/test_managed_identity.py index 3592a0591b11..0b0cec248aa7 100644 --- a/sdk/identity/azure-identity/tests/test_managed_identity.py +++ b/sdk/identity/azure-identity/tests/test_managed_identity.py @@ -173,9 +173,7 @@ def test_custom_hooks(environ, get_token_method): "token_type": "Bearer", } ) - transport = validating_transport( - requests=[Request()] * 2, responses=[expected_response] * 2 - ) + transport = validating_transport(requests=[Request()] * 2, responses=[expected_response] * 2) with mock.patch.dict(MANAGED_IDENTITY_ENVIRON, environ, clear=True): credential = ManagedIdentityCredential( @@ -213,9 +211,7 @@ def test_tenant_id(environ, get_token_method): "token_type": "Bearer", } ) - transport = validating_transport( - requests=[Request()] * 2, responses=[expected_response] * 2 - ) + transport = validating_transport(requests=[Request()] * 2, responses=[expected_response] * 2) with mock.patch.dict(MANAGED_IDENTITY_ENVIRON, environ, clear=True): credential = ManagedIdentityCredential( @@ -265,9 +261,7 @@ def test_cloud_shell(get_token_method): ) with mock.patch("os.environ", {EnvironmentVariables.MSI_ENDPOINT: endpoint}): - token = getattr( - ManagedIdentityCredential(transport=transport), get_token_method - )(scope) + token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -306,9 +300,7 @@ def test_cloud_shell_tenant_id(get_token_method): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr( - ManagedIdentityCredential(transport=transport), get_token_method - )(scope, **kwargs) + token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope, **kwargs) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -365,9 +357,7 @@ def test_azure_ml(get_token_method): }, clear=True, ): - token = getattr( - ManagedIdentityCredential(transport=transport), get_token_method - )(scope) + token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -432,9 +422,7 @@ def test_azure_ml_tenant_id(get_token_method): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr( - ManagedIdentityCredential(transport=transport), get_token_method - )(scope, **kwargs) + token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope, **kwargs) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -484,15 +472,11 @@ def test_cloud_shell_identity_config(get_token_method): {EnvironmentVariables.MSI_ENDPOINT: endpoint}, clear=True, ): - token = getattr( - ManagedIdentityCredential(transport=transport), get_token_method - )(scope) + token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 - credential = ManagedIdentityCredential( - transport=transport, identity_config={param_name: param_value} - ) + credential = ManagedIdentityCredential(transport=transport, identity_config={param_name: param_value}) token = getattr(credential, get_token_method)(scope) assert token.token == expected_token assert abs(token.expires_on - expires_on) <= 1 @@ -538,9 +522,7 @@ def test_prefers_app_service_2019_08_01(get_token_method): EnvironmentVariables.MSI_SECRET: secret, } with mock.patch.dict("os.environ", environ, clear=True): - token = getattr( - ManagedIdentityCredential(transport=transport), get_token_method - )(scope) + token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) assert token.token == access_token assert abs(token.expires_on - expires_on) <= 1 @@ -588,9 +570,7 @@ def send(request, **kwargs): }, clear=True, ): - token = getattr( - ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method - )(scope) + token = getattr(ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method)(scope) assert token.token == access_token assert abs(token.expires_on - expires_on) <= 1 @@ -641,9 +621,7 @@ def send(request, **kwargs): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr( - ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method - )(scope, **kwargs) + token = getattr(ManagedIdentityCredential(transport=mock.Mock(send=send)), get_token_method)(scope, **kwargs) assert token.token == access_token assert abs(token.expires_on - expires_on) <= 1 @@ -754,9 +732,7 @@ def test_imds(get_token_method): # ensure e.g. $MSI_ENDPOINT isn't set, so we get ImdsCredential with mock.patch.dict("os.environ", clear=True): - token = getattr( - ManagedIdentityCredential(transport=transport), get_token_method - )(scope) + token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope) assert token.token == expected_token @@ -795,9 +771,7 @@ def test_imds_tenant_id(get_token_method): kwargs = {"tenant_id": "tenant_id"} if get_token_method == "get_token_info": kwargs = {"options": kwargs} - token = getattr( - ManagedIdentityCredential(transport=transport), get_token_method - )(scope, **kwargs) + token = getattr(ManagedIdentityCredential(transport=transport), get_token_method)(scope, **kwargs) assert token.token == expected_token @@ -908,17 +882,11 @@ def send(request, **kwargs): if request.data: assert "client_id" not in request.body # Cloud Shell return mock_response( - json_payload=( - build_aad_response( - access_token=expected_access_token, expires_on="42", resource=scope - ) - ) + json_payload=(build_aad_response(access_token=expected_access_token, expires_on="42", resource=scope)) ) # IMDS - credential = ManagedIdentityCredential( - client_id=None, transport=mock.Mock(send=send) - ) + credential = ManagedIdentityCredential(client_id=None, transport=mock.Mock(send=send)) token = getattr(credential, get_token_method)(scope) assert token.token == expected_access_token @@ -928,9 +896,7 @@ def send(request, **kwargs): {EnvironmentVariables.MSI_ENDPOINT: "https://localhost"}, clear=True, ): - credential = ManagedIdentityCredential( - client_id=None, transport=mock.Mock(send=send) - ) + credential = ManagedIdentityCredential(client_id=None, transport=mock.Mock(send=send)) token = getattr(credential, get_token_method)(scope) assert token.token == expected_access_token @@ -1165,9 +1131,7 @@ def test_token_exchange(tmpdir, get_token_method): ) with mock.patch.dict("os.environ", mock_environ, clear=True): - credential = ManagedIdentityCredential( - client_id=nondefault_client_id, transport=transport - ) + credential = ManagedIdentityCredential(client_id=nondefault_client_id, transport=transport) token = getattr(credential, get_token_method)(scope) assert token.token == access_token @@ -1201,9 +1165,7 @@ def test_token_exchange(tmpdir, get_token_method): with pytest.raises(ValueError): ManagedIdentityCredential() - credential = ManagedIdentityCredential( - client_id=nondefault_client_id, transport=transport - ) + credential = ManagedIdentityCredential(client_id=nondefault_client_id, transport=transport) token = getattr(credential, get_token_method)(scope) assert token.token == access_token @@ -1276,17 +1238,11 @@ def test_validate_identity_config(): with pytest.raises(ValueError): ManagedIdentityCredential(identity_config={"object_id": "bar"}, client_id="bar") with pytest.raises(ValueError): - ManagedIdentityCredential( - identity_config={"resource_id": "bar"}, client_id="bar" - ) + ManagedIdentityCredential(identity_config={"resource_id": "bar"}, client_id="bar") with pytest.raises(ValueError): - ManagedIdentityCredential( - identity_config={"object_id": "bar", "resource_id": "foo"} - ) + ManagedIdentityCredential(identity_config={"object_id": "bar", "resource_id": "foo"}) with pytest.raises(ValueError): - ManagedIdentityCredential( - identity_config={"object_id": "bar", "client_id": "foo"} - ) + ManagedIdentityCredential(identity_config={"object_id": "bar", "client_id": "foo"}) def test_validate_identity_config_output(): @@ -1321,9 +1277,7 @@ def test_validate_cloud_shell_credential(): def test_log(caplog): - with caplog.at_level( - logging.INFO, logger="azure.identity._credentials.managed_identity" - ): + with caplog.at_level(logging.INFO, logger="azure.identity._credentials.managed_identity"): ManagedIdentityCredential() assert "ManagedIdentityCredential will use IMDS" in caplog.text From 3204c17c33b33be47f7e814f4599d6152fcde747 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:50:08 -0700 Subject: [PATCH 08/12] validate uami arc response --- .../azure/identity/_credentials/azure_arc.py | 36 ++++++++++++++----- .../identity/aio/_credentials/azure_arc.py | 8 ++++- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_arc.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_arc.py index b43edaecb7f9..d95660046cb0 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_arc.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_arc.py @@ -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 honors IMDS msi_res_id spelling + 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: @@ -78,6 +74,30 @@ 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 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. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_arc.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_arc.py index c452061ef2a1..67730ad9b4db 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_arc.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_arc.py @@ -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 From 756e89256e5570acb2d02b971fdb0c85bb304c6a Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:14:49 -0700 Subject: [PATCH 09/12] unit test for arc --- .../tests/test_managed_identity_async.py | 86 +++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/sdk/identity/azure-identity/tests/test_managed_identity_async.py b/sdk/identity/azure-identity/tests/test_managed_identity_async.py index 838a8914c6e4..8a05fe43792d 100644 --- a/sdk/identity/azure-identity/tests/test_managed_identity_async.py +++ b/sdk/identity/azure-identity/tests/test_managed_identity_async.py @@ -1073,19 +1073,89 @@ async def test_azure_arc_tenant_id(tmpdir, get_token_method): @pytest.mark.asyncio @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) -async def test_azure_arc_client_id(get_token_method): - """Azure Arc doesn't support user-assigned managed identity""" +@pytest.mark.parametrize( + "identity_type,request_parameter", + [ + ("client_id", "client_id"), + ("object_id", "object_id"), + ("resource_id", "msi_res_id"), + ], +) +@pytest.mark.parametrize("response_identity", ["matching", "missing", "mismatched"]) +async def test_azure_arc_user_assigned_identity( + tmp_path, get_token_method, identity_type, request_parameter, response_identity +): + access_token = "****" + api_version = "2020-06-01" + expires_on = 42 + identity_endpoint = "http://localhost:42/token" + imds_endpoint = "http://localhost:42" + scope = "scope" + secret_key = "XXXX" + requested_identity = "some-identity" + + key_file = tmp_path / "key_file.key" + key_file.write_text(secret_key) + + required_params = { + "api-version": api_version, + "resource": scope, + request_parameter: requested_identity, + } + response_payload = { + "access_token": access_token, + "expires_on": expires_on, + "resource": scope, + "token_type": "Bearer", + } + if response_identity != "missing": + response_payload[request_parameter] = ( + requested_identity.upper() if response_identity == "matching" else "another-identity" + ) + + transport = async_validating_transport( + requests=[ + Request( + base_url=identity_endpoint, + method="GET", + required_headers={"Metadata": "true"}, + required_params=required_params, + ), + Request( + base_url=identity_endpoint, + method="GET", + required_headers={"Metadata": "true", "Authorization": "Basic {}".format(secret_key)}, + required_params=required_params, + ), + ], + responses=[ + mock_response(status_code=401, headers={"WWW-Authenticate": "Basic realm={}".format(key_file)}), + mock_response(json_payload=response_payload), + ], + ) + with mock.patch( "os.environ", { - EnvironmentVariables.IDENTITY_ENDPOINT: "http://localhost:42/token", - EnvironmentVariables.IMDS_ENDPOINT: "http://localhost:42", + EnvironmentVariables.IDENTITY_ENDPOINT: identity_endpoint, + EnvironmentVariables.IMDS_ENDPOINT: imds_endpoint, }, ): - credential = ManagedIdentityCredential(client_id="some-guid") - - with pytest.raises(ClientAuthenticationError): - await getattr(credential, get_token_method)("scope") + with mock.patch("azure.identity._credentials.azure_arc._validate_key_file", lambda x: None): + if identity_type == "client_id": + credential = ManagedIdentityCredential(transport=transport, client_id=requested_identity) + else: + credential = ManagedIdentityCredential( + transport=transport, identity_config={identity_type: requested_identity} + ) + + if response_identity == "matching": + token = await getattr(credential, get_token_method)(scope) + assert token.token == access_token + assert token.expires_on == expires_on + else: + with pytest.raises(ClientAuthenticationError, match="did not confirm"): + await getattr(credential, get_token_method)(scope) @pytest.mark.asyncio From 78c78d6bfe6706e1ce1c4c90ffcd3a89109d09ef Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:53:28 -0700 Subject: [PATCH 10/12] changelog --- sdk/identity/azure-identity/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 8e2db68f9c68..31d0df571455 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -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) From 592f35843db85b6cd44c96807091a21a6489b4a0 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:06:45 -0700 Subject: [PATCH 11/12] allow mi_res_id, update servicefabric warning --- .../azure/identity/_credentials/azure_arc.py | 4 +++- .../azure/identity/_credentials/service_fabric.py | 12 +++++++++--- .../azure-identity/tests/test_managed_identity.py | 10 +++++++--- .../tests/test_managed_identity_async.py | 13 +++++++------ 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_arc.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_arc.py index d95660046cb0..a404cd3138b9 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_arc.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_arc.py @@ -21,7 +21,7 @@ def get_unavailable_message(self, desc: str = "") -> str: def _get_request(url: str, scope: str, identity_config: Dict) -> HttpRequest: params = {"api-version": "2020-06-01", "resource": scope} - # Azure Arc honors IMDS msi_res_id spelling + # 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) @@ -90,6 +90,8 @@ def _validate_user_assigned_identity(identity_config: Dict, content: Dict) -> No 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 " diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py index 626ce304b0f3..aff4e818a702 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py @@ -28,10 +28,16 @@ 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: - if kwargs.get("transport") is not None: + 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 transport argument is ignored for synchronous Service Fabric " - "managed identity credential because MSAL >= 1.38.0 requires a requests.Session for Service Fabric.", + "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, ) diff --git a/sdk/identity/azure-identity/tests/test_managed_identity.py b/sdk/identity/azure-identity/tests/test_managed_identity.py index 0b0cec248aa7..2a635fccf0e2 100644 --- a/sdk/identity/azure-identity/tests/test_managed_identity.py +++ b/sdk/identity/azure-identity/tests/test_managed_identity.py @@ -135,10 +135,14 @@ def test_service_fabric_context_manager(): close.assert_called_once_with() -def test_service_fabric_warns_when_transport_is_ignored(): +@pytest.mark.parametrize( + "option", + ["transport", "raw_request_hook", "raw_response_hook", "retry_policy", "proxy_policy"], +) +def test_service_fabric_warns_when_pipeline_option_is_ignored(option): with mock.patch.dict("os.environ", SERVICE_FABRIC_ENVIRON, clear=True): - with pytest.warns(UserWarning, match="transport argument is ignored"): - ManagedIdentityCredential(transport=mock.Mock()) + with pytest.warns(UserWarning, match=option): + ManagedIdentityCredential(**{option: mock.Mock()}) def test_close_incomplete_configuration(): diff --git a/sdk/identity/azure-identity/tests/test_managed_identity_async.py b/sdk/identity/azure-identity/tests/test_managed_identity_async.py index 8a05fe43792d..8f36c2a65960 100644 --- a/sdk/identity/azure-identity/tests/test_managed_identity_async.py +++ b/sdk/identity/azure-identity/tests/test_managed_identity_async.py @@ -1074,16 +1074,17 @@ async def test_azure_arc_tenant_id(tmpdir, get_token_method): @pytest.mark.asyncio @pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS) @pytest.mark.parametrize( - "identity_type,request_parameter", + "identity_type,request_parameter,response_parameter", [ - ("client_id", "client_id"), - ("object_id", "object_id"), - ("resource_id", "msi_res_id"), + ("client_id", "client_id", "client_id"), + ("object_id", "object_id", "object_id"), + ("resource_id", "msi_res_id", "msi_res_id"), + ("resource_id", "msi_res_id", "mi_res_id"), ], ) @pytest.mark.parametrize("response_identity", ["matching", "missing", "mismatched"]) async def test_azure_arc_user_assigned_identity( - tmp_path, get_token_method, identity_type, request_parameter, response_identity + tmp_path, get_token_method, identity_type, request_parameter, response_parameter, response_identity ): access_token = "****" api_version = "2020-06-01" @@ -1109,7 +1110,7 @@ async def test_azure_arc_user_assigned_identity( "token_type": "Bearer", } if response_identity != "missing": - response_payload[request_parameter] = ( + response_payload[response_parameter] = ( requested_identity.upper() if response_identity == "matching" else "another-identity" ) From a361b20deef27d1494bb31cfd4f0d179804c2493 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:55:24 -0700 Subject: [PATCH 12/12] fix pylint --- .../azure/identity/_credentials/service_fabric.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py index aff4e818a702..b5d7f6e3d056 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/service_fabric.py @@ -4,10 +4,11 @@ # ------------------------------------ import functools import os -import requests 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