From 4cc3a30626138830032600e7e9da5613a85ec7ea Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 14 Nov 2025 19:18:18 -0500 Subject: [PATCH 1/2] ImdsV2 is detected, even on a UAMI configuration error. --- .../V2/ImdsV2ManagedIdentitySource.cs | 45 ++++++++++++++++--- .../ManagedIdentityApplication.cs | 8 ++++ .../Microsoft.Identity.Client/MsalError.cs | 5 +++ .../MsalErrorMessage.cs | 3 ++ .../PublicApi/net462/PublicAPI.Unshipped.txt | 3 +- .../PublicApi/net472/PublicAPI.Unshipped.txt | 1 + .../net8.0-android/PublicAPI.Unshipped.txt | 3 +- .../net8.0-ios/PublicAPI.Unshipped.txt | 1 + .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 1 + .../netstandard2.0/PublicAPI.Unshipped.txt | 1 + .../Core/Mocks/MockHelpers.cs | 36 +++++++++------ .../ManagedIdentityTests/ImdsV2Tests.cs | 30 +++++++++++++ 12 files changed, 116 insertions(+), 21 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs index aa98c98121..d727d1fd8a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Http.Retry; @@ -91,15 +92,31 @@ public static async Task GetCsrMetadataAsync( { if (probeMode) { + if (IsUamiConfigurationError(response, requestContext.ServiceBundle.Config.ManagedIdentityId)) + { + requestContext.Logger.Info("[Managed Identity] IMDSv2 endpoint is available but UAMI configuration error detected during probe. Returning empty CsrMetadata to indicate endpoint availability."); + return new CsrMetadata(); + } + requestContext.Logger.Info(() => $"[Managed Identity] IMDSv2 managed identity is not available. Status code: {response.StatusCode}, Body: {response.Body}"); return null; } else { + string errorCode = MsalError.ManagedIdentityRequestFailed; + string errorMessage = "ImdsV2ManagedIdentitySource.GetCsrMetadataAsync failed due to HTTP error."; + + if (IsUamiConfigurationError(response, requestContext.ServiceBundle.Config.ManagedIdentityId)) + { + errorCode = MsalError.IdentityNotFound; + errorMessage = MsalErrorMessage.IdentityNotFound; + } + ThrowProbeFailedException( - $"ImdsV2ManagedIdentitySource.GetCsrMetadataAsync failed due to HTTP error. Status code: {response.StatusCode} Body: {response.Body}", + $"{errorMessage} Status code: {response.StatusCode} Body: {response.Body}", null, - (int)response.StatusCode); + (int)response.StatusCode, + errorCode); } } @@ -112,13 +129,31 @@ public static async Task GetCsrMetadataAsync( #endif } + /// + /// Determines if the HTTP response indicates a User Assigned Managed Identity (UAMI) configuration error rather than an endpoint availability issue. + /// + /// The HTTP response from IMDS + /// The managed identity configuration + /// True if this is a UAMI configuration error, false otherwise + private static bool IsUamiConfigurationError(HttpResponse response, ManagedIdentityId managedIdentityId) + { + if (managedIdentityId.IdType == ManagedIdentityIdType.SystemAssigned) + { + return false; + } + + return (response.StatusCode == HttpStatusCode.BadRequest) && + (response.Body?.Contains(MsalError.IdentityNotFound) == true); + } + private static void ThrowProbeFailedException( - String errorMessage, + string errorMessage, Exception ex = null, - int? statusCode = null) + int? statusCode = null, + string errorCode = MsalError.ManagedIdentityRequestFailed) { throw MsalServiceExceptionFactory.CreateManagedIdentityException( - MsalError.ManagedIdentityRequestFailed, + errorCode, $"[ImdsV2] {errorMessage}", ex, ManagedIdentitySource.ImdsV2, diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs index ea1fdb9c37..eff13da6c4 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs @@ -55,6 +55,14 @@ public AcquireTokenForManagedIdentityParameterBuilder AcquireTokenForManagedIden resource); } + // do probe for imdsv1 + + // currently sends without metadata header, if available will respond with 400 + + // figure out if there's a scenario where v1 should be chosen over v2. ask raghavendra + + // make probe configurable for v1 and v2 + /// public async Task GetManagedIdentitySourceAsync() { diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index 526718e7df..b903b718da 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -1227,5 +1227,10 @@ public static class MsalError /// mTLS PoP tokens are not supported in IMDS V1. /// public const string MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1"; + + /// + /// The provided user-assigned managed identity was not found on the VM. + /// + public const string IdentityNotFound = "identity_not_found"; } } diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index 59742eead3..a9768a9c59 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Drawing; using System.Globalization; namespace Microsoft.Identity.Client @@ -450,5 +451,7 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName) public const string InvalidCertificate = "The certificate received from the Imds server is invalid."; public const string CannotSwitchBetweenImdsVersionsForPreview = "ImdsV2 is currently experimental - A Bearer token has already been received; Please restart the application to receive a mTLS PoP token."; public const string MtlsPopTokenNotSupportedinImdsV1 = "A mTLS PoP token cannot be requested because the application\'s source was determined to be ImdsV1."; + public const string IdentityNotFound = "Managed Identity not found. To request credentials for this identity, please assign it first. Please see aka.ms/ManagedIdentityNotFound for more details"; + } } diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index a77a3542bb..eddc768d0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +const Microsoft.Identity.Client.MsalError.IdentityNotFound = "identity_not_found" -> string Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string @@ -9,4 +10,4 @@ Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Securi Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder -static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void \ No newline at end of file +static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index bb1898ebb1..eddc768d0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +const Microsoft.Identity.Client.MsalError.IdentityNotFound = "identity_not_found" -> string Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index a77a3542bb..eddc768d0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +const Microsoft.Identity.Client.MsalError.IdentityNotFound = "identity_not_found" -> string Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string @@ -9,4 +10,4 @@ Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Securi Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder -static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void \ No newline at end of file +static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index bb1898ebb1..eddc768d0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +const Microsoft.Identity.Client.MsalError.IdentityNotFound = "identity_not_found" -> string Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index bb1898ebb1..eddc768d0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +const Microsoft.Identity.Client.MsalError.IdentityNotFound = "identity_not_found" -> string Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index bb1898ebb1..eddc768d0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +const Microsoft.Identity.Client.MsalError.IdentityNotFound = "identity_not_found" -> string Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs index 0cd3aab2a6..b06b792286 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs @@ -621,7 +621,8 @@ public static MockHttpMessageHandler MockCsrResponse( string userAssignedId = null, string clientIdOverride = null, string tenantIdOverride = null, - string attestationEndpointOverride = null) + string attestationEndpointOverride = null, + string contentOverride = null) { IDictionary expectedQueryParams = new Dictionary(); IDictionary expectedRequestHeaders = new Dictionary(); @@ -639,7 +640,7 @@ public static MockHttpMessageHandler MockCsrResponse( expectedQueryParams.Add("cred-api-version", "2.0"); expectedRequestHeaders.Add("Metadata", "true"); - string content = + string content = contentOverride ?? "{" + "\"cuId\": { \"vmId\": \"fake_vmId\" }," + "\"clientId\": \"" + (clientIdOverride ?? TestConstants.ClientId) + "\"," + @@ -666,27 +667,34 @@ public static MockHttpMessageHandler MockCsrResponse( return handler; } - // used for unit tests in ManagedIdentityTests.cs - public static MockHttpMessageHandler MockCsrResponseFailure() + public static MockHttpMessageHandler MockCsrResponseFailure( + HttpStatusCode statusCode = HttpStatusCode.BadRequest, + string content = null, + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.None, + string userAssignedId = null) { // 400 doesn't trigger the retry policy - return MockCsrResponse(HttpStatusCode.BadRequest); + return MockCsrResponse( + statusCode, + userAssignedIdentityId: userAssignedIdentityId, + userAssignedId: userAssignedId, + contentOverride: content); } public static MockHttpMessageHandler MockCertificateRequestResponse( - UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.None, - string userAssignedId = null, - string certificate = TestConstants.ValidRawCertificate, - string clientIdOverride = null, - string tenantIdOverride = null, - string mtlsEndpointOverride = null) + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.None, + string userAssignedId = null, + string certificate = TestConstants.ValidRawCertificate, + string clientIdOverride = null, + string tenantIdOverride = null, + string mtlsEndpointOverride = null) { IDictionary expectedQueryParams = new Dictionary(); IDictionary expectedRequestHeaders = new Dictionary(); IList presentRequestHeaders = new List - { - OAuth2Header.XMsCorrelationId - }; + { + OAuth2Header.XMsCorrelationId + }; if (userAssignedIdentityId != UserAssignedIdentityId.None && userAssignedId != null) { diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs index ab9a5d2a4a..611c75743c 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs @@ -562,6 +562,36 @@ public async Task ProbeDoesNotFireWhenMtlsPopNotRequested( } } + [DataTestMethod] + [DataRow(UserAssignedIdentityId.ClientId, TestConstants.ClientId)] + [DataRow(UserAssignedIdentityId.ResourceId, TestConstants.MiResourceId)] + [DataRow(UserAssignedIdentityId.ObjectId, TestConstants.ObjectId)] + public async Task ImdsV2IsDetectedEvenWhenUamiNotFound( + UserAssignedIdentityId userAssignedIdentityId, + string userAssignedId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(ManagedIdentitySource.ImdsV2, TestConstants.ImdsEndpoint); + + string errorContent = $"{{\"error\":\"{MsalError.IdentityNotFound}\",\"error_description\":\"{MsalErrorMessage.IdentityNotFound}\"}}"; + httpManager.AddMockHandler(MockHelpers.MockCsrResponseFailure(HttpStatusCode.BadRequest, errorContent, userAssignedIdentityId, userAssignedId)); + + var managedIdentityApp = await CreateManagedIdentityAsync(httpManager, userAssignedIdentityId, userAssignedId, addProbeMock: false).ConfigureAwait(false); + + httpManager.AddMockHandler(MockHelpers.MockCsrResponseFailure(HttpStatusCode.BadRequest, errorContent, userAssignedIdentityId, userAssignedId)); + + var ex = await Assert.ThrowsExceptionAsync(async () => + await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .WithMtlsProofOfPossession() + .ExecuteAsync().ConfigureAwait(false) + ).ConfigureAwait(false); + + Assert.AreEqual(MsalError.IdentityNotFound, ex.ErrorCode); + } + } + #region Cuid Tests [TestMethod] public void TestCsrGeneration_OnlyVmId() From 7d4788b61ba6deb1c002248f7c3f45b2d90a5ca9 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 14 Nov 2025 19:23:42 -0500 Subject: [PATCH 2/2] Fixed typos --- .../ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs | 2 +- .../ManagedIdentityApplication.cs | 8 -------- src/client/Microsoft.Identity.Client/MsalErrorMessage.cs | 1 - 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs index d727d1fd8a..ff1c0b0833 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs @@ -94,7 +94,7 @@ public static async Task GetCsrMetadataAsync( { if (IsUamiConfigurationError(response, requestContext.ServiceBundle.Config.ManagedIdentityId)) { - requestContext.Logger.Info("[Managed Identity] IMDSv2 endpoint is available but UAMI configuration error detected during probe. Returning empty CsrMetadata to indicate endpoint availability."); + requestContext.Logger.Info("[Managed Identity] IMDSv2 endpoint is available but there is a UAMI configuration error. Returning empty CsrMetadata to indicate endpoint availability."); return new CsrMetadata(); } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs index eff13da6c4..ea1fdb9c37 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs @@ -55,14 +55,6 @@ public AcquireTokenForManagedIdentityParameterBuilder AcquireTokenForManagedIden resource); } - // do probe for imdsv1 - - // currently sends without metadata header, if available will respond with 400 - - // figure out if there's a scenario where v1 should be chosen over v2. ask raghavendra - - // make probe configurable for v1 and v2 - /// public async Task GetManagedIdentitySourceAsync() { diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index a9768a9c59..d39c0fe5b7 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Drawing; using System.Globalization; namespace Microsoft.Identity.Client