diff --git a/src/client/Microsoft.Identity.Client/Internal/Constants.cs b/src/client/Microsoft.Identity.Client/Internal/Constants.cs index d5cefd6f78..a7f2a9719a 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Constants.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Constants.cs @@ -28,6 +28,9 @@ internal static class Constants public const string CcsRoutingHintHeader = "x-anchormailbox"; public const string AadThrottledErrorCode = "AADSTS50196"; + public const string AadAccountTypeAndResourceIncompatibleErrorCode = "AADSTS500207"; + public const string AadMissingScopeErrorCode = "AADSTS900144"; + //Represents 5 minutes in Unit time stamp public const int DefaultJitterRangeInSeconds = 300; public static readonly TimeSpan AccessTokenExpirationBuffer = TimeSpan.FromMinutes(5); diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index d5b61c67e8..35a63934cd 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -442,5 +442,6 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName) public const string RegionRequiredForMtlsPopMessage = "Regional auto-detect failed. mTLS Proof-of-Possession requires a region to be specified, as there is no global endpoint for mTLS. See https://aka.ms/msal-net-pop for details."; public const string ForceRefreshAndTokenHasNotCompatible = "Cannot specify ForceRefresh and AccessTokenSha256ToRefresh in the same request."; public const string RequestTimeOut = "Request to the endpoint timed out."; + public const string MalformedOidcAuthorityFormat = "Possible cause: When using Entra External ID, you didn't append /v2.0, for example {0}/v2.0\""; } } diff --git a/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs b/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs index 2919312d0b..bfbff7181c 100644 --- a/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs +++ b/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Net; using System.Text; using Microsoft.Identity.Client.Http; @@ -41,7 +42,7 @@ internal static MsalServiceException FromHttpResponse( } else { - errorMessageToUse = errorMessage; + errorMessageToUse = errorMessage; } if (oAuth2Response.Claims == null) @@ -64,6 +65,13 @@ internal static MsalServiceException FromHttpResponse( innerException); } + var authorityInfo = context.ServiceBundle.Config.Authority.AuthorityInfo; + + if (IsOidcAuthorityError(authorityInfo, oAuth2Response.ErrorDescription)) + { + errorMessage += string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.MalformedOidcAuthorityFormat, $" {authorityInfo.CanonicalAuthority}"); + } + ex ??= new MsalServiceException(errorCode, GetErrorMessage(errorMessage, httpResponse, context), innerException); SetHttpExceptionData(ex, httpResponse); @@ -108,6 +116,16 @@ private static bool IsThrottled(OAuth2ResponseBase oAuth2Response) oAuth2Response.ErrorDescription.StartsWith(Constants.AadThrottledErrorCode); } + private static bool IsOidcAuthorityError(AuthorityInfo authortyInfo, string ErrorDescription) + { + return authortyInfo is not null && + authortyInfo.AuthorityType == AuthorityType.Generic && // Generic Oidc authority + !authortyInfo.CanonicalAuthority!.AbsoluteUri.EndsWith("/v2.0") && // Does not end with /v2.0 + ErrorDescription != null && + (ErrorDescription.StartsWith(Constants.AadAccountTypeAndResourceIncompatibleErrorCode) || // Certain error codes are returned + ErrorDescription.StartsWith(Constants.AadMissingScopeErrorCode)); + } + internal static MsalServiceException FromBrokerResponse( MsalTokenResponse msalTokenResponse, string errorMessage) diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index ae6384fe5f..e91c614f50 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -254,7 +254,7 @@ private static void ThrowServerException(HttpResponse response, RequestContext r MsalServiceException exceptionToThrow; try { - exceptionToThrow = ExtractErrorsFromTheResponse(response, ref shouldLogAsError); + exceptionToThrow = ExtractErrorsFromTheResponse(response, ref shouldLogAsError, requestContext); } catch (JsonException) // in the rare case we get an error response we cannot deserialize { @@ -304,7 +304,7 @@ private static void ThrowServerException(HttpResponse response, RequestContext r throw exceptionToThrow; } - private static MsalServiceException ExtractErrorsFromTheResponse(HttpResponse response, ref bool shouldLogAsError) + private static MsalServiceException ExtractErrorsFromTheResponse(HttpResponse response, ref bool shouldLogAsError, RequestContext context = null) { // In cases where the end-point is not found (404) response.body will be empty. if (string.IsNullOrWhiteSpace(response.Body)) @@ -347,7 +347,8 @@ private static MsalServiceException ExtractErrorsFromTheResponse(HttpResponse re return MsalServiceExceptionFactory.FromHttpResponse( msalTokenResponse.Error, msalTokenResponse.ErrorDescription, - response); + response, + context: context); } private Uri AddExtraQueryParams(Uri endPoint) diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs index 4db94cbb36..7050ae13e5 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs @@ -260,9 +260,10 @@ public static HttpResponseMessage CreateFailureTokenResponseMessage( string error, string subError = null, string correlationId = null, - HttpStatusCode? customStatusCode = null) + HttpStatusCode? customStatusCode = null, + string errorCode = "AADSTS00000") { - string message = "{\"error\":\"" + error + "\",\"error_description\":\"AADSTS00000: Error for test." + + string message = "{\"error\":\"" + error + "\",\"error_description\":\"" + errorCode + ": Error for test." + "Trace ID: f7ec686c-9196-4220-a754-cd9197de44e9Correlation ID: " + "04bb0cae-580b-49ac-9a10-b6c3316b1eaaTimestamp: 2015-09-16 07:24:55Z\"," + "\"error_codes\":[70002,70008],\"timestamp\":\"2015-09-16 07:24:55Z\"," + diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs index 7f8667d93f..565ca72e68 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs @@ -87,15 +87,18 @@ public static MockHttpMessageHandler AddFailureTokenEndpointResponse( this MockHttpManager httpManager, string error, string authority = TestConstants.AuthorityCommonTenant, - string correlationId = null) + string correlationId = null, + string AadErrorCode = "AADSTS00000", + string expectedUrl = null) { var handler = new MockHttpMessageHandler() { - ExpectedUrl = authority + "oauth2/v2.0/token", + ExpectedUrl = expectedUrl != null? expectedUrl : $"{authority}oauth2/v2.0/token", ExpectedMethod = HttpMethod.Post, ResponseMessage = MockHelpers.CreateFailureTokenResponseMessage( - error, - correlationId: correlationId) + error, + correlationId: correlationId, + errorCode: AadErrorCode) }; httpManager.AddMockHandler(handler); return handler; diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs index 3d89cc1bbe..4884764a65 100644 --- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs @@ -122,6 +122,8 @@ public static HashSet s_scope public const string CiamAuthorityMainFormat = "https://tenant.ciamlogin.com/"; public const string CiamAuthorityWithFriendlyName = "https://tenant.ciamlogin.com/tenant.onmicrosoft.com"; public const string CiamAuthorityWithGuid = "https://tenant.ciamlogin.com/aaaaaaab-aaaa-aaaa-cccc-aaaaaaaaaaaa"; + public const string CiamCUDAuthority = "https://login.msidlabsciam.com/aaaaaaab-aaaa-aaaa-cccc-aaaaaaaaaaaa/v2.0"; + public const string CiamCUDAuthorityMalformed = "https://login.msidlabsciam.com/aaaaaaab-aaaa-aaaa-cccc-aaaaaaaaaaaa"; public const string B2CLoginGlobal = ".b2clogin.com"; public const string B2CLoginUSGov = ".b2clogin.us"; @@ -229,6 +231,9 @@ public static HashSet s_scope public const string Pop = "PoP"; public const string FmiNodeClientId = "urn:microsoft:identity:fmi"; + public const string AadAccountTypeAndResourceIncompatibleErrorCode = "AADSTS500207"; + public const string AadMissingScopeErrorCode = "AADSTS900144"; + public static IDictionary ExtraQueryParameters { get diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/GenericAuthorityTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/GenericAuthorityTests.cs index 9f138860d4..25ec352dbd 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/GenericAuthorityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/GenericAuthorityTests.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -333,6 +334,60 @@ public async Task BadOidcResponse_ThrowsException_Async(string badOidcResponseTy } } + [TestMethod] + public async Task Oidc_Malformed_Failure_Async() + { + using (var httpManager = new MockHttpManager()) + { + string authority = TestConstants.CiamCUDAuthorityMalformed; + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithHttpManager(httpManager) + .WithOidcAuthority(authority) + .WithClientSecret(TestConstants.ClientSecret) + .Build(); + + httpManager.AddMockHandler( + CreateOidcHttpHandler(authority + "/" + Constants.WellKnownOpenIdConfigurationPath)); + + httpManager.AddFailureTokenEndpointResponse( + error: "error", + AadErrorCode: TestConstants.AadAccountTypeAndResourceIncompatibleErrorCode, + expectedUrl: $"{TestConstants.CiamCUDAuthorityMalformed}/connect/token"); + + Assert.AreEqual(authority, app.Authority); + var confidentailClientApp = (ConfidentialClientApplication)app; + Assert.AreEqual(AuthorityType.Generic, confidentailClientApp.AuthorityInfo.AuthorityType); + + var ex = await AssertException.TaskThrowsAsync(() => + app.AcquireTokenForClient(new[] { "api" }) + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.IsTrue(ex.Message.Contains( + string.Format( + CultureInfo.InvariantCulture, + MsalErrorMessage.MalformedOidcAuthorityFormat, + TestConstants.CiamCUDAuthorityMalformed))); + + httpManager.AddFailureTokenEndpointResponse( + error: "error", + AadErrorCode: TestConstants.AadMissingScopeErrorCode, + expectedUrl: $"{TestConstants.CiamCUDAuthorityMalformed}/connect/token"); + + ex = await AssertException.TaskThrowsAsync(() => + app.AcquireTokenForClient(new[] { "api" }) + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.IsTrue(ex.Message.Contains( + string.Format( + CultureInfo.InvariantCulture, + MsalErrorMessage.MalformedOidcAuthorityFormat, + TestConstants.CiamCUDAuthorityMalformed))); + } + } + [TestMethod] public async Task OidcIssuerValidation_ThrowsForNonMatchingIssuer_Async() {