Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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:
*
* <ol>
* <li>{@link EnvironmentCredential}</li>
* <li>{@link ManagedIdentityCredential}</li>
* <li>Fails if none of the credentials above could be created.</li>
* </ol>
*/
@Immutable
public final class AzureApplicationCredential extends ChainedTokenCredential {
/**
* Creates default AzureApplicationCredential instance to use. This will use environment variables to create

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove default

* {@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<TokenCredential> tokenCredentials) {
super(tokenCredentials);
}
}
Original file line number Diff line number Diff line change
@@ -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<AzureApplicationCredentialBuilder> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this final

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.
*
* <p>
* 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.
* </p>
*
* <p> 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. </p>
*
* @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<TokenCredential> getCredentialsChain() {
ArrayList<TokenCredential> output = new ArrayList<TokenCredential>(2);
output.add(new EnvironmentCredential(identityClientOptions));
output.add(new ManagedIdentityCredential(managedIdentityClientId, identityClientOptions));
return output;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,14 +16,16 @@
* @see EnvironmentCredential
*/
public class EnvironmentCredentialBuilder extends CredentialBuilderBase<EnvironmentCredentialBuilder> {
private String authorityHost;

/**
* 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 EnvironmentCredentialBuilder authorityHost(String authorityHost) {
ValidationUtil.validateAuthHost(getClass().getSimpleName(), authorityHost);
this.identityClientOptions.setAuthorityHost(authorityHost);
this.authorityHost = authorityHost;
return this;
}

Expand All @@ -46,12 +50,30 @@ public EnvironmentCredentialBuilder executorService(ExecutorService executorServ
return this;
}

/**
* Sets the configuration store that is used during construction of the credential.
*
* The default configuration store is a clone of the {@link Configuration#getGlobalConfiguration() global
* configuration store}.
*
* @param configuration The configuration store used to load Env variables and/or properties from.
*
* @return An updated instance of this builder with the configuration store set as specified.
*/
public EnvironmentCredentialBuilder configuration(Configuration configuration) {
identityClientOptions.setConfiguration(configuration);
return this;
}

/**
* Creates a new {@link EnvironmentCredential} with the current configurations.
*
* @return a {@link EnvironmentCredential} with the current configurations.
*/
public EnvironmentCredential build() {
if (!CoreUtils.isNullOrEmpty(authorityHost)) {
identityClientOptions.setAuthorityHost(authorityHost);
}
return new EnvironmentCredential(identityClientOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,16 @@ public final class IdentityClientOptions {
private TokenCachePersistenceOptions tokenCachePersistenceOptions;
private boolean cp1Disabled;
private RegionalAuthority regionalAuthority;
private Configuration configuration;

/**
* Creates an instance of IdentityClientOptions with default settings.
*/
public IdentityClientOptions() {
Configuration configuration = Configuration.getGlobalConfiguration();
authorityHost = configuration.get(Configuration.PROPERTY_AZURE_AUTHORITY_HOST,
AzureAuthorityHosts.AZURE_PUBLIC_CLOUD);
cp1Disabled = configuration.get(Configuration.PROPERTY_AZURE_IDENTITY_DISABLE_CP1, false);
ValidationUtil.validateAuthHost(getClass().getSimpleName(), authorityHost);
loadFromConfiugration(configuration);
maxRetry = MAX_RETRY_DEFAULT_LIMIT;
retryTimeout = i -> Duration.ofSeconds((long) Math.pow(2, i.getSeconds() - 1));
regionalAuthority = RegionalAuthority.fromString(
configuration.get(Configuration.PROPERTY_AZURE_REGIONAL_AUTHORITY_NAME));
}

/**
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}