Umbraco, Brock Allen Membership and Tenant Configuration

Since we were working on a multi-tentant solution with Umbraco, we needed a solution as Umbraco's membership did not support multi-tenant out of the box. Discussed in a previous article(Umbraco Multi-Tenant Membership), we chose Brock Allen's Membership Reboot. For the integration with Umbraco multi-tenant, there were three primary tasks: dependency injection update, tenant configuration, and content integration. Here we discuss the issues associated with Tenant Configuration.

Membership Reboot Single Tenant Configuration

The configuration for Membership Reboot is fairly straightforward. In the web.config there are three primary relevant sections: the connection strings, the email configuration for outbound notification emails and the Membership Reboot element. Connection strings are just like any other connection strings. The email configuration maybe appropriate for a single tenant, but a multi-tenant application will most likely want to configure the emails by tenant. The membershipReboot element follows along with the code that reads the values:

<membershipReboot requireAccountVerification="true" emailIsUsername="false" multiTenant="false" allowAccountDeletion="true" passwordHashingIterationCount="0" accountLockoutDuration="00:01:00" passwordResetFrequency="0" />

On startup, the code create the config and binds it to the NInject container.

var config = MembershipRebootConfig.Create();
 kernel.Bind<MembershipRebootConfiguration>().ToConstant(config);

Creation of the config, including setting urls for login, confirm, etc.:

public static MembershipRebootConfiguration Create()
        {
            var config = new MembershipRebootConfiguration();
            //config.RequireAccountVerification = false;
            config.AddEventHandler(new DebuggerEventHandler());

            var appinfo = new AspNetApplicationInformation("Test", "Test Email Signature",
                "UserAccount/Login",
                "UserAccount/ChangeEmail/Confirm/",
                "UserAccount/Register/Cancel/",
                "UserAccount/PasswordReset/Confirm/");
            var emailFormatter = new EmailMessageFormatter(appinfo);
            // uncomment if you want email notifications -- also update smtp settings in web.config
            config.AddEventHandler(new EmailAccountEventsHandler(emailFormatter));

            // uncomment to enable SMS notifications -- also update TwilloSmsEventHandler class below
            //config.AddEventHandler(new TwilloSmsEventHandler(appinfo));
            // uncomment to ensure proper password complexity
            //config.ConfigurePasswordComplexity();

            var debugging = false;
#if DEBUG
            debugging = true;
#endif
            // this config enables cookies to be issued once user logs in with mobile code
            config.ConfigureTwoFactorAuthenticationCookies(debugging);
            return config;
        }

The MembershipRebootConfiguration demonstrates the configurations available to be set in the web.config:

public MembershipRebootConfiguration(SecuritySettings securitySettings)
        {
            if (securitySettings == null) throw new ArgumentNullException("securitySettings");
            
            this.MultiTenant = securitySettings.MultiTenant;
            this.DefaultTenant = securitySettings.DefaultTenant;
            this.EmailIsUnique = securitySettings.EmailIsUnique;
            this.EmailIsUsername = securitySettings.EmailIsUsername;
            this.UsernamesUniqueAcrossTenants = securitySettings.UsernamesUniqueAcrossTenants;
            this.RequireAccountVerification = securitySettings.RequireAccountVerification;
            this.AllowLoginAfterAccountCreation = securitySettings.AllowLoginAfterAccountCreation;
            this.AccountLockoutFailedLoginAttempts = securitySettings.AccountLockoutFailedLoginAttempts;
            this.AccountLockoutDuration = securitySettings.AccountLockoutDuration;
            this.AllowAccountDeletion = securitySettings.AllowAccountDeletion;
            this.PasswordHashingIterationCount = securitySettings.PasswordHashingIterationCount;
            this.PasswordResetFrequency = securitySettings.PasswordResetFrequency;
            this.VerificationKeyLifetime = securitySettings.VerificationKeyLifetime;

            this.Crypto = new DefaultCrypto();
        }

 So, this shows that configuration for a single tenant application is quite straightforward. For a multi-tenant application, however, we will want to move some of these configuration values elsewhere.

Configuration with Multi-Tenant and Umbraco

Many of the values for configuration remain the same. For example, the application is either multi-tenant or it is not. Most items that are set in the configuration may change given the tenant site. For this reason, we wanted to create the configuration from a content item in Umbraco. Our convention used a "Site Settings" document type under the home node of the Site. This Site Settings node contains such items as email configuration for outbound emails. Additionally, there is a Site Account document type that sits below the Site Settings node that contains items specific to membership.

So rather than getting the configuration one time as in the single tenant, the code below is the StructureMap configuration which retrieves a unique per lifecycle configuration using a factory method.

var unique = new UniquePerRequestLifecycle();
            Container.Configure(
                x =>
                    x.For<MembershipRebootConfiguration>()
                        .LifecycleIs(unique)
                        .Use(ctx => ctx.GetInstance<IMembershipRebootConfigFactory>().GetMembershipRebootConfiguration()));

The factory method retrieves the CurrentSiteContentDto which is the data object for the Site Settings node. It first attempts to retrieve the configuration from cache. Otherwise, it constructs it using the data from the Site Settings node:

 public MembershipRebootConfiguration GetMembershipRebootConfiguration()
        {
            var siteContent = _httpContextHelper.GetContextItem<SiteContentDto>(CacheKeyConstants.CurrentSiteContentDto);
            var cacheKey = "MembershipRebootConfiguration_" + siteContent.Id;

            return _cacheService.Get(CacheNameEnum.GENERAL,
                cacheKey,
                () => GetMembershipRebootConfigurationForSite(siteContent),
                CachePriorityType.Default,
                new CacheExpiration { AbsoluteTime = DateTime.Now.AddSeconds(RandomUtility.GetNextRangeInt(1800, 2400)) }
                );
        }

        private MembershipRebootConfiguration GetMembershipRebootConfigurationForSite(SiteContentDto siteContent)
        {
            if (siteContent == null) throw new ArgumentNullException("siteContent");
            var config = new MembershipRebootConfiguration();
            //config.RequireAccountVerification = false;
            config.AddEventHandler(new DebuggerEventHandler());

            config.PasswordResetFrequency = siteContent.SiteAccount.PasswordResetFrequency;
            config.AccountLockoutDuration = siteContent.SiteAccount.AccountLockoutDuration;
            config.AllowLoginAfterAccountCreation = siteContent.SiteAccount.AllowLoginAfterAccountCreation;
            config.RequireAccountVerification = siteContent.SiteAccount.RequireAccountVerification;
            config.AccountLockoutFailedLoginAttempts = siteContent.SiteAccount.AccountLockoutFailedLoginAttempts;
            config.PasswordResetFrequency = siteContent.SiteAccount.PasswordResetFrequency;

            ....

            return config;
        }
   

Finally, the account urls also come from the Site Account node as well:

var mobileUrls = siteContentDto.SiteAccount.MobileSiteAccountUrls;
var fullSiteUrls = siteContentDto.SiteAccount.FullSiteAccountUrls;

msg = msg.Replace("{tokenLoginUrl}", forMobile ? mobileUrls.LoginUrl : fullSiteUrls.LoginUrl);

Conclusion

So here we were able to modify the configuration to allow for maintaining separate configuration per tenant. We were also able to maintain account urls by tenant as well. This was helpful for the purpose of providing urls that are relevant to the tenant language.

 

 

 



About The Author

Avatar
Jonathan Folland

Jonathan Folland is technologist with over 20 years experience in a variety of capacities. He held the position of Vice President of Operations of one of the fastest growing companies in the world. He has led high volume e-Commerce operations and is an expert at mixing technology and finance to produce high return results.


Related Articles