diff --git a/src/Microsoft.Identity.Client/Internal/Requests/SilentRequest.cs b/src/Microsoft.Identity.Client/Internal/Requests/SilentRequest.cs index 69352f4561..5931020518 100644 --- a/src/Microsoft.Identity.Client/Internal/Requests/SilentRequest.cs +++ b/src/Microsoft.Identity.Client/Internal/Requests/SilentRequest.cs @@ -19,7 +19,6 @@ internal class SilentRequest : RequestBase { private readonly AcquireTokenSilentParameters _silentParameters; private const string TheOnlyFamilyId = "1"; - private const string FociClientMismatchSubError = "client_mismatch"; public SilentRequest( IServiceBundle serviceBundle, @@ -163,7 +162,7 @@ private async Task TryGetTokenUsingFociAsync(CancellationToke return null; #else if (MsalError.InvalidGrantError.Equals(ex?.ErrorCode, StringComparison.OrdinalIgnoreCase) && - FociClientMismatchSubError.Equals(ex?.SubError, StringComparison.OrdinalIgnoreCase)) + OAuth2SubError.ClientMismatch.Equals(ex?.SubError, StringComparison.OrdinalIgnoreCase)) { logger.Error("[FOCI] FRT refresh failed - client mismatch"); return null; @@ -179,7 +178,6 @@ private async Task TryGetTokenUsingFociAsync(CancellationToke } return null; - } private async Task RefreshAccessTokenAsync(MsalRefreshTokenCacheItem msalRefreshTokenItem, CancellationToken cancellationToken) diff --git a/src/Microsoft.Identity.Client/MsalServiceException.cs b/src/Microsoft.Identity.Client/MsalServiceException.cs index 2e43d4d57e..3adbe47df3 100644 --- a/src/Microsoft.Identity.Client/MsalServiceException.cs +++ b/src/Microsoft.Identity.Client/MsalServiceException.cs @@ -17,6 +17,15 @@ namespace Microsoft.Identity.Client /// public class MsalServiceException : MsalException { + private const string ClaimsKey = "claims"; + private const string ResponseBodyKey = "response_body"; + private const string CorrelationIdKey = "correlation_id"; + private const string SubErrorKey = "sub_error"; + + private HttpResponse _httpResponse; + private OAuth2ResponseBase _oauth2ResponseBase; + + #region Constructors /// /// Initializes a new instance of the exception class with a specified /// error code, error message and a reference to the inner exception that is the cause of @@ -123,7 +132,7 @@ public MsalServiceException( Claims = claims; } - private HttpResponse _httpResponse; + #endregion internal HttpResponse HttpResponse { @@ -140,8 +149,6 @@ internal HttpResponse HttpResponse } } - private OAuth2ResponseBase _oauth2ResponseBase; - internal OAuth2ResponseBase OAuth2Response { get => _oauth2ResponseBase; @@ -220,11 +227,6 @@ public override string ToString() Headers); } - private const string ClaimsKey = "claims"; - private const string ResponseBodyKey = "response_body"; - private const string CorrelationIdKey = "correlation_id"; - private const string SubErrorKey = "sub_error"; - internal override void PopulateJson(JObject jobj) { base.PopulateJson(jobj); diff --git a/src/Microsoft.Identity.Client/MsalUiRequiredException.cs b/src/Microsoft.Identity.Client/MsalUiRequiredException.cs index 1ecd6b4208..7f8b598716 100644 --- a/src/Microsoft.Identity.Client/MsalUiRequiredException.cs +++ b/src/Microsoft.Identity.Client/MsalUiRequiredException.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using Microsoft.Identity.Client.OAuth2; namespace Microsoft.Identity.Client { @@ -14,6 +15,37 @@ namespace Microsoft.Identity.Client /// public class MsalUiRequiredException : MsalServiceException { + /// + /// Condition can be resolved by user interaction during the interactive authentication flow. + /// See https://aka.ms/msal-net-uirequiredexception-classification for details + /// + public const string BasicAction = OAuth2SubError.BasicAction; + + /// + /// Condition can be resolved by additional remedial interaction with the system, outside of the interactive authentication flow. + /// See https://aka.ms/msal-net-uirequiredexception-classification for details + /// + public const string AdditionalAction = OAuth2SubError.AdditionalAction; + + /// + /// Condition cannot be resolved at this time. Launching interactive authentication flow will show a message explaining the condition. + /// See https://aka.ms/msal-net-uirequiredexception-classification for details + /// + public const string MessageOnly = OAuth2SubError.MessageOnly; + + /// + /// User's password has expired. + /// See https://aka.ms/msal-net-uirequiredexception-classification for details + /// + public const string UserPasswordExpired = OAuth2SubError.UserPasswordExpired; + + /// + /// User consent is missing, or has been revoked. + /// See https://aka.ms/msal-net-uirequiredexception-classification for details + /// + public const string ConsentRequired = OAuth2SubError.ConsentRequired; + + /// /// Initializes a new instance of the exception class with a specified /// error code and error message. @@ -40,6 +72,38 @@ public MsalUiRequiredException(string errorCode, string errorMessage) : /// Represents the root cause of the exception. public MsalUiRequiredException(string errorCode, string errorMessage, Exception innerException) : base(errorCode, errorMessage, innerException) { + + } + + /// + /// Classification of the conditional access error, enabling you to do more actions or inform the user depending on your scenario. See https://aka.ms/msal-net-uirequiredexception-classification for details. + /// + /// This class lists most classification strings as constants. + public string Classification + { + get + { + switch (base.SubError) + { + case OAuth2SubError.BasicAction: + case OAuth2SubError.AdditionalAction: + case OAuth2SubError.MessageOnly: + case OAuth2SubError.ConsentRequired: + case OAuth2SubError.UserPasswordExpired: + return SubError; + + case OAuth2SubError.BadToken: + case OAuth2SubError.TokenExpired: + case OAuth2SubError.ProtectionPolicyRequired: + case OAuth2SubError.ClientMismatch: + case OAuth2SubError.DeviceAuthenticationFailed: + return string.Empty; + + // Forward compatibility - new sub-errors bubble through + default: + return SubError; + } + } } } } diff --git a/src/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs b/src/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs index da4e90fc93..cc503e9485 100644 --- a/src/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs +++ b/src/Microsoft.Identity.Client/OAuth2/OAuthConstants.cs @@ -83,6 +83,59 @@ internal static class OAuth2Error public const string AuthorizationPending = "authorization_pending"; } + internal static class OAuth2SubError + { + /// + /// Condition can be resolved by user interaction during the interactive authentication flow. + /// + public const string BasicAction = "basic_action"; + + /// + /// Condition can be resolved by additional remedial interaction with the system, outside of the interactive authentication flow. + /// + public const string AdditionalAction = "additional_action"; + + /// + /// Condition cannot be resolved at this time. Launching interactive authentication flow will show a message explaining the condition. + /// + public const string MessageOnly = "message_only"; + + /// + /// User's password has expired. + /// + public const string UserPasswordExpired = "user_password_expired"; + + /// + /// User consent is missing, or has been revoked. + /// + public const string ConsentRequired = "consent_required"; + + /// + /// Internal to MSALs. Indicates that no further silent calls should be made with this refresh token. + /// + public const string BadToken = "bad_token"; + + /// + /// Internal to MSALs. Indicates that no further silent calls should be made with this refresh token. + /// + public const string TokenExpired = "token_expired"; + + /// + /// Internal to MSALs. Needed in ios/android to complete the end-to-end true MAM flow. This suberror code is re-mapped to a different top level error code (IntuneAppProtectionPoliciesRequired), and not InteractionRequired + /// + public const string ProtectionPolicyRequired = "protection_policy_required"; + + /// + /// Internal to MSALs. Used in scenarios where an application is using family refresh token even though it is not part of FOCI (or vice versa). Needed to handle cases where app changes FOCI membership after being shipped. This is handled internally and doesn't need to be exposed to the calling app. Please see FOCI design document for more details. + /// + public const string ClientMismatch = "client_mismatch"; + + /// + /// Internal to MSALs. Indicates that device should be re-registered. + /// + public const string DeviceAuthenticationFailed = "device_authentication_failed"; + } + internal static class OAuth2Value { public static readonly string[] ReservedScopes = { ScopeOpenId, ScopeProfile, ScopeOfflineAccess }; diff --git a/tests/Microsoft.Identity.Test.Unit.net45/ExceptionTests/MsalExceptionFactoryTests.cs b/tests/Microsoft.Identity.Test.Unit.net45/ExceptionTests/MsalExceptionFactoryTests.cs deleted file mode 100644 index ffd207228d..0000000000 --- a/tests/Microsoft.Identity.Test.Unit.net45/ExceptionTests/MsalExceptionFactoryTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using Microsoft.Identity.Client; -using Microsoft.Identity.Client.Http; -using Microsoft.Identity.Client.Internal; -using Microsoft.Identity.Client.OAuth2; -using Microsoft.Identity.Client.Utils; -using Microsoft.Identity.Test.Common.Core.Helpers; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Microsoft.Identity.Test.Unit.ExceptionTests -{ - [TestClass] - public class MsalExceptionFactoryTests - { - private const string ExCode = "exCode"; - private const string ExMessage = "exMessage"; - - private const string jsonError = @"{ ""error"":""invalid_tenant"", ""suberror"":""some_suberror"", - ""claims"":""some_claims"", - ""error_description"":""AADSTS90002: Tenant 'x' not found. "", ""error_codes"":[90002],""timestamp"":""2019-01-28 14:16:04Z"", - ""trace_id"":""43f14373-8d7d-466e-a5f1-6e3889291e00"", - ""correlation_id"":""6347d33d-941a-4c35-9912-a9cf54fb1b3e""}"; - - [TestInitialize] - public void Init() - { - } - - [TestMethod] - public void ParamValidation() - { - AssertException.Throws(() => new MsalClientException(null, ExMessage)); - AssertException.Throws(() => new MsalClientException(string.Empty, ExMessage)); - - AssertException.Throws( - () => new MsalServiceException(ExCode, string.Empty)); - - AssertException.Throws( - () => new MsalServiceException(ExCode, null)); - } - - [TestMethod] - public void MsalClientException_FromMessageAndCode() - { - // Act - var msalException = new MsalClientException(ExCode, ExMessage); - - // Assert - var msalClientException = msalException as MsalClientException; - Assert.AreEqual(ExCode, msalClientException.ErrorCode); - Assert.AreEqual(ExMessage, msalClientException.Message); - Assert.IsNull(msalClientException.InnerException); - - // Act - string piiMessage = MsalLogger.GetPiiScrubbedExceptionDetails(msalException); - - // Assert - Assert.IsFalse(string.IsNullOrEmpty(piiMessage)); - Assert.IsTrue( - piiMessage.Contains(typeof(MsalClientException).Name), - "The pii message should contain the exception type"); - Assert.IsTrue(piiMessage.Contains(ExCode)); - Assert.IsFalse(piiMessage.Contains(ExMessage)); - } - - [TestMethod] - public void MsalServiceException_Oauth2Response_Only() - { - // Arrange - HttpResponse httpResponse = new HttpResponse() - { - Body = jsonError, - StatusCode = HttpStatusCode.BadRequest, // 400 - }; - - OAuth2ResponseBase oAuth2Response = - JsonHelper.TryToDeserializeFromJson(httpResponse?.Body); - - // Act - var msalException = new MsalServiceException(ExCode, ExMessage) - { - OAuth2Response = oAuth2Response - }; - - // Assert - var msalServiceException = msalException as MsalServiceException; - Assert.AreEqual(ExCode, msalServiceException.ErrorCode); - Assert.AreEqual(ExMessage, msalServiceException.Message); - Assert.AreEqual("some_claims", msalServiceException.Claims); - Assert.AreEqual("6347d33d-941a-4c35-9912-a9cf54fb1b3e", msalServiceException.CorrelationId); - Assert.AreEqual("some_suberror", msalServiceException.SubError); - } - - [TestMethod] - public void MsalServiceException_HttpResponse_OAuthResponse() - { - // Arrange - int statusCode = 400; - string innerExMsg = "innerExMsg"; - var innerException = new NotImplementedException(innerExMsg); - - HttpResponse httpResponse = new HttpResponse() - { - Body = jsonError, - StatusCode = HttpStatusCode.BadRequest, // 400 - }; - - // Act - var msalException = new MsalServiceException(ExCode, ExMessage, innerException) - { - HttpResponse = httpResponse - }; - - // Assert - var msalServiceException = msalException as MsalServiceException; - Assert.AreEqual(innerException, msalServiceException.InnerException); - Assert.AreEqual(ExCode, msalServiceException.ErrorCode); - Assert.AreEqual(jsonError, msalServiceException.ResponseBody); - Assert.AreEqual(ExMessage, msalServiceException.Message); - Assert.AreEqual(statusCode, msalServiceException.StatusCode); - - Assert.AreEqual("some_claims", msalServiceException.Claims); - Assert.AreEqual("6347d33d-941a-4c35-9912-a9cf54fb1b3e", msalServiceException.CorrelationId); - Assert.AreEqual("some_suberror", msalServiceException.SubError); - - // Act - string piiMessage = MsalLogger.GetPiiScrubbedExceptionDetails(msalException); - - // Assert - Assert.IsFalse(string.IsNullOrEmpty(piiMessage)); - Assert.IsTrue( - piiMessage.Contains(typeof(MsalServiceException).Name), - "The pii message should contain the exception type"); - Assert.IsTrue( - piiMessage.Contains(typeof(NotImplementedException).Name), - "The pii message should have the inner exception type"); - Assert.IsTrue(piiMessage.Contains(ExCode)); - Assert.IsTrue(piiMessage.Contains("6347d33d-941a-4c35-9912-a9cf54fb1b3e")); // Correlation Id - - Assert.IsFalse(piiMessage.Contains(ExMessage)); - Assert.IsFalse(piiMessage.Contains(innerExMsg)); - - } - - [TestMethod] - public void MsalUiRequiredException() - { - // Arrange - string innerExMsg = "innerExMsg"; - var innerException = new NotImplementedException(innerExMsg); - - // Act - var msalException = new MsalUiRequiredException( - ExCode, - ExMessage, - innerException); - - // Assert - var msalServiceException = msalException as MsalUiRequiredException; - Assert.AreEqual(innerException, msalServiceException.InnerException); - Assert.AreEqual(ExCode, msalServiceException.ErrorCode); - Assert.IsNull(msalServiceException.Claims); - Assert.IsNull(msalServiceException.ResponseBody); - Assert.AreEqual(ExMessage, msalServiceException.Message); - Assert.AreEqual(0, msalServiceException.StatusCode); - - // Act - string piiMessage = MsalLogger.GetPiiScrubbedExceptionDetails(msalException); - - // Assert - Assert.IsFalse(string.IsNullOrEmpty(piiMessage)); - Assert.IsTrue( - piiMessage.Contains(typeof(MsalUiRequiredException).Name), - "The pii message should contain the exception type"); - Assert.IsTrue( - piiMessage.Contains(typeof(NotImplementedException).Name), - "The pii message should have the inner exception type"); - Assert.IsTrue(piiMessage.Contains(ExCode)); - Assert.IsFalse(piiMessage.Contains(ExMessage)); - Assert.IsFalse(piiMessage.Contains(innerExMsg)); - } - - [TestMethod] - public void MsalServiceException_FromHttpResponse() - { - // Arrange - string responseBody = jsonError; - var statusCode = HttpStatusCode.BadRequest; - var retryAfterSpan = new TimeSpan(3600); - - var httpResponse = new HttpResponseMessage(statusCode) - { - Content = new StringContent(responseBody) - }; - - httpResponse.Headers.RetryAfter = new RetryConditionHeaderValue(retryAfterSpan); - HttpResponse msalhttpResponse = HttpManager.CreateResponseAsync(httpResponse).Result; - - // Act - var msalException = new MsalServiceException(ExCode, ExMessage) - { - HttpResponse = msalhttpResponse - }; - - // Assert - var msalServiceException = msalException as MsalServiceException; - Assert.AreEqual(ExCode, msalServiceException.ErrorCode); - Assert.AreEqual(responseBody, msalServiceException.ResponseBody); - Assert.AreEqual(ExMessage, msalServiceException.Message); - Assert.AreEqual((int)statusCode, msalServiceException.StatusCode); - Assert.AreEqual("some_suberror", msalServiceException.SubError); - - Assert.AreEqual(retryAfterSpan, msalServiceException.Headers.RetryAfter.Delta); - } - } -} diff --git a/tests/Microsoft.Identity.Test.Unit.net45/ExceptionTests/MsalExceptionTests.cs b/tests/Microsoft.Identity.Test.Unit.net45/ExceptionTests/MsalExceptionTests.cs index d30f9a3dbd..c966c54cfa 100644 --- a/tests/Microsoft.Identity.Test.Unit.net45/ExceptionTests/MsalExceptionTests.cs +++ b/tests/Microsoft.Identity.Test.Unit.net45/ExceptionTests/MsalExceptionTests.cs @@ -3,8 +3,14 @@ using System; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Unit.ExceptionTests @@ -12,6 +18,252 @@ namespace Microsoft.Identity.Test.Unit.ExceptionTests [TestClass] public class MsalExceptionTests { + private const string ExCode = "exCode"; + private const string ExMessage = "exMessage"; + + private const string JsonError = @"{ ""error"":""invalid_tenant"", ""suberror"":""some_suberror"", + ""claims"":""some_claims"", + ""error_description"":""AADSTS90002: Tenant 'x' not found. "", ""error_codes"":[90002],""timestamp"":""2019-01-28 14:16:04Z"", + ""trace_id"":""43f14373-8d7d-466e-a5f1-6e3889291e00"", + ""correlation_id"":""6347d33d-941a-4c35-9912-a9cf54fb1b3e""}"; + + + [TestMethod] + public void ParamValidation() + { + AssertException.Throws(() => new MsalClientException(null, ExMessage)); + AssertException.Throws(() => new MsalClientException(string.Empty, ExMessage)); + + AssertException.Throws( + () => new MsalServiceException(ExCode, string.Empty)); + + AssertException.Throws( + () => new MsalServiceException(ExCode, null)); + } + + [TestMethod] + public void MsalClientException_FromMessageAndCode() + { + // Act + var msalException = new MsalClientException(ExCode, ExMessage); + + // Assert + var msalClientException = msalException as MsalClientException; + Assert.AreEqual(ExCode, msalClientException.ErrorCode); + Assert.AreEqual(ExMessage, msalClientException.Message); + Assert.IsNull(msalClientException.InnerException); + + // Act + string piiMessage = MsalLogger.GetPiiScrubbedExceptionDetails(msalException); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(piiMessage)); + Assert.IsTrue( + piiMessage.Contains(typeof(MsalClientException).Name), + "The pii message should contain the exception type"); + Assert.IsTrue(piiMessage.Contains(ExCode)); + Assert.IsFalse(piiMessage.Contains(ExMessage)); + } + + [TestMethod] + public void MsalServiceException_Classification_Only() + { + ValidateClassification(null, string.Empty); + ValidateClassification(string.Empty, string.Empty); + ValidateClassification("new_value", "new_value"); + + ValidateClassification(OAuth2SubError.BasicAction, MsalUiRequiredException.BasicAction); + ValidateClassification(OAuth2SubError.AdditionalAction, MsalUiRequiredException.AdditionalAction); + ValidateClassification(OAuth2SubError.MessageOnly, MsalUiRequiredException.MessageOnly); + ValidateClassification(OAuth2SubError.ConsentRequired, MsalUiRequiredException.ConsentRequired); + ValidateClassification(OAuth2SubError.UserPasswordExpired, MsalUiRequiredException.UserPasswordExpired); + + ValidateClassification(OAuth2SubError.BadToken, string.Empty); + ValidateClassification(OAuth2SubError.TokenExpired, string.Empty); + ValidateClassification(OAuth2SubError.ProtectionPolicyRequired, string.Empty); + ValidateClassification(OAuth2SubError.ClientMismatch, string.Empty); + ValidateClassification(OAuth2SubError.DeviceAuthenticationFailed, string.Empty); + } + + private static void ValidateClassification(string suberror, string expectedClassification) + { + string newJsonError = JsonError.Replace("some_suberror", suberror); + + // Arrange + HttpResponse httpResponse = new HttpResponse() + { + Body = newJsonError, + StatusCode = HttpStatusCode.BadRequest, // 400 + }; + + OAuth2ResponseBase oAuth2Response = + JsonHelper.TryToDeserializeFromJson(httpResponse?.Body); + + // Act + var msalException = new MsalUiRequiredException(ExCode, ExMessage) + { + OAuth2Response = oAuth2Response + }; + + Assert.AreEqual(ExCode, msalException.ErrorCode); + Assert.AreEqual(ExMessage, msalException.Message); + Assert.AreEqual("some_claims", msalException.Claims); + Assert.AreEqual("6347d33d-941a-4c35-9912-a9cf54fb1b3e", msalException.CorrelationId); + Assert.AreEqual(suberror ?? "", msalException.SubError ); + + Assert.AreEqual(expectedClassification, msalException.Classification); + } + + [TestMethod] + public void MsalUiRequiredException_Oauth2Response() + { + // Arrange + HttpResponse httpResponse = new HttpResponse() + { + Body = JsonError, + StatusCode = HttpStatusCode.BadRequest, // 400 + }; + + OAuth2ResponseBase oAuth2Response = + JsonHelper.TryToDeserializeFromJson(httpResponse?.Body); + + // Act + var msalException = new MsalServiceException(ExCode, ExMessage) + { + OAuth2Response = oAuth2Response + }; + + // Assert + var msalServiceException = msalException as MsalServiceException; + Assert.AreEqual(ExCode, msalServiceException.ErrorCode); + Assert.AreEqual(ExMessage, msalServiceException.Message); + Assert.AreEqual("some_claims", msalServiceException.Claims); + Assert.AreEqual("6347d33d-941a-4c35-9912-a9cf54fb1b3e", msalServiceException.CorrelationId); + Assert.AreEqual("some_suberror", msalServiceException.SubError); + } + + [TestMethod] + public void MsalServiceException_HttpResponse_OAuthResponse() + { + // Arrange + int statusCode = 400; + string innerExMsg = "innerExMsg"; + var innerException = new NotImplementedException(innerExMsg); + + HttpResponse httpResponse = new HttpResponse() + { + Body = JsonError, + StatusCode = HttpStatusCode.BadRequest, // 400 + }; + + // Act + var msalException = new MsalServiceException(ExCode, ExMessage, innerException) + { + HttpResponse = httpResponse + }; + + // Assert + var msalServiceException = msalException as MsalServiceException; + Assert.AreEqual(innerException, msalServiceException.InnerException); + Assert.AreEqual(ExCode, msalServiceException.ErrorCode); + Assert.AreEqual(JsonError, msalServiceException.ResponseBody); + Assert.AreEqual(ExMessage, msalServiceException.Message); + Assert.AreEqual(statusCode, msalServiceException.StatusCode); + + Assert.AreEqual("some_claims", msalServiceException.Claims); + Assert.AreEqual("6347d33d-941a-4c35-9912-a9cf54fb1b3e", msalServiceException.CorrelationId); + Assert.AreEqual("some_suberror", msalServiceException.SubError); + + // Act + string piiMessage = MsalLogger.GetPiiScrubbedExceptionDetails(msalException); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(piiMessage)); + Assert.IsTrue( + piiMessage.Contains(typeof(MsalServiceException).Name), + "The pii message should contain the exception type"); + Assert.IsTrue( + piiMessage.Contains(typeof(NotImplementedException).Name), + "The pii message should have the inner exception type"); + Assert.IsTrue(piiMessage.Contains(ExCode)); + Assert.IsTrue(piiMessage.Contains("6347d33d-941a-4c35-9912-a9cf54fb1b3e")); // Correlation Id + + Assert.IsFalse(piiMessage.Contains(ExMessage)); + Assert.IsFalse(piiMessage.Contains(innerExMsg)); + } + + [TestMethod] + public void MsalUiRequiredExceptionProperties() + { + // Arrange + string innerExMsg = "innerExMsg"; + var innerException = new NotImplementedException(innerExMsg); + + // Act + var msalException = new MsalUiRequiredException( + ExCode, + ExMessage, + innerException); + + // Assert + var msalUiRequiredException = msalException as MsalUiRequiredException; + Assert.AreEqual(innerException, msalUiRequiredException.InnerException); + Assert.AreEqual(ExCode, msalUiRequiredException.ErrorCode); + Assert.IsNull(msalUiRequiredException.Claims); + Assert.IsNull(msalUiRequiredException.ResponseBody); + Assert.AreEqual(ExMessage, msalUiRequiredException.Message); + Assert.AreEqual(0, msalUiRequiredException.StatusCode); + Assert.AreEqual(null, msalUiRequiredException.Classification); + + // Act + string piiMessage = MsalLogger.GetPiiScrubbedExceptionDetails(msalException); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(piiMessage)); + Assert.IsTrue( + piiMessage.Contains(typeof(MsalUiRequiredException).Name), + "The pii message should contain the exception type"); + Assert.IsTrue( + piiMessage.Contains(typeof(NotImplementedException).Name), + "The pii message should have the inner exception type"); + Assert.IsTrue(piiMessage.Contains(ExCode)); + Assert.IsFalse(piiMessage.Contains(ExMessage)); + Assert.IsFalse(piiMessage.Contains(innerExMsg)); + } + + [TestMethod] + public void MsalServiceException_FromHttpResponse() + { + // Arrange + string responseBody = JsonError; + var statusCode = HttpStatusCode.BadRequest; + var retryAfterSpan = new TimeSpan(3600); + + var httpResponse = new HttpResponseMessage(statusCode) + { + Content = new StringContent(responseBody) + }; + + httpResponse.Headers.RetryAfter = new RetryConditionHeaderValue(retryAfterSpan); + HttpResponse msalhttpResponse = HttpManager.CreateResponseAsync(httpResponse).Result; + + // Act + var msalException = new MsalServiceException(ExCode, ExMessage) + { + HttpResponse = msalhttpResponse + }; + + // Assert + var msalServiceException = msalException as MsalServiceException; + Assert.AreEqual(ExCode, msalServiceException.ErrorCode); + Assert.AreEqual(responseBody, msalServiceException.ResponseBody); + Assert.AreEqual(ExMessage, msalServiceException.Message); + Assert.AreEqual((int)statusCode, msalServiceException.StatusCode); + Assert.AreEqual("some_suberror", msalServiceException.SubError); + + Assert.AreEqual(retryAfterSpan, msalServiceException.Headers.RetryAfter.Delta); + } + [TestMethod] public void ExceptionsArePubliclyCreatable_MsalException() { @@ -78,7 +330,6 @@ public void ServiceException_ToString() Assert.IsTrue(ex.ToString().Contains("MySuberror")); Assert.IsTrue(ex.ToString().Contains("some_claims")); Assert.IsTrue(ex.ToString().Contains("AADSTS90002")); - } } }