Friday 27 February 2015

Using Membership with Sitecore

Sitecore's authentication and user management uses asp.net membership with the membership schema in the Core database.  To use authentication in your own sites you can use this membership system yourself, or if you have your own existing membership database you want to use, or you want to implement membership on your site and don't want to use a separate database rather than keeping your users in the Core database, then it is possible to configure Sitecore to implement multiple asp.net membership databases.

Introduction

This article is going to cover how you integrate asp.net Membership with Sitecore.  It isn't a tutorial on Membership itself or how to configure and use it from scratch.  I am assuming you are already familiar with Membership and how it is normally configured and used.

In this article when I refer to "domain" I mean security domain, not internet domain or the domain of your website (eg blogger.com).

Using Sitecore's Membership provider and database with your own users


This is how Sitecore is configured out of the box;

<membership defaultProvider="sitecore" hashAlgorithmType="SHA1">
  <providers>
    <add name="sitecore" type="Sitecore.Security.SitecoreMembershipProvider, Sitecore.Kernel" realProviderName="sql" providerWildcard="%" raiseEvents="true" />
    <add name="sql" type="System.Web.Security.SqlMembershipProvider" connectionStringName="core" applicationName="sitecore" minRequiredPasswordLength="1" minRequiredNonalphanumericCharacters="0" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="256" />

The "sql" membership provider is the standard membership configuration, however you might notice that the default provider is set to "sitecore", and the "sitecore" provider links to the "sql" provider via the "realProviderName" attribute.  So unlike normal Membership where the provider you want to use is configured directly, the provider in use is the "sitecore" provider and that provider is going to work out the actual provider to use.  The reasons behind this abstraction will become clear later on.

Sitecore also expands on the Membership idea by introducing the concept of "domains" in an attempt to mimic the Windows authentication system.  Sitecore prefixes the username with the domain so that it can restrict certain users to certain sites.  For example you wouldn't want someone who has registered on your front end site to be able to use their credentials to access the Sitecore CMS.  The domain system keeps user accounts sandboxed.

The domain for a site is configured via the site node.

<sites>
  <site name="shell" virtualFolder="/sitecore/shell" physicalFolder="/sitecore/shell" rootPath="/sitecore/content" domain="sitecore" ... />

As you can see, the domain for the CMS is "sitecore".  If you log into the CMS as User1, looking in the membership table you'll see that your user is actually sitecore\User1. When you log in, Sitecore prefixes the username you supply with the domain of the site you are accessing.  Each domain you want to use has to be configured in the domains.config file

\App_Config\Security\Domains.config

Using membership for your site users

You should maintain your site users in their own domain to keep them separate, and for this document we'll be using a domain called MyDomain.

First step is to add the domain to domains.config

<domain name="MyDomain" ensureAnonymousUser="false" />

The ensureAnonymousUser=false setting means that you don't need an actual "anonymous" account generated.  When non-authenticated people are using your site they will be listed as MyDomain\Anonymous.  If that account doesn't exist they will effectively have a virtual account.  If you set this setting to true then Sitecore will create an anonymous account in your membership tables if one does not exist, using a random password.

Second step is to attach this domain to your site in the site config

<site hostName="localhost"
      name="website"
      domain="MyDomain"

In sitecore when you create a user via User Manager it will now have "MyDomain" listed as a possible domain, so create a test user in that domain.

Create a login page

As the concept of prefixing accounts isn't native to Membership, but a Sitecore idea, when you log people in you have to append the prefix yourself.  Below is the code to do this if you are using the in-built Membership Login control.
<asp:Login ID="SCLogin" runat="server" OnLoggingIn="SCLogin_LoggingIn" OnLoginError="SCLogin_LoginError">

Code:
namespace MyNamespace
{
    public partial class Login : System.Web.UI.Page
    {
        private string _usernameAsEntered = String.Empty;

        protected void SCLogin_LoggingIn(object sender, LoginCancelEventArgs e)
        {
            string domainUser = Sitecore.Context.Domain.GetFullName(SCLogin.UserName);

            if (System.Web.Security.Membership.GetUser(domainUser) != null)
            {
                _usernameAsEntered = SCLogin.UserName;
                SCLogin.UserName = domainUser; 
            }
        }

        protected void SCLogin_LoginError(object sender, EventArgs e)
        {
            SCLogin.UserName = _usernameAsEntered;
        }
    }
}

As you can see, Sitecore provides a GetFullName helper function to prefix the correct domain.  It gets the domain to use from the site configuration we did in the previous step.  If you are using your own login code then you won't be hooking into these events, this is just basic sample code, however you will still need to automatically prefix the domain using the relevant code above.

