From 7ca44be6d15310594d7ad2dcffea3e3c0747e7e7 Mon Sep 17 00:00:00 2001 From: JoshLozensky <103777376+JoshLozensky@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:45:10 -0800 Subject: [PATCH 1/2] Stabilize UI and Unit Tests (#2669) * Moved UI test process start logic inside the try block to ensure hitting the finally block anytime a process start is attempted. * Added retry logic for initial connection to the app to allow for variance in app launch time * More explicitly defined XUnit collection for preventing UI tests from running in parallel * Increased Playwright timeout lengths * Reduced UI tests to running only on .Net 8 --- build/template-run-unit-tests.yaml | 2 +- .../TokenAcquirerTests/TokenAcquirer.cs | 1 - .../B2CWebAppCallsWebApiLocally.cs | 42 ++++++++++++----- .../WebAppUiTests/TestingWebAppLocally.cs | 40 ++++++++++++---- .../E2E Tests/WebAppUiTests/UiTestHelpers.cs | 26 +++++----- .../WebAppUiTests/UiTestNoParallelization.cs | 12 +++++ .../WebAppCallsApiCallsGraphLocally.cs | 47 +++++++++++++------ .../WebAppUiTests/WebAppUiTests.csproj | 2 +- 8 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 tests/E2E Tests/WebAppUiTests/UiTestNoParallelization.cs diff --git a/build/template-run-unit-tests.yaml b/build/template-run-unit-tests.yaml index 9fd23f2cc..02677911f 100644 --- a/build/template-run-unit-tests.yaml +++ b/build/template-run-unit-tests.yaml @@ -15,6 +15,6 @@ steps: - task: PublishBuildArtifacts@1 displayName: 'Publish traces after test' inputs: - PathtoPublish: '$(Build.SourcesDirectory)/tests/IntegrationTests/PlaywrightTraces/' + PathtoPublish: '$(Build.SourcesDirectory)/tests/E2E Tests/PlaywrightTraces/' ArtifactName: 'traces-after-tests-$(Build.BuildNumber)' condition: failed() \ No newline at end of file diff --git a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs index f91813011..9d7e2f3b0 100644 --- a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs +++ b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs @@ -313,7 +313,6 @@ public async Task AcquireTokenWithManagedIdentity_UserAssigned() const string baseUrl = "https://vault.azure.net"; const string clientId = "9c5896db-a74a-4b1a-a259-74c5080a3a6a"; TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); - _ = tokenAcquirerFactory.Services; IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); // Act: Get the authorization header provider and add the options to tell it to use Managed Identity diff --git a/tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs b/tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs index 7b333b35e..a9aa53e1c 100644 --- a/tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs +++ b/tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs @@ -19,8 +19,7 @@ namespace WebAppUiTests #if !FROM_GITHUB_ACTION { // since these tests change environment variables we'd prefer it not run at the same time as other tests - [CollectionDefinition(nameof(B2CWebAppCallsWebApiLocally), DisableParallelization = true)] - [Collection("WebAppUiTests")] + [CollectionDefinition(nameof(UiTestNoParallelization), DisableParallelization = true)] public class B2CWebAppCallsWebApiLocally : IClassFixture { private const string KeyvaultEmailName = "IdWeb-B2C-user"; @@ -30,7 +29,7 @@ public class B2CWebAppCallsWebApiLocally : IClassFixture() { clientProcess, serviceProcess })) { Assert.Fail(TC.WebAppCrashedString); } - // Navigate to web app + // Navigate to web app the retry logic ensures the web app has time to start up to establish a connection. IPage page = await context.NewPageAsync(); - await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort); + uint InitialConnectionRetryCount = 5; + while (InitialConnectionRetryCount > 0) + { + try + { + await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort); + break; + } + catch (PlaywrightException ex) + { + await Task.Delay(1000); + InitialConnectionRetryCount--; + if (InitialConnectionRetryCount == 0) { throw ex; } + } + } LabResponse labResponse = await LabUserHelper.GetB2CLocalAccountAsync().ConfigureAwait(false); // Initial sign in @@ -150,8 +166,8 @@ public async Task Susi_B2C_LocalAccount_TodoAppFucntionsCorrectly() { // Add the following to make sure all processes and their children are stopped. Queue processes = new Queue(); - processes.Enqueue(serviceProcess!); - processes.Enqueue(clientProcess!); + if (serviceProcess != null) { processes.Enqueue(serviceProcess); } + if (clientProcess != null) { processes.Enqueue(clientProcess); } UiTestHelpers.KillProcessTrees(processes); // Stop tracing and export it into a zip archive. diff --git a/tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs b/tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs index debc048ee..5afbf8715 100644 --- a/tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs +++ b/tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs @@ -12,14 +12,14 @@ using Microsoft.Playwright; using Xunit; using Xunit.Abstractions; +using System.Threading; namespace WebAppUiTests; #if !FROM_GITHUB_ACTION && !AZURE_DEVOPS_BUILD -// since this test changes environment variables we'd prefer it not run at the same time as other tests -[CollectionDefinition(nameof(TestingWebAppLocally), DisableParallelization = true)] -[Collection("WebAppUiTests")] +// Since this test affects Kestrel environment variables it can cause a race condition when run in parallel with other UI tests. +[CollectionDefinition(nameof(UiTestNoParallelization), DisableParallelization = true)] public class TestingWebAppLocally : IClassFixture { private const string UrlString = "https://localhost:5001/MicrosoftIdentity/Account/signin"; @@ -28,8 +28,9 @@ public class TestingWebAppLocally : IClassFixture 0) + { + try + { + await page.GotoAsync(UrlString); + break; + } + catch (PlaywrightException ex) + { + await Task.Delay(1000); + InitialConnectionRetryCount--; + if (InitialConnectionRetryCount == 0) { throw ex; } + } + } + LabResponse labResponse = await LabUserHelper.GetDefaultUserAsync().ConfigureAwait(false); // Act @@ -60,8 +80,8 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPassword() await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPassword(page, email, labResponse.User.GetOrFetchPassword(), _output); // Assert - await Assertions.Expect(page.GetByText("Welcome")).ToBeVisibleAsync(); - await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(); + await Assertions.Expect(page.GetByText("Welcome")).ToBeVisibleAsync(_assertVisibleOptions); + await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions); } catch (Exception ex) { @@ -71,7 +91,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPassword() { // Cleanup the web app process and any child processes Queue processes = new(); - processes.Enqueue(p); + if (process != null) { processes.Enqueue(process); } UiTestHelpers.KillProcessTrees(processes); // Cleanup Playwright diff --git a/tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs b/tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs index 5e51fdba0..e6c7f3136 100644 --- a/tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs +++ b/tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs @@ -80,12 +80,12 @@ private static async Task SelectKnownAccountByEmail_MicrosoftIdFlow(IPage page, } /// - /// + /// The set of steps to take when given a password to enter and submit when logging in via Microsoft. /// - /// - /// - /// - /// + /// The browser page instance. + /// The password for the account you're logging into. + /// "Yes" or "No" to stay signed in for the given browsing session. + /// The writer for output to the test's console. public static async Task EnterPassword_MicrosoftIdFlow_ValidPassword(IPage page, string password, string staySignedInText, ITestOutputHelper? output = null) { // If using an account that has other non-password validation options, the below code should be uncommented @@ -120,9 +120,9 @@ private static void WriteLine(ITestOutputHelper? output, string message) /// /// This starts the recording of playwright trace files. The corresponsing EndAndWritePlaywrightTrace method will also need to be used. - /// This is not used anywhere by default and will need to be added to the code if desired + /// This is not used anywhere by default and will need to be added to the code if desired. /// - /// The page object whose context the trace will record + /// The page object whose context the trace will record. public static async Task StartPlaywrightTrace(IPage page) { await page.Context.Tracing.StartAsync(new() @@ -134,14 +134,14 @@ await page.Context.Tracing.StartAsync(new() } /// - /// Starts a process from an executable, sets its working directory, and redirects its output to the test's output + /// Starts a process from an executable, sets its working directory, and redirects its output to the test's output. /// - /// The path to the test's directory - /// The path to the processes directory - /// The name of the executable that launches the process - /// The port for the process to listen on + /// The path to the test's directory. + /// The path to the processes directory. + /// The name of the executable that launches the process. + /// The port for the process to listen on. /// If the launch URL is http or https. Default is https. - /// The started process + /// The started process. public static Process StartProcessLocally(string testAssemblyLocation, string appLocation, string executableName, Dictionary? environmentVariables = null) { string applicationWorkingDirectory = GetApplicationWorkingDirectory(testAssemblyLocation, appLocation); diff --git a/tests/E2E Tests/WebAppUiTests/UiTestNoParallelization.cs b/tests/E2E Tests/WebAppUiTests/UiTestNoParallelization.cs new file mode 100644 index 000000000..6ecc9d2ba --- /dev/null +++ b/tests/E2E Tests/WebAppUiTests/UiTestNoParallelization.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Xunit; + +namespace WebAppUiTests +{ + [CollectionDefinition(nameof(UiTestNoParallelization), DisableParallelization = true)] + public class UiTestNoParallelization + { + } +} diff --git a/tests/E2E Tests/WebAppUiTests/WebAppCallsApiCallsGraphLocally.cs b/tests/E2E Tests/WebAppUiTests/WebAppCallsApiCallsGraphLocally.cs index 2be49c316..eb0bde9ed 100644 --- a/tests/E2E Tests/WebAppUiTests/WebAppCallsApiCallsGraphLocally.cs +++ b/tests/E2E Tests/WebAppUiTests/WebAppCallsApiCallsGraphLocally.cs @@ -18,8 +18,7 @@ namespace WebAppUiTests #if !FROM_GITHUB_ACTION { // since these tests change environment variables we'd prefer it not run at the same time as other tests - [CollectionDefinition(nameof(WebAppCallsApiCallsGraphLocally), DisableParallelization = true)] - [Collection("WebAppUiTests")] + [CollectionDefinition(nameof(UiTestNoParallelization), DisableParallelization = true)] public class WebAppCallsApiCallsGraphLocally : IClassFixture { private const uint GrpcPort = 5001; @@ -27,7 +26,7 @@ public class WebAppCallsApiCallsGraphLocally : IClassFixture() { clientProcess, serviceProcess, grpcProcess })) { Assert.Fail(TC.WebAppCrashedString); @@ -83,7 +86,23 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Navigate to web app IPage page = await context.NewPageAsync(); - await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort); + + // The retry logic ensures the web app has time to start up to establish a connection. + uint InitialConnectionRetryCount = 5; + while (InitialConnectionRetryCount > 0) + { + try + { + await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort); + break; + } + catch (PlaywrightException ex) + { + await Task.Delay(1000); + InitialConnectionRetryCount--; + if (InitialConnectionRetryCount == 0) { throw ex; } + } + } LabResponse labResponse = await LabUserHelper.GetDefaultUserAsync().ConfigureAwait(false); // Initial sign in @@ -137,10 +156,10 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds finally { // Add the following to make sure all processes and their children are stopped. - Queue processes = new Queue(); - processes.Enqueue(serviceProcess!); - processes.Enqueue(clientProcess!); - processes.Enqueue(grpcProcess!); + Queue processes = new(); + if (serviceProcess != null) { processes.Enqueue(serviceProcess); } + if (clientProcess != null) { processes.Enqueue(clientProcess); } + if (grpcProcess != null) { processes.Enqueue(grpcProcess); } UiTestHelpers.KillProcessTrees(processes); // Stop tracing and export it into a zip archive. diff --git a/tests/E2E Tests/WebAppUiTests/WebAppUiTests.csproj b/tests/E2E Tests/WebAppUiTests/WebAppUiTests.csproj index 6c11b49a8..29e816870 100644 --- a/tests/E2E Tests/WebAppUiTests/WebAppUiTests.csproj +++ b/tests/E2E Tests/WebAppUiTests/WebAppUiTests.csproj @@ -1,7 +1,7 @@  - net6.0; net7.0; net8.0 + net8.0 ../../../build/MSAL.snk false From b6cc9eb4314ffd248cc3c966e1d0e1711811c468 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Sat, 24 Feb 2024 19:49:05 -0800 Subject: [PATCH 2/2] Fix ServiceDescriptor access when Keyed Services are present (#2676) * Fix ServiceDescriptor access when Keyed Services are present * Add unit test --------- Co-authored-by: Jean-Marc Prieur --- .../ServiceCollectionExtensions.cs | 18 +- ...tyWebApiAuthenticationBuilderExtensions.cs | 13 +- ...tyWebAppAuthenticationBuilderExtensions.cs | 1072 +++++++++-------- .../ServiceCollectionExtensionsTests.cs | 17 + 4 files changed, 580 insertions(+), 540 deletions(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs index 44087979f..039fd0d3a 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -44,20 +45,20 @@ public static IServiceCollection AddTokenAcquisition( bool forceSdk = !services.Any(s => s.ServiceType.FullName == "Microsoft.AspNetCore.Authentication.IAuthenticationService"); #endif - if (services.FirstOrDefault(s => s.ImplementationType == typeof(DefaultCertificateLoader)) == null) + if (!HasImplementationType(services, typeof(DefaultCertificateLoader))) { services.TryAddSingleton(); } - if (services.FirstOrDefault(s => s.ImplementationType == typeof(MicrosoftIdentityOptionsMerger)) == null) + if (!HasImplementationType(services, typeof(MicrosoftIdentityOptionsMerger))) { services.TryAddSingleton, MicrosoftIdentityOptionsMerger>(); } - if (services.FirstOrDefault(s => s.ImplementationType == typeof(MicrosoftIdentityApplicationOptionsMerger)) == null) + if (!HasImplementationType(services, typeof(MicrosoftIdentityApplicationOptionsMerger))) { services.TryAddSingleton, MicrosoftIdentityApplicationOptionsMerger>(); } - if (services.FirstOrDefault(s => s.ImplementationType == typeof(ConfidentialClientApplicationOptionsMerger)) == null) + if (!HasImplementationType(services, typeof(ConfidentialClientApplicationOptionsMerger))) { services.TryAddSingleton, ConfidentialClientApplicationOptionsMerger>(); } @@ -139,5 +140,14 @@ public static IServiceCollection AddTokenAcquisition( services.AddSingleton(); return services; } + + private static bool HasImplementationType(IServiceCollection services, Type implementationType) + { + return services.Any(s => +#if NET8_0_OR_GREATER + s.ServiceKey is null && +#endif + s.ImplementationType == implementationType); + } } } diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs index 6055ae897..76a18cc9a 100644 --- a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs +++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs @@ -148,11 +148,11 @@ private static void AddMicrosoftIdentityWebApiImplementation( builder.Services.AddRequiredScopeAuthorization(); builder.Services.AddRequiredScopeOrAppPermissionAuthorization(); builder.Services.AddOptions(); - if (builder.Services.FirstOrDefault(s => s.ImplementationType == typeof(MicrosoftIdentityOptionsMerger)) == null) + if (!HasImplementationType(builder.Services, typeof(MicrosoftIdentityOptionsMerger))) { builder.Services.TryAddSingleton, MicrosoftIdentityOptionsMerger>(); } - if (builder.Services.FirstOrDefault(s => s.ImplementationType == typeof(JwtBearerOptionsMerger)) == null) + if (!HasImplementationType(builder.Services, typeof(JwtBearerOptionsMerger))) { builder.Services.TryAddSingleton, JwtBearerOptionsMerger>(); } @@ -308,5 +308,14 @@ internal static void ChainOnTokenValidatedEventForClaimsValidation(JwtBearerEven await tokenValidatedHandler(context).ConfigureAwait(false); }; } + + private static bool HasImplementationType(IServiceCollection services, Type implementationType) + { + return services.Any(s => +#if NET8_0_OR_GREATER + s.ServiceKey is null && +#endif + s.ImplementationType == implementationType); + } } } diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs index a74ae466d..d7d1fb7c2 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs @@ -1,535 +1,539 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OAuth.Claims; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using Microsoft.Identity.Web.Resource; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; -using System.Linq; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Identity.Web -{ - /// - /// Extensions for the for startup initialization. - /// - public static class MicrosoftIdentityWebAppAuthenticationBuilderExtensions - { - /// - /// Add authentication to a web app with Microsoft identity platform. - /// This method expects the configuration file will have a section, named "AzureAd" as default, - /// with the necessary settings to initialize authentication options. - /// - /// The to which to add this configuration. - /// The configuration instance. - /// The configuration section with the necessary settings to initialize authentication options. - /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". - /// The cookie-based scheme name to be used. By default it uses "Cookies". - /// Set to true if you want to debug, or just understand the OpenID Connect events. - /// A display name for the authentication handler. - /// The builder for chaining. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Calls a trim-incompatible AddMicrosoftIdentityWebApp.")] -#endif - public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApp( - this AuthenticationBuilder builder, - IConfiguration configuration, - string configSectionName = Constants.AzureAd, - string openIdConnectScheme = OpenIdConnectDefaults.AuthenticationScheme, - string? cookieScheme = CookieAuthenticationDefaults.AuthenticationScheme, - bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false, - string? displayName = null) - { - if (configuration == null) - { - throw new ArgumentException(nameof(configuration)); - } - - if (string.IsNullOrEmpty(configSectionName)) - { - throw new ArgumentException(nameof(configSectionName)); - } - - IConfigurationSection configurationSection = configuration.GetSection(configSectionName); - - return builder.AddMicrosoftIdentityWebApp( - configurationSection, - openIdConnectScheme, - cookieScheme, - subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, - displayName); - } - - /// - /// Add authentication with Microsoft identity platform. - /// This method expects the configuration file will have a section, named "AzureAd" as default, with the necessary settings to initialize authentication options. - /// - /// The to which to add this configuration. - /// The configuration section from which to get the options. - /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". - /// The cookie-based scheme name to be used. By default it uses "Cookies". - /// Set to true if you want to debug, or just understand the OpenID Connect events. - /// A display name for the authentication handler. - /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif - public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApp( - this AuthenticationBuilder builder, - IConfigurationSection configurationSection, - string openIdConnectScheme = OpenIdConnectDefaults.AuthenticationScheme, - string? cookieScheme = CookieAuthenticationDefaults.AuthenticationScheme, - bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false, - string? displayName = null) - { - _ = Throws.IfNull(builder); - _ = Throws.IfNull(configurationSection); - - return builder.AddMicrosoftIdentityWebAppWithConfiguration( - options => configurationSection.Bind(options), - null, - openIdConnectScheme, - cookieScheme, - subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, - displayName, - configurationSection); - } - - /// - /// Add authentication with Microsoft identity platform. - /// - /// The to which to add this configuration. - /// The action to configure . - /// The action to configure . - /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". - /// The cookie-based scheme name to be used. By default it uses "Cookies". - /// Set to true if you want to debug, or just understand the OpenID Connect events. - /// A display name for the authentication handler. - /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER - [RequiresUnreferencedCode("Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.AddMicrosoftWebAppWithoutConfiguration(AuthenticationBuilder, Action, Action, String, String, Boolean, String).")] -#endif - public static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftIdentityWebApp( - this AuthenticationBuilder builder, - Action configureMicrosoftIdentityOptions, - Action? configureCookieAuthenticationOptions = null, - string openIdConnectScheme = OpenIdConnectDefaults.AuthenticationScheme, - string? cookieScheme = CookieAuthenticationDefaults.AuthenticationScheme, - bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false, - string? displayName = null) - { - _ = Throws.IfNull(builder); - - return builder.AddMicrosoftWebAppWithoutConfiguration( - configureMicrosoftIdentityOptions, - configureCookieAuthenticationOptions, - openIdConnectScheme, - cookieScheme, - subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, - displayName); - } - - /// - /// Add authentication with Microsoft identity platform. - /// - /// The to which to add this configuration. - /// The action to configure . - /// The action to configure . - /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". - /// The cookie-based scheme name to be used. By default it uses "Cookies". - /// Set to true if you want to debug, or just understand the OpenID Connect events. - /// A display name for the authentication handler. - /// Configuration section. - /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration(IServiceCollection, String, Action, IConfigurationSection)")] -#endif - private static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebAppWithConfiguration( - this AuthenticationBuilder builder, - Action configureMicrosoftIdentityOptions, - Action? configureCookieAuthenticationOptions, - string openIdConnectScheme, - string? cookieScheme, - bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, - string? displayName, - IConfigurationSection configurationSection) - { - AddMicrosoftIdentityWebAppInternal( - builder, - configureMicrosoftIdentityOptions, - configureCookieAuthenticationOptions, - openIdConnectScheme, - cookieScheme, - subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, - displayName); - - return new MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration( - builder.Services, - openIdConnectScheme, - configureMicrosoftIdentityOptions, - configurationSection); - } - - /// - /// Add authentication with Microsoft identity platform. - /// - /// The to which to add this configuration. - /// The action to configure . - /// The action to configure . - /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". - /// The cookie-based scheme name to be used. By default it uses "Cookies". - /// Set to true if you want to debug, or just understand the OpenID Connect events. - /// A display name for the authentication handler. - /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(IServiceCollection, String, Action, IConfigurationSection)")] -#endif - private static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftWebAppWithoutConfiguration( - this AuthenticationBuilder builder, - Action configureMicrosoftIdentityOptions, - Action? configureCookieAuthenticationOptions, - string openIdConnectScheme, - string? cookieScheme, - bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, - string? displayName) - { - if (!AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) - { - AddMicrosoftIdentityWebAppInternal( - builder, - configureMicrosoftIdentityOptions, - configureCookieAuthenticationOptions, - openIdConnectScheme, - cookieScheme, - subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, - displayName); - } - else - { - builder.Services.AddAuthentication(AppServicesAuthenticationDefaults.AuthenticationScheme) - .AddAppServicesAuthentication(); - } - - return new MicrosoftIdentityWebAppAuthenticationBuilder( - builder.Services, - openIdConnectScheme, - configureMicrosoftIdentityOptions, - null); - } - - private static void AddMicrosoftIdentityWebAppInternal( - AuthenticationBuilder builder, - Action configureMicrosoftIdentityOptions, - Action? configureCookieAuthenticationOptions, - string openIdConnectScheme, - string? cookieScheme, - bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, - string? displayName) - { - _ = Throws.IfNull(builder); - _ = Throws.IfNull(configureMicrosoftIdentityOptions); - - if (builder.Services.FirstOrDefault(s => s.ImplementationType == typeof(MicrosoftIdentityOptionsMerger)) == null) - { - builder.Services.AddSingleton, MicrosoftIdentityOptionsMerger>(); - } - - builder.Services.Configure(openIdConnectScheme, configureMicrosoftIdentityOptions); - builder.Services.AddSingleton(); - builder.Services.AddHttpClient(); - - if (!string.IsNullOrEmpty(cookieScheme)) - { - Action emptyOption = option => { }; - builder.AddCookie(cookieScheme, configureCookieAuthenticationOptions ?? emptyOption); - } - - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(ctx => - { - // ITempDataDictionaryFactory is not always available, so we don't require it - var tempFactory = ctx.GetService(); - var env = ctx.GetService(); // ex. Azure Functions will not have an env. - - if (env != null) - { - return TempDataLoginErrorAccessor.Create(tempFactory, env.IsDevelopment()); - } - else - { - return TempDataLoginErrorAccessor.Create(tempFactory, false); - } - }); - - if (subscribeToOpenIdConnectMiddlewareDiagnosticsEvents) - { - builder.Services.AddSingleton(); - } - - if (AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) - { - builder.Services.AddAuthentication(AppServicesAuthenticationDefaults.AuthenticationScheme) - .AddAppServicesAuthentication(); - return; - } - - if (!string.IsNullOrEmpty(displayName)) - { - builder.AddOpenIdConnect(openIdConnectScheme, displayName: displayName, options => { }); - } - else - { - builder.AddOpenIdConnect(openIdConnectScheme, options => { }); - } - - builder.Services.AddOptions(openIdConnectScheme) - .Configure, IOptions>(( - options, - serviceProvider, - mergedOptionsMonitor, - msIdOptionsMonitor, - msIdOptions) => - { - MicrosoftIdentityBaseAuthenticationBuilder.SetIdentityModelLogger(serviceProvider); - - msIdOptionsMonitor.Get(openIdConnectScheme); // needed for firing the PostConfigure. - MergedOptions mergedOptions = mergedOptionsMonitor.Get(openIdConnectScheme); - - MergedOptionsValidation.Validate(mergedOptions); - - if (mergedOptions.Authority != null) - { - mergedOptions.Authority = AuthorityHelpers.BuildCiamAuthorityIfNeeded(mergedOptions.Authority); - if (mergedOptions.ExtraQueryParameters != null) - { - options.MetadataAddress = mergedOptions.Authority + "/.well-known/openid-configuration?" + string.Join("&", mergedOptions.ExtraQueryParameters.Select(p => $"{p.Key}={p.Value}")); - } - } - - PopulateOpenIdOptionsFromMergedOptions(options, mergedOptions); - - var b2cOidcHandlers = new AzureADB2COpenIDConnectEventHandlers( - openIdConnectScheme, - mergedOptions, - serviceProvider.GetRequiredService()); - - if (!string.IsNullOrEmpty(cookieScheme)) - { - options.SignInScheme = cookieScheme; - } - - if (string.IsNullOrWhiteSpace(options.Authority)) - { - options.Authority = AuthorityHelpers.BuildAuthority(mergedOptions); - } - - // This is a Microsoft identity platform web app - options.Authority = AuthorityHelpers.EnsureAuthorityIsV2(options.Authority); - - // B2C doesn't have preferred_username claims - if (mergedOptions.IsB2C) - { - options.TokenValidationParameters.NameClaimType = ClaimConstants.Name; - } - else - { - options.TokenValidationParameters.NameClaimType = ClaimConstants.PreferredUserName; - } - - // If the developer registered an IssuerValidator, do not overwrite it - if (options.TokenValidationParameters.ValidateIssuer && options.TokenValidationParameters.IssuerValidator == null) - { - // If you want to restrict the users that can sign-in to several organizations - // Set the tenant value in the appsettings.json file to 'organizations', and add the - // issuers you want to accept to options.TokenValidationParameters.ValidIssuers collection - MicrosoftIdentityIssuerValidatorFactory microsoftIdentityIssuerValidatorFactory = - serviceProvider.GetRequiredService(); - - options.TokenValidationParameters.IssuerValidator = - microsoftIdentityIssuerValidatorFactory.GetAadIssuerValidator(options.Authority).Validate; - } - - // Avoids having users being presented the select account dialog when they are already signed-in - // for instance when going through incremental consent - var redirectToIdpHandler = options.Events.OnRedirectToIdentityProvider; - options.Events.OnRedirectToIdentityProvider = async context => - { - var loginHint = context.Properties.GetParameter(OpenIdConnectParameterNames.LoginHint); - if (!string.IsNullOrWhiteSpace(loginHint)) - { - context.ProtocolMessage.LoginHint = loginHint; - - context.ProtocolMessage.SetParameter(Constants.XAnchorMailbox, $"{Constants.Upn}:{loginHint}"); - // delete the login_hint from the Properties when we are done otherwise - // it will take up extra space in the cookie. - context.Properties.Parameters.Remove(OpenIdConnectParameterNames.LoginHint); - } - - var domainHint = context.Properties.GetParameter(OpenIdConnectParameterNames.DomainHint); - if (!string.IsNullOrWhiteSpace(domainHint)) - { - context.ProtocolMessage.DomainHint = domainHint; - - // delete the domain_hint from the Properties when we are done otherwise - // it will take up extra space in the cookie. - context.Properties.Parameters.Remove(OpenIdConnectParameterNames.DomainHint); - } - - context.ProtocolMessage.SetParameter(Constants.ClientInfo, Constants.One); - context.ProtocolMessage.SetParameter(Constants.TelemetryHeaderKey, IdHelper.CreateTelemetryInfo()); - - // Additional claims - if (context.Properties.Items.TryGetValue(OidcConstants.AdditionalClaims, out var additionClaims)) - { - context.ProtocolMessage.SetParameter( - OidcConstants.AdditionalClaims, - additionClaims); - } - - if (mergedOptions.ExtraQueryParameters != null) - { - foreach (var ExtraQP in mergedOptions.ExtraQueryParameters) - { - context.ProtocolMessage.SetParameter(ExtraQP.Key, ExtraQP.Value); - } - } - - if (mergedOptions.IsB2C) - { - // When a new Challenge is returned using any B2C user flow different than susi, we must change - // the ProtocolMessage.IssuerAddress to the desired user flow otherwise the redirect would use the susi user flow - await b2cOidcHandlers.OnRedirectToIdentityProvider(context).ConfigureAwait(false); - } - - await redirectToIdpHandler(context).ConfigureAwait(false); - }; - - if (mergedOptions.IsB2C) - { - var remoteFailureHandler = options.Events.OnRemoteFailure; - options.Events.OnRemoteFailure = async context => - { - // Handles the error when a user cancels an action on the Azure Active Directory B2C UI. - // Handle the error code that Azure Active Directory B2C throws when trying to reset a password from the login page - // because password reset is not supported by a "sign-up or sign-in user flow". - await b2cOidcHandlers.OnRemoteFailure(context).ConfigureAwait(false); - - await remoteFailureHandler(context).ConfigureAwait(false); - }; - } - - if (subscribeToOpenIdConnectMiddlewareDiagnosticsEvents) - { - var diagnostics = serviceProvider.GetRequiredService(); - - diagnostics.Subscribe(options.Events); - } - }); - } - - internal static void PopulateOpenIdOptionsFromMergedOptions( - OpenIdConnectOptions options, - MergedOptions mergedOptions) - { - options.Authority = mergedOptions.Authority; - options.ClientId = mergedOptions.ClientId; - options.ClientSecret = mergedOptions.ClientSecret ?? mergedOptions.ClientCredentials?.FirstOrDefault(c => c.CredentialType == Abstractions.CredentialType.Secret)?.ClientSecret; - options.Configuration = mergedOptions.Configuration; - options.ConfigurationManager = mergedOptions.ConfigurationManager; - options.GetClaimsFromUserInfoEndpoint = mergedOptions.GetClaimsFromUserInfoEndpoint; - - if (options.ClaimActions != mergedOptions.ClaimActions) - { - var claimActionArray = options.ClaimActions.ToArray(); - foreach (ClaimAction claimAction in mergedOptions.ClaimActions) - { - if (!claimActionArray.Any((c => c.ClaimType == claimAction.ClaimType && c.ValueType == claimAction.ValueType))) - { - options.ClaimActions.Add(claimAction); - } - } - } - - options.RequireHttpsMetadata = mergedOptions.RequireHttpsMetadata; - options.MetadataAddress = mergedOptions.MetadataAddress; - options.MaxAge = mergedOptions.MaxAge; - options.ProtocolValidator = mergedOptions.ProtocolValidator; - options.SignedOutCallbackPath = mergedOptions.SignedOutCallbackPath; - options.SignedOutRedirectUri = mergedOptions.SignedOutRedirectUri; - options.RefreshOnIssuerKeyNotFound = mergedOptions.RefreshOnIssuerKeyNotFound; - options.AuthenticationMethod = mergedOptions.AuthenticationMethod; - options.Resource = mergedOptions.Resource; - options.ResponseMode = mergedOptions.ResponseMode; - options.ResponseType = mergedOptions.ResponseType; - options.Prompt = mergedOptions.Prompt; - - if (options.Scope != mergedOptions.Scope) - { - var scopeArray = options.Scope.ToArray(); - foreach (string scope in mergedOptions.Scope) - { - if (!string.IsNullOrWhiteSpace(scope) && !scopeArray.Any(s => string.Equals(s, scope, StringComparison.OrdinalIgnoreCase))) - { - options.Scope.Add(scope); - } - } - } - - options.RemoteSignOutPath = mergedOptions.RemoteSignOutPath; - options.SignOutScheme = mergedOptions.SignOutScheme; - options.StateDataFormat = mergedOptions.StateDataFormat; - options.StringDataFormat = mergedOptions.StringDataFormat; -#if NET8_0_OR_GREATER - options.TokenHandler = mergedOptions.TokenHandler; -#else +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Web.Resource; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using System.Linq; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Identity.Web +{ + /// + /// Extensions for the for startup initialization. + /// + public static class MicrosoftIdentityWebAppAuthenticationBuilderExtensions + { + /// + /// Add authentication to a web app with Microsoft identity platform. + /// This method expects the configuration file will have a section, named "AzureAd" as default, + /// with the necessary settings to initialize authentication options. + /// + /// The to which to add this configuration. + /// The configuration instance. + /// The configuration section with the necessary settings to initialize authentication options. + /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". + /// The cookie-based scheme name to be used. By default it uses "Cookies". + /// Set to true if you want to debug, or just understand the OpenID Connect events. + /// A display name for the authentication handler. + /// The builder for chaining. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Calls a trim-incompatible AddMicrosoftIdentityWebApp.")] +#endif + public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApp( + this AuthenticationBuilder builder, + IConfiguration configuration, + string configSectionName = Constants.AzureAd, + string openIdConnectScheme = OpenIdConnectDefaults.AuthenticationScheme, + string? cookieScheme = CookieAuthenticationDefaults.AuthenticationScheme, + bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false, + string? displayName = null) + { + if (configuration == null) + { + throw new ArgumentException(nameof(configuration)); + } + + if (string.IsNullOrEmpty(configSectionName)) + { + throw new ArgumentException(nameof(configSectionName)); + } + + IConfigurationSection configurationSection = configuration.GetSection(configSectionName); + + return builder.AddMicrosoftIdentityWebApp( + configurationSection, + openIdConnectScheme, + cookieScheme, + subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, + displayName); + } + + /// + /// Add authentication with Microsoft identity platform. + /// This method expects the configuration file will have a section, named "AzureAd" as default, with the necessary settings to initialize authentication options. + /// + /// The to which to add this configuration. + /// The configuration section from which to get the options. + /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". + /// The cookie-based scheme name to be used. By default it uses "Cookies". + /// Set to true if you want to debug, or just understand the OpenID Connect events. + /// A display name for the authentication handler. + /// The authentication builder for chaining. +#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] +#endif + public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApp( + this AuthenticationBuilder builder, + IConfigurationSection configurationSection, + string openIdConnectScheme = OpenIdConnectDefaults.AuthenticationScheme, + string? cookieScheme = CookieAuthenticationDefaults.AuthenticationScheme, + bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false, + string? displayName = null) + { + _ = Throws.IfNull(builder); + _ = Throws.IfNull(configurationSection); + + return builder.AddMicrosoftIdentityWebAppWithConfiguration( + options => configurationSection.Bind(options), + null, + openIdConnectScheme, + cookieScheme, + subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, + displayName, + configurationSection); + } + + /// + /// Add authentication with Microsoft identity platform. + /// + /// The to which to add this configuration. + /// The action to configure . + /// The action to configure . + /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". + /// The cookie-based scheme name to be used. By default it uses "Cookies". + /// Set to true if you want to debug, or just understand the OpenID Connect events. + /// A display name for the authentication handler. + /// The authentication builder for chaining. +#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER + [RequiresUnreferencedCode("Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.AddMicrosoftWebAppWithoutConfiguration(AuthenticationBuilder, Action, Action, String, String, Boolean, String).")] +#endif + public static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftIdentityWebApp( + this AuthenticationBuilder builder, + Action configureMicrosoftIdentityOptions, + Action? configureCookieAuthenticationOptions = null, + string openIdConnectScheme = OpenIdConnectDefaults.AuthenticationScheme, + string? cookieScheme = CookieAuthenticationDefaults.AuthenticationScheme, + bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false, + string? displayName = null) + { + _ = Throws.IfNull(builder); + + return builder.AddMicrosoftWebAppWithoutConfiguration( + configureMicrosoftIdentityOptions, + configureCookieAuthenticationOptions, + openIdConnectScheme, + cookieScheme, + subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, + displayName); + } + + /// + /// Add authentication with Microsoft identity platform. + /// + /// The to which to add this configuration. + /// The action to configure . + /// The action to configure . + /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". + /// The cookie-based scheme name to be used. By default it uses "Cookies". + /// Set to true if you want to debug, or just understand the OpenID Connect events. + /// A display name for the authentication handler. + /// Configuration section. + /// The authentication builder for chaining. +#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER + [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration(IServiceCollection, String, Action, IConfigurationSection)")] +#endif + private static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebAppWithConfiguration( + this AuthenticationBuilder builder, + Action configureMicrosoftIdentityOptions, + Action? configureCookieAuthenticationOptions, + string openIdConnectScheme, + string? cookieScheme, + bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, + string? displayName, + IConfigurationSection configurationSection) + { + AddMicrosoftIdentityWebAppInternal( + builder, + configureMicrosoftIdentityOptions, + configureCookieAuthenticationOptions, + openIdConnectScheme, + cookieScheme, + subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, + displayName); + + return new MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration( + builder.Services, + openIdConnectScheme, + configureMicrosoftIdentityOptions, + configurationSection); + } + + /// + /// Add authentication with Microsoft identity platform. + /// + /// The to which to add this configuration. + /// The action to configure . + /// The action to configure . + /// The OpenID Connect scheme name to be used. By default it uses "OpenIdConnect". + /// The cookie-based scheme name to be used. By default it uses "Cookies". + /// Set to true if you want to debug, or just understand the OpenID Connect events. + /// A display name for the authentication handler. + /// The authentication builder for chaining. +#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER + [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(IServiceCollection, String, Action, IConfigurationSection)")] +#endif + private static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftWebAppWithoutConfiguration( + this AuthenticationBuilder builder, + Action configureMicrosoftIdentityOptions, + Action? configureCookieAuthenticationOptions, + string openIdConnectScheme, + string? cookieScheme, + bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, + string? displayName) + { + if (!AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) + { + AddMicrosoftIdentityWebAppInternal( + builder, + configureMicrosoftIdentityOptions, + configureCookieAuthenticationOptions, + openIdConnectScheme, + cookieScheme, + subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, + displayName); + } + else + { + builder.Services.AddAuthentication(AppServicesAuthenticationDefaults.AuthenticationScheme) + .AddAppServicesAuthentication(); + } + + return new MicrosoftIdentityWebAppAuthenticationBuilder( + builder.Services, + openIdConnectScheme, + configureMicrosoftIdentityOptions, + null); + } + + private static void AddMicrosoftIdentityWebAppInternal( + AuthenticationBuilder builder, + Action configureMicrosoftIdentityOptions, + Action? configureCookieAuthenticationOptions, + string openIdConnectScheme, + string? cookieScheme, + bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents, + string? displayName) + { + _ = Throws.IfNull(builder); + _ = Throws.IfNull(configureMicrosoftIdentityOptions); + + if (builder.Services.FirstOrDefault(s => +#if NET8_0_OR_GREATER + s.ServiceKey is null && +#endif + s.ImplementationType == typeof(MicrosoftIdentityOptionsMerger)) == null) + { + builder.Services.AddSingleton, MicrosoftIdentityOptionsMerger>(); + } + + builder.Services.Configure(openIdConnectScheme, configureMicrosoftIdentityOptions); + builder.Services.AddSingleton(); + builder.Services.AddHttpClient(); + + if (!string.IsNullOrEmpty(cookieScheme)) + { + Action emptyOption = option => { }; + builder.AddCookie(cookieScheme, configureCookieAuthenticationOptions ?? emptyOption); + } + + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(ctx => + { + // ITempDataDictionaryFactory is not always available, so we don't require it + var tempFactory = ctx.GetService(); + var env = ctx.GetService(); // ex. Azure Functions will not have an env. + + if (env != null) + { + return TempDataLoginErrorAccessor.Create(tempFactory, env.IsDevelopment()); + } + else + { + return TempDataLoginErrorAccessor.Create(tempFactory, false); + } + }); + + if (subscribeToOpenIdConnectMiddlewareDiagnosticsEvents) + { + builder.Services.AddSingleton(); + } + + if (AppServicesAuthenticationInformation.IsAppServicesAadAuthenticationEnabled) + { + builder.Services.AddAuthentication(AppServicesAuthenticationDefaults.AuthenticationScheme) + .AddAppServicesAuthentication(); + return; + } + + if (!string.IsNullOrEmpty(displayName)) + { + builder.AddOpenIdConnect(openIdConnectScheme, displayName: displayName, options => { }); + } + else + { + builder.AddOpenIdConnect(openIdConnectScheme, options => { }); + } + + builder.Services.AddOptions(openIdConnectScheme) + .Configure, IOptions>(( + options, + serviceProvider, + mergedOptionsMonitor, + msIdOptionsMonitor, + msIdOptions) => + { + MicrosoftIdentityBaseAuthenticationBuilder.SetIdentityModelLogger(serviceProvider); + + msIdOptionsMonitor.Get(openIdConnectScheme); // needed for firing the PostConfigure. + MergedOptions mergedOptions = mergedOptionsMonitor.Get(openIdConnectScheme); + + MergedOptionsValidation.Validate(mergedOptions); + + if (mergedOptions.Authority != null) + { + mergedOptions.Authority = AuthorityHelpers.BuildCiamAuthorityIfNeeded(mergedOptions.Authority); + if (mergedOptions.ExtraQueryParameters != null) + { + options.MetadataAddress = mergedOptions.Authority + "/.well-known/openid-configuration?" + string.Join("&", mergedOptions.ExtraQueryParameters.Select(p => $"{p.Key}={p.Value}")); + } + } + + PopulateOpenIdOptionsFromMergedOptions(options, mergedOptions); + + var b2cOidcHandlers = new AzureADB2COpenIDConnectEventHandlers( + openIdConnectScheme, + mergedOptions, + serviceProvider.GetRequiredService()); + + if (!string.IsNullOrEmpty(cookieScheme)) + { + options.SignInScheme = cookieScheme; + } + + if (string.IsNullOrWhiteSpace(options.Authority)) + { + options.Authority = AuthorityHelpers.BuildAuthority(mergedOptions); + } + + // This is a Microsoft identity platform web app + options.Authority = AuthorityHelpers.EnsureAuthorityIsV2(options.Authority); + + // B2C doesn't have preferred_username claims + if (mergedOptions.IsB2C) + { + options.TokenValidationParameters.NameClaimType = ClaimConstants.Name; + } + else + { + options.TokenValidationParameters.NameClaimType = ClaimConstants.PreferredUserName; + } + + // If the developer registered an IssuerValidator, do not overwrite it + if (options.TokenValidationParameters.ValidateIssuer && options.TokenValidationParameters.IssuerValidator == null) + { + // If you want to restrict the users that can sign-in to several organizations + // Set the tenant value in the appsettings.json file to 'organizations', and add the + // issuers you want to accept to options.TokenValidationParameters.ValidIssuers collection + MicrosoftIdentityIssuerValidatorFactory microsoftIdentityIssuerValidatorFactory = + serviceProvider.GetRequiredService(); + + options.TokenValidationParameters.IssuerValidator = + microsoftIdentityIssuerValidatorFactory.GetAadIssuerValidator(options.Authority).Validate; + } + + // Avoids having users being presented the select account dialog when they are already signed-in + // for instance when going through incremental consent + var redirectToIdpHandler = options.Events.OnRedirectToIdentityProvider; + options.Events.OnRedirectToIdentityProvider = async context => + { + var loginHint = context.Properties.GetParameter(OpenIdConnectParameterNames.LoginHint); + if (!string.IsNullOrWhiteSpace(loginHint)) + { + context.ProtocolMessage.LoginHint = loginHint; + + context.ProtocolMessage.SetParameter(Constants.XAnchorMailbox, $"{Constants.Upn}:{loginHint}"); + // delete the login_hint from the Properties when we are done otherwise + // it will take up extra space in the cookie. + context.Properties.Parameters.Remove(OpenIdConnectParameterNames.LoginHint); + } + + var domainHint = context.Properties.GetParameter(OpenIdConnectParameterNames.DomainHint); + if (!string.IsNullOrWhiteSpace(domainHint)) + { + context.ProtocolMessage.DomainHint = domainHint; + + // delete the domain_hint from the Properties when we are done otherwise + // it will take up extra space in the cookie. + context.Properties.Parameters.Remove(OpenIdConnectParameterNames.DomainHint); + } + + context.ProtocolMessage.SetParameter(Constants.ClientInfo, Constants.One); + context.ProtocolMessage.SetParameter(Constants.TelemetryHeaderKey, IdHelper.CreateTelemetryInfo()); + + // Additional claims + if (context.Properties.Items.TryGetValue(OidcConstants.AdditionalClaims, out var additionClaims)) + { + context.ProtocolMessage.SetParameter( + OidcConstants.AdditionalClaims, + additionClaims); + } + + if (mergedOptions.ExtraQueryParameters != null) + { + foreach (var ExtraQP in mergedOptions.ExtraQueryParameters) + { + context.ProtocolMessage.SetParameter(ExtraQP.Key, ExtraQP.Value); + } + } + + if (mergedOptions.IsB2C) + { + // When a new Challenge is returned using any B2C user flow different than susi, we must change + // the ProtocolMessage.IssuerAddress to the desired user flow otherwise the redirect would use the susi user flow + await b2cOidcHandlers.OnRedirectToIdentityProvider(context).ConfigureAwait(false); + } + + await redirectToIdpHandler(context).ConfigureAwait(false); + }; + + if (mergedOptions.IsB2C) + { + var remoteFailureHandler = options.Events.OnRemoteFailure; + options.Events.OnRemoteFailure = async context => + { + // Handles the error when a user cancels an action on the Azure Active Directory B2C UI. + // Handle the error code that Azure Active Directory B2C throws when trying to reset a password from the login page + // because password reset is not supported by a "sign-up or sign-in user flow". + await b2cOidcHandlers.OnRemoteFailure(context).ConfigureAwait(false); + + await remoteFailureHandler(context).ConfigureAwait(false); + }; + } + + if (subscribeToOpenIdConnectMiddlewareDiagnosticsEvents) + { + var diagnostics = serviceProvider.GetRequiredService(); + + diagnostics.Subscribe(options.Events); + } + }); + } + + internal static void PopulateOpenIdOptionsFromMergedOptions( + OpenIdConnectOptions options, + MergedOptions mergedOptions) + { + options.Authority = mergedOptions.Authority; + options.ClientId = mergedOptions.ClientId; + options.ClientSecret = mergedOptions.ClientSecret ?? mergedOptions.ClientCredentials?.FirstOrDefault(c => c.CredentialType == Abstractions.CredentialType.Secret)?.ClientSecret; + options.Configuration = mergedOptions.Configuration; + options.ConfigurationManager = mergedOptions.ConfigurationManager; + options.GetClaimsFromUserInfoEndpoint = mergedOptions.GetClaimsFromUserInfoEndpoint; + + if (options.ClaimActions != mergedOptions.ClaimActions) + { + var claimActionArray = options.ClaimActions.ToArray(); + foreach (ClaimAction claimAction in mergedOptions.ClaimActions) + { + if (!claimActionArray.Any((c => c.ClaimType == claimAction.ClaimType && c.ValueType == claimAction.ValueType))) + { + options.ClaimActions.Add(claimAction); + } + } + } + + options.RequireHttpsMetadata = mergedOptions.RequireHttpsMetadata; + options.MetadataAddress = mergedOptions.MetadataAddress; + options.MaxAge = mergedOptions.MaxAge; + options.ProtocolValidator = mergedOptions.ProtocolValidator; + options.SignedOutCallbackPath = mergedOptions.SignedOutCallbackPath; + options.SignedOutRedirectUri = mergedOptions.SignedOutRedirectUri; + options.RefreshOnIssuerKeyNotFound = mergedOptions.RefreshOnIssuerKeyNotFound; + options.AuthenticationMethod = mergedOptions.AuthenticationMethod; + options.Resource = mergedOptions.Resource; + options.ResponseMode = mergedOptions.ResponseMode; + options.ResponseType = mergedOptions.ResponseType; + options.Prompt = mergedOptions.Prompt; + + if (options.Scope != mergedOptions.Scope) + { + var scopeArray = options.Scope.ToArray(); + foreach (string scope in mergedOptions.Scope) + { + if (!string.IsNullOrWhiteSpace(scope) && !scopeArray.Any(s => string.Equals(s, scope, StringComparison.OrdinalIgnoreCase))) + { + options.Scope.Add(scope); + } + } + } + + options.RemoteSignOutPath = mergedOptions.RemoteSignOutPath; + options.SignOutScheme = mergedOptions.SignOutScheme; + options.StateDataFormat = mergedOptions.StateDataFormat; + options.StringDataFormat = mergedOptions.StringDataFormat; +#if NET8_0_OR_GREATER + options.TokenHandler = mergedOptions.TokenHandler; +#else options.SecurityTokenValidator = mergedOptions.SecurityTokenValidator; -#endif - options.TokenValidationParameters = mergedOptions.TokenValidationParameters; - options.UseTokenLifetime = mergedOptions.UseTokenLifetime; - options.SkipUnrecognizedRequests = mergedOptions.SkipUnrecognizedRequests; - options.DisableTelemetry = mergedOptions.DisableTelemetry; - options.NonceCookie = mergedOptions.NonceCookie; - options.UsePkce = mergedOptions.UsePkce; -#if NET5_0_OR_GREATER - options.AutomaticRefreshInterval = mergedOptions.AutomaticRefreshInterval; - options.RefreshInterval = mergedOptions.RefreshInterval; - options.MapInboundClaims = mergedOptions.MapInboundClaims; -#endif - options.BackchannelTimeout = mergedOptions.BackchannelTimeout; - options.BackchannelHttpHandler = mergedOptions.BackchannelHttpHandler; - options.Backchannel = mergedOptions.Backchannel; - options.DataProtectionProvider = mergedOptions.DataProtectionProvider; - options.CallbackPath = mergedOptions.CallbackPath; - options.AccessDeniedPath = mergedOptions.AccessDeniedPath; - options.ReturnUrlParameter = mergedOptions.ReturnUrlParameter; - options.SignInScheme = mergedOptions.SignInScheme; - options.RemoteAuthenticationTimeout = mergedOptions.RemoteAuthenticationTimeout; - options.SaveTokens = mergedOptions.SaveTokens; - options.CorrelationCookie = mergedOptions.CorrelationCookie; - options.ClaimsIssuer = mergedOptions.ClaimsIssuer; - options.Events = mergedOptions.Events; - options.EventsType = mergedOptions.EventsType; - options.ForwardDefault = mergedOptions.ForwardDefault; - options.ForwardAuthenticate = mergedOptions.ForwardAuthenticate; - options.ForwardChallenge = mergedOptions.ForwardChallenge; - options.ForwardForbid = mergedOptions.ForwardForbid; - options.ForwardSignIn = mergedOptions.ForwardSignIn; - options.ForwardSignOut = mergedOptions.ForwardSignOut; - options.ForwardDefaultSelector = mergedOptions.ForwardDefaultSelector; -#if NET8_0_OR_GREATER - options.TimeProvider = mergedOptions.TimeProvider; - options.UseSecurityTokenValidator = mergedOptions.UseSecurityTokenValidator; - options.TokenHandler = mergedOptions.TokenHandler; -#endif - } - } -} +#endif + options.TokenValidationParameters = mergedOptions.TokenValidationParameters; + options.UseTokenLifetime = mergedOptions.UseTokenLifetime; + options.SkipUnrecognizedRequests = mergedOptions.SkipUnrecognizedRequests; + options.DisableTelemetry = mergedOptions.DisableTelemetry; + options.NonceCookie = mergedOptions.NonceCookie; + options.UsePkce = mergedOptions.UsePkce; +#if NET5_0_OR_GREATER + options.AutomaticRefreshInterval = mergedOptions.AutomaticRefreshInterval; + options.RefreshInterval = mergedOptions.RefreshInterval; + options.MapInboundClaims = mergedOptions.MapInboundClaims; +#endif + options.BackchannelTimeout = mergedOptions.BackchannelTimeout; + options.BackchannelHttpHandler = mergedOptions.BackchannelHttpHandler; + options.Backchannel = mergedOptions.Backchannel; + options.DataProtectionProvider = mergedOptions.DataProtectionProvider; + options.CallbackPath = mergedOptions.CallbackPath; + options.AccessDeniedPath = mergedOptions.AccessDeniedPath; + options.ReturnUrlParameter = mergedOptions.ReturnUrlParameter; + options.SignInScheme = mergedOptions.SignInScheme; + options.RemoteAuthenticationTimeout = mergedOptions.RemoteAuthenticationTimeout; + options.SaveTokens = mergedOptions.SaveTokens; + options.CorrelationCookie = mergedOptions.CorrelationCookie; + options.ClaimsIssuer = mergedOptions.ClaimsIssuer; + options.Events = mergedOptions.Events; + options.EventsType = mergedOptions.EventsType; + options.ForwardDefault = mergedOptions.ForwardDefault; + options.ForwardAuthenticate = mergedOptions.ForwardAuthenticate; + options.ForwardChallenge = mergedOptions.ForwardChallenge; + options.ForwardForbid = mergedOptions.ForwardForbid; + options.ForwardSignIn = mergedOptions.ForwardSignIn; + options.ForwardSignOut = mergedOptions.ForwardSignOut; + options.ForwardDefaultSelector = mergedOptions.ForwardDefaultSelector; +#if NET8_0_OR_GREATER + options.TimeProvider = mergedOptions.TimeProvider; + options.UseSecurityTokenValidator = mergedOptions.UseSecurityTokenValidator; + options.TokenHandler = mergedOptions.TokenHandler; +#endif + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs index 73379843d..e94725e8a 100644 --- a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs @@ -104,6 +104,23 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime() }); } +#if NET8_0_OR_GREATER + [Fact] + public void AddTokenAcquisition_Sdk_SupportsKeyedServices() + { + var services = new ServiceCollection(); + + // Add a keyed service. + services.AddKeyedSingleton("test", this); + + // This should not throw. + services.AddTokenAcquisition(); + + // Verify the number of services added by AddTokenAcquisition (ignoring the service we added here). + Assert.Equal(10, services.Count(t => t.ServiceType != typeof(ServiceCollectionExtensionsTests))); + } +#endif + [Fact] public void AddTokenAcquisition_AbleToOverrideICredentialsLoader() {