diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityId.cs b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityId.cs index 6560d8a615..50277bf59f 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityId.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityId.cs @@ -19,25 +19,45 @@ namespace Microsoft.Identity.Client.AppConfig /// public class ManagedIdentityId { + /// + /// Gets the identifier for a user-assigned managed identity. + /// + /// + /// This property holds the unique identifier of the user-assigned managed identity. + /// It can be a client ID, a resource ID, or an object ID, depending on how the managed identity is configured. + /// + /// + /// The identifier string of the user-assigned managed identity. + /// internal string UserAssignedId { get; private set; } - internal ManagedIdentityIdType IdType { get; private set; } - internal readonly bool _isUserAssigned; + + /// + /// Gets the type of identifier used for the managed identity. + /// + /// + /// This property indicates the type of the managed identity identifier, + /// which can be either a client ID, a resource ID, or an object ID. + /// + /// + /// The enumeration value representing the managed identity identifier type. + /// + internal ManagedIdentityIdType IdType { get; } + + /// + /// Gets a value indicating whether the managed identity is user-assigned. + /// + /// + /// This property is true if the managed identity is user-assigned, and false if it is system-assigned. + /// + /// + /// True if the managed identity is user-assigned; otherwise, false. + /// + internal bool IsUserAssigned { get; } private ManagedIdentityId(ManagedIdentityIdType idType) { IdType = idType; - - switch (idType) - { - case ManagedIdentityIdType.SystemAssigned: - _isUserAssigned = false; - break; - case ManagedIdentityIdType.ClientId: - case ManagedIdentityIdType.ResourceId: - case ManagedIdentityIdType.ObjectId: - _isUserAssigned = true; - break; - } + IsUserAssigned = idType != ManagedIdentityIdType.SystemAssigned; } /// diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs index 75c08f8432..8578457f2c 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs @@ -2,18 +2,16 @@ // Licensed under the MIT License. using System; -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Utils; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Core; using System.Net; using Microsoft.Identity.Client.ApiConfig.Parameters; -using System.Net.Sockets; +using System.Collections.Generic; namespace Microsoft.Identity.Client.ManagedIdentity { @@ -21,7 +19,6 @@ namespace Microsoft.Identity.Client.ManagedIdentity /// Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/ManagedIdentitySource.cs /// internal abstract class AbstractManagedIdentity - { protected readonly RequestContext _requestContext; internal const string TimeoutError = "[Managed Identity] Authentication unavailable. The request to the managed identity endpoint timed out."; @@ -67,13 +64,9 @@ await _requestContext.ServiceBundle.HttpManager return await HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false); } - catch (HttpRequestException ex) - { - throw new MsalManagedIdentityException(MsalError.ManagedIdentityUnreachableNetwork, ex.Message, ex.InnerException, _sourceType); - } - catch (TaskCanceledException) + catch (Exception ex) { - _requestContext.Logger.Error(TimeoutError); + HandleException(ex); throw; } } @@ -83,28 +76,24 @@ protected virtual Task HandleResponseAsync( HttpResponse response, CancellationToken cancellationToken) { - string message; - Exception exception = null; - - try - { - if (response.StatusCode == HttpStatusCode.OK) - { - _requestContext.Logger.Info("[Managed Identity] Successful response received."); - return Task.FromResult(GetSuccessfulResponse(response)); - } - - message = GetMessageFromErrorResponse(response); - _requestContext.Logger.Error($"[Managed Identity] request failed, HttpStatusCode: {response.StatusCode} Error message: {message}"); - } - catch (Exception e) when (e is not MsalManagedIdentityException) + if (response.StatusCode == HttpStatusCode.OK) { - _requestContext.Logger.Error($"[Managed Identity] Exception: {e.Message} Http status code: {response?.StatusCode}"); - exception = e; - message = MsalErrorMessage.ManagedIdentityUnexpectedResponse; + _requestContext.Logger.Info("[Managed Identity] Successful response received."); + return Task.FromResult(GetSuccessfulResponse(response)); } - throw new MsalManagedIdentityException(MsalError.ManagedIdentityRequestFailed, message, exception, _sourceType, (int)response.StatusCode); + string message = GetMessageFromErrorResponse(response); + + _requestContext.Logger.Error($"[Managed Identity] request failed, HttpStatusCode: {response.StatusCode} Error message: {message}"); + + MsalException exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.ManagedIdentityRequestFailed, + message, + null, + _sourceType, + (int)response.StatusCode); + + throw exception; } protected abstract ManagedIdentityRequest CreateRequest(string resource); @@ -116,10 +105,15 @@ protected ManagedIdentityResponse GetSuccessfulResponse(HttpResponse response) if (managedIdentityResponse == null || managedIdentityResponse.AccessToken.IsNullOrEmpty() || managedIdentityResponse.ExpiresOn.IsNullOrEmpty()) { _requestContext.Logger.Error("[Managed Identity] Response is either null or insufficient for authentication."); - throw new MsalManagedIdentityException( - MsalError.ManagedIdentityRequestFailed, - MsalErrorMessage.ManagedIdentityInvalidResponse, - _sourceType); + + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.ManagedIdentityRequestFailed, + MsalErrorMessage.ManagedIdentityInvalidResponse, + null, + _sourceType, + null); + + throw exception; } return managedIdentityResponse; @@ -127,7 +121,7 @@ protected ManagedIdentityResponse GetSuccessfulResponse(HttpResponse response) internal string GetMessageFromErrorResponse(HttpResponse response) { - var managedIdentityErrorResponse = JsonHelper.TryToDeserializeFromJson(response?.Body); + ManagedIdentityErrorResponse managedIdentityErrorResponse = JsonHelper.TryToDeserializeFromJson(response?.Body); if (managedIdentityErrorResponse == null) { @@ -141,5 +135,47 @@ internal string GetMessageFromErrorResponse(HttpResponse response) return $"[Managed Identity] Error Code: {managedIdentityErrorResponse.Error} Error Message: {managedIdentityErrorResponse.ErrorDescription}"; } + + private void HandleException(Exception ex, + ManagedIdentitySource managedIdentitySource = ManagedIdentitySource.None, + string additionalInfo = null) + { + ManagedIdentitySource source = managedIdentitySource != ManagedIdentitySource.None ? managedIdentitySource : _sourceType; + + if (ex is HttpRequestException httpRequestException) + { + CreateAndThrowException(MsalError.ManagedIdentityUnreachableNetwork, httpRequestException.Message, httpRequestException, source); + } + else if (ex is TaskCanceledException) + { + _requestContext.Logger.Error(TimeoutError); + } + else if (ex is FormatException formatException) + { + string errorMessage = additionalInfo ?? formatException.Message; + _requestContext.Logger.Error($"[Managed Identity] Format Exception: {errorMessage}"); + CreateAndThrowException(MsalError.InvalidManagedIdentityEndpoint, errorMessage, formatException, source); + } + else if (ex is not MsalServiceException or TaskCanceledException) + { + _requestContext.Logger.Error($"[Managed Identity] Exception: {ex.Message}"); + CreateAndThrowException(MsalError.ManagedIdentityRequestFailed, ex.Message, ex, source); + } + } + + private void CreateAndThrowException(string errorCode, + string errorMessage, + Exception innerException, + ManagedIdentitySource source) + { + MsalException exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + errorCode, + errorMessage, + innerException, + source, + null); + + throw exception; + } } } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs index c722032a37..1b4002a5e5 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs @@ -55,9 +55,20 @@ private static bool TryValidateEnvVars(string msiEndpoint, string secret, ILogge } catch (FormatException ex) { - throw new MsalManagedIdentityException(MsalError.InvalidManagedIdentityEndpoint, string.Format( - CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "IDENTITY_ENDPOINT", msiEndpoint, "App Service"), - ex, ManagedIdentitySource.AppService); + string errorMessage = string.Format( + CultureInfo.InvariantCulture, + MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, + "IDENTITY_ENDPOINT", msiEndpoint, "App Service"); + + // Use the factory to create and throw the exception + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.InvalidManagedIdentityEndpoint, + errorMessage, + ex, + ManagedIdentitySource.AppService, + null); // statusCode is null in this case + + throw exception; } logger.Info($"[Managed Identity] Environment variables validation passed for app service managed identity. Endpoint URI: {endpointUri}. Creating App Service managed identity."); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index 4725127a88..370ed58f4e 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -40,9 +40,20 @@ public static AbstractManagedIdentity TryCreate(RequestContext requestContext) if (!Uri.TryCreate(identityEndpoint, UriKind.Absolute, out Uri endpointUri)) { - throw new MsalManagedIdentityException(MsalError.InvalidManagedIdentityEndpoint, string.Format( - CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "IDENTITY_ENDPOINT", identityEndpoint, AzureArc), - ManagedIdentitySource.AzureArc); + string errorMessage = string.Format( + CultureInfo.InvariantCulture, + MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, + "IDENTITY_ENDPOINT", identityEndpoint, AzureArc); + + // Use the factory to create and throw the exception + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.InvalidManagedIdentityEndpoint, + errorMessage, + null, + ManagedIdentitySource.AzureArc, + null); + + throw exception; } requestContext.Logger.Verbose(()=>"[Managed Identity] Creating Azure Arc managed identity. Endpoint URI: " + endpointUri); @@ -54,11 +65,18 @@ private AzureArcManagedIdentitySource(Uri endpoint, RequestContext requestContex { _endpoint = endpoint; - if (requestContext.ServiceBundle.Config.ManagedIdentityId._isUserAssigned) + if (requestContext.ServiceBundle.Config.ManagedIdentityId.IsUserAssigned) { - throw new MsalManagedIdentityException(MsalError.UserAssignedManagedIdentityNotSupported, - string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, AzureArc), - ManagedIdentitySource.AzureArc); + string errorMessage = string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, AzureArc); + + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.UserAssignedManagedIdentityNotSupported, + errorMessage, + null, + ManagedIdentitySource.AzureArc, + null); + + throw exception; } } @@ -85,9 +103,15 @@ protected override async Task HandleResponseAsync( if (!response.HeadersAsDictionary.TryGetValue("WWW-Authenticate", out string challenge)) { _requestContext.Logger.Error("[Managed Identity] WWW-Authenticate header is expected but not found."); - throw new MsalManagedIdentityException(MsalError.ManagedIdentityRequestFailed, - MsalErrorMessage.ManagedIdentityNoChallengeError, - ManagedIdentitySource.AzureArc); + + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.ManagedIdentityRequestFailed, + MsalErrorMessage.ManagedIdentityNoChallengeError, + null, + ManagedIdentitySource.AzureArc, + null); + + throw exception; } var splitChallenge = challenge.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries); @@ -95,9 +119,15 @@ protected override async Task HandleResponseAsync( if (splitChallenge.Length != 2) { _requestContext.Logger.Error("[Managed Identity] The WWW-Authenticate header for Azure arc managed identity is not an expected format."); - throw new MsalManagedIdentityException(MsalError.ManagedIdentityRequestFailed, - MsalErrorMessage.ManagedIdentityInvalidChallenge, - ManagedIdentitySource.AzureArc); + + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.ManagedIdentityRequestFailed, + MsalErrorMessage.ManagedIdentityInvalidChallenge, + null, + ManagedIdentitySource.AzureArc, + null); + + throw exception; } var authHeaderValue = "Basic " + File.ReadAllText(splitChallenge[1]); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs index 202252c01e..79284bdd6a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs @@ -36,9 +36,21 @@ public static AbstractManagedIdentity TryCreate(RequestContext requestContext) catch (FormatException ex) { requestContext.Logger.Error("[Managed Identity] Invalid endpoint found for the environment variable MSI_ENDPOINT: " + msiEndpoint); - throw new MsalManagedIdentityException(MsalError.InvalidManagedIdentityEndpoint, string.Format( - CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "MSI_ENDPOINT", msiEndpoint, CloudShell), - ex, ManagedIdentitySource.CloudShell); + + string errorMessage = string.Format( + CultureInfo.InvariantCulture, + MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, + "MSI_ENDPOINT", msiEndpoint, CloudShell); + + // Use the factory to create and throw the exception + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.InvalidManagedIdentityEndpoint, + errorMessage, + ex, + ManagedIdentitySource.CloudShell, + null); + + throw exception; } requestContext.Logger.Verbose(()=>"[Managed Identity] Creating cloud shell managed identity. Endpoint URI: " + msiEndpoint); @@ -50,11 +62,21 @@ private CloudShellManagedIdentitySource(Uri endpoint, RequestContext requestCont { _endpoint = endpoint; - if (requestContext.ServiceBundle.Config.ManagedIdentityId._isUserAssigned) + if (requestContext.ServiceBundle.Config.ManagedIdentityId.IsUserAssigned) { - throw new MsalManagedIdentityException(MsalError.UserAssignedManagedIdentityNotSupported, - string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, CloudShell), - ManagedIdentitySource.CloudShell); + string errorMessage = string.Format( + CultureInfo.InvariantCulture, + MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, + CloudShell); + + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.UserAssignedManagedIdentityNotSupported, + errorMessage, + null, + ManagedIdentitySource.CloudShell, + null); + + throw exception; } } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 08a612401e..46a2a9a608 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -27,7 +27,11 @@ internal class ImdsManagedIdentitySource : AbstractManagedIdentity private const string ImdsApiVersion = "2018-02-01"; private const string DefaultMessage = "[Managed Identity] Service request failed."; - internal const string IdentityUnavailableError = "[Managed Identity] Authentication unavailable. The requested identity has not been assigned to this resource."; + internal const string IdentityUnavailableError = "[Managed Identity] Authentication unavailable. " + + "Either the requested identity has not been assigned to this resource, or other errors could " + + "be present. Ensure the identity is correctly assigned and check the inner exception for more " + + "details. For more information, visit https://aka.ms/msal-managed-identity."; + internal const string GatewayError = "[Managed Identity] Authentication unavailable. The request failed due to a gateway error."; private readonly Uri _imdsEndpoint; @@ -105,8 +109,15 @@ protected override async Task HandleResponseAsync( message = message + Environment.NewLine + errorContentMessage; _requestContext.Logger.Error($"Error message: {message} Http status code: {response.StatusCode}"); - throw new MsalManagedIdentityException(MsalError.ManagedIdentityRequestFailed, message, - ManagedIdentitySource.Imds); + + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.ManagedIdentityRequestFailed, + message, + null, + ManagedIdentitySource.Imds, + null); + + throw exception; } // Default behavior to handle successful scenario and general errors. diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs index aaf0e5f5b9..fd6e1df82e 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs @@ -33,10 +33,18 @@ public static AbstractManagedIdentity TryCreate(RequestContext requestContext) if (!Uri.TryCreate(identityEndpoint, UriKind.Absolute, out Uri endpointUri)) { - throw new MsalManagedIdentityException(MsalError.InvalidManagedIdentityEndpoint, - string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, - "IDENTITY_ENDPOINT", identityEndpoint, "Service Fabric"), - ManagedIdentitySource.ServiceFabric); + string errorMessage = string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, + "IDENTITY_ENDPOINT", identityEndpoint, "Service Fabric"); + + // Use the factory to create and throw the exception + var exception = MsalServiceExceptionFactory.CreateManagedIdentityException( + MsalError.InvalidManagedIdentityEndpoint, + errorMessage, + null, + ManagedIdentitySource.ServiceFabric, + null); + + throw exception; } requestContext.Logger.Verbose(() => "[Managed Identity] Creating Service Fabric managed identity. Endpoint URI: " + identityEndpoint); @@ -49,7 +57,7 @@ private ServiceFabricManagedIdentitySource(RequestContext requestContext, Uri en _endpoint = endpoint; _identityHeaderValue = identityHeaderValue; - if (requestContext.ServiceBundle.Config.ManagedIdentityId._isUserAssigned) + if (requestContext.ServiceBundle.Config.ManagedIdentityId.IsUserAssigned) { requestContext.Logger.Warning(MsalErrorMessage.ManagedIdentityUserAssignedNotConfigurableAtRuntime); } diff --git a/src/client/Microsoft.Identity.Client/MsalException.cs b/src/client/Microsoft.Identity.Client/MsalException.cs index 3cca196951..3d58a11248 100644 --- a/src/client/Microsoft.Identity.Client/MsalException.cs +++ b/src/client/Microsoft.Identity.Client/MsalException.cs @@ -48,6 +48,10 @@ public class MsalException : Exception /// An property key, available when using desktop brokers. /// public const string BrokerTelemetry = "BrokerTelemetry"; + /// + /// An property key, available when using managed identity. + /// + public const string ManagedIdentitySource = "ManagedIdentitySource"; private string _errorCode; @@ -180,6 +184,7 @@ private class ExceptionSerializationKey internal const string BrokerErrorStatus = "broker_error_status"; internal const string BrokerErrorCode = "broker_error_code"; internal const string BrokerTelemetry = "broker_telemetry"; + internal const string ManagedIdentitySource = "managed_identity_source"; } internal virtual void PopulateJson(JObject jObject) @@ -211,6 +216,10 @@ internal virtual void PopulateJson(JObject jObject) { exceptionData[ExceptionSerializationKey.BrokerTelemetry] = brokerTelemetry; } + if(AdditionalExceptionData.TryGetValue(ManagedIdentitySource, out string managedIdentitySource)) + { + exceptionData[ExceptionSerializationKey.ManagedIdentitySource] = managedIdentitySource; + } jObject[ExceptionSerializationKey.AdditionalExceptionData] = exceptionData; } @@ -245,6 +254,11 @@ internal virtual void PopulateObjectFromJson(JObject jObject) exceptionData[BrokerTelemetry] = brokerTelemetry; exceptionData.Remove(ExceptionSerializationKey.BrokerTelemetry); } + if(exceptionData.TryGetValue(ExceptionSerializationKey.ManagedIdentitySource, out string managedIdentitySource)) + { + exceptionData[ManagedIdentitySource] = managedIdentitySource; + exceptionData.Remove(ExceptionSerializationKey.ManagedIdentitySource); + } AdditionalExceptionData = (IReadOnlyDictionary)exceptionData; } diff --git a/src/client/Microsoft.Identity.Client/MsalManagedIdentityException.cs b/src/client/Microsoft.Identity.Client/MsalManagedIdentityException.cs index d803f6305f..542196f64b 100644 --- a/src/client/Microsoft.Identity.Client/MsalManagedIdentityException.cs +++ b/src/client/Microsoft.Identity.Client/MsalManagedIdentityException.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ComponentModel; using System.Net; using Microsoft.Identity.Client.ManagedIdentity; @@ -9,8 +10,12 @@ namespace Microsoft.Identity.Client { /// /// This exception class is for exceptions generated from Managed Identity sources. + /// This class is deprecated and will be removed in a future release. + /// Catch MsalServiceException instead. /// For more details, see https://aka.ms/msal-net-managed-identity /// + [Obsolete("MsalManagedIdentityException is deprecated and will be removed in a future release. Catch MsalServiceException instead.", true)] + [EditorBrowsable(EditorBrowsableState.Never)] public class MsalManagedIdentityException : MsalServiceException { /// diff --git a/src/client/Microsoft.Identity.Client/MsalServiceException.cs b/src/client/Microsoft.Identity.Client/MsalServiceException.cs index 3c11fd2ef5..0b66886248 100644 --- a/src/client/Microsoft.Identity.Client/MsalServiceException.cs +++ b/src/client/Microsoft.Identity.Client/MsalServiceException.cs @@ -5,14 +5,15 @@ using System.Globalization; using System.Net; using System.Net.Http.Headers; -using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Utils; + #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text.Json.Serialization; using JObject = System.Text.Json.Nodes.JsonObject; #else using Microsoft.Identity.Json.Linq; #endif + namespace Microsoft.Identity.Client { @@ -232,11 +233,11 @@ public HttpResponseHeaders Headers protected virtual void UpdateIsRetryable() { IsRetryable = - (StatusCode >= 500 && StatusCode < 600) || - StatusCode == 429 || // too many requests - StatusCode == (int)HttpStatusCode.RequestTimeout || - string.Equals(ErrorCode, MsalError.RequestTimeout, StringComparison.OrdinalIgnoreCase) || - string.Equals(ErrorCode, "temporarily_unavailable", StringComparison.OrdinalIgnoreCase); // as per https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes#handling-error-codes-in-your-application + (StatusCode >= 500 && StatusCode < 600) || + StatusCode == 429 || // too many requests + StatusCode == (int)HttpStatusCode.RequestTimeout || + string.Equals(ErrorCode, MsalError.RequestTimeout, StringComparison.OrdinalIgnoreCase) || + string.Equals(ErrorCode, "temporarily_unavailable", StringComparison.OrdinalIgnoreCase); // as per https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes#handling-error-codes-in-your-application } /// diff --git a/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs b/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs index 42e54a955d..0617f8c134 100644 --- a/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs +++ b/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs @@ -126,21 +126,52 @@ internal static MsalServiceException FromImdsResponse( return ex; } - internal static MsalServiceException FromManagedIdentityResponse( + internal static MsalException CreateManagedIdentityException( string errorCode, - HttpResponse httpResponse) + string errorMessage, + Exception innerException, + ManagedIdentitySource managedIdentitySource, + int? statusCode) { - ManagedIdentityErrorResponse managedIdentityResponse = JsonHelper.TryToDeserializeFromJson(httpResponse?.Body); + MsalException exception; - string message = managedIdentityResponse == null ? - "Empty error response received." : - $"Error message: {managedIdentityResponse.Message}. Correlation Id: {managedIdentityResponse.CorrelationId}"; + if (statusCode.HasValue) + { + exception = new MsalServiceException(errorCode, errorMessage, (int)statusCode, innerException); - MsalServiceException ex = new MsalServiceException(errorCode, message, (int)httpResponse.StatusCode); + var isRetryable = statusCode switch + { + 404 or 408 or 429 or 500 or 503 or 504 => true, + _ => false, + }; - SetHttpExceptionData(ex, httpResponse); + exception.IsRetryable = isRetryable; + } + else if (innerException != null) + { + exception = new MsalServiceException(errorCode, errorMessage, innerException); + } + else + { + exception = new MsalServiceException(errorCode, errorMessage); + } - return ex; + exception = DecorateExceptionWithManagedIdentitySource(exception, managedIdentitySource); + return exception; + } + + private static MsalException DecorateExceptionWithManagedIdentitySource( + MsalException exception, + ManagedIdentitySource managedIdentitySource) + { + var result = new Dictionary() + { + { MsalException.ManagedIdentitySource, managedIdentitySource.ToString() } + }; + + exception.AdditionalExceptionData = result; + + return exception; } internal static MsalThrottledServiceException FromThrottledAuthenticationResponse(HttpResponse httpResponse) diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/ManagedIdentityTests.cs index 935c617e04..a8695b7572 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/ManagedIdentityTests.cs @@ -152,7 +152,7 @@ public async Task ManagedIdentityRequestFailureCheckAsync(MsiAzureResource azure IManagedIdentityApplication mia = CreateMIAWithProxy(uri, userIdentity, userAssignedIdentityId); //Act - MsalManagedIdentityException ex = await AssertException.TaskThrowsAsync(async () => + MsalServiceException ex = await AssertException.TaskThrowsAsync(async () => { await mia .AcquireTokenForManagedIdentity(s_msi_scopes) @@ -165,7 +165,7 @@ await mia //Assert Assert.IsTrue(ex.ErrorCode == MsalError.ManagedIdentityRequestFailed); - Assert.AreEqual(expectedResource, ex.ManagedIdentitySource); + Assert.AreEqual(expectedResource.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); } } @@ -194,7 +194,7 @@ public async Task MSIWrongScopesAsync(MsiAzureResource azureResource, string use IManagedIdentityApplication mia = CreateMIAWithProxy(uri, userIdentity, userAssignedIdentityId); //Act - MsalManagedIdentityException ex = await AssertException.TaskThrowsAsync(async () => + MsalServiceException ex = await AssertException.TaskThrowsAsync(async () => { await mia .AcquireTokenForManagedIdentity(s_wrong_msi_scopes) @@ -203,7 +203,7 @@ await mia //Assert Assert.IsTrue(ex.ErrorCode == MsalError.ManagedIdentityRequestFailed); - Assert.AreEqual(ManagedIdentitySource.AppService, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.AppService.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs index fb2b3e25ad..2ad9732daa 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs @@ -37,13 +37,13 @@ public async Task AppServiceInvalidEndpointAsync() var mi = miBuilder.Build(); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); Assert.AreEqual(MsalError.InvalidManagedIdentityEndpoint, ex.ErrorCode); - Assert.AreEqual(ManagedIdentitySource.AppService, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.AppService.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "IDENTITY_ENDPOINT", "127.0.0.1:41564/msi/token", AppService), ex.Message); } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AzureArcTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AzureArcTests.cs index ad38f691fb..d2bedff0ee 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AzureArcTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AzureArcTests.cs @@ -38,12 +38,12 @@ public async Task AzureArcUserAssignedManagedIdentityNotSupportedAsync(string us IManagedIdentityApplication mi = miBuilder.Build(); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity("scope") .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.AzureArc, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.AzureArc.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.UserAssignedManagedIdentityNotSupported, ex.ErrorCode); Assert.AreEqual(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, AzureArc), ex.Message); } @@ -67,12 +67,12 @@ public async Task AzureArcAuthHeaderMissingAsync() httpManager.AddManagedIdentityWSTrustMockHandler(ManagedIdentityTests.AzureArcEndpoint); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity("scope") .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.AzureArc, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.AzureArc.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); Assert.AreEqual(MsalErrorMessage.ManagedIdentityNoChallengeError, ex.Message); } @@ -96,12 +96,12 @@ public async Task AzureArcAuthHeaderInvalidAsync() httpManager.AddManagedIdentityWSTrustMockHandler(ManagedIdentityTests.AzureArcEndpoint, "somevalue=filepath"); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity("scope") .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.AzureArc, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.AzureArc.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); Assert.AreEqual(MsalErrorMessage.ManagedIdentityInvalidChallenge, ex.Message); } @@ -123,12 +123,12 @@ public async Task AzureArcInvalidEndpointAsync() var mi = miBuilder.Build(); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.AzureArc, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.AzureArc.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.InvalidManagedIdentityEndpoint, ex.ErrorCode); Assert.AreEqual(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "IDENTITY_ENDPOINT", "localhost/token", AzureArc), ex.Message); } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/CloudShellTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/CloudShellTests.cs index 602034eb0e..bf96544c49 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/CloudShellTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/CloudShellTests.cs @@ -37,12 +37,12 @@ public async Task CloudShellUserAssignedManagedIdentityNotSupportedAsync(string IManagedIdentityApplication mi = miBuilder.Build(); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity("scope") .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.CloudShell, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.CloudShell.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.UserAssignedManagedIdentityNotSupported, ex.ErrorCode); Assert.AreEqual(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, CloudShell), ex.Message); } @@ -64,12 +64,12 @@ public async Task CloudShellInvalidEndpointAsync() var mi = miBuilder.Build(); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.CloudShell, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.CloudShell.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.InvalidManagedIdentityEndpoint, ex.ErrorCode); } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 26a907e9fd..b28b9a9559 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -37,14 +37,14 @@ public async Task ImdsBadRequestTestAsync() httpManager.AddManagedIdentityMockHandler(ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, statusCode: HttpStatusCode.BadRequest); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.Imds, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.Imds.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); - Assert.IsTrue(ex.Message.Contains("The requested identity has not been assigned to this resource.")); + Assert.IsTrue(ex.Message.Contains(ImdsManagedIdentitySource.IdentityUnavailableError)); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 982dd20a90..a8ac70de0e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -290,12 +290,12 @@ public async Task ManagedIdentityTestWrongScopeAsync(string resource, ManagedIde httpManager.AddManagedIdentityMockHandler(endpoint, resource, MockHelpers.GetMsiErrorResponse(), managedIdentitySource, statusCode: HttpStatusCode.InternalServerError); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(managedIdentitySource, ex.ManagedIdentitySource); + Assert.AreEqual(managedIdentitySource.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); } } @@ -345,12 +345,12 @@ public async Task ManagedIdentityErrorResponseNoPayloadTestAsync(ManagedIdentity httpManager.AddManagedIdentityMockHandler(endpoint, "scope", "", managedIdentitySource, statusCode: HttpStatusCode.InternalServerError); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity("scope") .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(managedIdentitySource, ex.ManagedIdentitySource); + Assert.AreEqual(managedIdentitySource.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); Assert.AreEqual(MsalErrorMessage.ManagedIdentityNoResponseReceived, ex.Message); } @@ -384,12 +384,12 @@ public async Task ManagedIdentityNullResponseAsync(ManagedIdentitySource managed managedIdentitySource, statusCode: HttpStatusCode.OK); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(managedIdentitySource, ex.ManagedIdentitySource); + Assert.AreEqual(managedIdentitySource.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); Assert.AreEqual(MsalErrorMessage.ManagedIdentityInvalidResponse, ex.Message); } @@ -419,12 +419,12 @@ public async Task ManagedIdentityUnreachableNetworkAsync(ManagedIdentitySource m httpManager.AddFailingRequest(new HttpRequestException("A socket operation was attempted to an unreachable network.", new SocketException(10051))); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(managedIdentitySource, ex.ManagedIdentitySource); + Assert.AreEqual(managedIdentitySource.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.ManagedIdentityUnreachableNetwork, ex.ErrorCode); Assert.AreEqual("A socket operation was attempted to an unreachable network.", ex.Message); } @@ -469,12 +469,13 @@ public async Task ManagedIdentityTestRetryAsync(ManagedIdentitySource managedIde managedIdentitySource, statusCode: statusCode); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); + Assert.AreEqual(managedIdentitySource.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.IsTrue(ex.IsRetryable); } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs index dab11fe288..52a4855852 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs @@ -43,12 +43,12 @@ public async Task ServiceFabricInvalidEndpointAsync() var mi = miBuilder.Build(); - MsalManagedIdentityException ex = await Assert.ThrowsExceptionAsync(async () => + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.ServiceFabric, ex.ManagedIdentitySource); + Assert.AreEqual(ManagedIdentitySource.ServiceFabric.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); Assert.AreEqual(MsalError.InvalidManagedIdentityEndpoint, ex.ErrorCode); Assert.AreEqual(string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityEndpointInvalidUriError, "IDENTITY_ENDPOINT", "localhost/token", "Service Fabric"), ex.Message); }