At this point you will now have a working login with all accounts held in Sitecore's Membership system, with separate domains for customer accounts and CMS accounts.

Denying anonymous users

If you want to force users to login before accessing your site, or before accessing certain pages, folders etc, then this can all be done using standard Membership configuration.

<system.web>
  <authorization>
    <deny users="MyDomain\Anonymous"/>
  </authorization>
</system.web>

The above will deny access to any anonymous members of MyDomain.  The above configuration will trigger the standard Membership behaviour of forcing the user to the login page, and then redirecting them back to the previous page after a successful login.  You can configure your login page in the authentication section.

<authentication mode="Forms">
  <forms name=".ASPXAUTH" cookieless="UseCookies" loginUrl="/login.aspx"/>
</authentication>

As well as using the Membership configuration options, you can still use the standard Membership API to control who has access to what, who has access to what features etc.  All of the role based mechanisms and profiles still work as normal.

Configuring multiple providers

If you want to keep your customer accounts in a separate database then there are some additional configuration steps that are required.  Below we will configure the solution to use Sitecore's Membership database for logging into the CMS, and your own Membership database for customer logins.  If you have created any test accounts in the MyDomain domain when testing the above, then delete them.

Configure your custom provider

First add a connection string for your existing, or new, Membership database.

<connectionStrings>
    <add name="MembershipConnection" connectionString="Database=MyMembership;Server=(local);uid=sa;pwd=;" providerName="System.Data.SqlClient"/>

Add the relevant Membership. Role and Profile providers in the web.config

<membership defaultProvider="sitecore" hashAlgorithmType="SHA1">
  <providers>
    <clear />
    <add name="sitecore" type="Sitecore.Security.SitecoreMembershipProvider, Sitecore.Kernel" realProviderName="sql" providerWildcard="%" raiseEvents="true" />
    <add name="sql" type="System.Web.Security.SqlMembershipProvider" connectionStringName="core" applicationName="sitecore" minRequiredPasswordLength="1" minRequiredNonalphanumericCharacters="0" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="256" />
    <add name="switcher" type="Sitecore.Security.SwitchingMembershipProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/membership" />
    <add name="MyMembershipProvider" connectionStringName="MembershipConnection" applicationName="MyApplication" enablePasswordRetrieval="true" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" passwordFormat="Clear" minRequiredNonalphanumericCharacters="0" type="System.Web.Security.SqlMembershipProvider"/>  
  </providers>
</membership>
<roleManager defaultProvider="sitecore" enabled="true">
  <providers>
    <clear />
    <add name="sitecore" type="Sitecore.Security.SitecoreRoleProvider, Sitecore.Kernel" realProviderName="sql" raiseEvents="true" />
    <add name="sql" type="System.Web.Security.SqlRoleProvider" connectionStringName="core" applicationName="sitecore" />
    <add name="switcher" type="Sitecore.Security.SwitchingRoleProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/roleManager" />
    <add name="MyRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="MembershipConnection" applicationName="MyApplication" />
  </providers>
</roleManager>
<profile defaultProvider="sql" enabled="true" inherits="Sitecore.Security.UserProfile, Sitecore.Kernel">
  <providers>
     <clear />
     <add name="sql" type="System.Web.Profile.SqlProfileProvider" connectionStringName="core" applicationName="sitecore" />
     <add name="switcher" type="Sitecore.Security.SwitchingProfileProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/profile" />
     <add name="MyProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="MembershipConnection" applicationName="MyApplication" />
  </providers>
  <properties>
     <clear />
     <add type="System.String" name="SC_UserData" />
  </properties>
</profile>
The provider is given a name (MyMembershipProvider), the name of the connection string to use and the application name your logins are registered against in the Membership database.  I am using "MyApplication" as the Membership application name, this is something you have to configure in your membership tables.

Next configure the "sitecore" membership provider and the "sitecore" role provider to use "switcher" instead of "sql".  This is done by updating the realProviderName attribute.  Remember to make this change on the role provider too.  The updated configuration should look like this;

<add name="sitecore" type="Sitecore.Security.SitecoreMembershipProvider, Sitecore.Kernel" realProviderName="switcher" providerWildcard="%" raiseEvents="true" />

This tells Sitecore to use the switcher to work out which provider should be used in any given situation.  For the profile node set the defaultProvider to "switcher".

<profile defaultProvider="switcher" enabled="true" inherits="Sitecore.Security.UserProfile, Sitecore.Kernel">

The finished configuration should look something like this

