Exemple #1
0
        /// <summary>
        /// Adds and configures a fallback strategy for if the main strategy or remote authentication
        /// fail to resolve a tenant.
        /// </summary>
        public static FinbuckleMultiTenantBuilder WithFallbackStrategy(this FinbuckleMultiTenantBuilder builder,
                                                                       string identifier)
        {
            if (identifier == null)
            {
                throw new ArgumentNullException(nameof(identifier));
            }

            builder.Services.TryAddSingleton <FallbackStrategy>(sp => new FallbackStrategy(identifier));

            return(builder);
        }
        /// <summary>
        /// Adds and configures a ClaimsStrategy to the application.
        /// </summary>
        /// <remarks>
        /// The claims strategy does not rely on TenantContext.  Therefore, it is not necessary to
        /// register the tenant context here.  The only thing it relies on is a tenant claim name and this
        /// is registered via a TenantConfiguration object which is injected into the TenantConfigurations
        /// object.  The TenantConfigurations object will handle duplicates in case app settings are also
        /// registered and have a tenant claim name value.
        /// </remarks>
        /// <param name="builder"></param>
        /// <param name="tenantClaimName">The name of the claim holding the tenant id.</param>
        /// <returns></returns>
        public static FinbuckleMultiTenantBuilder WithClaimsStrategy(this FinbuckleMultiTenantBuilder builder, string tenantClaimName)
        {
            // add the claim name as a manual configuration
            builder.Services.AddSingleton <ITenantConfiguration>(new TenantConfiguration()
            {
                Key = Constants.TenantClaimName, Value = tenantClaimName
            });
            // add the configuration wrapper
            builder.Services.AddTenantConfigurations();
            // register the strategy with the built in finbuckly custom strategy
            builder.WithStrategy <ClaimsStrategy>(ServiceLifetime.Scoped);

            return(builder);
        }
