Introduction

This article provides a solution for using the SimpleMembershipProvider and newer WebSecurity APIs with Sitecore. This enables the use of external login providers, but doesn't work out-of-the-box due to the limitations of SimpleMembershipProvider as discussed in a previous article. The following solution allows these issues to be overcome, and also enables compatibility with Sitecore User Manager for out-of-box user administration.

Background

Sitecore provides the ability to physically partition users based on Sitecore domains. This helps increase the security of the system by mitigating possible exposure from certain types of attack e.g. Sql injection.

Physical separation of user accounts is achieved via Sitecore configuration, and by making use of Sitecore's switching provider. For instance to partition public facing extranet accounts from admin accounts you might configure a specific extranet MembershipProvider as follows, and link this to a particular Sitecore domain (extranet). In your server-side login code to might additionally enforce the domain by prefixing the domain to all usernames entered via the front-end of your public facing site.

<sitecore>
  ...
  <switchingProviders>
    <membership>
      <provider providerName="extranet"
        storeFullNames="false"
        wildcard="%"
        domains="extranet" />
      <provider providerName="sitecore"
        storeFullNames="true"
        wildcard="%"
        domains="*" />
    </membership>
  </switchingProviders>
  ...
  <sites>
    ...
    <site name="website" hostName="*.myApp.com" domain="extranet" ... />
    ...
  </sites>
</sitecore>
<system.web>
  ...
  <membership defaultProvider="switcher">
    <providers>
      <clear />
      <add name="switcher"
        type="Sitecore.Security.SwitchingMembershipProvider, Sitecore.Kernel"
        applicationName="sitecore" mappings="switchingProviders/membership" />
      <add name="extranet" type="App.ExtranetMembershipProvider, App" ... />
      <add name="sitecore" type="System.Web.Security.SqlMembershipProvider" ... />
    </providers>
  </membership>
  ...
</system.web>

The above example configuration is simplified for brevity and can be repeated for ProfileProvider and RoleProviders. It essentially tells Sitecore that usernames prefixed with extranet must use the named extranet provider, and everything else (wildcard) must use the named sitecore provider. All membership related API calls used by Sitecore and in any application logic will by default pass through Sitecore's switching provider, where they are delegated to the configured provider for that domain. The default domain is defined within the sites config, and by controlling the domain prefix appended to usernames.

The issue with plugging in SimpleMembershipProvider with this scenario is that firstly it does not initialise correctly unless it is configured as the default provider. Secondly many of the standard MembershipProvider methods from which SimpleMembershipProvider inherits are not implemented, and therefore will break the Sitecore's User Management interface.

Extending The Switching Provider

In order to work with the new WebSecurity APIs, the default provider must extend from ExtendedMembershipProvider, which is not the case with Sitecore's switching provider. To work around this we need to encapsulate Sitecore's SwitchingMembershipProvider and create a new implementation that does extend from ExtendedMembershipProvider. This can then be used to either delegate the new methods required to support WebSecurity to the relevant provider, or to wrap the default implementation.

public class SwitchingExtendedMembershipProvider : ExtendedMembershipProvider
{
  private readonly SwitchingMembershipProvider switchingMembershipProvider;

  public SwitchingExtendedMembershipProvider()
  {
    this.switchingMembershipProvider = new SwitchingMembershipProvider();
  }

  public override void Initialize(string name, NameValueCollection config)
  {
    this.switchingMembershipProvider.Initialize(name, config);
  }

  ...
}

All methods relating to MembershipProvider can simply pass through to the encapsulated instance as follows.

public override MembershipUser GetUser(string username, bool userIsOnline)
  {
    return this.switchingMembershipProvider.GetUser(username, userIsOnline);
  }

Previously unsupported methods relating to ExtendedMembershipProvider need to determine the provider implementation to use by inferring the appropriate provider from the Sitecore Wrappers based on domain prefix as follows.

public override bool DeleteAccount(string userName)
{
  var wrapper = this.switchingMembershipProvider.Wrappers.GetWrapper(userName);
  var provider = wrapper.Provider as ExtendedMembershipProvider;
  if (provider == null)  
  {
    throw new NotSupportedException();
  }

  return provider.DeleteAccount(wrapper.Localize(userName));
}

Where the API does not accept username, then the logic is a bit more fuzzy and needs to be implemented based on the context. For instance confirm account finds the first implementation of ExtendedMembershipProvider with a match on the token provided.

public override bool ConfirmAccount(string token)
{
   extendedProviders = this.switchingMembershipProvider.Wrappers
          .Where(x => x.Provider is ExtendedMembershipProvider)
                        .Select(x => x.Provider as ExtendedMembershipProvider)
                        .ToArray();

  return extendedProviders.Any(x => x.ConfirmAccount(token));
}

A full implementation of SwitchingExtendedMembershipProvider may be downloaded from this page.

Initialise SimpleMembershipProvider

The limitations for SimpleMembershipProvider mean it cannot be initialised unless it is configured as the default provider, and even our new switching provider cannot overcome this limitation due to some internal initialisation baked into SimpleMembershipProvider and WebSecurity.

To overcome this limitation a bit of reflection is required, and can be used to temporarily swap out the provider during the initialisation sequence at runtime. This not only allows the provider to be initialised, but also allows multiple instances of SimpleMembershipProvider to be registered with our new switching provider.