<membership defaultProvider="sitecore" hashAlgorithmType="SHA1">
  <providers>
    <clear />
    <add name="sitecore" type="Sitecore.Security.SitecoreMembershipProvider, Sitecore.Kernel" realProviderName="switcher" providerWildcard="%" raiseEvents="true" />
    <add name="sql" type="System.Web.Security.SqlMembershipProvider" connectionStringName="core" applicationName="sitecore" minRequiredPasswordLength="1" minRequiredNonalphanumericCharacters="0" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="256" />
    <add name="switcher" type="Sitecore.Security.SwitchingMembershipProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/membership" />
    <add name="MyMembershipProvider" connectionStringName="MembershipConnection" applicationName="MyApplication" enablePasswordRetrieval="true" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" passwordFormat="Clear" minRequiredNonalphanumericCharacters="0" type="System.Web.Security.SqlMembershipProvider"/>  
  </providers>
</membership>
<roleManager defaultProvider="sitecore" enabled="true">
  <providers>
    <clear />
    <add name="sitecore" type="Sitecore.Security.SitecoreRoleProvider, Sitecore.Kernel" realProviderName="switcher" raiseEvents="true" />
    <add name="sql" type="System.Web.Security.SqlRoleProvider" connectionStringName="core" applicationName="sitecore" />
    <add name="switcher" type="Sitecore.Security.SwitchingRoleProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/roleManager" />
    <add name="MyRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="MembershipConnection" applicationName="MyApplication" />
  </providers>
</roleManager>
<profile defaultProvider="switcher" enabled="true" inherits="Sitecore.Security.UserProfile, Sitecore.Kernel">
  <providers>
     <clear />
     <add name="sql" type="System.Web.Profile.SqlProfileProvider" connectionStringName="core" applicationName="sitecore" />
     <add name="switcher" type="Sitecore.Security.SwitchingProfileProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/profile" />
     <add name="MyProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="MembershipConnection" applicationName="MyApplication" />
  </providers>
  <properties>
     <clear />
     <add type="System.String" name="SC_UserData" />
  </properties>
</profile>

The switcher is going to use the configured domain for the site the user is browsing to decide which membership provider to use.  This allows it to use the default provider for CMS users (the admin site uses the "Sitecore" domain), and your own provider for your site which you have configured to use the "MyDomain" domain. To make this work we need to configure the switcher so that it knows what provider handles what domain.  Under the "switchingProviders" section the default sql provider that Sitecore uses is already configured so add a provider setting for your membership, roles and profile.

<switchingProviders>
  <membership>
    <provider providerName="sql" storeFullNames="true" wildcard="%" domains="*" />
    <provider providerName="MyMembershipProvider" storeFullNames="false" wildcard="%" domains="MyDomain"/>
  </membership>

  <roleManager>
    <provider providerName="sql" storeFullNames="true" wildcard="%" domains="*" ignoredUserDomains="" allowedUserDomains="" />
    <provider providerName="MyRoleProvider" storeFullNames="false" wildcard="%" domains="MyDomain"/>
  </roleManager>

  <profile>
    <provider providerName="sql" storeFullNames="true" wildcard="%" domains="*" ignoredDomains="" />
    <provider providerName="MyProfileProvider" storeFullNames="false" wildcard="%" domains="MyDomain"/>  
  </profile>
</switchingProviders>

The "domains" attribute lets Sitecore know what domains to use your provider for.  We have set this to MyDomain, so when a customer logs in on our site, as the domain of the site is MyDomain it will use the MyMembershipProvider to handle the authentication.  However if someone tries to login to the CMS, then it will fall back to the "sql" provider as that is set to handle all domains.

The "wildcard" attribute is set to whatever the underlying data access system uses as a wildcard.  We know our MyMembershipProvider is a SQL Server based provider, and the SQL wildcard is "%".

The "storeFullNames" setting is important.  As the idea of prefixing usernames with domains is not a standard Membership practice, it is likely that if you are using an existing Membership system that none of the usernames in your existing system have domain prefixes.  Setting storeFullNames to false lets the Sitecore security API know that the domain isn't stored with the username, so it has to be added or removed on the fly as needed.  For example if I use User Manager to create a user in the MyDomain domain called User1 then that user will show as MyDomain\User1, however if you look at the Membership tables you'll see the username is just User1.  Likewise if I get, or authenticate the user MyDomain\User1 then it will successfully find the user even though the user's name in Membership is just User1.

Managing Membership via Sitecore

If you use the User Manager in Sitecore you will see the user accounts from all of your configured providers are managed via the single User Manager.  You can create, amend and delete these users with Sitecore seamlessly representing all of your users as if they came from a single database.

Using Membership APIs

Any time you use the in-built Membership APIs, or Sitecore's APIs, the appropriate provider is always used for you based on the "domain" setting of the site currently being browsed, you don't have to implement any special coding.

1 comment: