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() {