From a8514d498d64b13a5557262a9af2760c01d85a5f Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Sat, 5 Sep 2026 01:58:08 -0700 Subject: [PATCH 1/3] Support Azure Arc user-assigned managed identity Upgrade MSAL4J to 1.26.0 and allow Arc UAMI requests to reach MSAL in both identity implementations. Preserve the existing Cloud Shell and Service Fabric restrictions, and cover direct and DefaultAzureCredential selector paths. --- eng/versioning/external_dependencies.txt | 2 +- .../microsoft-azure-eventhubs/pom.xml | 2 +- sdk/identity-v2/azure-identity/CHANGELOG.md | 8 ++ sdk/identity-v2/azure-identity/pom.xml | 4 +- .../v2/identity/ChainedTokenCredential.java | 2 +- .../identity/ManagedIdentityCredential.java | 8 +- .../ManagedIdentityCredentialBuilder.java | 3 + .../identity/DefaultAzureCredentialTest.java | 78 +++++++++++++ .../ManagedIdentityCredentialTest.java | 75 ++++++++++++ sdk/identity/azure-identity-broker/pom.xml | 4 +- sdk/identity/azure-identity/CHANGELOG.md | 6 + sdk/identity/azure-identity/README.md | 4 + sdk/identity/azure-identity/pom.xml | 4 +- .../azure/identity/ArcIdentityCredential.java | 8 -- .../identity/ManagedIdentityCredential.java | 8 +- .../ManagedIdentityCredentialBuilder.java | 3 + .../identity/DefaultAzureCredentialTest.java | 54 ++++++++- .../ManagedIdentityCredentialTest.java | 107 ++++++++++++++---- 18 files changed, 329 insertions(+), 51 deletions(-) create mode 100644 sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/DefaultAzureCredentialTest.java diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index c3829f0d3b30b..cda245472c6dd 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -109,7 +109,7 @@ com.microsoft.azure:azure-mgmt-graph-rbac;1.3.0 com.microsoft.azure:azure-mgmt-network;1.33.0 com.microsoft.azure:azure-mgmt-resources;1.3.0 com.microsoft.azure:azure-storage;8.0.0 -com.microsoft.azure:msal4j;1.23.1 +com.microsoft.azure:msal4j;1.26.0 com.microsoft.azure:msal4j-brokers;1.0.0 com.microsoft.azure:msal4j-persistence-extension;1.3.0 io.opentelemetry:opentelemetry-api;1.58.0 diff --git a/sdk/eventhubs/microsoft-azure-eventhubs/pom.xml b/sdk/eventhubs/microsoft-azure-eventhubs/pom.xml index 71309138b5a91..3411b81f5923c 100644 --- a/sdk/eventhubs/microsoft-azure-eventhubs/pom.xml +++ b/sdk/eventhubs/microsoft-azure-eventhubs/pom.xml @@ -77,7 +77,7 @@ com.microsoft.azure msal4j - 1.23.1 + 1.26.0 test diff --git a/sdk/identity-v2/azure-identity/CHANGELOG.md b/sdk/identity-v2/azure-identity/CHANGELOG.md index 567d6975765a0..af33a161dac29 100644 --- a/sdk/identity-v2/azure-identity/CHANGELOG.md +++ b/sdk/identity-v2/azure-identity/CHANGELOG.md @@ -4,8 +4,16 @@ ### Features Added +- Added support for user-assigned managed identities on Azure Arc in `ManagedIdentityCredential` and `DefaultAzureCredential`. + ### Breaking Changes ### Bugs Fixed +- Fixed `DefaultAzureCredential` failing after a credential successfully acquired a token. + ### Other Changes + +#### Dependency Updates + +- Upgraded `msal4j` from `1.23.1` to `1.26.0`. diff --git a/sdk/identity-v2/azure-identity/pom.xml b/sdk/identity-v2/azure-identity/pom.xml index 8b4a6a163ad3e..d2c57f15a2c34 100644 --- a/sdk/identity-v2/azure-identity/pom.xml +++ b/sdk/identity-v2/azure-identity/pom.xml @@ -76,7 +76,7 @@ com.microsoft.azure msal4j - 1.23.1 + 1.26.0 @@ -125,7 +125,7 @@ - com.microsoft.azure:msal4j:[1.23.1] + com.microsoft.azure:msal4j:[1.26.0] com.microsoft.azure:msal4j-persistence-extension:[1.3.0] diff --git a/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ChainedTokenCredential.java b/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ChainedTokenCredential.java index b3e2aaa1a4dea..9fa63faf1db17 100644 --- a/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ChainedTokenCredential.java +++ b/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ChainedTokenCredential.java @@ -81,7 +81,7 @@ public AccessToken getToken(TokenRequestContext request) { try { AccessToken accessToken = credential.getToken(request); LOGGER.atInfo() - .addKeyValue("credentialType", selectedCredential.get().getClass().getCanonicalName()) + .addKeyValue("credentialType", credential.getClass().getCanonicalName()) .log("Azure Identity => Attempted credential returns a token."); selectedCredential.set(credential); return accessToken; diff --git a/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredential.java b/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredential.java index 95cc74234934a..c0a0340dbfcac 100644 --- a/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredential.java +++ b/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredential.java @@ -45,6 +45,11 @@ * Sets * * + *

On Azure Arc-enabled servers, this credential supports both system-assigned and user-assigned managed + * identities. Use {@link ManagedIdentityCredentialBuilder#clientId(String)}, + * {@link ManagedIdentityCredentialBuilder#resourceId(String)}, or + * {@link ManagedIdentityCredentialBuilder#objectId(String)} to select a user-assigned identity.

+ * *

Sample: Construct a simple ManagedIdentityCredential

* *

The following code sample demonstrates the creation of a ManagedIdentityCredential, @@ -97,8 +102,7 @@ public AccessToken getToken(TokenRequestContext request) { if (!CoreUtils.isNullOrEmpty(managedIdentityId)) { ManagedIdentitySourceType managedIdentitySourceType = ManagedIdentityApplication.getManagedIdentitySource(); - if (ManagedIdentitySourceType.CLOUD_SHELL.equals(managedIdentitySourceType) - || ManagedIdentitySourceType.AZURE_ARC.equals(managedIdentitySourceType)) { + if (ManagedIdentitySourceType.CLOUD_SHELL.equals(managedIdentitySourceType)) { throw LOGGER.throwableAtError() .log("ManagedIdentityCredential authentication unavailable. " + "User-assigned managed identity is not supported in " + managedIdentitySourceType diff --git a/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredentialBuilder.java b/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredentialBuilder.java index b4e3a827f7683..afea0f8c308ba 100644 --- a/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredentialBuilder.java +++ b/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredentialBuilder.java @@ -26,6 +26,9 @@ * managed identity authentication * documentation.

* + *

Azure Arc-enabled servers support user-assigned managed identities selected by client ID, resource ID, or object + * ID.

+ * *

Sample: Construct a simple ManagedIdentityCredential

* *

The following code sample demonstrates the creation of a {@link ManagedIdentityCredential}, diff --git a/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/DefaultAzureCredentialTest.java b/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/DefaultAzureCredentialTest.java new file mode 100644 index 0000000000000..8b428da4eb7e1 --- /dev/null +++ b/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/DefaultAzureCredentialTest.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.v2.identity; + +import com.azure.v2.core.credentials.TokenRequestContext; +import com.azure.v2.identity.implementation.client.ManagedIdentityClient; +import com.azure.v2.identity.implementation.models.ManagedIdentityClientOptions; +import com.azure.v2.identity.util.TestConfigurationSource; +import com.azure.v2.identity.util.TestUtils; +import com.microsoft.aad.msal4j.ManagedIdentityApplication; +import com.microsoft.aad.msal4j.ManagedIdentitySourceType; +import io.clientcore.core.credentials.oauth.AccessToken; +import io.clientcore.core.utils.configuration.Configuration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DefaultAzureCredentialTest { + private static final String CLIENT_ID = UUID.randomUUID().toString(); + private static final String RESOURCE_ID = "/subscriptions/" + UUID.randomUUID() + + "/resourcegroups/aresourcegroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ident"; + + @ParameterizedTest + @ValueSource(strings = { "clientId", "resourceId" }) + public void testUseArcUserAssignedManagedIdentityCredential(String identityType) { + // setup + String token = "token"; + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); + Configuration configuration = TestUtils.createTestConfiguration(new TestConfigurationSource()); + + String clientId = "clientId".equals(identityType) ? CLIENT_ID : null; + String resourceId = "resourceId".equals(identityType) ? RESOURCE_ID : null; + + // mock + try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class); + MockedConstruction managedIdentityMock + = mockConstruction(ManagedIdentityClient.class, (miClient, context) -> { + ManagedIdentityClientOptions options = (ManagedIdentityClientOptions) context.arguments().get(0); + Assertions.assertEquals(clientId, options.getClientId()); + Assertions.assertEquals(resourceId, options.getResourceId()); + when(miClient.authenticate(request)).thenReturn(TestUtils.getMockAccessToken(token, expiresAt)); + })) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.AZURE_ARC); + + DefaultAzureCredentialBuilder builder = new DefaultAzureCredentialBuilder().configuration(configuration); + if (clientId != null) { + builder.managedIdentityClientId(clientId); + } else { + builder.managedIdentityResourceId(resourceId); + } + DefaultAzureCredential credential = builder.build(); + + // test + AccessToken firstToken = credential.getToken(request); + AccessToken cachedToken = credential.getToken(request); + Assertions.assertEquals(token, firstToken.getToken()); + Assertions.assertEquals(token, cachedToken.getToken()); + Assertions.assertEquals(expiresAt.getSecond(), firstToken.getExpiresAt().getSecond()); + Assertions.assertEquals(1, managedIdentityMock.constructed().size()); + verify(managedIdentityMock.constructed().get(0), times(2)).authenticate(request); + } + } +} diff --git a/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/ManagedIdentityCredentialTest.java b/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/ManagedIdentityCredentialTest.java index 6af4251d3c390..b9d58a3e906f6 100644 --- a/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/ManagedIdentityCredentialTest.java +++ b/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/ManagedIdentityCredentialTest.java @@ -3,26 +3,37 @@ package com.azure.v2.identity; +import com.azure.v2.identity.exceptions.CredentialUnavailableException; import com.azure.v2.identity.implementation.client.ManagedIdentityClient; +import com.azure.v2.identity.implementation.models.ManagedIdentityClientOptions; import com.azure.v2.identity.util.TestConfigurationSource; import com.azure.v2.identity.util.TestUtils; import com.azure.v2.core.credentials.TokenRequestContext; +import com.microsoft.aad.msal4j.ManagedIdentityApplication; +import com.microsoft.aad.msal4j.ManagedIdentitySourceType; import io.clientcore.core.credentials.oauth.AccessToken; import io.clientcore.core.utils.configuration.Configuration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.UUID; import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; public class ManagedIdentityCredentialTest { private static final String CLIENT_ID = UUID.randomUUID().toString(); + private static final String OBJECT_ID = UUID.randomUUID().toString(); + private static final String RESOURCE_ID = "/subscriptions/" + UUID.randomUUID() + + "/resourcegroups/aresourcegroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ident"; @Test public void testMiAuthFlow() { @@ -52,4 +63,68 @@ public void testMiAuthFlow() { Assertions.assertNotNull(managedIdentityMock); } } + + @ParameterizedTest + @ValueSource(strings = { "clientId", "resourceId", "objectId" }) + public void testArcUserAssigned(String identityType) { + // setup + String token = "token"; + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); + Configuration configuration = TestUtils.createTestConfiguration(new TestConfigurationSource()); + + String clientId = "clientId".equals(identityType) ? CLIENT_ID : null; + String resourceId = "resourceId".equals(identityType) ? RESOURCE_ID : null; + String objectId = "objectId".equals(identityType) ? OBJECT_ID : null; + + // mock + try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class); + MockedConstruction managedIdentityMock + = mockConstruction(ManagedIdentityClient.class, (miClient, context) -> { + ManagedIdentityClientOptions options = (ManagedIdentityClientOptions) context.arguments().get(0); + Assertions.assertEquals(clientId, options.getClientId()); + Assertions.assertEquals(resourceId, options.getResourceId()); + Assertions.assertEquals(objectId, options.getObjectId()); + when(miClient.authenticate(request)).thenReturn(TestUtils.getMockAccessToken(token, expiresAt)); + })) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.AZURE_ARC); + + ManagedIdentityCredentialBuilder builder + = new ManagedIdentityCredentialBuilder().configuration(configuration); + if (clientId != null) { + builder.clientId(clientId); + } else if (resourceId != null) { + builder.resourceId(resourceId); + } else { + builder.objectId(objectId); + } + + // test + AccessToken accessToken = builder.build().getToken(request); + Assertions.assertEquals(token, accessToken.getToken()); + Assertions.assertEquals(expiresAt.getSecond(), accessToken.getExpiresAt().getSecond()); + Assertions.assertEquals(1, managedIdentityMock.constructed().size()); + } + } + + @Test + public void testCloudShellUserAssigned() { + // setup + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + Configuration configuration = TestUtils.createTestConfiguration(new TestConfigurationSource()); + + try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class)) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.CLOUD_SHELL); + + // test + ManagedIdentityCredential credential + = new ManagedIdentityCredentialBuilder().configuration(configuration).objectId(OBJECT_ID).build(); + CredentialUnavailableException exception + = Assertions.assertThrows(CredentialUnavailableException.class, () -> credential.getToken(request)); + Assertions.assertTrue( + exception.getMessage().contains("User-assigned managed identity is not supported in CLOUD_SHELL")); + } + } } diff --git a/sdk/identity/azure-identity-broker/pom.xml b/sdk/identity/azure-identity-broker/pom.xml index dde164700249f..b85196ac00fb8 100644 --- a/sdk/identity/azure-identity-broker/pom.xml +++ b/sdk/identity/azure-identity-broker/pom.xml @@ -42,7 +42,7 @@ com.microsoft.azure msal4j - 1.23.1 + 1.26.0 com.microsoft.azure @@ -67,7 +67,7 @@ - com.microsoft.azure:msal4j:[1.23.1] + com.microsoft.azure:msal4j:[1.26.0] com.microsoft.azure:msal4j-brokers:[1.0.0] diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 404eb32d5b65a..0e382f8104947 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added support for user-assigned managed identities on Azure Arc in `ManagedIdentityCredential` and `DefaultAzureCredential`. + ### Breaking Changes ### Bugs Fixed @@ -14,6 +16,10 @@ - Improved `AzureDeveloperCliCredential` error handling to extract meaningful messages from `azd auth token` JSON output, providing cleaner error messages to users. +#### Dependency Updates + +- Upgraded `msal4j` from `1.23.1` to `1.26.0`. + ## 1.18.5 (2026-08-24) ### Other Changes diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index 1fe34673fee88..40a5114a066a7 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -158,6 +158,10 @@ The [Managed identity authentication](https://learn.microsoft.com/entra/identity - [Azure Virtual Machines](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-to-use-vm-token) - [Azure Virtual Machines Scale Sets](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/qs-configure-powershell-windows-vmss) +Azure Arc supports system-assigned and user-assigned managed identities. `ManagedIdentityCredential` can select a +user-assigned identity by client ID, resource ID, or object ID. `DefaultAzureCredential` supports client ID and +resource ID. + **Note:** Use `azure-identity` version `1.7.0` or later to utilize [token caching](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/identity/azure-identity/TOKEN_CACHING.md) support for managed identity authentication. ## Cloud / Sovereign configuration diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index c16935f8f74f0..d5df65adbd42c 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -52,7 +52,7 @@ com.microsoft.azure msal4j - 1.23.1 + 1.26.0 com.microsoft.azure @@ -109,7 +109,7 @@ - com.microsoft.azure:msal4j:[1.23.1] + com.microsoft.azure:msal4j:[1.26.0] com.microsoft.azure:msal4j-persistence-extension:[1.3.0] net.java.dev.jna:jna-platform:[5.17.0] diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ArcIdentityCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ArcIdentityCredential.java index eb61c02d5fc92..16896bdf54c63 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ArcIdentityCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ArcIdentityCredential.java @@ -6,7 +6,6 @@ import com.azure.core.annotation.Immutable; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenRequestContext; -import com.azure.core.exception.ClientAuthenticationException; import com.azure.core.util.Configuration; import com.azure.core.util.logging.ClientLogger; import com.azure.identity.implementation.IdentityClient; @@ -43,13 +42,6 @@ class ArcIdentityCredential extends ManagedIdentityServiceCredential { * @return A publisher that emits an {@link AccessToken}. */ public Mono authenticate(TokenRequestContext request) { - if (getClientId() != null) { - return Mono.error(LOGGER.logExceptionAsError(new ClientAuthenticationException( - "User assigned identity is not supported by the Azure Arc Managed Identity Endpoint. To authenticate " - + "with the system assigned identity omit the client id when constructing the" - + " ManagedIdentityCredential.", - null))); - } return identityClient.authenticateWithManagedIdentityMsalClient(request); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java index b1f5e22ba49e6..3fd77317cbbc0 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredential.java @@ -46,6 +46,11 @@ * Sets * * + *

On Azure Arc-enabled servers, this credential supports both system-assigned and user-assigned managed + * identities. Use {@link ManagedIdentityCredentialBuilder#clientId(String)}, + * {@link ManagedIdentityCredentialBuilder#resourceId(String)}, or + * {@link ManagedIdentityCredentialBuilder#objectId(String)} to select a user-assigned identity.

+ * *

Sample: Construct a simple ManagedIdentityCredential

* *

The following code sample demonstrates the creation of a ManagedIdentityCredential, @@ -155,8 +160,7 @@ public Mono getToken(TokenRequestContext request) { // Check a couple cases that are not supported for user-assigned managed identity. if (!CoreUtils.isNullOrEmpty(managedIdentityId)) { ManagedIdentitySourceType managedIdentitySourceType = ManagedIdentityApplication.getManagedIdentitySource(); - if (ManagedIdentitySourceType.CLOUD_SHELL.equals(managedIdentitySourceType) - || ManagedIdentitySourceType.AZURE_ARC.equals(managedIdentitySourceType)) { + if (ManagedIdentitySourceType.CLOUD_SHELL.equals(managedIdentitySourceType)) { return Mono.error(LoggingUtil.logCredentialUnavailableException(LOGGER, identityClientOptions, new CredentialUnavailableException("ManagedIdentityCredential authentication unavailable. " + "User-assigned managed identity is not supported in " + managedIdentitySourceType diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredentialBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredentialBuilder.java index a55cafdba5b4c..cd1d479b19166 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredentialBuilder.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/ManagedIdentityCredentialBuilder.java @@ -23,6 +23,9 @@ * managed identity authentication * documentation.

* + *

Azure Arc-enabled servers support user-assigned managed identities selected by client ID, resource ID, or object + * ID.

+ * *

Sample: Construct a simple ManagedIdentityCredential

* *

The following code sample demonstrates the creation of a {@link ManagedIdentityCredential}, diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java index 7859aa920d0bf..bc0617166af6e 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java @@ -13,12 +13,15 @@ import com.azure.identity.implementation.IdentityClient; import com.azure.identity.util.EmptyEnvironmentConfigurationSource; import com.azure.identity.util.TestUtils; +import com.microsoft.aad.msal4j.ManagedIdentityApplication; +import com.microsoft.aad.msal4j.ManagedIdentitySourceType; import com.microsoft.aad.msal4j.MsalServiceException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -37,12 +40,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; public class DefaultAzureCredentialTest { private static final String TENANT_ID = "contoso.com"; private static final String CLIENT_ID = UUID.randomUUID().toString(); + private static final String RESOURCE_ID = "/subscriptions/" + UUID.randomUUID() + + "/resourcegroups/aresourcegroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ident"; @Test public void testUseEnvironmentCredential() { @@ -113,6 +119,48 @@ public void testUseManagedIdentityCredential() { } } + @ParameterizedTest + @ValueSource(strings = { "clientId", "resourceId" }) + public void testUseArcUserAssignedManagedIdentityCredential(String identityType) { + // setup + String token = "token"; + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); + EmptyEnvironmentConfigurationSource source = new EmptyEnvironmentConfigurationSource(); + Configuration configuration = new ConfigurationBuilder(source, source, source).build(); + + // mock + try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class); + MockedConstruction mocked + = mockConstruction(IdentityClient.class, (identityClient, context) -> { + when(identityClient.authenticateWithAzureDeveloperCli(request)).thenReturn(Mono.empty()); + when(identityClient.authenticateWithManagedIdentityMsalClient(request)) + .thenReturn(TestUtils.getMockAccessToken(token, expiresAt)); + }); + MockedConstruction intelliJCredentialMock + = mockConstruction(IntelliJCredential.class, (intelliJCredential, context) -> { + when(intelliJCredential.getToken(request)).thenReturn(Mono.empty()); + })) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.AZURE_ARC); + + DefaultAzureCredentialBuilder builder = new DefaultAzureCredentialBuilder().configuration(configuration); + if ("clientId".equals(identityType)) { + builder.managedIdentityClientId(CLIENT_ID); + } else { + builder.managedIdentityResourceId(RESOURCE_ID); + } + + // test + StepVerifier.create(builder.build().getToken(request)) + .expectNextMatches(accessToken -> token.equals(accessToken.getToken()) + && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) + .verifyComplete(); + Assertions.assertNotNull(mocked); + Assertions.assertNotNull(intelliJCredentialMock); + } + } + @Test public void testUseWorkloadIdentityCredentialWithManagedIdentityClientId() { // setup @@ -450,14 +498,10 @@ public void testCredentialUnavailableSync() { @Test public void testInvalidIdCombination() { - // setup - String resourceId = "/subscriptions/" + UUID.randomUUID() - + "/resourcegroups/aresourcegroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ident"; - // test Assertions.assertThrows(IllegalStateException.class, () -> new DefaultAzureCredentialBuilder().managedIdentityClientId(CLIENT_ID) - .managedIdentityResourceId(resourceId) + .managedIdentityResourceId(RESOURCE_ID) .build()); } diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/ManagedIdentityCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/ManagedIdentityCredentialTest.java index 00e94d573990a..a7f4cf4ed265c 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/ManagedIdentityCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/ManagedIdentityCredentialTest.java @@ -12,11 +12,14 @@ import com.azure.identity.implementation.IdentityClient; import com.azure.identity.implementation.IdentityClientOptions; import com.azure.identity.util.TestUtils; +import com.microsoft.aad.msal4j.ManagedIdentityApplication; +import com.microsoft.aad.msal4j.ManagedIdentitySourceType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; import reactor.test.StepVerifier; import java.nio.charset.StandardCharsets; @@ -28,12 +31,15 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; public class ManagedIdentityCredentialTest { private static final String CLIENT_ID = UUID.randomUUID().toString(); private static final String OBJECT_ID = UUID.randomUUID().toString(); + private static final String RESOURCE_ID = "/subscriptions/" + UUID.randomUUID() + + "/resourcegroups/aresourcegroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ident"; @Test public void testVirtualMachineMSICredentialConfigurations() { @@ -128,52 +134,103 @@ public void testIMDS() { } } - @Test - public void testArcUserAssigned() { + @ParameterizedTest + @ValueSource(strings = { "clientId", "resourceId", "objectId" }) + public void testArcUserAssigned(String identityType) { // setup - String endpoint = "http://localhost"; + String token = "token"; TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); - Configuration configuration = TestUtils.createTestConfiguration( - new TestConfigurationSource().put("IDENTITY_ENDPOINT", endpoint).put("IMDS_ENDPOINT", endpoint)); + OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); + Configuration configuration = TestUtils.createTestConfiguration(new TestConfigurationSource()); - // test - ManagedIdentityCredential credential - = new ManagedIdentityCredentialBuilder().configuration(configuration).clientId(CLIENT_ID).build(); - StepVerifier.create(credential.getToken(request)) - .expectErrorMatches(t -> t instanceof ClientAuthenticationException) - .verify(); + String clientId = "clientId".equals(identityType) ? CLIENT_ID : null; + String resourceId = "resourceId".equals(identityType) ? RESOURCE_ID : null; + String objectId = "objectId".equals(identityType) ? OBJECT_ID : null; + + // mock + try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class); + MockedConstruction identityClientMock + = mockConstruction(IdentityClient.class, (identityClient, context) -> { + assertEquals(clientId, context.arguments().get(1)); + assertEquals(resourceId, context.arguments().get(5)); + assertEquals(objectId, context.arguments().get(6)); + when(identityClient.authenticateWithManagedIdentityMsalClient(request)) + .thenReturn(TestUtils.getMockAccessToken(token, expiresAt)); + })) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.AZURE_ARC); + + // test + ManagedIdentityCredentialBuilder builder + = new ManagedIdentityCredentialBuilder().configuration(configuration); + if (clientId != null) { + builder.clientId(clientId); + } else if (resourceId != null) { + builder.resourceId(resourceId); + } else { + builder.objectId(objectId); + } + + StepVerifier.create(builder.build().getToken(request)) + .expectNextMatches(accessToken -> token.equals(accessToken.getToken()) + && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) + .verifyComplete(); + assertEquals(1, identityClientMock.constructed().size()); + } } @Test public void testCloudshellUserAssigned() { // setup - String endpoint = "http://localhost"; TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); - Configuration configuration - = TestUtils.createTestConfiguration(new TestConfigurationSource().put("MSI_ENDPOINT", endpoint)); + Configuration configuration = TestUtils.createTestConfiguration(new TestConfigurationSource()); - // test - ManagedIdentityCredential credential - = new ManagedIdentityCredentialBuilder().configuration(configuration).objectId(OBJECT_ID).build(); - StepVerifier.create(credential.getToken(request)) - .expectErrorMatches(t -> t instanceof ClientAuthenticationException) - .verify(); + try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class)) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.CLOUD_SHELL); + + // test + ManagedIdentityCredential credential + = new ManagedIdentityCredentialBuilder().configuration(configuration).objectId(OBJECT_ID).build(); + StepVerifier.create(credential.getToken(request)) + .expectErrorMatches(t -> t instanceof CredentialUnavailableException + && t.getMessage().contains("User-assigned managed identity is not supported in CLOUD_SHELL")) + .verify(); + } + } + + @Test + public void testServiceFabricUserAssigned() { + // setup + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + Configuration configuration = TestUtils.createTestConfiguration(new TestConfigurationSource()); + + try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class)) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.SERVICE_FABRIC); + + // test + ManagedIdentityCredential credential + = new ManagedIdentityCredentialBuilder().configuration(configuration).resourceId(RESOURCE_ID).build(); + StepVerifier.create(credential.getToken(request)) + .expectErrorMatches(t -> t instanceof CredentialUnavailableException + && t.getMessage().contains("Service Fabric managed identity environment")) + .verify(); + } } @Test public void testInvalidIdCombination() { // setup - String resourceId = "/subscriptions/" + UUID.randomUUID() - + "/resourcegroups/aresourcegroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ident"; String objectId = "2323-sd2323s-32323-32334-34343"; // test assertThrows(IllegalStateException.class, - () -> new ManagedIdentityCredentialBuilder().clientId(CLIENT_ID).resourceId(resourceId).build()); + () -> new ManagedIdentityCredentialBuilder().clientId(CLIENT_ID).resourceId(RESOURCE_ID).build()); assertThrows(IllegalStateException.class, () -> new ManagedIdentityCredentialBuilder().clientId(CLIENT_ID) - .resourceId(resourceId) + .resourceId(RESOURCE_ID) .objectId(objectId) .build()); @@ -181,7 +238,7 @@ public void testInvalidIdCombination() { () -> new ManagedIdentityCredentialBuilder().clientId(CLIENT_ID).objectId(objectId).build()); assertThrows(IllegalStateException.class, - () -> new ManagedIdentityCredentialBuilder().resourceId(resourceId).objectId(objectId).build()); + () -> new ManagedIdentityCredentialBuilder().resourceId(RESOURCE_ID).objectId(objectId).build()); } @Test From 670f04cfe4c8e9c3e840d08d4e42a57b11a2b6af Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Thu, 10 Sep 2026 17:56:13 -0700 Subject: [PATCH 2/3] Strengthen Azure Arc managed identity tests Verify that DefaultAzureCredential forwards configured client and resource IDs to the managed identity client. Rename the reused credential result so the test does not imply access token caching. --- .../identity/DefaultAzureCredentialTest.java | 4 +-- .../identity/DefaultAzureCredentialTest.java | 26 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/DefaultAzureCredentialTest.java b/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/DefaultAzureCredentialTest.java index 8b428da4eb7e1..712815c341d30 100644 --- a/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/DefaultAzureCredentialTest.java +++ b/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/DefaultAzureCredentialTest.java @@ -67,9 +67,9 @@ public void testUseArcUserAssignedManagedIdentityCredential(String identityType) // test AccessToken firstToken = credential.getToken(request); - AccessToken cachedToken = credential.getToken(request); + AccessToken tokenFromCachedCredential = credential.getToken(request); Assertions.assertEquals(token, firstToken.getToken()); - Assertions.assertEquals(token, cachedToken.getToken()); + Assertions.assertEquals(token, tokenFromCachedCredential.getToken()); Assertions.assertEquals(expiresAt.getSecond(), firstToken.getExpiresAt().getSecond()); Assertions.assertEquals(1, managedIdentityMock.constructed().size()); verify(managedIdentityMock.constructed().get(0), times(2)).authenticate(request); diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java index bc0617166af6e..51c1684609264 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java @@ -49,6 +49,9 @@ public class DefaultAzureCredentialTest { private static final String CLIENT_ID = UUID.randomUUID().toString(); private static final String RESOURCE_ID = "/subscriptions/" + UUID.randomUUID() + "/resourcegroups/aresourcegroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ident"; + private static final int IDENTITY_CLIENT_CLIENT_ID_ARGUMENT_INDEX = 1; + private static final int IDENTITY_CLIENT_RESOURCE_ID_ARGUMENT_INDEX = 5; + private static final int IDENTITY_CLIENT_OBJECT_ID_ARGUMENT_INDEX = 6; @Test public void testUseEnvironmentCredential() { @@ -126,29 +129,29 @@ public void testUseArcUserAssignedManagedIdentityCredential(String identityType) String token = "token"; TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); - EmptyEnvironmentConfigurationSource source = new EmptyEnvironmentConfigurationSource(); - Configuration configuration = new ConfigurationBuilder(source, source, source).build(); + Configuration configuration = TestUtils.createTestConfiguration( + new TestConfigurationSource().put("AZURE_TOKEN_CREDENTIALS", "ManagedIdentityCredential")); + String clientId = "clientId".equals(identityType) ? CLIENT_ID : null; + String resourceId = "resourceId".equals(identityType) ? RESOURCE_ID : null; // mock try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class); MockedConstruction mocked = mockConstruction(IdentityClient.class, (identityClient, context) -> { - when(identityClient.authenticateWithAzureDeveloperCli(request)).thenReturn(Mono.empty()); + assertEquals(clientId, context.arguments().get(IDENTITY_CLIENT_CLIENT_ID_ARGUMENT_INDEX)); + assertEquals(resourceId, context.arguments().get(IDENTITY_CLIENT_RESOURCE_ID_ARGUMENT_INDEX)); + Assertions.assertNull(context.arguments().get(IDENTITY_CLIENT_OBJECT_ID_ARGUMENT_INDEX)); when(identityClient.authenticateWithManagedIdentityMsalClient(request)) .thenReturn(TestUtils.getMockAccessToken(token, expiresAt)); - }); - MockedConstruction intelliJCredentialMock - = mockConstruction(IntelliJCredential.class, (intelliJCredential, context) -> { - when(intelliJCredential.getToken(request)).thenReturn(Mono.empty()); })) { applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) .thenReturn(ManagedIdentitySourceType.AZURE_ARC); DefaultAzureCredentialBuilder builder = new DefaultAzureCredentialBuilder().configuration(configuration); - if ("clientId".equals(identityType)) { - builder.managedIdentityClientId(CLIENT_ID); + if (clientId != null) { + builder.managedIdentityClientId(clientId); } else { - builder.managedIdentityResourceId(RESOURCE_ID); + builder.managedIdentityResourceId(resourceId); } // test @@ -156,8 +159,7 @@ public void testUseArcUserAssignedManagedIdentityCredential(String identityType) .expectNextMatches(accessToken -> token.equals(accessToken.getToken()) && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) .verifyComplete(); - Assertions.assertNotNull(mocked); - Assertions.assertNotNull(intelliJCredentialMock); + assertEquals(1, mocked.constructed().size()); } } From 9f005389f480ffc32477e4da02d2dbe537c1c32f Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Mon, 14 Sep 2026 08:41:39 -0700 Subject: [PATCH 3/3] Align v2 Service Fabric managed identity validation Reject user-assigned identity selectors in v2 Service Fabric environments, matching the existing azure-identity behavior. Add regression coverage and record the MSAL4J dependency update for azure-identity-broker. --- sdk/identity-v2/azure-identity/CHANGELOG.md | 1 + .../identity/ManagedIdentityCredential.java | 9 +++++++++ .../ManagedIdentityCredentialTest.java | 19 +++++++++++++++++++ .../azure-identity-broker/CHANGELOG.md | 4 ++++ 4 files changed, 33 insertions(+) diff --git a/sdk/identity-v2/azure-identity/CHANGELOG.md b/sdk/identity-v2/azure-identity/CHANGELOG.md index af33a161dac29..b52c100ddf485 100644 --- a/sdk/identity-v2/azure-identity/CHANGELOG.md +++ b/sdk/identity-v2/azure-identity/CHANGELOG.md @@ -11,6 +11,7 @@ ### Bugs Fixed - Fixed `DefaultAzureCredential` failing after a credential successfully acquired a token. +- Aligned user-assigned managed identity validation for Azure Service Fabric with `azure-identity`. ### Other Changes diff --git a/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredential.java b/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredential.java index c0a0340dbfcac..de99a1beccdfa 100644 --- a/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredential.java +++ b/sdk/identity-v2/azure-identity/src/main/java/com/azure/v2/identity/ManagedIdentityCredential.java @@ -109,6 +109,15 @@ public AccessToken getToken(TokenRequestContext request) { + ". To use system-assigned managed identity, remove the configured client ID on " + "the ManagedIdentityCredentialBuilder.", CredentialUnavailableException::new); } + + if (ManagedIdentitySourceType.SERVICE_FABRIC.equals(managedIdentitySourceType)) { + throw LOGGER.throwableAtError() + .log("ManagedIdentityCredential authentication unavailable. " + + "Specifying a client ID, resource ID, or object ID is not supported by the Service Fabric " + + "managed identity environment. The managed identity configuration is determined by the " + + "Service Fabric cluster resource configuration. See https://aka.ms/servicefabricmi for more " + + "information.", CredentialUnavailableException::new); + } } try { diff --git a/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/ManagedIdentityCredentialTest.java b/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/ManagedIdentityCredentialTest.java index b9d58a3e906f6..76dc35a31457f 100644 --- a/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/ManagedIdentityCredentialTest.java +++ b/sdk/identity-v2/azure-identity/src/test/java/com/azure/v2/identity/ManagedIdentityCredentialTest.java @@ -127,4 +127,23 @@ public void testCloudShellUserAssigned() { exception.getMessage().contains("User-assigned managed identity is not supported in CLOUD_SHELL")); } } + + @Test + public void testServiceFabricUserAssigned() { + // setup + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + Configuration configuration = TestUtils.createTestConfiguration(new TestConfigurationSource()); + + try (MockedStatic applicationMock = mockStatic(ManagedIdentityApplication.class)) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.SERVICE_FABRIC); + + // test + ManagedIdentityCredential credential + = new ManagedIdentityCredentialBuilder().configuration(configuration).resourceId(RESOURCE_ID).build(); + CredentialUnavailableException exception + = Assertions.assertThrows(CredentialUnavailableException.class, () -> credential.getToken(request)); + Assertions.assertTrue(exception.getMessage().contains("Service Fabric managed identity environment")); + } + } } diff --git a/sdk/identity/azure-identity-broker/CHANGELOG.md b/sdk/identity/azure-identity-broker/CHANGELOG.md index ca560a4b7347c..8c70bc9eccbd8 100644 --- a/sdk/identity/azure-identity-broker/CHANGELOG.md +++ b/sdk/identity/azure-identity-broker/CHANGELOG.md @@ -10,6 +10,10 @@ ### Other Changes +#### Dependency Updates + +- Upgraded `msal4j` from `1.23.1` to version `1.26.0`. + ## 1.1.22 (2026-08-24) ### Other Changes