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:
+ *
+ *
+ * - {@link EnvironmentCredential}
+ * - {@link ManagedIdentityCredential}
+ * - Fails if none of the credentials above could be created.
+ *
+ */
+@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();
+ }
+}