Skip to content

Commit a8d22b8

Browse files
authored
Allow DAC to select specific credentials via AZURE_TOKEN_CREDENTIALS env var. (#45864)
1 parent 98bbe48 commit a8d22b8

File tree

4 files changed

+144
-36
lines changed

4 files changed

+144
-36
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
### Features Added
77

88
- **Visual Studio Code Credential** has been re-enabled and now supports **broker authentication** using the Azure account signed in via Visual Studio Code. [#45715](https://github.com/Azure/azure-sdk-for-java/pull/45715)
9+
- `DefaultAzureCredential` can be configured to use a specific credential type by setting the `AZURE_TOKEN_CREDENTIALS` environment variable. When set, it will only attempt authentication using the specified credential type. For example, setting `AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential` will restrict authentication to workload identity only.
910

1011
### Breaking Changes
1112

sdk/identity/azure-identity/TROUBLESHOOTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@ The underlying MSAL library, MSAL4J, also has detailed logging. It is highly ver
8080
8181
## Troubleshoot `DefaultAzureCredential` authentication issues
8282

83-
| Error |Description| Mitigation |
83+
| Error | Description | Mitigation |
8484
|---|---|---|
85-
|`CredentialUnavailableException` raised with message. "DefaultAzureCredential failed to retrieve a token from the included credentials."|All credentials in the `DefaultAzureCredential` chain failed to retrieve a token, each throwing a `CredentialUnavailableException`| <ul><li>[Enable logging](#enable-and-configure-logging) to verify the credentials being tried, and get further diagnostic information.</li><li>Consult the troubleshooting guide for underlying credential types for more information.</li><ul><li>[EnvironmentCredential](#troubleshoot-environmentcredential-authentication-issues)</li><li>[ManagedIdentityCredential](#troubleshoot-managedidentitycredential-authentication-issues)</li><li>[AzureCLICredential](#troubleshoot-azureclicredential-authentication-issues)</li><li>[AzurePowershellCredential](#troubleshoot-azurepowershellcredential-authentication-issues)</li></ul> |
86-
|`HttpResponseException` raised from the client with a status code of 401 or 403|Authentication succeeded but the authorizing Azure service responded with a 401 (Authenticate), or 403 (Forbidden) status code. This can often be caused by the `DefaultAzureCredential` authenticating an account other than the intended or that the intended account does not have the correct permissions or roles assigned.| <ul><li>[Enable logging](#enable-and-configure-logging) to determine which credential in the chain returned the authenticating token.</li><li>In the case a credential other than the expected is returning a token, look too bypass this by signing out of the corresponding development tool.`</li><li>Ensure that the correct role is assigned to the account being used. For example, a service specific role rather than the subscription Owner role.</li></ul> |
85+
| `CredentialUnavailableException` raised with message. "DefaultAzureCredential failed to retrieve a token from the included credentials." |All credentials in the `DefaultAzureCredential` chain failed to retrieve a token, each throwing a `CredentialUnavailableException`| <ul><li>[Enable logging](#enable-and-configure-logging) to verify the credentials being tried, and get further diagnostic information.</li><li>Consult the troubleshooting guide for underlying credential types for more information.</li><ul><li>[EnvironmentCredential](#troubleshoot-environmentcredential-authentication-issues)</li><li>[ManagedIdentityCredential](#troubleshoot-managedidentitycredential-authentication-issues)</li><li>[AzureCLICredential](#troubleshoot-azureclicredential-authentication-issues)</li><li>[AzurePowershellCredential](#troubleshoot-azurepowershellcredential-authentication-issues)</li></ul> |
86+
| `HttpResponseException` raised from the client with a status code of 401 or 403 |Authentication succeeded but the authorizing Azure service responded with a 401 (Authenticate), or 403 (Forbidden) status code. This can often be caused by the `DefaultAzureCredential` authenticating an account other than the intended or that the intended account does not have the correct permissions or roles assigned.| <ul><li>[Enable logging](#enable-and-configure-logging) to determine which credential in the chain returned the authenticating token.</li><li>In the case a credential other than the expected is returning a token, look too bypass this by signing out of the corresponding development tool.`</li><li>Ensure that the correct role is assigned to the account being used. For example, a service specific role rather than the subscription Owner role.</li></ul> |
87+
| `IllegalArgumentException` raised with message "Invalid value for AZURE_TOKEN_CREDENTIALS..." | The value provided in env var `AZURE_TOKEN_CREDENTIALS` doesn't map to one of `prod`, `dev`, or a specific credential name such as `EnvironmentCredential`, `ManagedIdentityCredential`, etc. | Ensure you specify a valid value as per your application's requirements. Specifying `prod` activates production environment credentials (`EnvironmentCredential`, `WorkloadIdentityCredential`, and `ManagedIdentityCredential`). Specifying `dev` activates development tool credentials (`IntelliJCredential`, `AzureCliCredential`, `AzurePowershellCredential`, `AzureDeveloperCliCredential`, and `VisualStudioCodeCredential`). Specifying a specific credential name targets that individual credential only as part of `DefaultAzureCredential`. |
8788

8889
## Troubleshoot `EnvironmentCredential` authentication issues
8990
`CredentialUnavailableException`

sdk/identity/azure-identity/src/main/java/com/azure/identity/DefaultAzureCredentialBuilder.java

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
import com.azure.identity.implementation.util.IdentityUtil;
1212

1313
import java.time.Duration;
14-
import java.util.ArrayList;
1514
import java.util.Arrays;
15+
import java.util.ArrayList;
1616
import java.util.List;
17+
import java.util.Locale;
1718
import java.util.Objects;
1819
import java.util.concurrent.ExecutorService;
1920

@@ -272,44 +273,73 @@ private ArrayList<TokenCredential> getCredentialsChain() {
272273
Configuration configuration = identityClientOptions.getConfiguration() == null
273274
? Configuration.getGlobalConfiguration().clone()
274275
: identityClientOptions.getConfiguration();
275-
String selectedCredentials = configuration.get("AZURE_TOKEN_CREDENTIALS");
276-
boolean useProductionCredentials = false;
277-
boolean useDeveloperCredentials = false;
278-
if (!CoreUtils.isNullOrEmpty(selectedCredentials)) {
279-
selectedCredentials = selectedCredentials.trim();
280-
if ("prod".equalsIgnoreCase(selectedCredentials)) {
281-
useProductionCredentials = true;
282-
} else if ("dev".equalsIgnoreCase(selectedCredentials)) {
283-
useDeveloperCredentials = true;
276+
277+
String selectedCredential = configuration.get("AZURE_TOKEN_CREDENTIALS");
278+
ArrayList<TokenCredential> credentials = new ArrayList<>(8);
279+
280+
if (!CoreUtils.isNullOrEmpty(selectedCredential)) {
281+
selectedCredential = selectedCredential.trim().toLowerCase(Locale.ROOT);
282+
283+
// Use a map to associate credential names to their adders
284+
java.util.Map<String, Runnable> credentialMap = new java.util.HashMap<>();
285+
credentialMap.put("prod", () -> addProdCredentials(credentials));
286+
credentialMap.put("dev", () -> addDevCredentials(credentials));
287+
credentialMap.put("environmentcredential",
288+
() -> credentials.add(new EnvironmentCredential(identityClientOptions.clone())));
289+
credentialMap.put("workloadidentitycredential", () -> credentials.add(getWorkloadIdentityCredential()));
290+
credentialMap.put("managedidentitycredential",
291+
() -> credentials.add(new ManagedIdentityCredential(managedIdentityClientId, managedIdentityResourceId,
292+
null, identityClientOptions.clone())));
293+
credentialMap.put("intellijcredential",
294+
() -> credentials.add(new IntelliJCredential(tenantId, identityClientOptions.clone())));
295+
credentialMap.put("azureclicredential",
296+
() -> credentials.add(new AzureCliCredential(tenantId, identityClientOptions.clone())));
297+
credentialMap.put("azurepowershellcredential",
298+
() -> credentials.add(new AzurePowerShellCredential(tenantId, identityClientOptions.clone())));
299+
credentialMap.put("azuredeveloperclicredential",
300+
() -> credentials.add(new AzureDeveloperCliCredential(tenantId, identityClientOptions.clone())));
301+
credentialMap.put("visualstudiocodecredential",
302+
() -> credentials.add(new VisualStudioCodeCredential(tenantId, identityClientOptions.clone())));
303+
304+
Runnable adder = credentialMap.get(selectedCredential);
305+
if (adder != null) {
306+
adder.run();
307+
return credentials;
284308
} else {
285-
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
286-
"Invalid value for AZURE_TOKEN_CREDENTIALS. Valid values are 'prod' or 'dev'."));
309+
throw LOGGER
310+
.logExceptionAsError(new IllegalArgumentException("Invalid value for AZURE_TOKEN_CREDENTIALS: '"
311+
+ selectedCredential + "'. " + "Valid values are: 'prod', 'dev', or one of "
312+
+ "[EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential, "
313+
+ "IntelliJCredential, AzureCliCredential, AzurePowerShellCredential, "
314+
+ "AzureDeveloperCliCredential, VisualStudioCodeCredential] (case-insensitive). "
315+
+ "To mitigate this issue, please refer to the troubleshooting guidelines here at "
316+
+ "https://aka.ms/azure-identity-java-default-azure-credential-troubleshoot"));
287317
}
288318
}
289-
if (!useProductionCredentials && !useDeveloperCredentials) {
290-
useProductionCredentials = true;
291-
useDeveloperCredentials = true;
292-
}
293319

294-
ArrayList<TokenCredential> output = new ArrayList<TokenCredential>(7);
295-
if (useProductionCredentials) {
296-
output.add(new EnvironmentCredential(identityClientOptions.clone()));
297-
output.add(getWorkloadIdentityCredential());
298-
output.add(new ManagedIdentityCredential(managedIdentityClientId, managedIdentityResourceId, null,
299-
identityClientOptions.clone()));
300-
}
320+
// Default case: full chain (prod + dev)
321+
addProdCredentials(credentials);
322+
addDevCredentials(credentials);
323+
return credentials;
324+
}
301325

302-
if (useDeveloperCredentials) {
303-
output.add(new IntelliJCredential(tenantId, identityClientOptions.clone()));
304-
output.add(new AzureCliCredential(tenantId, identityClientOptions.clone()));
305-
output.add(new AzurePowerShellCredential(tenantId, identityClientOptions.clone()));
306-
output.add(new AzureDeveloperCliCredential(tenantId, identityClientOptions.clone()));
307-
if (IdentityUtil.isVsCodeBrokerAuthAvailable()) {
308-
output.add(new VisualStudioCodeCredential(tenantId, identityClientOptions.clone()));
309-
}
310-
}
326+
// Helper to add prod credentials
327+
private void addProdCredentials(List<TokenCredential> credentials) {
328+
credentials.add(new EnvironmentCredential(identityClientOptions.clone()));
329+
credentials.add(getWorkloadIdentityCredential());
330+
credentials.add(new ManagedIdentityCredential(managedIdentityClientId, managedIdentityResourceId, null,
331+
identityClientOptions.clone()));
332+
}
311333

312-
return output;
334+
// Helper to add dev credentials
335+
private void addDevCredentials(List<TokenCredential> credentials) {
336+
credentials.add(new IntelliJCredential(tenantId, identityClientOptions.clone()));
337+
credentials.add(new AzureCliCredential(tenantId, identityClientOptions.clone()));
338+
credentials.add(new AzurePowerShellCredential(tenantId, identityClientOptions.clone()));
339+
credentials.add(new AzureDeveloperCliCredential(tenantId, identityClientOptions.clone()));
340+
if (IdentityUtil.isVsCodeBrokerAuthAvailable()) {
341+
credentials.add(new VisualStudioCodeCredential(tenantId, identityClientOptions.clone()));
342+
}
313343
}
314344

315345
private WorkloadIdentityCredential getWorkloadIdentityCredential() {

sdk/identity/azure-identity/src/test/java/com/azure/identity/DefaultAzureCredentialTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.time.ZoneOffset;
2828
import java.util.ArrayList;
2929
import java.util.List;
30+
import java.util.Locale;
3031
import java.util.UUID;
3132

3233
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -667,6 +668,81 @@ public void testDeveloperOnlyCredentialsChain(String devValue) {
667668
assertInstanceOf(AzureDeveloperCliCredential.class, credentials.get(3));
668669
}
669670

671+
@ParameterizedTest
672+
@ValueSource(
673+
strings = {
674+
"AzureCliCredential",
675+
"azureclicredential",
676+
"AZURECLICREDENTIAL",
677+
"IntelliJCredential",
678+
"intellijcredential",
679+
"AzurePowerShellCredential",
680+
"azurepowershellcredential",
681+
"AzureDeveloperCliCredential",
682+
"azuredeveloperclicredential",
683+
"EnvironmentCredential",
684+
"environmentcredential",
685+
"WorkloadIdentityCredential",
686+
"workloadidentitycredential",
687+
"ManagedIdentityCredential",
688+
"managedidentitycredential",
689+
"VisualStudioCodeCredential",
690+
"visualstudiocodecredential" })
691+
public void testTargetedCredentialSelection(String credentialValue) {
692+
// Setup config with targeted credential value (case-insensitive)
693+
TestConfigurationSource configSource
694+
= new TestConfigurationSource().put("AZURE_TOKEN_CREDENTIALS", credentialValue);
695+
Configuration configuration = TestUtils.createTestConfiguration(configSource);
696+
697+
// Build the credential with the test configuration
698+
DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().configuration(configuration).build();
699+
List<TokenCredential> credentials = extractCredentials(credential);
700+
701+
// Should contain exactly one credential
702+
assertEquals(1, credentials.size());
703+
704+
// Assert that the only credential matches expected type
705+
Class<? extends TokenCredential> expectedType;
706+
switch (credentialValue.toLowerCase(Locale.ROOT)) {
707+
case "azureclicredential":
708+
expectedType = AzureCliCredential.class;
709+
break;
710+
711+
case "intellijcredential":
712+
expectedType = IntelliJCredential.class;
713+
break;
714+
715+
case "azurepowershellcredential":
716+
expectedType = AzurePowerShellCredential.class;
717+
break;
718+
719+
case "azuredeveloperclicredential":
720+
expectedType = AzureDeveloperCliCredential.class;
721+
break;
722+
723+
case "environmentcredential":
724+
expectedType = EnvironmentCredential.class;
725+
break;
726+
727+
case "workloadidentitycredential":
728+
expectedType = WorkloadIdentityCredential.class;
729+
break;
730+
731+
case "managedidentitycredential":
732+
expectedType = ManagedIdentityCredential.class;
733+
break;
734+
735+
case "visualstudiocodecredential":
736+
expectedType = VisualStudioCodeCredential.class;
737+
break;
738+
739+
default:
740+
throw new IllegalArgumentException("Unsupported test value: " + credentialValue);
741+
}
742+
743+
assertInstanceOf(expectedType, credentials.get(0));
744+
}
745+
670746
@ParameterizedTest
671747
@ValueSource(strings = { "invalid", "PRODUCTION", "DEVELOPER", "both", "p r o d", "d e v" })
672748
public void testInvalidCredentialsConfiguration(String configValue) {

0 commit comments

Comments
 (0)