• Publisert
  • 9 min

Implementing a custom membership provider for EPiServer, part 2

A walkthrough of the basics involved in implementing a custom membership provider for EPiServer. Part 2 of 2.

There are a lot of pages out there describing the basics of a .NET custom membership provider, while others describe configuring a ready-made custom provider for EPiServer. Here's a two-part tutorial that combines this info into a practical example.

Part 2 covers the technical side of creating a simple custom membership provider. Jump to part 1 for an overview of the basic concepts.

Disclaimer: The examples are simplified to illustrate the basic concepts. The code/SQL samples may contain bugs and are IN NO WAY to be considered production-ready. 

Note: Examples shown use SQL tables and Stored Procedures similar to the default .NET Membership model. For many developers, RavenDB/MongoDB/Entity Framework/LINQ to SQL/other ORMs will be the preferred choice for database/storage interaction. 

Creating the user table

In this example, we'll store customers in a custom SQL table, which will be almost identical to the default .NET aspnet_Membership table except for a few columns:

Sample user table schema.

Above: Table schema.
See the /SQL folder in the MyMembershipProvider project in the downloadable solution.
Open up SQL Management Studio and manually add a user in your table (since we have not yet implemented a CreateUser method). 

 

Adding a connectionstring to your database/storage

Because this example uses an SQL database, adding a connectionstring is exactly like the default "EPiServerDB" connection string which is added during installation. Modify the string to match your database name, username, password etc. For other storage types, modify your connectionstring accordingly.

<connectionStrings>
<clear />
<add name="EPiServerDB" connectionString="Data Source=MYPC\SQLEXPRESS;Initial Catalog=dbMyEPiServerSite;Integrated Security=False;User ID=dbUserMyEPiServerSite;Password=MyPassword;Connect Timeout=10" providerName="System.Data.SqlClient" />
<add name="MySqlConnection" connectionString="Data Source=MYPC\SQLEXPRESS;Initial Catalog=dbMyEPiServerSite;Integrated Security=False;User ID=dbUserMyEPiServerSite;Password=MyPassword;Connect Timeout=10" providerName="System.Data.SqlClient" />
</connectionStrings> 

Creating the membership provider class

Create a new class that inherits from System.Web.Security.MembershipProvider:

using System.Web.Security;

namespace Test
{   
public class MyMembershipProvider : MembershipProvider  
{
         public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(name, config); // Required to correctly initialize the custom provider
}
 
// Provider methods go here
}
}

When inheriting from System.Web.Security.MembershipProvider, we must implement a long list of method/attribute overrides. For this example, we'll add the overrides, but the ones we don't need yet will just "throw new NotImplementedException();".

Full list and descriptions of the default .NET Membership methods can be found at http://msdn.microsoft.com/en-us/library/f1kyba5e.aspx.


Adding a helper class

You'll probably want to add some sort of helper class to keep the "dirty work" away from the MyMembershipProvider class, so add a new class file MyMembershipHelper.cs:

using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Web.Security;

namespace MyMembershipProvider
{   
class MyMembershipHelper   
{
// "Dirty work" methods go here
}

 

Adding provider methods

We'll add some methods which enable EPiServer to find user accounts in your custom table. Add the following methods in MyMembershipProvider.cs:

public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
MyMembershipHelper helper = new MyMembershipHelper();
MembershipUserCollection users = helper.FindUsersByName(usernameToMatch, pageIndex, pageSize);
totalRecords = users != null ? users.Count : 0;
return users;
}

public override MembershipUser GetUser(string username, bool userIsOnline)       
{           
MyMembershipHelper helper = new MyMembershipHelper ();           
MembershipUser user = helper.GetUserByName(username);           
return user ?? null;       
}

Next, add the FindUsersByName and GetUserByName methods in MyMembershipHelper.cs:

