Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Expand Up @@ -241,7 +241,7 @@ public static string ImdsQueryParamsHelper(
return queryParams;
}

public static async Task<bool> ProbeImdsEndpointAsync(
public static async Task<(bool success, string failureReason)> ProbeImdsEndpointAsync(
RequestContext requestContext,
ImdsVersion imdsVersion,
CancellationToken cancellationToken)
Expand All @@ -255,8 +255,8 @@ public static async Task<bool> ProbeImdsEndpointAsync(
{
case ImdsVersion.V2:
#if NET462
requestContext.Logger.Info("[Managed Identity] IMDSv2 flow is not supported on .NET Framework 4.6.2. Cryptographic operations required for managed identity authentication are unavailable on this platform. Skipping IMDSv2 probe.");
return false;
requestContext.Logger.Info("[Managed Identity] IMDSv2 flow is not supported on .NET Framework 4.6.2. Cryptographic operations required for managed identity authentication are unavailable on this platform. Skipping IMDSv2 probe.");
return (false, "IMDSv2 is not supported on .NET Framework 4.6.2");
#else
apiVersionQueryParam = ImdsV2ManagedIdentitySource.ApiVersionQueryParam;
imdsApiVersion = ImdsV2ManagedIdentitySource.ImdsV2ApiVersion;
Expand Down Expand Up @@ -303,22 +303,24 @@ public static async Task<bool> ProbeImdsEndpointAsync(
retryPolicy: retryPolicy)
.ConfigureAwait(false);
}
catch (Exception ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
requestContext.Logger.Info($"[Managed Identity] {imdsStringHelper} probe endpoint failure. Exception occurred while sending request to probe endpoint: {ex}");
return false;
string failureMessage = $"{imdsStringHelper} probe failed. Exception: {ex.Message}";
requestContext.Logger.Info(() => $"[Managed Identity] {failureMessage}");
return (false, failureMessage);
}

// probe omits the "Metadata: true" header and then treats 400 Bad Request as success
if (response.StatusCode == HttpStatusCode.BadRequest)
{
requestContext.Logger.Info(() => $"[Managed Identity] {imdsStringHelper} managed identity is available.");
return true;
return (true, null);
}
else
{
string failureMessage = $"{imdsStringHelper} probe failed. Status code: {response.StatusCode}, Body: {response.Body}";
requestContext.Logger.Info(() => $"[Managed Identity] {imdsStringHelper} managed identity is not available. Status code: {response.StatusCode}, Body: {response.Body}");
return false;
return (false, failureMessage);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ private async Task<AbstractManagedIdentity> GetOrSelectManagedIdentitySourceAsyn
{
requestContext.Logger.Info($"[Managed Identity] Selecting managed identity source if not cached. Cached value is {s_sourceName} ");

ManagedIdentitySourceResult sourceResult = null;
ManagedIdentitySource source;

// If the source is not already set, determine it
if (s_sourceName == ManagedIdentitySource.None)
{
// First invocation: detect and cache
source = await GetManagedIdentitySourceAsync(requestContext, isMtlsPopRequested, cancellationToken).ConfigureAwait(false);
sourceResult = await GetManagedIdentitySourceAsync(requestContext, isMtlsPopRequested, cancellationToken).ConfigureAwait(false);
source = sourceResult.Source;
}
else
{
Expand Down Expand Up @@ -96,14 +98,14 @@ private async Task<AbstractManagedIdentity> GetOrSelectManagedIdentitySourceAsyn
ManagedIdentitySource.AzureArc => AzureArcManagedIdentitySource.Create(requestContext),
ManagedIdentitySource.ImdsV2 => ImdsV2ManagedIdentitySource.Create(requestContext),
ManagedIdentitySource.Imds => ImdsManagedIdentitySource.Create(requestContext),
_ => throw new MsalClientException(MsalError.ManagedIdentityAllSourcesUnavailable, MsalErrorMessage.ManagedIdentityAllSourcesUnavailable)
_ => throw CreateManagedIdentityUnavailableException(sourceResult)
};
}
}

// Detect managed identity source based on the availability of environment variables and csr metadata probe request.
// This method is perf sensitive any changes should be benchmarked.
internal async Task<ManagedIdentitySource> GetManagedIdentitySourceAsync(
internal async Task<ManagedIdentitySourceResult> GetManagedIdentitySourceAsync(
RequestContext requestContext,
bool isMtlsPopRequested,
CancellationToken cancellationToken)
Expand All @@ -113,36 +115,46 @@ internal async Task<ManagedIdentitySource> GetManagedIdentitySourceAsync(
if (source != ManagedIdentitySource.None)
{
s_sourceName = source;
return source;
return new ManagedIdentitySourceResult(source);
}

string imdsV2FailureReason = null;
string imdsV1FailureReason = null;

// skip the ImdsV2 probe if MtlsPop was NOT requested
if (isMtlsPopRequested)
{
var imdsV2Response = await ImdsManagedIdentitySource.ProbeImdsEndpointAsync(requestContext, ImdsVersion.V2, cancellationToken).ConfigureAwait(false);
if (imdsV2Response)
var (imdsV2Success, imdsV2Failure) = await ImdsManagedIdentitySource.ProbeImdsEndpointAsync(requestContext, ImdsVersion.V2, cancellationToken).ConfigureAwait(false);
if (imdsV2Success)
{
requestContext.Logger.Info("[Managed Identity] ImdsV2 detected.");
s_sourceName = ManagedIdentitySource.ImdsV2;
return s_sourceName;
return new ManagedIdentitySourceResult(s_sourceName);
}
imdsV2FailureReason = imdsV2Failure;
}
else
{
requestContext.Logger.Info("[Managed Identity] Mtls Pop was not requested; skipping ImdsV2 probe.");
}

var imdsV1Response = await ImdsManagedIdentitySource.ProbeImdsEndpointAsync(requestContext, ImdsVersion.V1, cancellationToken).ConfigureAwait(false);
if (imdsV1Response)
var (imdsV1Success, imdsV1Failure) = await ImdsManagedIdentitySource.ProbeImdsEndpointAsync(requestContext, ImdsVersion.V1, cancellationToken).ConfigureAwait(false);
if (imdsV1Success)
{
requestContext.Logger.Info("[Managed Identity] ImdsV1 detected.");
s_sourceName = ManagedIdentitySource.Imds;
return s_sourceName;
return new ManagedIdentitySourceResult(s_sourceName);
}
imdsV1FailureReason = imdsV1Failure;

requestContext.Logger.Info($"[Managed Identity] {MsalErrorMessage.ManagedIdentityAllSourcesUnavailable}");
s_sourceName = ManagedIdentitySource.None;
return s_sourceName;

return new ManagedIdentitySourceResult(s_sourceName)
{
ImdsV1FailureReason = imdsV1FailureReason,
ImdsV2FailureReason = imdsV2FailureReason
};
}

/// <summary>
Expand Down Expand Up @@ -229,13 +241,40 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string
return false;
}

/// <summary>
/// Creates an MsalClientException for when no managed identity source is available,
/// including detailed failure information from IMDS probes if available.
/// </summary>
private static MsalClientException CreateManagedIdentityUnavailableException(ManagedIdentitySourceResult sourceResult)
{
string errorMessage = MsalErrorMessage.ManagedIdentityAllSourcesUnavailable;

if (sourceResult != null)
{
if (!string.IsNullOrEmpty(sourceResult.ImdsV1FailureReason) || !string.IsNullOrEmpty(sourceResult.ImdsV2FailureReason))
{
errorMessage += " MSAL was not able to detect the Azure Instance Metadata Service (IMDS) that runs on VMs:";
if (!string.IsNullOrEmpty(sourceResult.ImdsV2FailureReason))
{
errorMessage += $" IMDSv2: {sourceResult.ImdsV2FailureReason}.";
}
if (!string.IsNullOrEmpty(sourceResult.ImdsV1FailureReason))
{
errorMessage += $" IMDSv1: {sourceResult.ImdsV1FailureReason}.";
}
}
}

return new MsalClientException(MsalError.ManagedIdentityAllSourcesUnavailable, errorMessage);
}

/// <summary>
/// Sets (or replaces) the in-memory binding certificate used to prime the mtls_pop scheme on subsequent requests.
/// The certificate is intentionally NOT disposed here to avoid invalidating caller-held references (e.g., via AuthenticationResult).
/// </summary>
/// <remarks>
/// Lifetime considerations:
/// - The binding certificate is ephemeral and valid for the tokens binding duration.
/// - The binding certificate is ephemeral and valid for the token's binding duration.
/// - If rotation occurs, older certificates will be eligible for GC once no longer referenced.
/// - Explicit disposal can be revisited if a deterministic rotation / shutdown strategy is introduced.
/// </remarks>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Identity.Client.ManagedIdentity
{
/// <summary>
/// Result of managed identity source detection, including the detected source and any failure information from IMDS probes.
/// </summary>
/// <remarks>
/// This class is returned by <see cref="ManagedIdentityApplication.GetManagedIdentitySourceAsync"/> to provide
/// detailed information about managed identity source detection, including failure reasons when IMDS probes fail.
/// This information is useful for credential chains like DefaultAzureCredential to determine whether to skip
/// managed identity authentication entirely.
/// </remarks>
public class ManagedIdentitySourceResult
{
/// <summary>
/// Gets the detected managed identity source.
/// </summary>
/// <value>
/// The <see cref="ManagedIdentitySource"/> that was detected on the environment.
/// Returns <see cref="ManagedIdentitySource.None"/> if no managed identity source was detected.
/// </value>
public ManagedIdentitySource Source { get; }

/// <summary>
/// Gets or sets the failure reason from the IMDSv1 probe, if it failed.
/// </summary>
/// <value>
/// A string describing why the IMDSv1 probe failed, or <c>null</c> if the probe succeeded or was not attempted.
/// </value>
public string ImdsV1FailureReason { get; set; }

/// <summary>
/// Gets or sets the failure reason from the IMDSv2 probe, if it failed.
/// </summary>
/// <value>
/// A string describing why the IMDSv2 probe failed, or <c>null</c> if the probe succeeded or was not attempted.
/// </value>
public string ImdsV2FailureReason { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="ManagedIdentitySourceResult"/> class.
/// </summary>
/// <param name="source">The detected managed identity source.</param>
public ManagedIdentitySourceResult(ManagedIdentitySource source)
{
Source = source;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ public AcquireTokenForManagedIdentityParameterBuilder AcquireTokenForManagedIden
}

/// <inheritdoc/>
public async Task<ManagedIdentitySource> GetManagedIdentitySourceAsync(CancellationToken cancellationToken)
public async Task<ManagedIdentitySourceResult> GetManagedIdentitySourceAsync(CancellationToken cancellationToken)
{
if (ManagedIdentityClient.s_sourceName != ManagedIdentitySource.None)
{
return ManagedIdentityClient.s_sourceName;
return new ManagedIdentitySourceResult(ManagedIdentityClient.s_sourceName);
}

// Create a temporary RequestContext for the logger and the IMDS probe request.
Expand Down
5 changes: 5 additions & 0 deletions src/client/Microsoft.Identity.Client/MsalError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1232,5 +1232,10 @@ public static class MsalError
/// All managed identity sources are unavailable.
/// </summary>
public const string ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable";

/// <summary>
/// Represents the error code returned when an IMDS operation fails.
/// </summary>
public const string ImdsServiceError = "imds_service_error";
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
const Microsoft.Identity.Client.MsalError.ImdsServiceError = "imds_service_error" -> string
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task<bool>
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary<string, string>
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ManagedIdentitySourceResult(Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource source) -> void
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.Source.get -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult>
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
const Microsoft.Identity.Client.MsalError.ImdsServiceError = "imds_service_error" -> string
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task<bool>
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary<string, string>
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ManagedIdentitySourceResult(Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource source) -> void
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.Source.get -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult>
Loading
Loading