Exemple #3
0
        /// <summary>
        /// Adds a HttpRemoteSTore to the application.
        /// </summary>
        /// <param name="endpointTemplate">The endpoint URI template.</param>
        /// <param name="clientConfig">An action to configure the underlying HttpClient.</param>
        public static FinbuckleMultiTenantBuilder WithHttpRemoteStore(this FinbuckleMultiTenantBuilder builder,
                                                                      string endpointTemplate,
                                                                      Action <IHttpClientBuilder> clientConfig)
        {
            var httpClientBuilder = builder.Services.AddHttpClient(typeof(HttpRemoteStoreClient).FullName);

            if (clientConfig != null)
            {
                clientConfig(httpClientBuilder);
            }

            builder.Services.TryAddSingleton <HttpRemoteStoreClient>();

            return(builder.WithStore <HttpRemoteStore>(ServiceLifetime.Singleton, endpointTemplate));
        }
        /// <summary>
        /// Adds and configures a RouteStrategy to the application.
        /// </summary>
        /// <param name="tenantParam">The name of the route parameter used to determine the tenant identifier.</param>
        /// <param name="configRoutes">Delegate to configure the routes.</param>
        /// <returns>The same MultiTenantBuilder passed into the method.</returns>
        public static FinbuckleMultiTenantBuilder WithRouteStrategy(this FinbuckleMultiTenantBuilder builder,
                                                                    string tenantParam,
                                                                    Action <IRouteBuilder> configRoutes)
        {
            if (string.IsNullOrWhiteSpace(tenantParam))
            {
                throw new ArgumentException("Invalud value for \"tenantParam\"", nameof(tenantParam));
            }

            if (configRoutes == null)
            {
                throw new ArgumentNullException(nameof(configRoutes));
            }

            return(builder.WithStrategy <RouteStrategy>(ServiceLifetime.Singleton, new object[] { tenantParam, configRoutes }));
        }
        /// <summary>
        /// Adds and configures a FormStrategy to the application with a configuration section configuration which reloads with changes.
        /// </summary>
        /// <remarks>
        /// The form strategy does not rely on TenantContext.  Therefore, it is not necessary to
        /// register the tenant context here.  The only thing it relies on is a FormStrategyConfiguration
        /// object.  This object is either added as a parameter or injected via IOptionsSnapshot.
        /// </remarks>
        /// <param name="builder"></param>
        /// <param name="formStrategyConfiguration">The configuration section defining the form strategy configuration and resolved with IOptionsSnapshot<></param>
        /// <returns></returns>
        public static FinbuckleMultiTenantBuilder WithFormStrategy(this FinbuckleMultiTenantBuilder builder, IConfigurationSection configurationSection)
        {
            // validate the configuration section
            var formStrategyConfiguration = new FormStrategyConfiguration();

            configurationSection.Bind(formStrategyConfiguration);

            ValidateFormStrategyConfiguration(formStrategyConfiguration);

            // register configuration so it will reload with changes
            builder.Services.Configure <FormStrategyConfiguration>(configurationSection);

            // register the strategy with the built in finbuckly custom strategy
            builder.WithStrategy <FormStrategy>(ServiceLifetime.Scoped);

            return(builder);
        }
        private static void BypassSessionPrincipalValidation <TTenantInfo>(
            FinbuckleMultiTenantBuilder <TTenantInfo> builder)
            where TTenantInfo : class, ITenantInfo, new()
        {
            builder.Services.ConfigureAll <CookieAuthenticationOptions>(options =>
            {
                var origOnValidatePrincipal        = options.Events.OnValidatePrincipal;
                options.Events.OnValidatePrincipal = async context =>
                {
                    // Skip if bypass set (e.g. ClaimStrategy in effect)
                    if (context.HttpContext.Items.Keys.Contains(
                            $"{Constants.TenantToken}__bypass_validate_principal__"))
                    {
                        return;
                    }

                    if (origOnValidatePrincipal != null)
                    {
                        await origOnValidatePrincipal(context);
                    }
                };
            });
        }
        /// <summary>
        /// Configures core functionality for per-tenant authentication.
        /// </summary>
        /// <param name="builder">MultiTenantBuilder instance.</param>
        /// <param name="config">Authentication options config</param>
        /// <returns>The same MultiTenantBuilder passed into the method.</returns>
        public static FinbuckleMultiTenantBuilder <TTenantInfo> WithPerTenantAuthenticationCore <TTenantInfo>(
            this FinbuckleMultiTenantBuilder <TTenantInfo> builder, Action <MultiTenantAuthenticationOptions>?config =
            null)
            where TTenantInfo : class, ITenantInfo, new()
        {
            config ??= _ => { };
            builder.Services.Configure(config);

            // We need to "decorate" IAuthenticationService so callbacks so that
            // remote authentication can get the tenant from the authentication
            // properties in the state parameter.
            if (builder.Services.All(s => s.ServiceType != typeof(IAuthenticationService)))
            {
                throw new MultiTenantException(
                          "WithPerTenantAuthenticationCore() must be called after AddAuthentication() in ConfigureServices.");
            }
            builder.Services.DecorateService <IAuthenticationService, MultiTenantAuthenticationService <TTenantInfo> >();

            // We need to "decorate" IAuthenticationScheme provider.
            builder.Services.DecorateService <IAuthenticationSchemeProvider, MultiTenantAuthenticationSchemeProvider>();

            return(builder);
        }
 /// <summary>
 /// Adds and configures a SessionStrategy to the application.
 /// </summary>
 /// <param name="builder">MultiTenantBuilder instance.</param>
 /// <param name="tenantKey">The session key to use.</param>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithSessionStrategy <TTenantInfo>(
     this FinbuckleMultiTenantBuilder <TTenantInfo> builder, string tenantKey)
     where TTenantInfo : class, ITenantInfo, new()
 => builder.WithStrategy <SessionStrategy>(ServiceLifetime.Singleton, tenantKey);
 /// <summary>
 /// Adds and configures a SessionStrategy to the application.
 /// </summary>
 /// <param name="builder">MultiTenantBuilder instance.</param>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithSessionStrategy <TTenantInfo>(
     this FinbuckleMultiTenantBuilder <TTenantInfo> builder)
     where TTenantInfo : class, ITenantInfo, new()
 => builder.WithStrategy <SessionStrategy>(ServiceLifetime.Singleton, Constants.TenantToken);
 /// <summary>
 /// Adds and configures a BasePathStrategy to the application.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.></returns>
 public static FinbuckleMultiTenantBuilder WithBasePathStrategy(this FinbuckleMultiTenantBuilder builder)
 => builder.WithStrategy <BasePathStrategy>(ServiceLifetime.Singleton);
        /// <summary>
        /// Configures conventional functionality for per-tenant authentication.
        /// </summary>
        /// <returns>The same MultiTenantBuilder passed into the method.</returns>
        public static FinbuckleMultiTenantBuilder <TTenantInfo> WithPerTenantAuthenticationConventions <TTenantInfo>(
            this FinbuckleMultiTenantBuilder <TTenantInfo> builder, Action <MultiTenantAuthenticationOptions> config = null)
            where TTenantInfo : class, ITenantInfo, new()
        {
            // Set events to set and validate tenant for each cookie based authentication principal.
            builder.Services.ConfigureAll <CookieAuthenticationOptions>(options =>
            {
                // Validate that claimed tenant matches current tenant.
                var origOnValidatePrincipal        = options.Events.OnValidatePrincipal;
                options.Events.OnValidatePrincipal = async context =>
                {
                    await origOnValidatePrincipal(context);

                    // Skip if no principal or bypass set
                    if (context.Principal == null || context.HttpContext.Items.Keys.Contains($"{Constants.TenantToken}__bypass_validate_principle__"))
                    {
                        return;
                    }

                    var currentTenant = context.HttpContext.GetMultiTenantContext <TTenantInfo>()?.TenantInfo?.Identifier;

                    // If no current tenant and no tenant claim then OK
                    if (currentTenant == null && !context.Principal.Claims.Any(c => c.Type == Constants.TenantToken))
                    {
                        return;
                    }

                    // Does a tenant claim for the principal match the current tenant?
                    if (!context.Principal.Claims.Where(c => c.Type == Constants.TenantToken && String.Equals(c.Value, currentTenant, StringComparison.OrdinalIgnoreCase)).Any())
                    {
                        context.RejectPrincipal();
                    }
                };

                // Set the tenant claim when signing in.
                var origOnSigningIn        = options.Events.OnSigningIn;
                options.Events.OnSigningIn = async context =>
                {
                    await origOnSigningIn(context);

                    if (context.Principal == null)
                    {
                        return;
                    }

                    var identity      = (ClaimsIdentity)context.Principal.Identity;
                    var currentTenant = context.HttpContext.GetMultiTenantContext <TTenantInfo>()?.TenantInfo?.Identifier;

                    if (currentTenant != null &&
                        !identity.Claims.Where(c => c.Type == Constants.TenantToken && c.Value == currentTenant).Any())
                    {
                        identity.AddClaim(new Claim(Constants.TenantToken, currentTenant));
                    }
                };
            });

            // Set per-tenant cookie options by convention.
            builder.WithPerTenantOptions <CookieAuthenticationOptions>((options, tc) =>
            {
                var d = (dynamic)tc;
                try { options.LoginPath = ((string)d.CookieLoginPath).Replace(Constants.TenantToken, tc.Identifier); } catch { }
                try { options.LogoutPath = ((string)d.CookieLogoutPath).Replace(Constants.TenantToken, tc.Identifier); } catch { }
                try { options.AccessDeniedPath = ((string)d.CookieAccessDeniedPath).Replace(Constants.TenantToken, tc.Identifier); } catch { }
            });

            // Set per-tenant OpenIdConnect options by convention.
            builder.WithPerTenantOptions <OpenIdConnectOptions>((options, tc) =>
            {
                var d = (dynamic)tc;
                try { options.Authority = ((string)d.OpenIdConnectAuthority).Replace(Constants.TenantToken, tc.Identifier); } catch { }
                try { options.ClientId = ((string)d.OpenIdConnectClientId).Replace(Constants.TenantToken, tc.Identifier); } catch { }
                try { options.ClientSecret = ((string)d.OpenIdConnectClientSecret).Replace(Constants.TenantToken, tc.Identifier); } catch { }
            });

            var challengeSchemeProp = typeof(TTenantInfo).GetProperty("ChallengeScheme");

            if (challengeSchemeProp != null && challengeSchemeProp.PropertyType == typeof(string))
            {
                builder.WithPerTenantOptions <AuthenticationOptions>((options, tc)
                                                                     => options.DefaultChallengeScheme = (string)challengeSchemeProp.GetValue(tc) ?? options.DefaultChallengeScheme);
            }

            return(builder);
        }
