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
1 change: 1 addition & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Added a new `DefaultAzureCredential` constructor that accepts a custom environment variable name for credential configuration. This provides flexibility beyond the default `AZURE_TOKEN_CREDENTIALS` environment variable. The constructor accepts any environment variable name and uses the same credential selection logic as the existing `AZURE_TOKEN_CREDENTIALS` processing.
- Added `DefaultAzureCredential.DefaultEnvironmentVariableName` constant property that returns `"AZURE_TOKEN_CREDENTIALS"` for convenience when referencing the default environment variable name.
- `AzureCliCredential`, `AzurePowerShellCredential`, and `AzureDeveloperCliCredential` now throw an `AuthenticationFailedException` when the `TokenRequestContext` includes claims, as these credentials do not support claims challenges. The exception message includes guidance for handling such scenarios.
- When `AZURE_TOKEN_CREDENTIALS` or the equivalent custom environment variable is configured to `ManagedIdentityCredential`, the `DefaultAzureCredential` does not issue a probe request and performs retries with exponential backoff.

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ private TokenCredential[] CreateSpecificCredentialChain(string credentialSelecti
Constants.AzureDeveloperCliCredential => [CreateAzureDeveloperCliCredential()],
Constants.EnvironmentCredential => [CreateEnvironmentCredential()],
Constants.WorkloadIdentityCredential => [CreateWorkloadIdentityCredential()],
Constants.ManagedIdentityCredential => [CreateManagedIdentityCredential()],
Constants.ManagedIdentityCredential => [CreateManagedIdentityCredential(false)],
Constants.InteractiveBrowserCredential => [CreateInteractiveBrowserCredential()],
Constants.BrokerCredential =>
TryCreateDevelopmentBrokerOptions(out InteractiveBrowserCredentialOptions brokerOptions)
Expand Down Expand Up @@ -279,10 +279,10 @@ public virtual TokenCredential CreateWorkloadIdentityCredential()
return new WorkloadIdentityCredential(options);
}

public virtual TokenCredential CreateManagedIdentityCredential()
public virtual TokenCredential CreateManagedIdentityCredential(bool isProbeEnabled = true)
{
var options = Options.Clone<DefaultAzureCredentialOptions>();
options.IsChainedCredential = true;
options.IsChainedCredential = isProbeEnabled;

if (options.ManagedIdentityClientId != null && options.ManagedIdentityResourceId != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,112 @@ public async Task DefaultAzureCredentialProbeUses1secTimeoutWithNoRetries()
CollectionAssert.AreEqual(expectedTimeouts, networkTimeouts);
}

[Test]
public async Task DefaultAzureCredentialDoesNotProbeAndAttemptsRetriesWhenMICredIsConfiguredViaEnv()
{
using (new TestEnvVar(new Dictionary<string, string>
{
{ "AZURE_TOKEN_CREDENTIALS", "ManagedIdentityCredential" },
}))
{
int callCount = 0;
List<TimeSpan?> networkTimeouts = new();

var mockTransport = MockTransport.FromMessageCallback(msg =>
{
callCount++;
networkTimeouts.Add(msg.NetworkTimeout);
// Validate that there is no probe request (which does not have the Metadata header)
Assert.IsTrue(msg.Request.Headers.TryGetValue(ImdsManagedIdentityProbeSource.metadataHeaderName, out string val) && val == "true");
return callCount switch
{
< 6 => CreateMockResponse(500, "{ \"Error\": \"Some error occurred\" }").WithHeader("Content-Type", "application/json"),
_ => CreateMockResponse(200, "token").WithHeader("Content-Type", "application/json")
};
});

var cred = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
ExcludeAzureCliCredential = true,
ExcludeAzureDeveloperCliCredential = true,
ExcludeAzurePowerShellCredential = true,
ExcludeEnvironmentCredential = true,
ExcludeSharedTokenCacheCredential = true,
ExcludeVisualStudioCodeCredential = true,
ExcludeVisualStudioCredential = true,
ExcludeWorkloadIdentityCredential = true,
Transport = mockTransport,
IsForceRefreshEnabled = true,
Retry = { Delay = TimeSpan.Zero }
});

// When ManagedIdentityCredential is configured via environment variable, there are no timeouts and retries are performed
await cred.GetTokenAsync(new(new[] { "test" }));

var expectedTimeouts = new TimeSpan?[] { null, null, null, null, null, null };
CollectionAssert.AreEqual(expectedTimeouts, networkTimeouts);
networkTimeouts.Clear();

await cred.GetTokenAsync(new(new[] { "test" }));

expectedTimeouts = new TimeSpan?[] { null };
CollectionAssert.AreEqual(expectedTimeouts, networkTimeouts);
}
}

