diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureApplicationCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureApplicationCredential.java new file mode 100644 index 000000000000..87b8db33e205 --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureApplicationCredential.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity; + +import com.azure.core.annotation.Immutable; +import com.azure.core.credential.TokenCredential; + +import java.util.List; + +/** + * Creates a credential using environment variables for Azure hosted Environments. It tries to create a valid credential + * in the following order: + * + *
    + *
  1. {@link EnvironmentCredential}
  2. + *
  3. {@link ManagedIdentityCredential}
  4. + *
  5. Fails if none of the credentials above could be created.
  6. + *
+ */ +@Immutable +public final class AzureApplicationCredential extends ChainedTokenCredential { + /** + * Creates default AzureApplicationCredential instance to use. This will use environment variables to create + * {@link EnvironmentCredential} + * + * If these environment variables are not available, then this will attempt to use Managed Identity Authentication + * via {@link ManagedIdentityCredential}. + * + * @param tokenCredentials the list of credentials to execute for authentication. + */ + AzureApplicationCredential(List tokenCredentials) { + super(tokenCredentials); + } +} diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureApplicationCredentialBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureApplicationCredentialBuilder.java new file mode 100644 index 000000000000..e6b250882725 --- /dev/null +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/AzureApplicationCredentialBuilder.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.util.Configuration; + +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; + +/** + * Fluent credential builder for instantiating a {@link AzureApplicationCredential}. + * + * @see AzureApplicationCredential + */ +public class AzureApplicationCredentialBuilder extends CredentialBuilderBase { + private String managedIdentityClientId; + + /** + * Creates an instance of a AzureApplicationCredentialBuilder. + */ + public AzureApplicationCredentialBuilder() { + Configuration configuration = Configuration.getGlobalConfiguration().clone(); + managedIdentityClientId = configuration.get(Configuration.PROPERTY_AZURE_CLIENT_ID); + } + + /** + * Specifies the Azure Active Directory endpoint to acquire tokens. + * @param authorityHost the Azure Active Directory endpoint + * @return An updated instance of this builder with the authority host set as specified. + */ + public AzureApplicationCredentialBuilder authorityHost(String authorityHost) { + this.identityClientOptions.setAuthorityHost(authorityHost); + return this; + } + + + /** + * Specifies the client ID of user assigned or system assigned identity, when this credential is running + * in an environment with managed identities. If unset, the value in the AZURE_CLIENT_ID environment variable + * will be used. If neither is set, the default value is null and will only work with system assigned + * managed identities and not user assigned managed identities. + * + * @param clientId the client ID + * @return An updated instance of this builder with the managed identity client id set as specified. + */ + public AzureApplicationCredentialBuilder managedIdentityClientId(String clientId) { + this.managedIdentityClientId = clientId; + return this; + } + + /** + * Specifies the ExecutorService to be used to execute the authentication requests. + * Developer is responsible for maintaining the lifecycle of the ExecutorService. + * + *

+ * If this is not configured, the {@link ForkJoinPool#commonPool()} will be used which is + * also shared with other application tasks. If the common pool is heavily used for other tasks, authentication + * requests might starve and setting up this executor service should be considered. + *

+ * + *

The executor service and can be safely shutdown if the TokenCredential is no longer being used by the + * Azure SDK clients and should be shutdown before the application exits.

+ * + * @param executorService the executor service to use for executing authentication requests. + * @return An updated instance of this builder with the executor service set as specified. + */ + public AzureApplicationCredentialBuilder executorService(ExecutorService executorService) { + this.identityClientOptions.setExecutorService(executorService); + return this; + } + + /** + * Creates new {@link AzureApplicationCredential} with the configured options set. + * + * @return a {@link AzureApplicationCredential} with the current configurations. + */ + public AzureApplicationCredential build() { + return new AzureApplicationCredential(getCredentialsChain()); + } + + private ArrayList getCredentialsChain() { + ArrayList output = new ArrayList(2); + output.add(new EnvironmentCredential(identityClientOptions)); + output.add(new ManagedIdentityCredential(managedIdentityClientId, identityClientOptions)); + return output; + } +} + diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java index 2387ccf1978c..ddedd404519d 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredential.java @@ -47,7 +47,8 @@ public class EnvironmentCredential implements TokenCredential { * @param identityClientOptions the options for configuring the identity client */ EnvironmentCredential(IdentityClientOptions identityClientOptions) { - this.configuration = Configuration.getGlobalConfiguration().clone(); + this.configuration = identityClientOptions.getConfiguration() == null + ? Configuration.getGlobalConfiguration().clone() : identityClientOptions.getConfiguration(); TokenCredential targetCredential = null; String clientId = configuration.get(Configuration.PROPERTY_AZURE_CLIENT_ID); diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredentialBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredentialBuilder.java index 2f7fa1184683..ae41633ad224 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredentialBuilder.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/EnvironmentCredentialBuilder.java @@ -3,6 +3,8 @@ package com.azure.identity; +import com.azure.core.util.Configuration; +import com.azure.core.util.CoreUtils; import com.azure.identity.implementation.util.ValidationUtil; import java.util.concurrent.ExecutorService; @@ -14,6 +16,8 @@ * @see EnvironmentCredential */ public class EnvironmentCredentialBuilder extends CredentialBuilderBase { + private String authorityHost; + /** * Specifies the Azure Active Directory endpoint to acquire tokens. * @param authorityHost the Azure Active Directory endpoint @@ -21,7 +25,7 @@ public class EnvironmentCredentialBuilder extends CredentialBuilderBase Duration.ofSeconds((long) Math.pow(2, i.getSeconds() - 1)); - regionalAuthority = RegionalAuthority.fromString( - configuration.get(Configuration.PROPERTY_AZURE_REGIONAL_AUTHORITY_NAME)); } /** @@ -328,4 +324,41 @@ public IdentityClientOptions setRegionalAuthority(RegionalAuthority regionalAuth public RegionalAuthority getRegionalAuthority() { return regionalAuthority; } + + + /** + * Sets the specified configuration store. + * + * @param configuration the configuration store to be used to read env variables and/or system properties. + * @return the updated identity client options + */ + public IdentityClientOptions setConfiguration(Configuration configuration) { + this.configuration = configuration; + loadFromConfiugration(configuration); + return this; + } + + /** + * Gets the configured configuration store. + * + * @return the configured {@link Configuration} store. + */ + public Configuration getConfiguration() { + return this.configuration; + } + + /** + * Loads the details from the specified Configuration Store. + * + * @return the regional authority value if specified + */ + private IdentityClientOptions loadFromConfiugration(Configuration configuration) { + authorityHost = configuration.get(Configuration.PROPERTY_AZURE_AUTHORITY_HOST, + AzureAuthorityHosts.AZURE_PUBLIC_CLOUD); + ValidationUtil.validateAuthHost(getClass().getSimpleName(), authorityHost); + cp1Disabled = configuration.get(Configuration.PROPERTY_AZURE_IDENTITY_DISABLE_CP1, false); + regionalAuthority = RegionalAuthority.fromString( + configuration.get(Configuration.PROPERTY_AZURE_REGIONAL_AUTHORITY_NAME)); + return this; + } } diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/AzureApplicationCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/AzureApplicationCredentialTest.java new file mode 100644 index 000000000000..56c0031507ae --- /dev/null +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/AzureApplicationCredentialTest.java @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.identity; + +import com.azure.core.credential.TokenRequestContext; +import com.azure.core.util.Configuration; +import com.azure.identity.implementation.IdentityClient; +import com.azure.identity.util.TestUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(fullyQualifiedNames = "com.azure.identity.*") +@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.net.ssl.*", + "io.netty.handler.ssl.*", "io.netty.buffer.*", "io.netty.channel.*"}) +public class AzureApplicationCredentialTest { + + private static final String TENANT_ID = "contoso.com"; + private static final String CLIENT_ID = UUID.randomUUID().toString(); + + @Test + public void testUseEnvironmentCredential() throws Exception { + Configuration configuration = Configuration.getGlobalConfiguration(); + + try { + // setup + String secret = "secret"; + String token1 = "token1"; + TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com"); + OffsetDateTime expiresOn = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); + configuration.put("AZURE_CLIENT_ID", CLIENT_ID); + configuration.put("AZURE_CLIENT_SECRET", secret); + configuration.put("AZURE_TENANT_ID", TENANT_ID); + + // mock + IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); + when(identityClient.authenticateWithConfidentialClientCache(any())).thenReturn(Mono.empty()); + when(identityClient.authenticateWithConfidentialClient(request1)).thenReturn(TestUtils.getMockAccessToken(token1, expiresOn)); + PowerMockito.whenNew(IdentityClient.class).withArguments(eq(TENANT_ID), eq(CLIENT_ID), eq(secret), isNull(), isNull(), isNull(), eq(false), any()).thenReturn(identityClient); + + // test + AzureApplicationCredential credential = new AzureApplicationCredentialBuilder().build(); + StepVerifier.create(credential.getToken(request1)) + .expectNextMatches(accessToken -> token1.equals(accessToken.getToken()) + && expiresOn.getSecond() == accessToken.getExpiresAt().getSecond()) + .verifyComplete(); + } finally { + // clean up + configuration.remove("AZURE_CLIENT_ID"); + configuration.remove("AZURE_CLIENT_SECRET"); + configuration.remove("AZURE_TENANT_ID"); + } + } + + @Test + public void testUseManagedIdentityCredential() throws Exception { + // setup + String token1 = "token1"; + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); + + // mock + IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); + when(identityClient.authenticateToIMDSEndpoint(request)).thenReturn(TestUtils.getMockAccessToken(token1, expiresAt)); + PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); + + IntelliJCredential intelliJCredential = PowerMockito.mock(IntelliJCredential.class); + when(intelliJCredential.getToken(request)) + .thenReturn(Mono.empty()); + PowerMockito.whenNew(IntelliJCredential.class).withAnyArguments() + .thenReturn(intelliJCredential); + + // test + AzureApplicationCredential credential = new AzureApplicationCredentialBuilder().build(); + StepVerifier.create(credential.getToken(request)) + .expectNextMatches(accessToken -> token1.equals(accessToken.getToken()) + && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) + .verifyComplete(); + } + + @Test + public void testNoCredentialWorks() throws Exception { + // setup + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + + // mock + IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); + when(identityClient.authenticateToIMDSEndpoint(request)) + .thenReturn(Mono.error(new CredentialUnavailableException("Cannot get token from managed identity"))); + PowerMockito.whenNew(IdentityClient.class).withAnyArguments() + .thenReturn(identityClient); + + // test + AzureApplicationCredential credential = new AzureApplicationCredentialBuilder().build(); + StepVerifier.create(credential.getToken(request)) + .expectErrorMatches(t -> t instanceof CredentialUnavailableException && t.getMessage() + .startsWith("EnvironmentCredential authentication unavailable. ")) + .verify(); + } + + @Test + public void testCredentialUnavailable() throws Exception { + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com"); + + ManagedIdentityCredential managedIdentityCredential = PowerMockito.mock(ManagedIdentityCredential.class); + when(managedIdentityCredential.getToken(request)) + .thenReturn(Mono.error( + new CredentialUnavailableException("Cannot get token from Managed Identity credential"))); + PowerMockito.whenNew(ManagedIdentityCredential.class).withAnyArguments() + .thenReturn(managedIdentityCredential); + + // test + AzureApplicationCredential credential = new AzureApplicationCredentialBuilder() + .build(); + StepVerifier.create(credential.getToken(request)) + .expectErrorMatches(t -> t instanceof CredentialUnavailableException && t.getMessage() + .startsWith("EnvironmentCredential authentication unavailable. ")) + .verify(); + } +}