// #endif

        /// <summary>
        /// Adds and configures a HostStrategy with template "__tenant__.*" to the application.
        /// </summary>
        /// <returns>The same MultiTenantBuilder passed into the method.</returns>
        public static FinbuckleMultiTenantBuilder <TTenantInfo> WithHostStrategy <TTenantInfo>(
            this FinbuckleMultiTenantBuilder <TTenantInfo> builder)
            where TTenantInfo : class, ITenantInfo, new()
        => builder.WithHostStrategy($"{Constants.TenantToken}.*");
 /// <summary>
 /// Adds and configures a ClaimStrategy for claim name "__tenant__" to the application. Uses the default authentication handler scheme.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithClaimStrategy <TTenantInfo>(
     this FinbuckleMultiTenantBuilder <TTenantInfo> builder) where TTenantInfo : class, ITenantInfo, new()
 {
     return(builder.WithClaimStrategy(Constants.TenantToken));
 }
 /// <summary>
 /// Adds and configures a BasePathStrategy to the application.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.></returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithBasePathStrategy <TTenantInfo>(this FinbuckleMultiTenantBuilder <TTenantInfo> builder)
     where TTenantInfo : class, ITenantInfo, new()
 => builder.WithStrategy <BasePathStrategy>(ServiceLifetime.Singleton);
 /// <summary>
 /// Adds and configures a Header to the application.
 /// </summary>
 /// <param name="builder">MultiTenantBuilder instance.</param>
 /// <param name="tenantKey">The template for determining the tenant identifier in the host.</param>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithHeaderStrategy <TTenantInfo>(
     this FinbuckleMultiTenantBuilder <TTenantInfo> builder, string tenantKey)
     where TTenantInfo : class, ITenantInfo, new()
 {
     return(builder.WithStrategy <HeaderStrategy>(ServiceLifetime.Singleton, tenantKey));
 }
 /// <summary>
 /// Adds and configures InMemoryStore to the application using the provided ConfigurationSeciont.
 /// </summary>
 /// <param name="config">The ConfigurationSection which contains the InMemoryStore configuartion settings.</param>
 /// <param name="ignoreCase">Whether the store should ignore case.</param>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder WithInMemoryStore(this FinbuckleMultiTenantBuilder builder,
                                                             IConfigurationSection configurationSection,
                                                             bool ignoreCase)
 => builder.WithInMemoryStore(o => configurationSection.Bind(o), ignoreCase);
 /// <summary>
 /// Adds and configures a case-insensitive InMemoryStore to the application using the provided action.
 /// </summary>
 /// <param name="config">A delegate or lambda for configuring the tenant.</param>
 /// <param name="ignoreCase">Whether the store should ignore case.</param>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder WithInMemoryStore(this FinbuckleMultiTenantBuilder builder,
                                                             Action <InMemoryStoreOptions> config)
 => builder.WithInMemoryStore(config, true);
 /// <summary>
 /// Adds an empty InMemoryStore to the application.
 /// </summary>
 /// <param name="ignoreCase">Whether the store should ignore case.</param>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder WithInMemoryStore(this FinbuckleMultiTenantBuilder builder,
                                                             bool ignoreCase)
 => builder.WithInMemoryStore(_ => { }, ignoreCase);
 /// <summary>
 /// Adds an empty, case-insensitive InMemoryStore to the application.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder WithInMemoryStore(this FinbuckleMultiTenantBuilder builder)
 => builder.WithInMemoryStore(true);
 /// <summary>
 /// Adds and configures a HostStrategy with template "\_\_tenant\_\_.*" to the application.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder WithHostStrategy(this FinbuckleMultiTenantBuilder builder)
 => builder.WithHostStrategy("__tenant__.*");
 /// <summary>
 /// Adds and configures a RemoteAuthenticationCallbackStrategy to the application.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithRemoteAuthenticationCallbackStrategy <TTenantInfo>(
     this FinbuckleMultiTenantBuilder <TTenantInfo> builder)
     where TTenantInfo : class, ITenantInfo, new()
 {
     return(builder.WithStrategy <RemoteAuthenticationCallbackStrategy>(ServiceLifetime.Singleton));
 }
