diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 8fad58e42382..8922046953e4 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -8,6 +8,8 @@ ### Bugs Fixed +- `ManagedIdentityCredential` throws `CredentialUnavailableException` when the IMDS endpoint is unavailable. This addresses a regression in how it behaves in the `ChainedTokenCredential` ([47057](https://github.com/Azure/azure-sdk-for-net/issues/47057)). + ### Other Changes ## 1.14.0 (2025-05-13) diff --git a/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs b/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs index 9feef57957db..cdad21aa14eb 100644 --- a/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs +++ b/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs @@ -87,10 +87,19 @@ public async ValueTask AuthenticateAsync(bool async, TokenRequestCo return await tokenExchangeManagedIdentitySource.AuthenticateAsync(async, context, cancellationToken).ConfigureAwait(false); } - // The default case is to use the MSAL implementation, which does no probing of the IMDS endpoint. - result = async ? - await _msalManagedIdentityClient.AcquireTokenForManagedIdentityAsync(context, cancellationToken).ConfigureAwait(false) : - _msalManagedIdentityClient.AcquireTokenForManagedIdentity(context, cancellationToken); + try + { + // The default case is to use the MSAL implementation, which does no probing of the IMDS endpoint. + result = async ? + 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 managed identity is not found, throw a more specific exception. + throw new CredentialUnavailableException(MsiUnavailableError, ex); + } return result.ToAccessToken(); } @@ -127,5 +136,19 @@ private static ManagedIdentitySource SelectManagedIdentitySource(ManagedIdentity return TokenExchangeManagedIdentitySource.TryCreate(options) ?? new ImdsManagedIdentityProbeSource(options, client); } + + private static bool HasInnerExceptionMatching(Exception exception, Func condition) + { + var current = exception; + while (current != null) + { + if (condition(current)) + { + return true; + } + current = current.InnerException; + } + return false; + } } } diff --git a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs index da218d18fdf3..157e4082d155 100644 --- a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs @@ -18,6 +18,7 @@ using Azure.Core.TestFramework; using Azure.Identity.Tests.Mock; using Microsoft.AspNetCore.Http; +using Microsoft.Identity.Client; using NUnit.Framework; using NUnit.Framework.Internal; @@ -841,6 +842,30 @@ public async Task VerifyMsiUnavailableOnIMDSRequestFailedExcpetion() await Task.CompletedTask; } + [NonParallelizable] + [Test] + public async Task ThrowsCredentialUnavailableWhenIMDSTimesOut() + { + 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("Operation timed out (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(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)