diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index eb322a08a8..27cd51b728 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -241,7 +241,7 @@ public static string ImdsQueryParamsHelper( return queryParams; } - public static async Task ProbeImdsEndpointAsync( + public static async Task<(bool success, string failureReason)> ProbeImdsEndpointAsync( RequestContext requestContext, ImdsVersion imdsVersion, CancellationToken cancellationToken) @@ -255,8 +255,8 @@ public static async Task 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; @@ -303,22 +303,24 @@ public static async Task 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); } } } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs index 0ae3cd10f9..3276160050 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs @@ -54,13 +54,15 @@ private async Task 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 { @@ -96,14 +98,14 @@ private async Task 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 GetManagedIdentitySourceAsync( + internal async Task GetManagedIdentitySourceAsync( RequestContext requestContext, bool isMtlsPopRequested, CancellationToken cancellationToken) @@ -113,36 +115,46 @@ internal async Task 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 + }; } /// @@ -229,13 +241,40 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string return false; } + /// + /// Creates an MsalClientException for when no managed identity source is available, + /// including detailed failure information from IMDS probes if available. + /// + 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); + } + /// /// 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). /// /// /// Lifetime considerations: - /// - The binding certificate is ephemeral and valid for the token’s 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. /// diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySourceResult.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySourceResult.cs new file mode 100644 index 0000000000..5ae7eb7a22 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySourceResult.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + /// + /// Result of managed identity source detection, including the detected source and any failure information from IMDS probes. + /// + /// + /// This class is returned by 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. + /// + public class ManagedIdentitySourceResult + { + /// + /// Gets the detected managed identity source. + /// + /// + /// The that was detected on the environment. + /// Returns if no managed identity source was detected. + /// + public ManagedIdentitySource Source { get; } + + /// + /// Gets or sets the failure reason from the IMDSv1 probe, if it failed. + /// + /// + /// A string describing why the IMDSv1 probe failed, or null if the probe succeeded or was not attempted. + /// + public string ImdsV1FailureReason { get; set; } + + /// + /// Gets or sets the failure reason from the IMDSv2 probe, if it failed. + /// + /// + /// A string describing why the IMDSv2 probe failed, or null if the probe succeeded or was not attempted. + /// + public string ImdsV2FailureReason { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The detected managed identity source. + public ManagedIdentitySourceResult(ManagedIdentitySource source) + { + Source = source; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs index 9a0e2e46e9..c2016a63e2 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs @@ -56,11 +56,11 @@ public AcquireTokenForManagedIdentityParameterBuilder AcquireTokenForManagedIden } /// - public async Task GetManagedIdentitySourceAsync(CancellationToken cancellationToken) + public async Task 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. diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index 688458a3cf..221042e069 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -1232,5 +1232,10 @@ public static class MsalError /// All managed identity sources are unavailable. /// public const string ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable"; + + /// + /// Represents the error code returned when an IMDS operation fails. + /// + public const string ImdsServiceError = "imds_service_error"; } } 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 ed17fdb6a4..d23bfb04d1 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,8 +1,16 @@ -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 Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary -Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +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 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 ed17fdb6a4..d23bfb04d1 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1,8 +1,16 @@ -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 Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary -Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +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 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 ed17fdb6a4..d23bfb04d1 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,8 +1,16 @@ -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 Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary -Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +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 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 ed17fdb6a4..d23bfb04d1 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,8 +1,16 @@ -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 Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary -Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +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 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 ed17fdb6a4..d23bfb04d1 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,8 +1,16 @@ -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 Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary -Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +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 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 ed17fdb6a4..d23bfb04d1 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,8 +1,16 @@ -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 Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary -Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +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 diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs index 508ace58cc..60b024df1e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs @@ -70,7 +70,8 @@ public async Task TestAppServiceUpgradeScenario( ManagedIdentityApplication mi = miBuilder.Build() as ManagedIdentityApplication; - Assert.AreEqual(expectedManagedIdentitySource, await mi.GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false)); + var miSourceResult = await mi.GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + Assert.AreEqual(expectedManagedIdentitySource, miSourceResult.Source); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 204074482d..ade71e97b4 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -3,7 +3,6 @@ using System; using System.Net; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; @@ -434,36 +433,5 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.AreEqual(Num504Errors, requestsMade); } } - - [TestMethod] - public async Task ProbeImdsEndpointAsync_TimesOutAfterOneSecond() - { - using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned); - - miBuilder - .WithHttpManager(httpManager) - .WithRetryPolicyFactory(_testRetryPolicyFactory); - - var managedIdentityApp = miBuilder.Build(); - - httpManager.AddMockHandler(MockHelpers.MockImdsProbeFailure(ImdsVersion.V2)); - httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); - - var imdsProbesCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(0)).Token; // timeout immediately - - var miSource = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(imdsProbesCancellationToken).ConfigureAwait(false); - Assert.AreEqual(ManagedIdentitySource.None, miSource); // Probe timed out, no source available - - var ex = await Assert.ThrowsExceptionAsync(async () => - await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false) - ).ConfigureAwait(false); - - Assert.AreEqual(MsalError.ManagedIdentityAllSourcesUnavailable, ex.ErrorCode); - } - } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs index 7e61d7d908..189bd7b5d7 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs @@ -129,8 +129,8 @@ private async Task CreateManagedIdentityAsync( if (addSourceCheck) { - var miSource = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); - Assert.AreEqual(ManagedIdentitySource.ImdsV2, miSource); + var miSourceResult = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + Assert.AreEqual(ManagedIdentitySource.ImdsV2, miSourceResult.Source); } // Choose deterministic key source for tests. @@ -392,8 +392,8 @@ public async Task ApplicationsCannotSwitchBetweenImdsVersionsForPreview( Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); // even though the app fell back to ImdsV1, the source should still be ImdsV2 - var miSource = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); - Assert.AreEqual(ManagedIdentitySource.ImdsV2, miSource); + var miSourceResult = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + Assert.AreEqual(ManagedIdentitySource.ImdsV2, miSourceResult.Source); // none of the mocks from AddMocksToGetEntraToken are needed since checking the cache occurs before the network requests var ex = await Assert.ThrowsExceptionAsync(async () => @@ -452,8 +452,35 @@ public async Task ProbeImdsEndpointAsyncFails404WhichIsNonRetriableAndRetryPolic var managedIdentityApp = await CreateManagedIdentityAsync(httpManager, addProbeMock: false, addSourceCheck: false).ConfigureAwait(false); - var miSource = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); - Assert.AreEqual(ManagedIdentitySource.Imds, miSource); + var miSourceResult = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + Assert.AreEqual(ManagedIdentitySource.Imds, miSourceResult.Source); + } + } + + [TestMethod] + public async Task ImdsProbeEndpointAsync_TimeOutThrowsOperationCanceledException() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned); + + miBuilder + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); + + var managedIdentityApp = miBuilder.Build(); + + httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V2)); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + var imdsProbesCancellationToken = cts.Token; + + await Assert.ThrowsExceptionAsync(async () => + await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(imdsProbesCancellationToken) + .ConfigureAwait(false)) + .ConfigureAwait(false); } } #endregion Probe Tests @@ -493,8 +520,8 @@ public async Task NonMtlsRequest_FallsBackToImdsV1( Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); // indicates ImdsV2 is still available - var miSource = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); - Assert.AreEqual(ManagedIdentitySource.ImdsV2, miSource); + var miSourceResult = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + Assert.AreEqual(ManagedIdentitySource.ImdsV2, miSourceResult.Source); } } @@ -517,8 +544,8 @@ public async Task ImdsV2ProbeFailsMaxRetries_FallsBackToImdsV1() var managedIdentityApp = await CreateManagedIdentityAsync(httpManager, addProbeMock: false, addSourceCheck: false).ConfigureAwait(false); - var miSource = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); - Assert.AreEqual(ManagedIdentitySource.Imds, miSource); + var miSourceResult = await (managedIdentityApp as ManagedIdentityApplication).GetManagedIdentitySourceAsync(ManagedIdentityTests.ImdsProbesCancellationToken).ConfigureAwait(false); + Assert.AreEqual(ManagedIdentitySource.Imds, miSourceResult.Source); } } #endregion diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index a4039d0791..a1c6653451 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -92,7 +92,8 @@ public async Task GetManagedIdentityTests( httpManager.AddMockHandler(MockHelpers.MockImdsProbe(ImdsVersion.V1)); } - Assert.AreEqual(managedIdentitySource, await mi.GetManagedIdentitySourceAsync(ImdsProbesCancellationToken).ConfigureAwait(false)); + var miSourceResult = await mi.GetManagedIdentitySourceAsync(ImdsProbesCancellationToken).ConfigureAwait(false); + Assert.AreEqual(managedIdentitySource, miSourceResult.Source); } }