/// <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); }
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> /// 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() { 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 principle 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 { } }); builder.WithRemoteAuthenticationCallbackStrategy(); // 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.Where(s => s.ServiceType == typeof(IAuthenticationService)).Any()) { throw new MultiTenantException("WithRemoteAuthenticationCallbackStrategy() must be called after AddAuthorization() in ConfigureServices."); } builder.Services.DecorateService <IAuthenticationService, MultiTenantAuthenticationService <TTenantInfo> >(); // Set per-tenant OpenIdConnect options by convention. builder.WithPerTenantOptions <OpenIdConnectOptions>((options, tc) => { var d = (dynamic)tc; try { options.Authority = d.OpenIdConnectAuthority; } catch { } try { options.ClientId = d.OpenIdConnectClientId; } catch { } try { options.ClientSecret = d.OpenIdConnectClientSecret; } catch { } }); // Replace IAuthenticationSchemeProvider so that the options aren't // cached and can be used per-tenant. builder.Services.Replace(ServiceDescriptor.Singleton <IAuthenticationSchemeProvider, MultiTenantAuthenticationSchemeProvider>()); 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); }