A Custom Membership Provider in Sitecore (Part 2)

In order to implement a Custom Membership Provider, there is a minimum number of methods that need to be supported, essentially these are a method to get a given user and validate a user. This example code assumes an appropriate database (in this case “MPEntities”), which has fields as required by the Membership provider framework.

Minimal Read-Only Membership Provider
The GetUser() Method

public override MembershipUser GetUser(string username, bool userIsOnline)
{
    MembershipUser u = null;
    using (var db = new MPEntities())
    {
        var result = db.Users
                    .Where(user => user.Username == username);

        if (result.Count() != 0)
        {
            var dbuser = result.FirstOrDefault();

            if (dbuser != null)
            {
                string dbUsername = dbuser.Username;
                int providerUserKey = (int)dbuser.Id;
                string comment = dbuser.Comments;
                string email = dbuser.EmailAddress;
                DateTime creationDate = dbuser.CreatedDateTime;
                DateTime lastLoginDate = dbuser.LastLoginDateTime;
                DateTime lastActivityDate = dbuser.LastActivityDateTime;
                DateTime lastPasswordChangedDate = dbuser.LastPasswordChangeDateTime;

                u = new MembershipUser("MembershipProvider",
                                                          dbUsername,
                                                          providerUserKey,
                                                          email,
                                                          "",
                                                          comment,
                                                          true,
                                                          false,
                                                          creationDate,
                                                          lastLoginDate,
                                                          lastActivityDate,
                                                          lastPasswordChangedDate,
                                                          DateTime.MinValue);

            }
        }
        if (u == null)
        {
            MembershipProvider provider = Membership.Providers["sql"];
            if (provider != null)
            {
                u = provider.GetUser(username, false);
            }
        }
    }

    return u;
}

The minimum implementation also requires a method to validate a user given a username and a password. Really this can be any function that returns true if a given user is valid for the system with the credentials (the password) provided. The hashing or encryption used for this are entirely the choice of the implementer.

The ValidateUser() Method

public override bool ValidateUser(string username, string password)
{
    using (var db = new MPEntities())
    {
        var result = db.Users
                     .Where(user => user.Username == username);

        if (result.Count() != 0)
        {
            var dbuser = result.First();

            return dbuser.Password == HashPassword(password);
        }
        else
        {
            return false;
        }
    }
}

Here is a simple hasing method for use in my ValidateUser method:

private static string HashPassword(string plaintextPassword)
{
    byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintextPassword);
    // insert salting process here if implemented
    byte[] saltedBytes = plaintextBytes;

    var hasher = new SHA512Managed();
    byte[] hashedBytes = hasher.ComputeHash(saltedBytes);

    return Convert.ToBase64String(hashedBytes);
}

The minimum implementation of a Membership provider does NOT have to implement methods to Create Users or get them as a collection, or many other functions that may be needed, but these methods, again, can be implemented with the entity framework as required.

Additional Methods
The CreateUser() Method

public override MembershipUser CreateUser(string username, string password,
string email, string passwordQuestion, string passwordAnswer, bool isApproved,
 object providerUserKey, out MembershipCreateStatus status)
{
    using (var db = new MPEntities())
    {
        var user = new User
        {
            Username = username,
            Password = CreatePasswordHash(password, ""),
            CreatedDateTime = DateTime.Now,
        };

        db.AddToUsers(user);
        db.SaveChanges();

        status = MembershipCreateStatus.Success;

        return GetUser(username);
    }
}
private static string CreatePasswordHash(string pwd, string salt)
{
    string saltAndPwd = String.Concat(pwd, salt);
    string hashedPwd =
        FormsAuthentication.HashPasswordForStoringInConfigFile(
        saltAndPwd, "sha1");

    return hashedPwd;
}

The GetAllUsers() Method

This method supplies details of all the Users known to the system (and is used under the hood by Sitecore’s User Manager). As you can see this method returns both the users in the database covered by this provider and also the Sitecore system users, which are stored in the Sitecore “core” database. Access to the Sitecore built-in Users and roles can be made via the “sql” provider, as below.

public MembershipUserCollection GetAllUsers(int pageIndex, int pageSize,
 out int totalRecords)
{
    var membershipCollection = new MembershipUserCollection();
    int sysRecords = 0;
    int total = 0;
    using (var db = new MPEntities())
    {
        var selected = db.Users
                          .OrderBy(user => user.Username)
                          .Skip(pageIndex * pageSize)
                          .Take(pageSize).ToList();
        total = db.Users.Count();
        foreach (var user in selected)
        {
            membershipCollection.Add(new MembershipUser("MembershipProvider",
                                         PrependDomain(user.Username),
                                         user.EntityId,
                                         user.EmailAddress,
                                         "",
                                         user.Comments,
                                         true,
                                         false,
                                         user.CreatedDateTime,
                                         user.LastLoginDateTime,
                                         user.LastActivityDateTime,
                                         user.LastPasswordChangeDateTime,
                                         DateTime.MinValue));
        }
    }
    MembershipProvider provider = Membership.Providers["sql"];
    if (provider != null)
    {
        provider.GetAllUsers(0, 15, out sysRecords);
    }

    totalRecords = sysRecords + total;

    return membershipCollection;
}

The GetUsersByName() Method

This method can be used to search for Users, returning a collection object. The index and page size parameters allow search within sub-ranges, and iterative searches:

public MembershipUserCollection GetUsersByName(string usernameToMatch,
 int pageIndex, int pageSize, out int totalRecords)
{
    var membershipCollection = new MembershipUserCollection();

    usernameToMatch = usernameToMatch.Replace("%", "");

    using (var db = new MPEntities())
    {
        var selected = db.Users
            .Where(user => user.Username.Contains(usernameToMatch))
            .OrderBy(user => user.Username)
            .Skip(pageIndex * pageSize)
            .Take(pageSize).ToList();

        foreach (var user in selected)
        {
            membershipCollection.Add(new MembershipUser("MembershipProvider",
                                          PrependDomain(user.Username),
                                          user.EntityId,
                                          user.EmailAddress,
                                          "",
                                          user.Comments,
                                          true,
                                          false,
                                          user.CreatedDateTime,
                                          user.LastLoginDateTime,
                                          user.LastActivityDateTime,
                                          user.LastPasswordChangeDateTime,
                                          DateTime.MinValue));
        }

        totalRecords = selected.Count();
    }

    return membershipCollection;
}