From 6e2042528ebad27336205b77a7c54999ff04f1e5 Mon Sep 17 00:00:00 2001 From: Bogdan Gavril Date: Fri, 22 May 2026 15:19:06 +0100 Subject: [PATCH 1/2] Add IsMtlsPopSupportedByHost to ManagedIdentitySourceResult During IMDS discovery, after the v1 probe succeeds, fetch compute metadata from /metadata/instance/compute to determine if the host VM supports mTLS PoP. The flag is true when the VM is Windows and has a TVM (TrustedLaunch) or CVM (ConfidentialVM) security profile. - Add internal ComputeMetadataResponse/ComputeSecurityProfile models - Add ImdsComputeMetadataManager to fetch and evaluate compute metadata - Add IsMtlsPopSupportedByHost public property on ManagedIdentitySourceResult - Add 5 tests covering Windows TVM/CVM, Linux, no security profile, 404 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ComputeMetadataResponse.cs | 39 ++++++ .../ImdsComputeMetadataManager.cs | 99 +++++++++++++++ .../ManagedIdentity/ManagedIdentityClient.cs | 14 ++- .../ManagedIdentitySourceResult.cs | 11 ++ .../net/MsalJsonSerializerContext.cs | 2 + .../PublicApi/net462/PublicAPI.Unshipped.txt | 1 + .../PublicApi/net472/PublicAPI.Unshipped.txt | 1 + .../net8.0-android/PublicAPI.Unshipped.txt | 1 + .../net8.0-ios/PublicAPI.Unshipped.txt | 1 + .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 1 + .../netstandard2.0/PublicAPI.Unshipped.txt | 1 + .../Http/MockHelpers.cs | 44 +++++++ .../ManagedIdentityTests/ImdsV2Tests.cs | 114 +++++++++++++++++- .../ManagedIdentityTests.cs | 3 +- 14 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/ManagedIdentity/ComputeMetadataResponse.cs create mode 100644 src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsComputeMetadataManager.cs diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ComputeMetadataResponse.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ComputeMetadataResponse.cs new file mode 100644 index 0000000000..b358685ead --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ComputeMetadataResponse.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Client.Platforms.net; +using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + /// + /// Represents compute metadata retrieved from the Azure Instance Metadata Service (IMDS). + /// + [JsonObject] + [Preserve(AllMembers = true)] + internal class ComputeMetadataResponse + { + /// Operating system type (e.g., Windows, Linux). + [JsonProperty("osType")] + public string OsType { get; set; } + + /// + /// Security profile indicating platform security posture. May be null when IMDS + /// does not return security profile information for the current VM. + /// + [JsonProperty("securityProfile")] + public ComputeSecurityProfile SecurityProfile { get; set; } + } + + /// + /// Represents the security profile of an Azure VM from IMDS compute metadata. + /// + [JsonObject] + [Preserve(AllMembers = true)] + internal class ComputeSecurityProfile + { + /// Security type of the VM (e.g., TrustedLaunch, ConfidentialVM). + [JsonProperty("securityType")] + public string SecurityType { get; set; } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsComputeMetadataManager.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsComputeMetadataManager.cs new file mode 100644 index 0000000000..b45d1e23ca --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsComputeMetadataManager.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; +using Microsoft.Identity.Client.Utils; + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + /// + /// Fetches compute metadata from the Azure Instance Metadata Service (IMDS) + /// to determine VM characteristics such as OS type and security profile. + /// + internal static class ImdsComputeMetadataManager + { + internal const string ImdsComputePath = "/metadata/instance/compute"; + internal const string ImdsComputeApiVersion = "2021-02-01"; + + internal static async Task GetComputeMetadataAsync( + IHttpManager httpManager, + ILoggerAdapter logger, + CancellationToken cancellationToken) + { + var headers = new Dictionary + { + { "Metadata", "true" } + }; + + try + { + string queryParams = + $"{ImdsManagedIdentitySource.ApiVersionQueryParam}={ImdsComputeApiVersion}"; + + Uri endpoint = ImdsManagedIdentitySource.GetValidatedEndpoint( + logger, + ImdsComputePath, + queryParams); + + HttpResponse response = await httpManager.SendRequestAsync( + endpoint, + headers, + body: null, + method: HttpMethod.Get, + logger: logger, + doNotThrow: true, + mtlsCertificate: null, + validateServerCertificate: null, + cancellationToken: cancellationToken, + retryPolicy: new ImdsRetryPolicy()) + .ConfigureAwait(false); + + if (response is null || response.StatusCode != HttpStatusCode.OK) + { + logger.Info($"[Managed Identity] IMDS compute metadata request failed. " + + $"StatusCode: {response?.StatusCode}"); + return null; + } + + return JsonHelper.TryToDeserializeFromJson(response.Body); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + logger.Info($"[Managed Identity] IMDS compute metadata request failed with exception: {ex.Message}"); + return null; + } + } + + /// + /// Determines whether the host VM supports mTLS PoP based on compute metadata. + /// mTLS PoP is supported when the VM runs Windows and is a TVM (TrustedLaunch) or CVM (ConfidentialVM). + /// + internal static bool IsMtlsPopSupported(ComputeMetadataResponse metadata) + { + if (metadata is null) + { + return false; + } + + bool isWindows = string.Equals(metadata.OsType, "Windows", StringComparison.OrdinalIgnoreCase); + + string securityType = metadata.SecurityProfile?.SecurityType; + bool isTvmOrCvm = string.Equals(securityType, "TrustedLaunch", StringComparison.OrdinalIgnoreCase) + || string.Equals(securityType, "ConfidentialVM", StringComparison.OrdinalIgnoreCase); + + return isWindows && isTvmOrCvm; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs index ede1bc50cc..d6ca786ac9 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs @@ -156,7 +156,19 @@ internal async Task GetManagedIdentitySourceAsync( if (imdsV1Success) { requestContext.Logger.Info("[Managed Identity] IMDS detected via v1 probe."); - return CacheDiscoveryResult(new ManagedIdentitySourceResult(ManagedIdentitySource.Imds)); + + var result = new ManagedIdentitySourceResult(ManagedIdentitySource.Imds); + + // Fetch compute metadata to determine mTLS PoP support + var computeMetadata = await ImdsComputeMetadataManager.GetComputeMetadataAsync( + requestContext.ServiceBundle.HttpManager, + requestContext.Logger, + cancellationToken).ConfigureAwait(false); + + result.IsMtlsPopSupportedByHost = ImdsComputeMetadataManager.IsMtlsPopSupported(computeMetadata); + requestContext.Logger.Info($"[Managed Identity] mTLS PoP supported by host: {result.IsMtlsPopSupportedByHost}"); + + return CacheDiscoveryResult(result); } requestContext.Logger.Info($"[Managed Identity] {MsalErrorMessage.ManagedIdentityAllSourcesUnavailable}"); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySourceResult.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySourceResult.cs index a7f1da2872..0c13f1c9f2 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySourceResult.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySourceResult.cs @@ -31,6 +31,17 @@ public class ManagedIdentitySourceResult /// public string ImdsFailureReason { get; set; } + /// + /// Gets a value indicating whether the host VM supports mTLS Proof-of-Possession (PoP) tokens. + /// + /// + /// true if the VM runs Windows and has a security profile indicating a Trusted VM (TVM) + /// or Confidential VM (CVM); false if the compute metadata could not be retrieved, + /// the VM is not Windows, or the VM does not have a TVM/CVM security profile. + /// This property is only meaningful when is . + /// + public bool IsMtlsPopSupportedByHost { get; internal set; } + /// /// Initializes a new instance of the class. /// diff --git a/src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs b/src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs index 4fc00ab332..e9b6384cc0 100644 --- a/src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs +++ b/src/client/Microsoft.Identity.Client/Platforms/net/MsalJsonSerializerContext.cs @@ -42,6 +42,8 @@ namespace Microsoft.Identity.Client.Platforms.net [JsonSerializable(typeof(CuidInfo))] [JsonSerializable(typeof(CertificateRequestBody))] [JsonSerializable(typeof(CertificateRequestResponse))] + [JsonSerializable(typeof(ComputeMetadataResponse))] + [JsonSerializable(typeof(ComputeSecurityProfile))] [JsonSerializable(typeof(Dictionary))] [JsonSourceGenerationOptions] internal partial class MsalJsonSerializerContext : JsonSerializerContext 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 c9221575fb..3cc3d0b463 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsFailur *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.IsMtlsPopSupportedByHost.get -> bool 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 c9221575fb..3cc3d0b463 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsFailur *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.IsMtlsPopSupportedByHost.get -> bool 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 c9221575fb..3cc3d0b463 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 @@ -11,3 +11,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsFailur *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.IsMtlsPopSupportedByHost.get -> bool 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 c9221575fb..3cc3d0b463 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 @@ -11,3 +11,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsFailur *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.IsMtlsPopSupportedByHost.get -> bool 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 c9221575fb..3cc3d0b463 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 @@ -11,3 +11,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsFailur *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.IsMtlsPopSupportedByHost.get -> bool 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 c9221575fb..3cc3d0b463 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 @@ -11,3 +11,4 @@ Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsFailur *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string *REMOVED*Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void +Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.IsMtlsPopSupportedByHost.get -> bool diff --git a/src/client/Microsoft.Identity.Lab.Api/Http/MockHelpers.cs b/src/client/Microsoft.Identity.Lab.Api/Http/MockHelpers.cs index ecc4643f96..59d55f3eae 100644 --- a/src/client/Microsoft.Identity.Lab.Api/Http/MockHelpers.cs +++ b/src/client/Microsoft.Identity.Lab.Api/Http/MockHelpers.cs @@ -948,6 +948,50 @@ internal static MockHttpMessageHandler MockImdsProbeFailure( return MockImdsProbe(imdsVersion, userAssignedIdentityId, userAssignedId, success: false, retry: retry); } + /// + /// Creates a mock IMDS compute metadata response handler. + /// + /// The OS type to return (e.g., "Windows", "Linux"). + /// The security profile type (e.g., "TrustedLaunch", "ConfidentialVM"), or null. + /// A configured . + internal static MockHttpMessageHandler MockImdsComputeMetadata( + string osType = "Windows", + string securityType = "TrustedLaunch") + { + string securityProfileJson = securityType != null + ? $", \"securityProfile\": {{ \"securityType\": \"{securityType}\" }}" + : ""; + + string body = $"{{ \"osType\": \"{osType}\"{securityProfileJson} }}"; + + return new MockHttpMessageHandler() + { + ExpectedUrl = $"{ImdsManagedIdentitySource.DefaultImdsBaseEndpoint}{ImdsComputeMetadataManager.ImdsComputePath}", + ExpectedMethod = HttpMethod.Get, + ResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(body), + } + }; + } + + /// + /// Creates a mock IMDS compute metadata 404 (not found) response handler. + /// + /// A configured . + internal static MockHttpMessageHandler MockImdsComputeMetadataNotFound() + { + return new MockHttpMessageHandler() + { + ExpectedUrl = $"{ImdsManagedIdentitySource.DefaultImdsBaseEndpoint}{ImdsComputeMetadataManager.ImdsComputePath}", + ExpectedMethod = HttpMethod.Get, + ResponseMessage = new HttpResponseMessage(HttpStatusCode.NotFound) + { + Content = new StringContent(""), + } + }; + } + /// /// Creates a mock CSR metadata response handler for IMDS v2 flows. /// diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs index 71686fe209..5b06ae3f1a 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs @@ -128,6 +128,7 @@ private async Task CreateManagedIdentityAsync( { // Discovery probes V1 (succeeds) → Imds cached httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1, userAssignedIdentityId, userAssignedId)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata()); if (addSourceCheck) { @@ -142,6 +143,7 @@ private async Task CreateManagedIdentityAsync( { // Discovery probes V1 (succeeds) → Imds cached; mTLS PoP requests are routed to IMDSv2 automatically httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1, userAssignedIdentityId, userAssignedId)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata()); } if (addSourceCheck) @@ -377,10 +379,12 @@ public async Task ImdsV1Cached_MtlsPopRequested_RoutesToImdsV2( // Regression test for issue #6024: // Azure SDK calls GetManagedIdentitySourceAsync first, which probes IMDSv1 and caches "Imds". httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1, userAssignedIdentityId, userAssignedId)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata()); var sourceResult = await (managedIdentityApp as ManagedIdentityApplication) .GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken) .ConfigureAwait(false); Assert.AreEqual(ManagedIdentitySource.Imds, sourceResult.Source); + Assert.IsTrue(sourceResult.IsMtlsPopSupportedByHost); // Now an mTLS PoP request should route to IMDSv2 despite v1 being cached. AddMocksToGetEntraToken(httpManager, userAssignedIdentityId, userAssignedId); @@ -461,8 +465,9 @@ public async Task ProbeImdsEndpointAsyncSucceeds() { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - // Discovery probes V1 (succeeds) + // Discovery probes V1 (succeeds), then fetches compute metadata httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata()); await CreateManagedIdentityAsync(httpManager, addProbeMock: false).ConfigureAwait(false); } @@ -515,6 +520,113 @@ await Assert.ThrowsAsync(async () => } #endregion Probe Tests + #region IsMtlsPopSupportedByHost Tests + [TestMethod] + public async Task IsMtlsPopSupportedByHost_WindowsTvm_ReturnsTrue() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata(osType: "Windows", securityType: "TrustedLaunch")); + + var managedIdentityApp = await CreateManagedIdentityAsync(httpManager, addProbeMock: false, addSourceCheck: false).ConfigureAwait(false); + + var result = await (managedIdentityApp as ManagedIdentityApplication) + .GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + + Assert.AreEqual(ManagedIdentitySource.Imds, result.Source); + Assert.IsTrue(result.IsMtlsPopSupportedByHost); + } + } + + [TestMethod] + public async Task IsMtlsPopSupportedByHost_WindowsCvm_ReturnsTrue() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata(osType: "Windows", securityType: "ConfidentialVM")); + + var managedIdentityApp = await CreateManagedIdentityAsync(httpManager, addProbeMock: false, addSourceCheck: false).ConfigureAwait(false); + + var result = await (managedIdentityApp as ManagedIdentityApplication) + .GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + + Assert.AreEqual(ManagedIdentitySource.Imds, result.Source); + Assert.IsTrue(result.IsMtlsPopSupportedByHost); + } + } + + [TestMethod] + public async Task IsMtlsPopSupportedByHost_Linux_ReturnsFalse() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata(osType: "Linux", securityType: "TrustedLaunch")); + + var managedIdentityApp = await CreateManagedIdentityAsync(httpManager, addProbeMock: false, addSourceCheck: false).ConfigureAwait(false); + + var result = await (managedIdentityApp as ManagedIdentityApplication) + .GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + + Assert.AreEqual(ManagedIdentitySource.Imds, result.Source); + Assert.IsFalse(result.IsMtlsPopSupportedByHost); + } + } + + [TestMethod] + public async Task IsMtlsPopSupportedByHost_WindowsNoSecurityProfile_ReturnsFalse() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata(osType: "Windows", securityType: null)); + + var managedIdentityApp = await CreateManagedIdentityAsync(httpManager, addProbeMock: false, addSourceCheck: false).ConfigureAwait(false); + + var result = await (managedIdentityApp as ManagedIdentityApplication) + .GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + + Assert.AreEqual(ManagedIdentitySource.Imds, result.Source); + Assert.IsFalse(result.IsMtlsPopSupportedByHost); + } + } + + [TestMethod] + public async Task IsMtlsPopSupportedByHost_ComputeMetadata404_ReturnsFalse() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadataNotFound()); + + var managedIdentityApp = await CreateManagedIdentityAsync(httpManager, addProbeMock: false, addSourceCheck: false).ConfigureAwait(false); + + var result = await (managedIdentityApp as ManagedIdentityApplication) + .GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + + Assert.AreEqual(ManagedIdentitySource.Imds, result.Source); + Assert.IsFalse(result.IsMtlsPopSupportedByHost); + } + } + #endregion IsMtlsPopSupportedByHost Tests + #region Fallback Behavior Tests // Verifies non-mTLS request after IMDS detection uses IMDSv1 (Bearer), [TestMethod] diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 400280577d..0c07fd4d80 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -69,8 +69,9 @@ public async Task GetManagedIdentityTests( if (managedIdentitySource == ManagedIdentitySource.Imds) { - // Discovery probes V1 only + // Discovery probes V1 only, then fetches compute metadata httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); + httpManager.AddMockHandler(MockHelpers.MockImdsComputeMetadata()); } var miSourceResult = await mi.GetManagedIdentitySourceAsync(ImdsProbesCancellationToken).ConfigureAwait(false); From 85d93520d0635c2c451d6eaec6ee8a02445bd12c Mon Sep 17 00:00:00 2001 From: Bogdan Gavril Date: Fri, 22 May 2026 15:37:01 +0100 Subject: [PATCH 2/2] Skip compute metadata fetch on net462, return false Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ManagedIdentity/ManagedIdentityClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs index d6ca786ac9..0deb2d5e5b 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs @@ -159,7 +159,8 @@ internal async Task GetManagedIdentitySourceAsync( var result = new ManagedIdentitySourceResult(ManagedIdentitySource.Imds); - // Fetch compute metadata to determine mTLS PoP support +#if !NET462 + // Fetch compute metadata to determine mTLS PoP support (not available on .NET Framework 4.6.2) var computeMetadata = await ImdsComputeMetadataManager.GetComputeMetadataAsync( requestContext.ServiceBundle.HttpManager, requestContext.Logger, @@ -167,6 +168,7 @@ internal async Task GetManagedIdentitySourceAsync( result.IsMtlsPopSupportedByHost = ImdsComputeMetadataManager.IsMtlsPopSupported(computeMetadata); requestContext.Logger.Info($"[Managed Identity] mTLS PoP supported by host: {result.IsMtlsPopSupportedByHost}"); +#endif return CacheDiscoveryResult(result); }