Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents compute metadata retrieved from the Azure Instance Metadata Service (IMDS).
/// </summary>
[JsonObject]
[Preserve(AllMembers = true)]
internal class ComputeMetadataResponse
{
/// <summary>Operating system type (e.g., Windows, Linux).</summary>
[JsonProperty("osType")]
public string OsType { get; set; }

/// <summary>
/// Security profile indicating platform security posture. May be null when IMDS
/// does not return security profile information for the current VM.
/// </summary>
[JsonProperty("securityProfile")]
public ComputeSecurityProfile SecurityProfile { get; set; }
}

/// <summary>
/// Represents the security profile of an Azure VM from IMDS compute metadata.
/// </summary>
[JsonObject]
[Preserve(AllMembers = true)]
internal class ComputeSecurityProfile
{
/// <summary>Security type of the VM (e.g., TrustedLaunch, ConfidentialVM).</summary>
[JsonProperty("securityType")]
public string SecurityType { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Fetches compute metadata from the Azure Instance Metadata Service (IMDS)
/// to determine VM characteristics such as OS type and security profile.
/// </summary>
internal static class ImdsComputeMetadataManager
{
internal const string ImdsComputePath = "/metadata/instance/compute";
internal const string ImdsComputeApiVersion = "2021-02-01";

internal static async Task<ComputeMetadataResponse> GetComputeMetadataAsync(
IHttpManager httpManager,
ILoggerAdapter logger,
CancellationToken cancellationToken)
{
var headers = new Dictionary<string, string>
{
{ "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<ComputeMetadataResponse>(response.Body);
}
catch (OperationCanceledException)
{
throw;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why catch throw?

}
catch (Exception ex)
{
logger.Info($"[Managed Identity] IMDS compute metadata request failed with exception: {ex.Message}");
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logs it as error

return null;
}
}

/// <summary>
/// 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).
/// </summary>
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,21 @@ internal async Task<ManagedIdentitySourceResult> 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);

#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,
cancellationToken).ConfigureAwait(false);

result.IsMtlsPopSupportedByHost = ImdsComputeMetadataManager.IsMtlsPopSupported(computeMetadata);
requestContext.Logger.Info($"[Managed Identity] mTLS PoP supported by host: {result.IsMtlsPopSupportedByHost}");
#endif

return CacheDiscoveryResult(result);
}

requestContext.Logger.Info($"[Managed Identity] {MsalErrorMessage.ManagedIdentityAllSourcesUnavailable}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ public class ManagedIdentitySourceResult
/// </value>
public string ImdsFailureReason { get; set; }

/// <summary>
/// Gets a value indicating whether the host VM supports mTLS Proof-of-Possession (PoP) tokens.
/// </summary>
/// <value>
/// <c>true</c> if the VM runs Windows and has a security profile indicating a Trusted VM (TVM)
/// or Confidential VM (CVM); <c>false</c> 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 <see cref="Source"/> is <see cref="ManagedIdentitySource.Imds"/>.
/// </value>
public bool IsMtlsPopSupportedByHost { get; internal set; }

/// <summary>
/// Initializes a new instance of the <see cref="ManagedIdentitySourceResult"/> class.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object>))]
[JsonSourceGenerationOptions]
internal partial class MsalJsonSerializerContext : JsonSerializerContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions src/client/Microsoft.Identity.Lab.Api/Http/MockHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,50 @@ internal static MockHttpMessageHandler MockImdsProbeFailure(
return MockImdsProbe(imdsVersion, userAssignedIdentityId, userAssignedId, success: false, retry: retry);
}

/// <summary>
/// Creates a mock IMDS compute metadata response handler.
/// </summary>
/// <param name="osType">The OS type to return (e.g., "Windows", "Linux").</param>
/// <param name="securityType">The security profile type (e.g., "TrustedLaunch", "ConfidentialVM"), or null.</param>
/// <returns>A configured <see cref="MockHttpMessageHandler"/>.</returns>
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),
}
};
}

/// <summary>
/// Creates a mock IMDS compute metadata 404 (not found) response handler.
/// </summary>
/// <returns>A configured <see cref="MockHttpMessageHandler"/>.</returns>
internal static MockHttpMessageHandler MockImdsComputeMetadataNotFound()
{
return new MockHttpMessageHandler()
{
ExpectedUrl = $"{ImdsManagedIdentitySource.DefaultImdsBaseEndpoint}{ImdsComputeMetadataManager.ImdsComputePath}",
ExpectedMethod = HttpMethod.Get,
ResponseMessage = new HttpResponseMessage(HttpStatusCode.NotFound)
{
Content = new StringContent(""),
}
};
}

/// <summary>
/// Creates a mock CSR metadata response handler for IMDS v2 flows.
/// </summary>
Expand Down
Loading
Loading