Skip to content
Draft
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 @@ -13,6 +13,7 @@

- Fixed `AzureDeveloperCliCredential` hanging when the `AZD_DEBUG` environment variable is set by adding the `--no-prompt` flag to prevent interactive prompts ([#52005](https://github.com/Azure/azure-sdk-for-net/issues/52005)).
- `BrokerCredential` is now included in the chain when `AZURE_TOKEN_CREDENTIALS` is set to `dev` and the `Azure.Identity.Broker` package is installed.
- `ManagedIdentityCredential` now correctly surfaces common IMDS unavailability/network errors (timeouts, "Host is down", "No route to host", system-assigned "Identity not found") as `CredentialUnavailableException` instead of `AuthenticationFailedException`, allowing `ChainedTokenCredential` to continue to the next credential in local/dev environments.

### Other Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC
{
throw scope.FailWrapAndThrow(new CredentialUnavailableException(MsiUnavailableError, e), Troubleshooting);
}
// Preserve CredentialUnavailableException so ChainedTokenCredential can continue to next credential.
catch (CredentialUnavailableException)
{
throw;
}
catch (Exception e)
{
// This exception pattern indicates that the MI endpoint is not available after exhausting all retries.
Expand Down
25 changes: 22 additions & 3 deletions sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,9 @@ public async ValueTask<AccessToken> AuthenticateAsync(bool async, TokenRequestCo
await _msalManagedIdentityClient.AcquireTokenForManagedIdentityAsync(context, cancellationToken).ConfigureAwait(false) :
_msalManagedIdentityClient.AcquireTokenForManagedIdentity(context, cancellationToken);
}
// If the IMDS endpoint is not available, we will throw a CredentialUnavailableException.
catch (MsalServiceException ex) when (HasInnerExceptionMatching(ex, e => e is RequestFailedException && e.Message.Contains("timed out")))
// If the IMDS endpoint appears unavailable, map to CredentialUnavailable so chained credentials can fall through.
catch (MsalServiceException ex) when (IsEndpointUnavailable(ex))
{
// If the managed identity is not found, throw a more specific exception.
throw new CredentialUnavailableException(MsiUnavailableError, ex);
}

Expand Down Expand Up @@ -150,5 +149,25 @@ private static bool HasInnerExceptionMatching(Exception exception, Func<Exceptio
}
return false;
}

private static bool ContainsIgnoreCase(string source, string value) => source?.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;

private bool IsEndpointUnavailable(MsalServiceException ex)
{
bool timeout = HasInnerExceptionMatching(ex, e => e is RequestFailedException rfe && ContainsIgnoreCase(rfe.Message, "timed out"));

// Other network/host errors frequently seen locally or in dev boxes when IMDS is not genuinely reachable.
bool hostErrors = HasInnerExceptionMatching(ex, e => e is RequestFailedException rfe && (
ContainsIgnoreCase(rfe.Message, "host is down") ||
ContainsIgnoreCase(rfe.Message, "no route to host") ||
ContainsIgnoreCase(rfe.Message, "connection refused") ||
ContainsIgnoreCase(rfe.Message, "network is unreachable") ||
ContainsIgnoreCase(rfe.Message, "unreachable host") ||
ContainsIgnoreCase(rfe.Message, "unreachable network")));

bool identityNotFound = (ManagedIdentityId?._idType == ManagedIdentityIdType.SystemAssigned) && ContainsIgnoreCase(ex.Message, "Identity not found");

return timeout || hostErrors || identityNotFound;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,52 @@ public async Task ThrowsCredentialUnavailableWhenIMDSTimesOut()
await Task.CompletedTask;
}

[NonParallelizable]
[Test]
public async Task ThrowsCredentialUnavailableWhenIMDSHostDown()
{
using var environment = new TestEnvVar(new() { { "MSI_ENDPOINT", null }, { "MSI_SECRET", null }, { "IDENTITY_ENDPOINT", null }, { "IDENTITY_HEADER", null }, { "AZURE_POD_IDENTITY_AUTHORITY_HOST", null } });

var mockTransport = new MockTransport(req =>
{
throw new MsalServiceException(MsalError.ManagedIdentityRequestFailed, "Retry failed", new RequestFailedException("Host is down (169.254.169.254:80)"));
});
var options = new TokenCredentialOptions() { IsChainedCredential = false, Transport = mockTransport };

ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(
new ManagedIdentityClient(
new ManagedIdentityClientOptions() { IsForceRefreshEnabled = true, Options = options, Pipeline = CredentialPipeline.GetInstance(options, IsManagedIdentityCredential: true) })
));

var ex = Assert.ThrowsAsync<CredentialUnavailableException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)));

Assert.That(ex.Message, Does.Contain(ManagedIdentityClient.MsiUnavailableError));

await Task.CompletedTask;
}

[NonParallelizable]
[Test]
public async Task ThrowsCredentialUnavailableWhenIdentityNotFoundSystemAssigned()
{
using var environment = new TestEnvVar(new() { { "MSI_ENDPOINT", null }, { "MSI_SECRET", null }, { "IDENTITY_ENDPOINT", null }, { "IDENTITY_HEADER", null }, { "AZURE_POD_IDENTITY_AUTHORITY_HOST", null } });

var mockTransport = new MockTransport(req =>
{
throw new MsalServiceException(MsalError.ManagedIdentityRequestFailed, "Retry failed", new RequestFailedException("Identity not found"));
});
var options = new TokenCredentialOptions() { IsChainedCredential = false, Transport = mockTransport };

ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(
new ManagedIdentityClient(
new ManagedIdentityClientOptions() { IsForceRefreshEnabled = true, Options = options, Pipeline = CredentialPipeline.GetInstance(options, IsManagedIdentityCredential: true), ManagedIdentityId = ManagedIdentityId.SystemAssigned })
));

var ex = Assert.ThrowsAsync<CredentialUnavailableException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)));
Assert.That(ex.Message, Does.Contain(ManagedIdentityClient.MsiUnavailableError));
await Task.CompletedTask;
}

[NonParallelizable]
[Test]
public async Task VerifyMsiUnavailableOnIMDSGatewayErrorResponse([Values(502, 504)] int statusCode)
Expand Down
Loading