4
votes

I have a multi-tenant application where each tenant can define their own MetaData URl, ClientId, Authority, etc for either WsFed or OpenIdConnect(Azure) or Shibboleth(Kentor). All the tenants are stored in DB table and registered in the OwinStartup as below:

        // Configure the db context, user manager and signin manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

        // Enable the application to use a cookie to store information for the signed in user
        // and to use a cookie to temporarily store information about a user logging in with a third party login provider
        // Configure the sign in cookie

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            //  CookieName = "Kuder.SSO",
            LoginPath = new PathString("/Account/Login-register"),
            Provider = new CookieAuthenticationProvider
            {
                //Enables the application to validate the security stamp when the user logs in.
                //This is a security feature which is used when you change a password or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });

        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

    OrganizationModel objOrg = new OrganizationModel();
    var orgList = objOrg.GetOrganizationList();
    foreach (OrganizationModel org in orgList)
    {
        switch (org.AuthenticationName)
        {
            case "ADFS":
            WsFederationAuthenticationOptions objAdfs = null;
             objAdfs = new WsFederationAuthenticationOptions
                {
                    AuthenticationType = org.AuthenticationType,
                    Caption = org.Caption,
                    BackchannelCertificateValidator = null,
                    MetadataAddress = org.MetadataUrl,
                    Wtrealm = org.Realm,
                    SignOutWreply = org.Realm,
                    Notifications = new WsFederationAuthenticationNotifications
                    {
                        AuthenticationFailed = context =>
                        {
                            context.HandleResponse();
                            Logging.Logger.LogAndEmailException(context.Exception);
                            context.Response.Redirect(ConfigurationManager.AppSettings["CustomErrorPath"].ToString() + context.Exception.Message);
                            return Task.FromResult(0);
                        }
                    },
                    TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false },
                };
             app.UseWsFederationAuthentication(objAdfs);
                break;
            case "Azure":
                OpenIdConnectAuthenticationOptions azure = null;
                azure = new OpenIdConnectAuthenticationOptions
                {
                    AuthenticationType = org.AuthenticationType,
                    Caption = org.Caption,
                    BackchannelCertificateValidator = null,
                    Authority = org.MetadataUrl,
                    ClientId = org.IDPProvider.Trim(),
                    RedirectUri = org.Realm,
                    PostLogoutRedirectUri = org.Realm, 
                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        AuthenticationFailed = context =>
                        {
                            context.HandleResponse();
                            Logging.Logger.LogAndEmailException(context.Exception);
                            context.Response.Redirect(ConfigurationManager.AppSettings["CustomErrorPath"].ToString() + context.Exception.Message);
                            return Task.FromResult(0);
                        }
                    },
                    TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false },
                };
                app.UseOpenIdConnectAuthentication(azure);
                break;
            case "Shibboleth":
                var english = CultureInfo.GetCultureInfo("en-us");
                var organization = new Organization();
                organization.Names.Add(new LocalizedName("xxx", english));
                organization.DisplayNames.Add(new LocalizedName("xxx Inc.", english));
                organization.Urls.Add(new LocalizedUri(new Uri("http://www.aaa.com"), english));

                var authServicesOptions = new KentorAuthServicesAuthenticationOptions(false)
               {
                   SPOptions = new SPOptions
                   {
                       EntityId = new EntityId(org.Realm),
                       ReturnUrl = new Uri(org.Realm),
                      Organization = organization,
                   },
                   AuthenticationType = org.AuthenticationType,
                   Caption = org.Caption,
                  SignInAsAuthenticationType = "ExternalCookie",
               };
                authServicesOptions.IdentityProviders.Add(new IdentityProvider(
                new EntityId(org.IDPProvider), authServicesOptions.SPOptions)
                 {
                     MetadataLocation = org.MetadataUrl,
                     LoadMetadata = true,
                    SingleLogoutServiceUrl = new Uri(org.Realm),
                 });
                app.UseKentorAuthServicesAuthentication(authServicesOptions);
                break;
            default:
                break;
        }
    }

When multiple orgnaizations of same provider (ADFS, Azure or Shibboleth) are enabled in Db, I get errors. I tried "app.Map" to extend it. But unsuccessful though. Also, I use below code to logout for all providers (ADFS and Azure) but logout also fails.

Provider is unique Authentication type I use across organizations.

HttpContext.GetOwinContext().Authentication.SignOut(provider, Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ApplicationCookie, DefaultAuthenticationTypes.ExternalCookie);

Looking for help / guidance. Note: whenever new tenant gets added, am fine to recycle appdomain, not needed to dynamically rebuild the pipeline to make things complex.

2

2 Answers

0
votes

The Kentor.AuthServices middleware supports multiple instances, but you need to assign a specific ModulePath to each of them. Without that, the first Kentor.AuthServices middleware will handle all incoming requests and throw errors on messages from IdentityProviders that are configured with other instances.

I know that some of the other Katana providers have similar "hidden" endpoints that are used during callback, but I don't know how they behave if multiple middleware instances are loaded.

As an alternative, The Kentor.AuthServices middleware also supports having multiple identity providers registered with one instance. Then you can add and rmove IdentityProvider instances during runtime to the KentorAuthServicesAuthenticationOptions and have it take effect immediately. However that might not be an ideal solution if you are using one middleware per tenant for the other protocols.

0
votes

Wouldn't it be possible to create different owin pipelines per tenant based on a condition in app.MapWhen("tenant1", ctx=> ctx.configureSpecificTenant)

MapWhen also accepts a function, so you could base it on some other condition like a subdomain with a foreach iteration on a list.