diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisitionAspnetCoreHost.cs b/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisitionAspnetCoreHost.cs index 317247982..934d87a47 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisitionAspnetCoreHost.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisitionAspnetCoreHost.cs @@ -81,12 +81,32 @@ public MergedOptions GetOptions(string? authenticationScheme, out string effecti _serviceProvider.GetService>()?.Get(effectiveAuthenticationScheme); } + // Get the merged options again after the merging has occurred + mergedOptions = _mergedOptionsMonitor.Get(effectiveAuthenticationScheme); + if (string.IsNullOrEmpty(mergedOptions.Instance)) { - var availableSchemes = _serviceProvider.GetService()?.GetAllSchemesAsync()?.Result?.Select(a => a.Name); - string msg = string.Format(CultureInfo.InvariantCulture, IDWebErrorMessage.ProvidedAuthenticationSchemeIsIncorrect, - authenticationScheme, effectiveAuthenticationScheme, availableSchemes != null ? string.Join(",", availableSchemes) : string.Empty); - throw new InvalidOperationException(msg); + // Check if the issue is that MicrosoftIdentityApplicationOptions are not configured + // vs. an incorrect authentication scheme + var microsoftIdentityApplicationOptions = _serviceProvider.GetService>()?.Get(effectiveAuthenticationScheme); + bool isMicrosoftIdentityApplicationOptionsConfigured = microsoftIdentityApplicationOptions != null && + (!string.IsNullOrEmpty(microsoftIdentityApplicationOptions.Instance) || + (!string.IsNullOrEmpty(microsoftIdentityApplicationOptions.Authority) && microsoftIdentityApplicationOptions.Authority != "//v2.0") || + !string.IsNullOrEmpty(microsoftIdentityApplicationOptions.ClientId)); + + if (!isMicrosoftIdentityApplicationOptionsConfigured) + { + string msg = string.Format(CultureInfo.InvariantCulture, IDWebErrorMessage.MicrosoftIdentityApplicationOptionsNotConfigured, + effectiveAuthenticationScheme); + throw new InvalidOperationException(msg); + } + else + { + var availableSchemes = _serviceProvider.GetService()?.GetAllSchemesAsync()?.Result?.Select(a => a.Name); + string msg = string.Format(CultureInfo.InvariantCulture, IDWebErrorMessage.ProvidedAuthenticationSchemeIsIncorrect, + authenticationScheme, effectiveAuthenticationScheme, availableSchemes != null ? string.Join(",", availableSchemes) : string.Empty); + throw new InvalidOperationException(msg); + } } DefaultCertificateLoader.UserAssignedManagedIdentityClientId = mergedOptions.UserAssignedManagedIdentityClientId; diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquisitionHost.cs b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquisitionHost.cs index 3f0546754..1b73c9c93 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquisitionHost.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquisitionHost.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Net; using System.Security.Claims; using System.Threading.Tasks; @@ -61,6 +62,33 @@ public MergedOptions GetOptions(string? authenticationScheme, out string effecti _microsoftIdentityOptionsMonitor.Get(effectiveAuthenticationScheme); _MicrosoftIdentityApplicationOptionsMonitor.Get(effectiveAuthenticationScheme); + // Get the merged options again after the merging has occurred + mergedOptions = _mergedOptionsMonitor.Get(effectiveAuthenticationScheme); + + if (string.IsNullOrEmpty(mergedOptions.Instance)) + { + // Check if the issue is that MicrosoftIdentityApplicationOptions are not configured + var microsoftIdentityApplicationOptions = _MicrosoftIdentityApplicationOptionsMonitor.Get(effectiveAuthenticationScheme); + + bool isMicrosoftIdentityApplicationOptionsConfigured = microsoftIdentityApplicationOptions != null && + (!string.IsNullOrEmpty(microsoftIdentityApplicationOptions.Instance) || + (!string.IsNullOrEmpty(microsoftIdentityApplicationOptions.Authority) && microsoftIdentityApplicationOptions.Authority != "//v2.0") || + !string.IsNullOrEmpty(microsoftIdentityApplicationOptions.ClientId)); + + if (!isMicrosoftIdentityApplicationOptionsConfigured) + { + string msg = string.Format(System.Globalization.CultureInfo.InvariantCulture, IDWebErrorMessage.MicrosoftIdentityApplicationOptionsNotConfigured, + effectiveAuthenticationScheme); + throw new InvalidOperationException(msg); + } + else + { + string msg = string.Format(System.Globalization.CultureInfo.InvariantCulture, IDWebErrorMessage.ProvidedAuthenticationSchemeIsIncorrect, + authenticationScheme, effectiveAuthenticationScheme, string.Empty); + throw new InvalidOperationException(msg); + } + } + DefaultCertificateLoader.UserAssignedManagedIdentityClientId = mergedOptions.UserAssignedManagedIdentityClientId; return mergedOptions; } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs b/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs index d01e300ae..9c638c18c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs @@ -53,6 +53,7 @@ internal static class IDWebErrorMessage public const string MicrosoftIdentityWebChallengeUserException = "IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. " + "See https://aka.ms/ms-id-web/ca_incremental-consent. "; public const string ProvidedAuthenticationSchemeIsIncorrect = "IDW10503: Cannot determine the cloud Instance. The provided authentication scheme was '{0}'. Microsoft.Identity.Web inferred '{1}' as the authentication scheme. Available authentication schemes are '{2}'. See https://aka.ms/id-web/authSchemes. "; + public const string MicrosoftIdentityApplicationOptionsNotConfigured = "IDW10503: Cannot determine the cloud Instance because MicrosoftIdentityApplicationOptions are not configured for the authentication scheme '{0}'. Please ensure the MicrosoftIdentityApplicationOptions are properly configured in your application setup. See https://aka.ms/ms-id-web/configuration for details. "; public const string InvalidAssertion = "IDW10504: Invalid assertion: contains unsupported character(s)."; public const string InvalidSubAssertion = "IDW10505: Invalid sub_assertion: contains unsupported character(s)."; diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt index 7dc5c5811..878083781 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityApplicationOptionsNotConfigured = "IDW10503: Cannot determine the cloud Instance because MicrosoftIdentityApplicationOptions are not configured for the authentication scheme '{0}'. Please ensure the MicrosoftIdentityApplicationOptions are properly configured in your application setup. See https://aka.ms/ms-id-web/configuration for details. " -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt index 7dc5c5811..878083781 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityApplicationOptionsNotConfigured = "IDW10503: Cannot determine the cloud Instance because MicrosoftIdentityApplicationOptions are not configured for the authentication scheme '{0}'. Please ensure the MicrosoftIdentityApplicationOptions are properly configured in your application setup. See https://aka.ms/ms-id-web/configuration for details. " -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 7dc5c5811..878083781 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityApplicationOptionsNotConfigured = "IDW10503: Cannot determine the cloud Instance because MicrosoftIdentityApplicationOptions are not configured for the authentication scheme '{0}'. Please ensure the MicrosoftIdentityApplicationOptions are properly configured in your application setup. See https://aka.ms/ms-id-web/configuration for details. " -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 7dc5c5811..878083781 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityApplicationOptionsNotConfigured = "IDW10503: Cannot determine the cloud Instance because MicrosoftIdentityApplicationOptions are not configured for the authentication scheme '{0}'. Please ensure the MicrosoftIdentityApplicationOptions are properly configured in your application setup. See https://aka.ms/ms-id-web/configuration for details. " -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index 7dc5c5811..878083781 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityApplicationOptionsNotConfigured = "IDW10503: Cannot determine the cloud Instance because MicrosoftIdentityApplicationOptions are not configured for the authentication scheme '{0}'. Please ensure the MicrosoftIdentityApplicationOptions are properly configured in your application setup. See https://aka.ms/ms-id-web/configuration for details. " -> string! diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionHostErrorMessagesIntegrationTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionHostErrorMessagesIntegrationTests.cs new file mode 100644 index 000000000..af64f6bc4 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionHostErrorMessagesIntegrationTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Hosts; +using Microsoft.Identity.Web.Test.Common; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class TokenAcquisitionHostErrorMessagesIntegrationTests + { + [Fact] + public void GetOptions_WhenMicrosoftIdentityApplicationOptionsNotConfigured_ThrowsSpecificError() + { + // Arrange - Create a service collection with minimal setup (no MicrosoftIdentityApplicationOptions configured) + var services = new ServiceCollection(); + services.AddSingleton(new MergedOptionsStore()); + + // Register the options mergers to enable the merging behavior + services.AddSingleton, MicrosoftIdentityOptionsMerger>(); + services.AddSingleton, MicrosoftIdentityApplicationOptionsMerger>(); + services.AddSingleton, ConfidentialClientApplicationOptionsMerger>(); + + services.Configure("test", opt => { }); + services.Configure("test", opt => { }); + // Note: We don't configure MicrosoftIdentityApplicationOptions to simulate the error condition + + var serviceProvider = services.BuildServiceProvider(); + + var mergedOptionsMonitor = serviceProvider.GetRequiredService(); + var ccaOptionsMonitor = serviceProvider.GetRequiredService>(); + var microsoftIdentityOptionsMonitor = serviceProvider.GetRequiredService>(); + var microsoftIdentityApplicationOptionsMonitor = serviceProvider.GetRequiredService>(); + + var host = new DefaultTokenAcquisitionHost( + microsoftIdentityOptionsMonitor, + mergedOptionsMonitor, + ccaOptionsMonitor, + microsoftIdentityApplicationOptionsMonitor); + + // Act & Assert + var exception = Assert.Throws(() => + host.GetOptions("test", out string effectiveScheme)); + + // Debug output + Console.WriteLine($"Actual error message: {exception.Message}"); + + // This should now use the new error message + Assert.Contains("MicrosoftIdentityApplicationOptions are not configured", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void GetOptions_WhenMicrosoftIdentityApplicationOptionsConfiguredWithInstance_Succeeds() + { + // Arrange - Create a service collection with properly configured MicrosoftIdentityApplicationOptions + var services = new ServiceCollection(); + services.AddSingleton(new MergedOptionsStore()); + + // Register the options mergers to enable the merging behavior + services.AddSingleton, MicrosoftIdentityOptionsMerger>(); + services.AddSingleton, MicrosoftIdentityApplicationOptionsMerger>(); + services.AddSingleton, ConfidentialClientApplicationOptionsMerger>(); + + services.Configure("test", opt => { }); + services.Configure("test", opt => { }); + services.Configure("test", opt => + { + opt.Instance = "https://login.microsoftonline.com/"; + opt.ClientId = "test-client-id"; + }); + + var serviceProvider = services.BuildServiceProvider(); + + var mergedOptionsMonitor = serviceProvider.GetRequiredService(); + var ccaOptionsMonitor = serviceProvider.GetRequiredService>(); + var microsoftIdentityOptionsMonitor = serviceProvider.GetRequiredService>(); + var microsoftIdentityApplicationOptionsMonitor = serviceProvider.GetRequiredService>(); + + var host = new DefaultTokenAcquisitionHost( + microsoftIdentityOptionsMonitor, + mergedOptionsMonitor, + ccaOptionsMonitor, + microsoftIdentityApplicationOptionsMonitor); + + // Act + var result = host.GetOptions("test", out string effectiveScheme); + + // Assert + Assert.NotNull(result); + Assert.Equal("https://login.microsoftonline.com/", result.Instance); + } + } +} \ No newline at end of file diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionHostErrorMessagesTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionHostErrorMessagesTests.cs new file mode 100644 index 000000000..0926b23d1 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionHostErrorMessagesTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Hosts; +using Microsoft.Identity.Web.Test.Common; +using NSubstitute; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class TokenAcquisitionHostErrorMessagesTests + { + [Fact] + public void GetOptions_WhenMicrosoftIdentityApplicationOptionsNotConfigured_ThrowsSpecificError() + { + // Arrange + var mergedOptionsMonitor = Substitute.For(); + var ccaOptionsMonitor = Substitute.For>(); + var microsoftIdentityOptionsMonitor = Substitute.For>(); + var microsoftIdentityApplicationOptionsMonitor = Substitute.For>(); + + // Configure merged options to have empty Instance (this is the condition that triggers the error) + var emptyMergedOptions = new MergedOptions(); + mergedOptionsMonitor.Get(Arg.Any()).Returns(emptyMergedOptions); + + // Configure MicrosoftIdentityApplicationOptions to be empty (not configured) + // Note: The default Authority value is "//v2.0" which should be ignored + var emptyAppOptions = new MicrosoftIdentityApplicationOptions(); + microsoftIdentityApplicationOptionsMonitor.Get(Arg.Any()).Returns(emptyAppOptions); + + var host = new DefaultTokenAcquisitionHost( + microsoftIdentityOptionsMonitor, + mergedOptionsMonitor, + ccaOptionsMonitor, + microsoftIdentityApplicationOptionsMonitor); + + // Act & Assert + var exception = Assert.Throws(() => + host.GetOptions("testScheme", out string effectiveScheme)); + + Assert.Contains("MicrosoftIdentityApplicationOptions are not configured", exception.Message, StringComparison.Ordinal); + Assert.Contains("testScheme", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void GetOptions_WhenMicrosoftIdentityApplicationOptionsConfiguredButInstanceEmpty_ThrowsSchemeError() + { + // Arrange + var mergedOptionsMonitor = Substitute.For(); + var ccaOptionsMonitor = Substitute.For>(); + var microsoftIdentityOptionsMonitor = Substitute.For>(); + var microsoftIdentityApplicationOptionsMonitor = Substitute.For>(); + + // Configure merged options to have empty Instance + var emptyMergedOptions = new MergedOptions(); + mergedOptionsMonitor.Get(Arg.Any()).Returns(emptyMergedOptions); + + // Configure MicrosoftIdentityApplicationOptions to have ClientId configured (indicating it is configured) + var configuredAppOptions = new MicrosoftIdentityApplicationOptions + { + ClientId = "test-client-id" + }; + microsoftIdentityApplicationOptionsMonitor.Get(Arg.Any()).Returns(configuredAppOptions); + + var host = new DefaultTokenAcquisitionHost( + microsoftIdentityOptionsMonitor, + mergedOptionsMonitor, + ccaOptionsMonitor, + microsoftIdentityApplicationOptionsMonitor); + + // Act & Assert + var exception = Assert.Throws(() => + host.GetOptions("testScheme", out string effectiveScheme)); + + Assert.Contains("Cannot determine the cloud Instance", exception.Message, StringComparison.Ordinal); + Assert.Contains("authentication scheme", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void GetOptions_WhenMicrosoftIdentityApplicationOptionsHasInstance_DoesNotThrow() + { + // Arrange + var mergedOptionsMonitor = Substitute.For(); + var ccaOptionsMonitor = Substitute.For>(); + var microsoftIdentityOptionsMonitor = Substitute.For>(); + var microsoftIdentityApplicationOptionsMonitor = Substitute.For>(); + + // Configure merged options to have Instance populated (normal working case) + var configuredMergedOptions = new MergedOptions + { + Instance = "https://login.microsoftonline.com/" + }; + mergedOptionsMonitor.Get(Arg.Any()).Returns(configuredMergedOptions); + + var host = new DefaultTokenAcquisitionHost( + microsoftIdentityOptionsMonitor, + mergedOptionsMonitor, + ccaOptionsMonitor, + microsoftIdentityApplicationOptionsMonitor); + + // Act + var result = host.GetOptions("testScheme", out string effectiveScheme); + + // Assert + Assert.NotNull(result); + Assert.Equal("https://login.microsoftonline.com/", result.Instance); + } + } +} \ No newline at end of file