public MembershipUserCollection FindUsersByName(string userNameToMatch, int pageIndex, int pageSize)
{
MembershipUser user = null;
try
{
string mySqlConnectionString = ConfigurationManager.ConnectionStrings["MySqlConnection"].ToString(); // from connectionStrings.config
SqlConnection dbConnection = new SqlConnection(mySqlConnectionString);
SqlCommand cmdFindUsers = new SqlCommand("sp_FindUsersByName", dbConnection);
cmdFindUsers.CommandType = CommandType.StoredProcedure;
cmdFindUsers.Parameters.AddWithValue("@UserNameToMatch", "%" + userNameToMatch.Replace("%", "") + "%"); // SQL wildcard "%"

// Open connection and read from db
dbConnection.Open();
SqlDataReader sqlReader = cmdFindUsers.ExecuteReader();

// Retrieve users, if existing
MembershipUserCollection userColl = new MembershipUserCollection();
while (sqlReader.Read())
{
string providerName = "MyMembershipProvider"; // ProviderName could be stored e.g. in AppSettings
string userName = sqlReader["UserName"] as string;
string customerId = sqlReader["CustomerId"] as string;
string email = sqlReader["Email"] as string;
bool isApproved = (bool)sqlReader["IsApproved"];
bool isLockedOut = (bool)sqlReader["IsLockedOut"];
DateTime lastLoginDate = (DateTime)sqlReader["LastLoginDate"];
DateTime createDate = (DateTime)sqlReader["CreateDate"];
DateTime lastPasswordChangedDate = (DateTime)sqlReader["LastPasswordChangedDate"];
DateTime lastLockedOutDate = (DateTime)sqlReader["LastLockedoutDate"];

user = new MembershipUser(
providerName, userName, customerId, email, null, null, isApproved, isLockedOut, createDate, lastLoginDate, lastLoginDate, lastPasswordChangedDate, lastLockedOutDate);
userColl.Add(user);
}

// Close connections
sqlReader.Close();
dbConnection.Close();

return userColl;

catch (Exception ex)
{
return null;
}
}


public MembershipUser GetUserByName(string username)       
{           
MembershipUser user = null;           
try           
{               
string mySqlConnectionString = ConfigurationManager.ConnectionStrings["MySqlConnection"].ToString(); // a valid connection from connectionStrings.config
SqlConnection dbConnection = new SqlConnection(mySqlConnectionString); 
SqlCommand cmdGetUser = new SqlCommand("sp_GetUserByName", dbConnection); // Defining which Stored Procedure to use               

cmdGetUser.CommandType = CommandType.StoredProcedure;
cmdGetUser.Parameters.Add("@UserName", SqlDbType.VarChar, 128);               
cmdGetUser.Parameters["@UserName"].Value = username.ToLower();
               
// Open connection and read from db               
dbConnection.Open();               
SqlDataReader sqlReader = cmdGetUser.ExecuteReader();
               
// Retrieve user data               
while (sqlReader.Read())               
{                   
string providerName = sqlReader["ApplicationName"] as string;
string userName = sqlReader["UserName"] as string;                   
string customerId = sqlReader["CustomerId"] as string;                   
string email = sqlReader["Email"] as string;                   
bool isApproved = (bool)sqlReader["IsApproved"];                   
bool isLockedOut = (bool)sqlReader["IsLockedOut"];                   
DateTime lastLoginDate = (DateTime)sqlReader["LastLoginDate"];                   
DateTime createDate = (DateTime)sqlReader["CreateDate"];                   
DateTime lastPasswordChangedDate = (DateTime)sqlReader["LastPasswordChangedDate"];                   
DateTime lastLockedOutDate = (DateTime)sqlReader["LastLockedoutDate"];
                   
user = new MembershipUser(                       
providerName, userName, customerId, email, null, null, isApproved, isLockedOut, createDate, lastLoginDate, lastLoginDate, lastPasswordChangedDate, lastLockedOutDate);
}
// Close connections               
sqlReader.Close();               
dbConnection.Close();
return user;           
}           
catch (Exception ex)           
{               
return null;           
}       
}

Adding stored procedures

Add the "sp_FindUsersByName" and "sp_GetUserByName" Stored Procedures to your database.
See the /SQL folder in the MyMembershipProvider project in the downloadable solution.

Configuring your custom provider with EPiServer

Build your MyMembershipProvider project, then reference this assembly in your EPiServer website project.

Next, open Web.config in your EPiServer project and configure the MultiplexingMembershipProvider to use both SqlMembershipProvider and your custom MyMembershipProvider:

<system.web>
<membership defaultProvider="MultiplexingMembershipProvider" userIsOnlineTimeWindow="10">     
<providers>       
<clear /> 
<add name="MultiplexingMembershipProvider" type="EPiServer.Security.MultiplexingMembershipProvider, EPiServer" provider1="SqlServerMembershipProvider" provider2="MyMembershipProvider" /> 
<add name="SqlServerMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="EPiServerDB" requiresQuestionAndAnswer="false" applicationName="EPiServerSample" requiresUniqueEmail="true" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" passwordStrengthRegularExpression="" />
<add name="MyMembershipProvider" type="MyMembershipProvider.MyMembershipProvider, MyMembershipProvider, Version=1.0.0.0, Culture=neutral"  connectionStringName="MySqlConnection" applicationName="MyEPiServerSite" maxInvalidPasswordAttempts="3" passwordAttemptWindow="10" requiresQuestionAndAnswer="false"/>
</providers>
</membership>
< ... >
</system.web> 

<add name="SqlServerMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="EPiServerDB" requiresQuestionAndAnswer="false" applicationName="EPiServerSample" requiresUniqueEmail="true" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" passwordStrengthRegularExpression="" />

Note: If you set your role provider config to either MultiPlexingRoleProvider or SqlRoleProvider, your custom membership provider can use the built-in role system right away. This means that you can create SQL-based groups in admin mode, and assign users from your custom provider to these groups, so you get the benefit of EPiServer's built-in access rights system.
The way this works is that a copy of your custom user will be created in the dbo.aspnet_Users table, along with entries in the dbo.aspnet_UserInRoles and dbo.aspnet_Profile tables. Your custom user will still show up as registered with your custom MembershipProvider, though.

Of course, you could also code your own custom RoleProvider (which is not covered by this blog post).

Testing your custom provider

Go to admin mode in your EPiServer site and try the "Search Users/Group" feature. When searching using the "Name" field, your provider should kick in and your FindUsersByName method should run and retrieve any matching users from your custom user table:

Searching users via the custom provider

Editing a user via the custom provider


So, these are the basics of implementing your own custom membership provider. If you have made it this far, go ahead and expand your provider with more provider methods.

For tips on which methods to implement and how they should behave:

  • use IntelliSense to see which methods are available in the System.Web.Security.MembershipProvider namespace
  • use Reflector on the System.Web.Security namespace to see what the methods do
  • take a look at the .NET membership stored procedures in the database to see how they interact with the user tables

It's recommended that a membership provider integrated with EPiServer have at least the following methods:

  • FindUsersByName - used when searching for users by their username
  • FindUsersByEmail - used when searching for users by their email 
  • GetAllUsers - used when wildcard searching for users
  • GetUser - used to retrieve a specific user, usually before an update/delete operation
  • ChangePassword/ResetPassword - used to change/reset the password for a specific user
  • UpdateUser - used to update a specific user, e.g. email/name/password, dates for lastLogin, lastFailedPasswordAttempt, lastLockout etc
  • ValidateUser - used for login scenarios, comparing user input against the password stored in the user table
  • DeleteUser - used to delete a specific user
  • UnlockUser - used to remove the IsLockedOut flag for a specific user (if implemented)

Download a sample provider

Code examples can be found in this sample custom membership provider project.

References

For more information, check out: