Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to migrate SimpleMembership user data to ASPNET Core Identity

I have an application that I originally built when I was young and foolish and its authentication is set up using the SimpleMembership framework, with all user data contained in a webpages_Membership table. I am very interested in rebuilding my backend as an AspNetCore Web API with AspNetCore Identity via SQL Server, without losing user information.

I've had good luck with coming up with SQL scripts to move everything into an AspNetUsers table in preparation for working with Identity instead of SimpleMembership, but where I'm running into an issue is password hashing. I gather from articles like this and this that my best bet is to override PasswordHasher<IdentityUser> to duplicate the SimpleMembership crypto flow, and then rehash passwords as they come in to gradually migrate the database.

The trouble is I can't find out how to achieve this flow duplication in .NET Core. The latter article linked above states that the SimpleMembership flow is achieved via the System.Web.Helpers.Crypto package, which does not appear to exist in the .NET Core library, and I can't figure out if its implementation is documented anywhere. (Its MSDN documentation says that it is using RFC2898 hashing but I don't know enough about crypto to know if that's enough to go on by itself. This isn't my area of expertise. :( )

Any insight on how to approach this would be much appreciated. Thank you!

like image 539
rosalindwills Avatar asked Oct 01 '17 02:10

rosalindwills


People also ask

How do I migrate to .NET core?

You can migrate your old project to the Core project using the 'dotnet migrate; command, which migrates the project. json and any other files that are required by the web application. The dotnet migrate command will not change your code in any way.

What is AspNet core identity?

ASP.NET Core Identity: Is an API that supports user interface (UI) login functionality. Manages users, passwords, profile data, roles, claims, tokens, email confirmation, and more.


1 Answers

For anyone else who may be running into the same trouble -- I was able to find a copy of the System.Web.Helpers.Crypto code somewhere on GitHub, and more or less copied it into a custom password hasher class thus:

public class CustomPasswordHasher : PasswordHasher<IdentityUser>
{
    public override PasswordVerificationResult VerifyHashedPassword(IdentityUser user, string hashedPassword,
        string providedPassword)
    {
        var isValidPasswordWithLegacyHash = VerifyHashedPassword(hashedPassword, providedPassword);
        return isValidPasswordWithLegacyHash
            ? PasswordVerificationResult.SuccessRehashNeeded
            : base.VerifyHashedPassword(user, hashedPassword, providedPassword);
    }

    private const int _pbkdf2IterCount = 1000;
    private const int _pbkdf2SubkeyLength = 256 / 8;
    private const int _saltSize = 128 / 8;

    public static bool VerifyHashedPassword(string hashedPassword, string password)
    {
        //Checks password using legacy hashing from System.Web.Helpers.Crypto
        var hashedPasswordBytes = Convert.FromBase64String(hashedPassword);
        if (hashedPasswordBytes.Length != (1 + _saltSize + _pbkdf2SubkeyLength) || hashedPasswordBytes[0] != 0x00)
        {
            return false;
        }
        var salt = new byte[_saltSize];
        Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, _saltSize);
        var storedSubkey = new byte[_pbkdf2SubkeyLength];
        Buffer.BlockCopy(hashedPasswordBytes, 1 + _saltSize, storedSubkey, 0, _pbkdf2SubkeyLength);
        byte[] generatedSubkey;
        using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, _pbkdf2IterCount))
        {
            generatedSubkey = deriveBytes.GetBytes(_pbkdf2SubkeyLength);
        }
        return ByteArraysEqual(storedSubkey, generatedSubkey);
    }

    internal static string BinaryToHex(byte[] data)
    {
        var hex = new char[data.Length * 2];
        for (var iter = 0; iter < data.Length; iter++)
        {
            var hexChar = (byte) (data[iter] >> 4);
            hex[iter * 2] = (char) (hexChar > 9 ? hexChar + 0x37 : hexChar + 0x30);
            hexChar = (byte) (data[iter] & 0xF);
            hex[iter * 2 + 1] = (char) (hexChar > 9 ? hexChar + 0x37 : hexChar + 0x30);
        }
        return new string(hex);
    }

    [MethodImpl(MethodImplOptions.NoOptimization)]
    private static bool ByteArraysEqual(byte[] a, byte[] b)
    {
        if (ReferenceEquals(a, b))
        {
            return true;
        }
        if (a == null || b == null || a.Length != b.Length)
        {
            return false;
        }
        var areSame = true;
        for (var i = 0; i < a.Length; i++)
        {
            areSame &= (a[i] == b[i]);
        }
        return areSame;
    }
}

This class overrides VerifyHashedPassword and checks whether the user's provided password works with the old Crypto hashing; if so, the method returns PasswordVerificationResult.SuccessRehashNeeded. Otherwise, it passes the password off to the base class's method and verifies it as normal with the .NET Core hashing behavior.

You can then instruct your UserManager to use this password hasher instead of the default by including it in your dependency injection configuration in Startup.cs:

public class Startup 
{
...
    public void ConfigureServices(IServiceCollection services)
    {
    ...
        services.AddScoped<IPasswordHasher<IdentityUser>, CustomPasswordHasher>();
    }
...
} 

My eventual intention is to have my controller trigger a rehash of the user's password when that SuccessRehashNeeded result is returned, allowing a gradual migration of all users to the correct hashing schema.

like image 107
rosalindwills Avatar answered Oct 04 '22 01:10

rosalindwills