Skip to content

Commit 81b099e

Browse files
authored
Add API to enforce Env Vars in DAC (Azure#46595)
1 parent 80e20eb commit 81b099e

File tree

5 files changed

+240
-1
lines changed

5 files changed

+240
-1
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- Added claims challenge handling support to `AzureCliCredential`. When a token request includes claims, the credential will now throw a `CredentialUnavailableException` with instructions to use Azure PowerShell directly with the appropriate `-ClaimsChallenge` parameter.
66
- Added claims challenge handling support to `AzurePowerShellCredential`. When a token request includes claims, the credential will now throw a `CredentialUnavailableException` with instructions to use Azure PowerShell directly with the appropriate `-ClaimsChallenge` parameter.
7+
- Added `AzureIdentityEnvVars` expandable string enum for type-safe environment variable names used in Azure Identity credentials.
8+
- Added `requireEnvVars(AzureIdentityEnvVars... envVars)` method to `DefaultAzureCredentialBuilder` to enforce the presence of specific environment variables at build time. When configured, the credential will throw an `IllegalStateException` during `build()` if any of the specified environment variables are missing or empty.
79

810
### Features Added
911

@@ -1111,4 +1113,4 @@ for more details. User authentication will be added in an upcoming preview
11111113
release.
11121114

11131115
This release supports only global Microsoft Entra tenants, i.e. those
1114-
using the https://login.microsoftonline.com authentication endpoint.
1116+
using the https://login.microsoftonline.com authentication endpoint.

sdk/identity/azure-identity/TROUBLESHOOTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ The underlying MSAL library, MSAL4J, also has detailed logging. It is highly ver
8787
| `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> |
8888
| `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> |
8989
| `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`. |
90+
| `IllegalStateException` raised with message "Required environment variables are missing: [variable names]" | The `DefaultAzureCredential` was configured to require specific environment variables using `requireEnvVars(AzureIdentityEnvVars...)`, but one or more of those variables are not set or are empty. | <ul><li>Set the missing environment variables listed in the error message.</li><li>Ensure environment variables are set **prior to application startup**.</li><li>Verify variable names are spelled correctly and match exactly what was specified in `requireEnvVars()`. When using predefined `AzureIdentityEnvVars` constants, ensure the correct enum values are used. When using custom environment variables, ensure they were created with `AzureIdentityEnvVars.fromString()`.</li><li>Check that variables are not set to empty strings or null values.</li><li>If using containerized environments, ensure environment variables are properly passed to the container.</li></ul> |
91+
9092

9193
## Troubleshoot `EnvironmentCredential` authentication issues
9294
`CredentialUnavailableException`
@@ -376,3 +378,4 @@ You may also log in another MSA account by selecting "Microsoft account":
376378
## Get additional help
377379

378380
Additional information on ways to reach out for support can be found in the [SUPPORT.md](https://github.com/Azure/azure-sdk-for-java/blob/main/SUPPORT.md) at the root of the repo.
381+
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.identity;
5+
6+
import com.azure.core.util.ExpandableStringEnum;
7+
8+
import java.util.Collection;
9+
10+
/**
11+
* <p>Defines well-known Azure Identity environment variable names that can be used with
12+
* {@link DefaultAzureCredentialBuilder#requireEnvVars(AzureIdentityEnvVars...)}.</p>
13+
*
14+
* <p>This expandable enum provides a type-safe way to reference common Azure Identity environment
15+
* variables while still allowing for custom environment variable names.</p>
16+
*
17+
* @see DefaultAzureCredentialBuilder#requireEnvVars(AzureIdentityEnvVars...)
18+
*/
19+
public final class AzureIdentityEnvVars extends ExpandableStringEnum<AzureIdentityEnvVars> {
20+
21+
/**
22+
* Creates a new instance of {@link AzureIdentityEnvVars} without a {@link #toString()} value.
23+
* <p>
24+
* This constructor shouldn't be called as it will produce a {@link AzureIdentityEnvVars} which doesn't have a String enum
25+
* value.
26+
*
27+
* @deprecated Use one of the constants or the {@link #fromString(String)} factory method.
28+
*/
29+
@Deprecated
30+
public AzureIdentityEnvVars() {
31+
}
32+
33+
/**
34+
* The Azure tenant ID environment variable.
35+
*/
36+
public static final AzureIdentityEnvVars AZURE_TENANT_ID = fromString("AZURE_TENANT_ID");
37+
38+
/**
39+
* The Azure client ID environment variable.
40+
*/
41+
public static final AzureIdentityEnvVars AZURE_CLIENT_ID = fromString("AZURE_CLIENT_ID");
42+
43+
/**
44+
* The Azure client secret environment variable.
45+
*/
46+
public static final AzureIdentityEnvVars AZURE_CLIENT_SECRET = fromString("AZURE_CLIENT_SECRET");
47+
48+
/**
49+
* The Azure client certificate path environment variable.
50+
*/
51+
public static final AzureIdentityEnvVars AZURE_CLIENT_CERTIFICATE_PATH
52+
= fromString("AZURE_CLIENT_CERTIFICATE_PATH");
53+
54+
/**
55+
* The Azure client certificate password environment variable.
56+
*/
57+
public static final AzureIdentityEnvVars AZURE_CLIENT_CERTIFICATE_PASSWORD
58+
= fromString("AZURE_CLIENT_CERTIFICATE_PASSWORD");
59+
60+
/**
61+
* The Azure authority host environment variable.
62+
*/
63+
public static final AzureIdentityEnvVars AZURE_AUTHORITY_HOST = fromString("AZURE_AUTHORITY_HOST");
64+
65+
/**
66+
* The Azure token credentials environment variable for selecting credential types.
67+
*/
68+
public static final AzureIdentityEnvVars AZURE_TOKEN_CREDENTIALS = fromString("AZURE_TOKEN_CREDENTIALS");
69+
70+
/**
71+
* The Azure client send certificate chain environment variable.
72+
*/
73+
public static final AzureIdentityEnvVars AZURE_CLIENT_SEND_CERTIFICATE_CHAIN
74+
= fromString("AZURE_CLIENT_SEND_CERTIFICATE_CHAIN");
75+
76+
/**
77+
* Creates or finds an AzureIdentityEnvVars from its string representation.
78+
*
79+
* @param name a name to look for.
80+
* @return the corresponding AzureIdentityEnvVars.
81+
*/
82+
public static AzureIdentityEnvVars fromString(String name) {
83+
if (name == null) {
84+
return null;
85+
}
86+
return fromString(name, AzureIdentityEnvVars.class);
87+
}
88+
89+
/**
90+
* Gets known AzureIdentityEnvVars values.
91+
*
92+
* @return known AzureIdentityEnvVars values.
93+
*/
94+
public static Collection<AzureIdentityEnvVars> values() {
95+
return values(AzureIdentityEnvVars.class);
96+
}
97+
}

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class DefaultAzureCredentialBuilder extends CredentialBuilderBase<Default
6060
private String managedIdentityResourceId;
6161
private List<String> additionallyAllowedTenants
6262
= IdentityUtil.getAdditionalTenantsFromEnvironment(Configuration.getGlobalConfiguration().clone());
63+
private AzureIdentityEnvVars[] requiredEnvVars;
6364

6465
/**
6566
* Creates an instance of a DefaultAzureCredentialBuilder.
@@ -238,6 +239,29 @@ public DefaultAzureCredentialBuilder disableInstanceDiscovery() {
238239
return this;
239240
}
240241

242+
/**
243+
* Specifies environment variables that must be present when building the credential.
244+
* If any of the specified environment variables are missing, {@link #build()} will throw an
245+
* {@link IllegalStateException}.
246+
*
247+
* @param envVars the environment variables that must be present
248+
* @return An updated instance of this builder with the required environment variables set as specified.
249+
*/
250+
public DefaultAzureCredentialBuilder requireEnvVars(AzureIdentityEnvVars... envVars) {
251+
Objects.requireNonNull(envVars, "envVars cannot be null");
252+
253+
// Check for null elements in the array
254+
for (int i = 0; i < envVars.length; i++) {
255+
if (envVars[i] == null) {
256+
throw LOGGER.logExceptionAsError(
257+
new IllegalArgumentException("Environment variable at index " + i + " cannot be null"));
258+
}
259+
}
260+
261+
this.requiredEnvVars = envVars.clone();
262+
return this;
263+
}
264+
241265
/**
242266
* Creates new {@link DefaultAzureCredential} with the configured options set.
243267
*
@@ -252,6 +276,34 @@ public DefaultAzureCredential build() {
252276
"Only one of managedIdentityClientId and managedIdentityResourceId can be specified."));
253277
}
254278

279+
// Check required environment variables
280+
if (requiredEnvVars != null && requiredEnvVars.length > 0) {
281+
Configuration configuration = identityClientOptions.getConfiguration() == null
282+
? Configuration.getGlobalConfiguration().clone()
283+
: identityClientOptions.getConfiguration();
284+
285+
List<String> missingVars = new ArrayList<>();
286+
for (AzureIdentityEnvVars envVar : requiredEnvVars) {
287+
if (CoreUtils.isNullOrEmpty(configuration.get(envVar.toString()))) {
288+
missingVars.add(envVar.toString());
289+
}
290+
}
291+
292+
if (!missingVars.isEmpty()) {
293+
String errorMessage;
294+
if (missingVars.size() == 1) {
295+
errorMessage = "Required environment variable is missing: " + missingVars.get(0)
296+
+ ". Ensure this environment variable is set before creating the DefaultAzureCredential.";
297+
} else {
298+
errorMessage = "Required environment variables are missing: " + String.join(", ", missingVars)
299+
+ ". Ensure these environment variables are set before creating the DefaultAzureCredential.";
300+
}
301+
302+
throw LOGGER.logExceptionAsError(new IllegalStateException(errorMessage
303+
+ " See https://aka.ms/azsdk/java/identity/defaultazurecredential/troubleshoot for more information."));
304+
}
305+
}
306+
255307
if (!CoreUtils.isNullOrEmpty(additionallyAllowedTenants)) {
256308
identityClientOptions.setAdditionallyAllowedTenants(additionallyAllowedTenants);
257309
}

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.UUID;
3232

3333
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
import static org.junit.jupiter.api.Assertions.assertFalse;
3435
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
3536
import static org.junit.jupiter.api.Assertions.assertThrows;
3637
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -811,4 +812,88 @@ private List<TokenCredential> extractCredentials(DefaultAzureCredential credenti
811812
throw new RuntimeException("Failed to extract credentials", e);
812813
}
813814
}
815+
816+
@Test
817+
public void testRequireEnvVarsSuccess() {
818+
// Setup - create configuration with required environment variables present
819+
TestConfigurationSource configSource = new TestConfigurationSource().put("AZURE_CLIENT_ID", CLIENT_ID)
820+
.put("AZURE_TENANT_ID", TENANT_ID)
821+
.put("AZURE_CLIENT_SECRET", "test-secret");
822+
Configuration configuration = TestUtils.createTestConfiguration(configSource);
823+
824+
// Test - should not throw when all required env vars are present
825+
DefaultAzureCredential credential
826+
= new DefaultAzureCredentialBuilder()
827+
.requireEnvVars(AzureIdentityEnvVars.AZURE_CLIENT_ID, AzureIdentityEnvVars.AZURE_TENANT_ID,
828+
AzureIdentityEnvVars.AZURE_CLIENT_SECRET)
829+
.configuration(configuration)
830+
.build();
831+
832+
// Verify the credential was created successfully
833+
Assertions.assertNotNull(credential);
834+
}
835+
836+
@Test
837+
public void testRequireEnvVarsSingleMissing() {
838+
// Setup - create configuration missing one required environment variable
839+
TestConfigurationSource configSource
840+
= new TestConfigurationSource().put("AZURE_CLIENT_ID", CLIENT_ID).put("AZURE_TENANT_ID", TENANT_ID);
841+
// AZURE_CLIENT_SECRET is missing
842+
Configuration configuration = TestUtils.createTestConfiguration(configSource);
843+
844+
// Test - should throw when required env var is missing
845+
IllegalStateException exception = assertThrows(IllegalStateException.class,
846+
() -> new DefaultAzureCredentialBuilder()
847+
.requireEnvVars(AzureIdentityEnvVars.AZURE_CLIENT_ID, AzureIdentityEnvVars.AZURE_TENANT_ID,
848+
AzureIdentityEnvVars.AZURE_CLIENT_SECRET)
849+
.configuration(configuration)
850+
.build());
851+
852+
// Verify error message
853+
assertTrue(exception.getMessage().contains("Required environment variable is missing: AZURE_CLIENT_SECRET"));
854+
}
855+
856+
@Test
857+
public void testRequireEnvVarsMultipleMissing() {
858+
// Setup - create configuration missing multiple required environment variables
859+
TestConfigurationSource configSource = new TestConfigurationSource().put("AZURE_CLIENT_ID", CLIENT_ID);
860+
// AZURE_TENANT_ID and AZURE_CLIENT_SECRET are missing
861+
Configuration configuration = TestUtils.createTestConfiguration(configSource);
862+
863+
// Test - should throw when multiple required env vars are missing
864+
IllegalStateException exception = assertThrows(IllegalStateException.class,
865+
() -> new DefaultAzureCredentialBuilder()
866+
.requireEnvVars(AzureIdentityEnvVars.AZURE_CLIENT_ID, AzureIdentityEnvVars.AZURE_TENANT_ID,
867+
AzureIdentityEnvVars.AZURE_CLIENT_SECRET)
868+
.configuration(configuration)
869+
.build());
870+
871+
// Verify error message contains all missing variables
872+
assertTrue(exception.getMessage().contains("Required environment variables are missing:"));
873+
assertTrue(exception.getMessage().contains("AZURE_TENANT_ID"));
874+
assertTrue(exception.getMessage().contains("AZURE_CLIENT_SECRET"));
875+
// Should not contain AZURE_CLIENT_ID since it is present
876+
String message = exception.getMessage();
877+
assertFalse(message.contains("AZURE_CLIENT_ID"));
878+
}
879+
880+
@Test
881+
public void testRequireEnvVarsEmptyValue() {
882+
// Setup - create configuration with empty string for required environment variable
883+
TestConfigurationSource configSource = new TestConfigurationSource().put("AZURE_CLIENT_ID", CLIENT_ID)
884+
.put("AZURE_TENANT_ID", TENANT_ID)
885+
.put("AZURE_CLIENT_SECRET", ""); // Empty string should be treated as missing
886+
Configuration configuration = TestUtils.createTestConfiguration(configSource);
887+
888+
// Test - should throw when required env var is empty
889+
IllegalStateException exception = assertThrows(IllegalStateException.class,
890+
() -> new DefaultAzureCredentialBuilder()
891+
.requireEnvVars(AzureIdentityEnvVars.AZURE_CLIENT_ID, AzureIdentityEnvVars.AZURE_TENANT_ID,
892+
AzureIdentityEnvVars.AZURE_CLIENT_SECRET)
893+
.configuration(configuration)
894+
.build());
895+
896+
// Verify error message
897+
assertTrue(exception.getMessage().contains("Required environment variable is missing: AZURE_CLIENT_SECRET"));
898+
}
814899
}

0 commit comments

Comments
 (0)