/// <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);
        }
Example #3
0
        /// <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);
        }