Exemple #22
0
 /// <summary>
 /// Adds an EFCore based multitenant store to the application. Will also add the database context service unless it is already added.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithEFCoreStore <TEFCoreStoreDbContext, TTenantInfo>(this FinbuckleMultiTenantBuilder <TTenantInfo> builder)
     where TEFCoreStoreDbContext : EFCoreStoreDbContext <TTenantInfo>
     where TTenantInfo : class, ITenantInfo, new()
 {
     builder.Services.AddDbContext <TEFCoreStoreDbContext>(); // Note, will not override existing context if already added.
     return(builder.WithStore <EFCoreStore <TEFCoreStoreDbContext, TTenantInfo> >(ServiceLifetime.Scoped));
 }
 /// <summary>
 /// Adds and configures a BasePathStrategy to the application.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.></returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithBasePathStrategy <TTenantInfo>(
     this FinbuckleMultiTenantBuilder <TTenantInfo> builder)
     where TTenantInfo : class, ITenantInfo, new() => WithBasePathStrategy(builder, configureOptions =>
 {
     configureOptions.RebaseAspNetCorePathBase = false;
 });
 /// <summary>
 /// Adds and configures a ClaimStrategy with tenantKey "__tenant__" to the application.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithClaimStrategy <TTenantInfo>(this FinbuckleMultiTenantBuilder <TTenantInfo> builder) where TTenantInfo : class, ITenantInfo, new()
 {
     return(builder.WithStrategy <ClaimStrategy>(ServiceLifetime.Singleton, Constants.TenantToken));
 }
 /// <summary>
 /// Configures authentication options to enable per-tenant behavior.
 /// </summary>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithPerTenantAuthentication <TTenantInfo>(
     this FinbuckleMultiTenantBuilder <TTenantInfo> builder)
     where TTenantInfo : class, ITenantInfo, new()
 {
     return(WithPerTenantAuthentication(builder, _ => { }));
 }
        /// <summary>
        /// Configures per-tenant authentication behavior.
        /// </summary>
        /// <param name="config">Authentication options config</param>
        /// <returns>The same MultiTenantBuilder passed into the method.</returns>
        public static FinbuckleMultiTenantBuilder <TTenantInfo> WithPerTenantAuthentication <TTenantInfo>(this FinbuckleMultiTenantBuilder <TTenantInfo> builder, Action <MultiTenantAuthenticationOptions> config)
            where TTenantInfo : class, ITenantInfo, new()
        {
            builder.WithPerTenantAuthenticationCore(config);
            builder.WithPerTenantAuthenticationConventions();
            builder.WithRemoteAuthenticationCallbackStrategy();

            return(builder);
        }
 /// <summary>
 /// Adds and configures a RouteStrategy with a route parameter Constants.TenantToken to the application.
 /// </summary>
 /// <param name="configRoutes">Delegate to configure the routes.</param>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder <TTenantInfo> WithRouteStrategy <TTenantInfo>(this FinbuckleMultiTenantBuilder <TTenantInfo> builder,
                                                                                         Action <IRouteBuilder> configRoutes)
     where TTenantInfo : class, ITenantInfo, new()
 => builder.WithRouteStrategy(Constants.TenantToken, configRoutes);
 /// <summary>
 /// Adds and configures a RouteStrategy with a route parameter "\_\_tenant\_\_" to the application.
 /// </summary>
 /// <param name="configRoutes">Delegate to configure the routes.</param>
 /// <returns>The same MultiTenantBuilder passed into the method.</returns>
 public static FinbuckleMultiTenantBuilder WithRouteStrategy(this FinbuckleMultiTenantBuilder builder,
                                                             Action <IRouteBuilder> configRoutes)
 => builder.WithRouteStrategy("__tenant__", configRoutes);
        public static FinbuckleMultiTenantBuilder <TTenantInfo> WithPerTenantAuthenticationConventions <TTenantInfo>(
            this FinbuckleMultiTenantBuilder <TTenantInfo> builder,
            Action <MultiTenantAuthenticationOptions>?config = null)
            where TTenantInfo : class, ITenantInfo, new()
        {
            // Set events to set and validate tenant for each cookie based authentication principal.
            builder.Services.ConfigureAll <CookieAuthenticationOptions>(options =>
            {
                // Validate that claimed tenant matches current tenant.
                var origOnValidatePrincipal        = options.Events.OnValidatePrincipal;
                options.Events.OnValidatePrincipal = async context =>
                {
                    // Skip if bypass set (e.g. ClaimsStrategy in effect)
                    if (context.HttpContext.Items.Keys.Contains(
                            $"{Constants.TenantToken}__bypass_validate_principal__"))
                    {
                        return;
                    }

                    var currentTenant = context.HttpContext.GetMultiTenantContext <TTenantInfo>()?.TenantInfo
                                        ?.Identifier;
                    string?authTenant = null;
                    if (context.Properties.Items.ContainsKey(Constants.TenantToken))
                    {
                        authTenant = context.Properties.Items[Constants.TenantToken];
                    }
                    else
                    {
                        var loggerFactory = context.HttpContext.RequestServices.GetService <ILoggerFactory>();
                        loggerFactory?.CreateLogger <FinbuckleMultiTenantBuilder <TTenantInfo> >()
                        .LogWarning("No tenant found in authentication properties.");
                    }

                    // Does the current tenant match the auth property tenant?
                    if (!string.Equals(currentTenant, authTenant, StringComparison.OrdinalIgnoreCase))
                    {
                        context.RejectPrincipal();
                    }

                    await origOnValidatePrincipal(context);
                };
            });

            // Set per-tenant cookie options by convention.
            builder.WithPerTenantOptions <CookieAuthenticationOptions>((options, tc) =>
            {
                var d = (dynamic)tc;
                try
                {
                    options.LoginPath = ((string)d.CookieLoginPath).Replace(Constants.TenantToken, tc.Identifier);
                }
                catch
                {
                }

                try
                {
                    options.LogoutPath = ((string)d.CookieLogoutPath).Replace(Constants.TenantToken, tc.Identifier);
                }
                catch
                {
                }

                try
                {
                    options.AccessDeniedPath =
                        ((string)d.CookieAccessDeniedPath).Replace(Constants.TenantToken, tc.Identifier);
                }
                catch
                {
                }
            });

            // Set per-tenant OpenIdConnect options by convention.
            builder.WithPerTenantOptions <OpenIdConnectOptions>((options, tc) =>
            {
                var d = (dynamic)tc;
                try
                {
                    options.Authority =
                        ((string)d.OpenIdConnectAuthority).Replace(Constants.TenantToken, tc.Identifier);
                }
                catch
                {
                }

                try
                {
                    options.ClientId = ((string)d.OpenIdConnectClientId).Replace(Constants.TenantToken, tc.Identifier);
                }
                catch
                {
                }

                try
                {
                    options.ClientSecret =
                        ((string)d.OpenIdConnectClientSecret).Replace(Constants.TenantToken, tc.Identifier);
                }
                catch
                {
                }
            });

            var challengeSchemeProp = typeof(TTenantInfo).GetProperty("ChallengeScheme");

            if (challengeSchemeProp != null && challengeSchemeProp.PropertyType == typeof(string))
            {
                builder.WithPerTenantOptions <AuthenticationOptions>((options, tc)
                                                                     => options.DefaultChallengeScheme =
                                                                         (string?)challengeSchemeProp.GetValue(tc) ?? options.DefaultChallengeScheme);
            }

            return(builder);
        }
        /// <summary>
        /// Adds and configures a HostStrategy to the application.
        /// </summary>
        /// <param name="template">The template for determining the tenant identifier in the host.</param>
        /// <returns>The same MultiTenantBuilder passed into the method.</returns>
        public static FinbuckleMultiTenantBuilder <TTenantInfo> WithHostStrategy <TTenantInfo>(this FinbuckleMultiTenantBuilder <TTenantInfo> builder, string template)
            where TTenantInfo : class, ITenantInfo, new()
        {
            if (string.IsNullOrWhiteSpace(template))
            {
                throw new ArgumentException("Invalid value for \"template\"", nameof(template));
            }

            return(builder.WithStrategy <HostStrategy>(ServiceLifetime.Singleton, new object[] { template }));
        }