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..712815c341d30 --- /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 tokenFromCachedCredential = credential.getToken(request); + Assertions.assertEquals(token, firstToken.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-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..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 @@ -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,18 @@ 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"; + 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() { @@ -113,6 +122,47 @@ 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); + 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) -> { + 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)); + })) { + applicationMock.when(ManagedIdentityApplication::getManagedIdentitySource) + .thenReturn(ManagedIdentitySourceType.AZURE_ARC); + + DefaultAzureCredentialBuilder builder = new DefaultAzureCredentialBuilder().configuration(configuration); + if (clientId != null) { + builder.managedIdentityClientId(clientId); + } else { + builder.managedIdentityResourceId(resourceId); + } + + // test + StepVerifier.create(builder.build().getToken(request)) + .expectNextMatches(accessToken -> token.equals(accessToken.getToken()) + && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) + .verifyComplete(); + assertEquals(1, mocked.constructed().size()); + } + } + @Test public void testUseWorkloadIdentityCredentialWithManagedIdentityClientId() { // setup @@ -450,14 +500,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