Monday, March 14, 2016

How to validate old passwords when migrating from Membership provider to ASP.NET Identity in Episerver

Wouldn’t be nice to be able to validate the old passwords when you migrate from Membership provider to ASP.NET Identity? This was one of my headaches in an earlier project, but here is the solution.

Prerequisite 
The example in this post are based on the code from here: http://sveinaandahl.blogspot.no/2015/08/how-to-integrate-aspnet-identity-with.html

Note!
One of the things you should check out first in the original Episerver solution's web.config. Here you will find the hash algorithm type that’s used in the Membership provider. In the example below you can see «HMACSHA512» is used. 

From Web.config:

<membership defaultProvider="SqlServerMembershipProvider" userIsOnlineTimeWindow="10" hashAlgorithmType="HMACSHA512">

The whole clue when you migrate the passwords is to put the hashed password together with the password format, that’s always 1, and the password salt. Then you put it into the Hashed Password field in the new database.

e.g.: aspnet_Membership.Password+'|'+ 1 +'|'+aspnet_Membership.PasswordSalt

In the User Manager code you override the PasswordHasher function. When you get a user that has a password with this type of password format use the original way to encrypt the passwords in Membership provider, or else you can use the base version of PasswordHasher. Use the same hash algorithm type that you used to create in the original solution.

IdentityConfig.cs
Override the Password hasher function.

namespace TestSite
{
    // Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application.
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(ApplicationUserStore store)
            : base(store)
        {
            PasswordHasher = new MyPasswordHasher();
        }
 

MyPasswordHasher.cs
Add a new class called MyPasswordHasher.cs

using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNet.Identity;
 
namespace TestSite
{
    public class MyPasswordHasher : PasswordHasher
    {
        public override string HashPassword(string password)
        {          
            return base.HashPassword(password);
        }
 
        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            var passwordProperties = hashedPassword.Split('|');
            if (passwordProperties.Length != 3)
            {
                return base.VerifyHashedPassword(hashedPassword, providedPassword);
            }
            else
            {
                var passwordHash = passwordProperties[0];
                const int passwordformat = 1;
                var salt = passwordProperties[2];
                if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
                {
                    return PasswordVerificationResult.SuccessRehashNeeded;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
            }
        }
 
        private string EncryptPassword(string pass, int passwordFormat, string salt)
        {
            if (passwordFormat == 0) 
                return pass;
 
            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;
 
            if (passwordFormat == 1)
            {  
                HashAlgorithm hm = HashAlgorithm.Create("HMACSHA512"); 
 
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length; )
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }
            return Convert.ToBase64String(bRet);
        }
 
    }
}

Migration of the database.
Since the database is created by the Entity Framework the first time you run the Episerver application with ASP.NET Identity, I recommend you not to try to use any create tables in the migration script. Since this often end up being a corrupt database for the Entity Framework.

The simple version would be just to do inserts on a existing database. The migration script could look something like the script below. Notice how the password format is set together.

The script below is not well tested, but it gives you an idea how it could work.

INSERT INTO AspNetUsers(Id,Email,EmailConfirmed,PasswordHash,SecurityStamp,
PhoneNumber,PhoneNumberConfirmed,TwoFactorEnabled,LockoutEndDateUtc,LockoutEnabled,
AccessFailedCount,UserName)
SELECT aspnet_Users.UserId,aspnet_Membership.Email,'true',
(aspnet_Membership.Password+'|'+CAST(aspnet_Membership.PasswordFormat as varchar)+'|'+aspnet_Membership.PasswordSalt),
NewID(),NULL,'false','true',aspnet_Membership.LastLockoutDate,'true','0',aspnet_Users.UserName
FROM aspnet_Users
LEFT OUTER JOIN aspnet_Membership ON aspnet_Membership.ApplicationId = aspnet_Users.ApplicationId 
AND aspnet_Users.UserId = aspnet_Membership.UserId;


TIPS
If you are going to run this script several times without getting duplicates you should probably look into using cursor scripts in sql. This way you would avoid inserting a user that's already migrated.

Have fun!

1 comment: