Skip to content

Commit c166df8

Browse files
authored
Fix scenario where IMDS probe succeeds, but MI is unavailable (Azure#46787)
1 parent 47f3c9b commit c166df8

File tree

4 files changed

+61
-7
lines changed

4 files changed

+61
-7
lines changed

sdk/identity/Azure.Identity/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
### Breaking Changes
88

99
### Bugs Fixed
10-
- Fixed an issue that prevented `ManagedIdentityCredential` from attempting to detect if Workload Identity is enabled in the current environment. [#46653](https://github.com/Azure/azure-sdk-for-net/issues/46653)
11-
- Fixed an issue that prevented `DefaultAzureCredential` from progressing past `ManagedIdentityCredential` in some scenarios where the identity was not available. [#46709](https://github.com/Azure/azure-sdk-for-net/issues/46709)
10+
- Fixed a regression that prevented `ManagedIdentityCredential` from attempting to detect if Workload Identity is enabled in the current environment. [#46653](https://github.com/Azure/azure-sdk-for-net/issues/46653)
11+
- Fixed a regression that prevented `DefaultAzureCredential` from progressing past `ManagedIdentityCredential` in some scenarios where the identity was not available. [#46709](https://github.com/Azure/azure-sdk-for-net/issues/46709)
1212

1313
### Other Changes
1414

sdk/identity/Azure.Identity/src/ImdsManagedIdentityProbeSource.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading;
77
using System.Threading.Tasks;
88
using Azure.Core;
9+
using Microsoft.Identity.Client;
910

1011
namespace Azure.Identity
1112
{
@@ -22,6 +23,7 @@ internal class ImdsManagedIdentityProbeSource : ManagedIdentitySource
2223
internal const string TimeoutError = "ManagedIdentityCredential authentication unavailable. The request to the managed identity endpoint timed out.";
2324
internal const string GatewayError = "ManagedIdentityCredential authentication unavailable. The request failed due to a gateway error.";
2425
internal const string AggregateError = "ManagedIdentityCredential authentication unavailable. Multiple attempts failed to obtain a token from the managed identity endpoint.";
26+
internal const string UnknownError = "ManagedIdentityCredential authentication unavailable. An unexpected error has occurred.";
2527

2628
private readonly ManagedIdentityId _managedIdentityId;
2729
private readonly Uri _imdsEndpoint;
@@ -97,6 +99,7 @@ protected override HttpMessage CreateHttpMessage(Request request)
9799

98100
public async override ValueTask<AccessToken> AuthenticateAsync(bool async, TokenRequestContext context, CancellationToken cancellationToken)
99101
{
102+
bool continueIMDSRequestAfterProbe;
100103
try
101104
{
102105
return await base.AuthenticateAsync(async, context, cancellationToken).ConfigureAwait(false);
@@ -127,12 +130,29 @@ public async override ValueTask<AccessToken> AuthenticateAsync(bool async, Token
127130
catch (ProbeRequestResponseException)
128131
{
129132
// This was an expected response from the IMDS endpoint without the Metadata header set.
130-
// Re-issue the request (CreateRequest will add the appropriate header).
133+
// Fall-through to the code below to re-issue the request through MsalManagedIdentityClient.
134+
continueIMDSRequestAfterProbe = true;
135+
}
136+
137+
if (continueIMDSRequestAfterProbe)
138+
{
139+
try
140+
{
131141
#pragma warning disable AZC0110 // DO NOT use await keyword in possibly synchronous scope.
132-
var authResult = await _client.AcquireTokenForManagedIdentityAsync(context, cancellationToken).ConfigureAwait(false);
133-
return authResult.ToAccessToken();
142+
var authResult = await _client.AcquireTokenForManagedIdentityAsync(context, cancellationToken).ConfigureAwait(false);
134143
#pragma warning restore AZC0110 // DO NOT use await keyword in possibly synchronous scope.
144+
return authResult.ToAccessToken();
145+
}
146+
catch (MsalServiceException e)
147+
{
148+
if (e.Message.Contains("unavailable"))
149+
{
150+
throw new CredentialUnavailableException(IdentityUnavailableError, e);
151+
}
152+
throw new CredentialUnavailableException(UnknownError, e);
153+
}
135154
}
155+
throw new CredentialUnavailableException(UnknownError);
136156
}
137157

138158
protected override async ValueTask<AccessToken> HandleResponseAsync(bool async, TokenRequestContext context, HttpMessage message, CancellationToken cancellationToken)

sdk/identity/Azure.Identity/tests/ImdsManagedIdentitySourceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public void DefaultAzureCredentialRetryBehaviorIsOverriddenWithOptions()
142142

143143
var cred = new DefaultAzureCredential(credOptions);
144144

145-
Assert.ThrowsAsync<AuthenticationFailedException>(async () => await cred.GetTokenAsync(new(new[] { "test" })));
145+
Assert.ThrowsAsync<CredentialUnavailableException>(async () => await cred.GetTokenAsync(new(new[] { "test" })));
146146

147147
var expectedTimeouts = new TimeSpan?[] { TimeSpan.FromSeconds(1), null, null, null, null, null, null, null, null };
148148
CollectionAssert.AreEqual(expectedTimeouts, networkTimeouts);

sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialTests.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,37 @@ public void VerifyImdsRequestFailureWithValidJsonIdentityNotFoundErrorThrowsCUE(
447447
}
448448
}
449449

450+
[NonParallelizable]
451+
[Test]
452+
[TestCase(true)]
453+
[TestCase(false)]
454+
public void VerifyImdsProbeRequestSuccessWithIdentityNotFoundErrorThrowsCUE(bool isChained)
455+
{
456+
using var environment = new TestEnvVar(new() { { "MSI_ENDPOINT", null }, { "MSI_SECRET", null }, { "IDENTITY_ENDPOINT", null }, { "IDENTITY_HEADER", null }, { "AZURE_POD_IDENTITY_AUTHORITY_HOST", null } });
457+
458+
var mockTransport = new MockTransport(req =>
459+
{
460+
if (!req.Headers.TryGetValue(ImdsManagedIdentityProbeSource.metadataHeaderName, out _))
461+
{
462+
return CreateResponse(400, """{"error":"invalid_request","error_description":"Required metadata header not specified"}""");
463+
}
464+
return CreateResponse(400, """{"error":"invalid_request","error_description":"Identity not found"}""");
465+
});
466+
var options = new TokenCredentialOptions() { Transport = mockTransport, IsChainedCredential = isChained };
467+
var pipeline = CredentialPipeline.GetInstance(options);
468+
469+
ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential("mock-client-id", pipeline, options));
470+
if (isChained)
471+
{
472+
var ex = Assert.ThrowsAsync<CredentialUnavailableException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Alternate)));
473+
Assert.That(ex.Message, Does.Contain(ImdsManagedIdentityProbeSource.IdentityUnavailableError));
474+
}
475+
else
476+
{
477+
var ex = Assert.ThrowsAsync<AuthenticationFailedException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Alternate)));
478+
}
479+
}
480+
450481
[NonParallelizable]
451482
[Test]
452483
[TestCase(502, ImdsManagedIdentitySource.GatewayError)]
@@ -476,7 +507,10 @@ public async Task VerifyIMDSRequestWithPodIdentityEnvVarMockAsync(string clientI
476507
using var environment = new TestEnvVar(new() { { "MSI_ENDPOINT", null }, { "MSI_SECRET", null }, { "IDENTITY_ENDPOINT", null }, { "IDENTITY_HEADER", null }, { "AZURE_POD_IDENTITY_AUTHORITY_HOST", "https://mock.podid.endpoint/" } });
477508

478509
var response = CreateMockResponse(200, ExpectedToken);
479-
var mockTransport = new MockTransport(response);
510+
var mockTransport = new MockTransport(req =>
511+
{
512+
return response;
513+
});
480514
var options = new TokenCredentialOptions() { Transport = mockTransport };
481515
var pipeline = CredentialPipeline.GetInstance(options);
482516

0 commit comments

Comments
 (0)