/// <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); }
/// <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)); }
/// <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 })); }