[Test]
public async Task DefaultAzureCredentialDoesNotProbeAndAttemptsRetriesWhenMICredIsConfiguredViaCustomEnv()
{
using (new TestEnvVar(new Dictionary<string, string>
{
{ "MY_CUSTOM_ENV_VAR", "ManagedIdentityCredential" },
}))
{
int callCount = 0;
List<TimeSpan?> networkTimeouts = new();

var mockTransport = MockTransport.FromMessageCallback(msg =>
{
callCount++;
networkTimeouts.Add(msg.NetworkTimeout);
// Validate that there is no probe request (which does not have the Metadata header)
Assert.IsTrue(msg.Request.Headers.TryGetValue(ImdsManagedIdentityProbeSource.metadataHeaderName, out string val) && val == "true", "Expected Metadata header with value 'true'");
return callCount switch
{
< 6 => CreateMockResponse(500, "{ \"Error\": \"Some error occurred\" }").WithHeader("Content-Type", "application/json"),
_ => CreateMockResponse(200, "token").WithHeader("Content-Type", "application/json")
};
});

var cred = new DefaultAzureCredential("MY_CUSTOM_ENV_VAR", new DefaultAzureCredentialOptions
{
ExcludeAzureCliCredential = true,
ExcludeAzureDeveloperCliCredential = true,
ExcludeAzurePowerShellCredential = true,
ExcludeEnvironmentCredential = true,
ExcludeSharedTokenCacheCredential = true,
ExcludeVisualStudioCodeCredential = true,
ExcludeVisualStudioCredential = true,
ExcludeWorkloadIdentityCredential = true,
Transport = mockTransport,
IsForceRefreshEnabled = true,
Retry = { Delay = TimeSpan.Zero }
});

// First request validates that there are no network timeouts and retries are performed
await cred.GetTokenAsync(new(new[] { "test" }));

var expectedTimeouts = new TimeSpan?[] { null, null, null, null, null, null };
CollectionAssert.AreEqual(expectedTimeouts, networkTimeouts);
networkTimeouts.Clear();

await cred.GetTokenAsync(new(new[] { "test" }));

expectedTimeouts = new TimeSpan?[] { null };
CollectionAssert.AreEqual(expectedTimeouts, networkTimeouts);
}
}

[Test]
public async Task DefaultAzureCredentialUsesFirstRequestBehaviorUntilFirstResponse()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public override TokenCredential CreateWorkloadIdentityCredential()
return mockWorkloadIdentityCredential.Object;
}

public override TokenCredential CreateManagedIdentityCredential()
public override TokenCredential CreateManagedIdentityCredential(bool isProbeEnabled = true)
{
OnCreateManagedIdentityCredential?.Invoke(mockManagedIdentityCredential);
return mockManagedIdentityCredential.Object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public TestDefaultAzureCredentialFactory(DefaultAzureCredentialOptions options,
public override TokenCredential CreateEnvironmentCredential()
=> new EnvironmentCredential(Pipeline, Options);

public override TokenCredential CreateManagedIdentityCredential()
public override TokenCredential CreateManagedIdentityCredential(bool isProbeEnabled = true)
=> new ManagedIdentityCredential(Options.ManagedIdentityClientId, Pipeline, Options);

public override TokenCredential CreateSharedTokenCacheCredential()
Expand Down