The following code shows an example of swapping out the non-default provider at runtime, and could be added to the application start routine. Note the provider is set as the default prior to calling WebSecurity.InitializeDatabaseConnection, and then swapped back. This can be repeated for any number of providers requiring initialisation.

var simpleProvider = Membership.Providers["extranet"];
var defaultProvider = Membership.Provider;
var membershipField = typeof(Membership).GetField("s_Provider", BindingFlags.NonPublic | BindingFlags.Static);

// make simpleProvider the default provider and initialise
membershipField.SetValue(null, simpleProvider);
WebSecurity.InitializeDatabaseConnection(connectionString, "UserProfile", "UserId", "UserName", true);

// put back the default provider
membershipField.SetValue(null, defaultProvider);

Implementing MembershipProvider

The last bit of the puzzle is the implementation of MembershipProvider. This is to support out-of-box User Management features provided by Sitecore, which aggregates users across all of the configured providers. Although ExtendedMembershipProvider and thus SimpleMembershipProvider extend from MembershipProvider, they do not implement most of the older APIs and will throw NotImplementedException if called. This of course breaks the Sitecore User Management interface, so requires some additional work.

The solution is to extend SimpleMembershipProvider and implement the missing members in order to make the provider compatible with MembershipProvider. Luckily back porting the missing methods isn't too difficult, and most of the older APIs can be mapped to properties on the new SimpleMembershipProvider schema. For this I recommend using my BetterMembership.Net library which additionally takes care of the initialisation routine described above. It also provides an implementation of ProfileProvider that maps profile properties to columns on the UserProfile table, and provides a more compatible RoleProvider.

Putting It All Together

Adding BetterMembership.Net to the Sitecore solution, we can configure 1 or more extranet membership provider instances based on SimpleMembershipProvider schema, each with it's own connection string and linked to a particular Sitecore domain. These can be used side-by-side with the default SqlMembershipProvider implementations as required. The final configuration looks like this:

<sitecore>
  <switchingProviders>
    <membership>
      <provider providerName="extranet"
        storeFullNames="false"
        wildcard="%"
        domains="extranet" />
      <provider providerName="sitecore"
        storeFullNames="true"
        wildcard="%"
        domains="*" />
    </membership>
    <roleManager>
      <provider providerName="extranet"
        storeFullNames="false"
        wildcard="%"
        domains="extranet" />
      <provider providerName="sitecore"
        storeFullNames="true"
        wildcard="%"
        domains="*" />
    </roleManager>
 <profile>
      <provider providerName="extranet"
        storeFullNames="false"
        wildcard="%"
        domains="extranet" />
      <provider providerName="sitecore"
        storeFullNames="true"
        wildcard="%"
        domains="*" />
    </profile>
    </switchingProviders>
    ...
    <sites>
     ...
     <site name="website" hostName="*.myApp.com" domain="extranet" ... />
     ...
   </sites>
</sitecore>
<system.web>
  <membership defaultProvider="switcher" >
    <providers>
      <clear />
      <add name="switcher"
         type="CodeKing.Security.SwitchingExtendedMembershipProvider, CodeKing"
         applicationName="sitecore" mappings="switchingProviders/membership" />
      <add name="extranet"
         type="BetterMembership.Web.BetterMembershipProvider, BetterMembership"
         connectionStringName="ExtranetConnection" userEmailColumn="Email" autoInitialize="true"  />
      <add name="sitecore"
         type="System.Web.Security.SqlMembershipProvider"
         connectionStringName="core" applicationName="sitecore" />
    </providers>
  </membership>
  <roleManager defaultProvider="switcher" enabled="true" >
    <providers>
      <clear />
      <add name="switcher"
         type="Sitecore.Security.SwitchingRoleProvider, Sitecore.Kernel"
         applicationName="sitecore" mappings="switchingProviders/roleManager" />
      <add name="extranet"
         type="BetterMembership.Web.BetterRoleProvider, BetterMembership"
         connectionStringName="ExtranetConnection" autoInitialize="true"  />
      <add name="sitecore"
         type="System.Web.Security.SqlRoleProvider"
         connectionStringName="core" applicationName="sitecore" />
    </providers>
  </roleManager>
  <profile defaultProvider="sql" enabled="true" inherits="Sitecore.Security.UserProfile, Sitecore.Kernel">
    <providers>
      <clear />
      <add name="switcher"
         type="Sitecore.Security.SwitchingProfileProvider, Sitecore.Kernel"
      applicationName="sitecore" mappings="switchingProviders/profile" />
      <add name="extranet"
         type="BetterMembership.Web.BetterProfileProvider, BetterMembership"
         membershipProviderName="extranet"  />
      <add name="sitecore"
      type="System.Web.Profile.SqlProfileProvider"
      connectionStringName="core" applicationName="sitecore" />
    </providers>
  </profile>
  ...
</system.web>

With this configuration it's possible to use Sitecore User Manager to administer users, and also possible to use WebSecurity APIs to integrate external logins into the system. When using the WebSecurity it's necessary to use fully qualified usernames including the domain. e.g.

WebSecurity.Login(string.Concat("extranet\",userName), password, persistCookie)

This ensure's that the call is delegated to the correct provider configured for the specified domain.