Password (hash) doesn't match when re-using existing Microsoft Identity user tables

As per the documentation located: https://github.com/aspnet/Identity/blob/a8ba99bc5b11c5c48fc31b9b0532c0d6791efdc8/src/Microsoft.AspNetCore.Identity/PasswordHasher.cs

    /* =======================
     * HASHED PASSWORD FORMATS
     * =======================
     * 
     * Version 2:
     * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
     * (See also: SDL crypto guidelines v5.1, Part III)
     * Format: { 0x00, salt, subkey }
     *
     * Version 3:
     * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
     * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
     * (All UInt32s are stored big-endian.)
     */

At one point, identity used a different hashing algorithm - maybe it's using the version 2 format in one, and the version 3 format in the other?

The constructor of the class takes in options, you can try tweaking that to get the correct hash?

public PasswordHasher(IOptions<PasswordHasherOptions> optionsAccessor = null)

EDIT:

I found the Identity v2.0 source here: https://aspnetidentity.codeplex.com/ and git repo: https://git01.codeplex.com/aspnetidentity

Looking through source, you come across its hashing method.

Crypto.HashPassword.cs

public static string HashPassword(string password)
    {
        if (password == null)
        {
            throw new ArgumentNullException("password");
        }

        // Produce a version 0 (see comment above) text hash.
        byte[] salt;
        byte[] subkey;
        using (var deriveBytes = new Rfc2898DeriveBytes(password, SaltSize, PBKDF2IterCount))
        {
            salt = deriveBytes.Salt;
            subkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
        }

        var outputBytes = new byte[1 + SaltSize + PBKDF2SubkeyLength];
        Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
        Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, PBKDF2SubkeyLength);
        return Convert.ToBase64String(outputBytes);
    }

Compared to v2 in aspnet identity core:

    private static byte[] HashPasswordV2(string password, RandomNumberGenerator rng)
    {
        const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.HMACSHA1; // default for Rfc2898DeriveBytes
        const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
        const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits
        const int SaltSize = 128 / 8; // 128 bits

        // Produce a version 2 (see comment above) text hash.
        byte[] salt = new byte[SaltSize];
        rng.GetBytes(salt);
        byte[] subkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength);

        var outputBytes = new byte[1 + SaltSize + Pbkdf2SubkeyLength];
        outputBytes[0] = 0x00; // format marker
        Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
        Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, Pbkdf2SubkeyLength);
        return outputBytes;
    }

Identity v2 hashing and identity core v2 hashing seem pretty similar, now compared to identity core v3 hash:

    private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
    {
        // Produce a version 3 (see comment above) text hash.
        byte[] salt = new byte[saltSize];
        rng.GetBytes(salt);
        byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);

        var outputBytes = new byte[13 + salt.Length + subkey.Length];
        outputBytes[0] = 0x01; // format marker
        WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
        WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
        WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
        Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
        Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
        return outputBytes;
    }

I'm not going to pretend to understand what's going on in these methods, but from identity v2, and identity core, we went from a parameterless constructor to one that takes in configuration options. V2 uses SHA1, V3 uses SHA256 (among other things).

It looks like identity core by default would hash using V3 method, which did not exist in the older version of identity - which would be the cause of your problem.

https://github.com/aspnet/Identity/blob/a8ba99bc5b11c5c48fc31b9b0532c0d6791efdc8/src/Microsoft.AspNetCore.Identity/PasswordHasherOptions.cs

Note in the above source, V3 is used as the default.

    /// <summary>
    /// Gets or sets the compatibility mode used when hashing passwords.
    /// </summary>
    /// <value>
    /// The compatibility mode used when hashing passwords.
    /// </value>
    /// <remarks>
    /// The default compatibility mode is 'ASP.NET Identity version 3'.
    /// </remarks>
    public PasswordHasherCompatibilityMode CompatibilityMode { get; set; } = PasswordHasherCompatibilityMode.IdentityV3;

Unfortunately, that looks like it means your passwords that were hashed in identity core cannot be hashed the same in an older version of identity, as that older method was not implemented. Perhaps you could create your own mimicking what was done in v3?


To follow up Kritner's excellent answer with a TLDR:

TLDR if you are migrating from Microsoft.AspNet.Identity.Core to Microsoft.AspNetCore.Identity (note the subtle difference) you want to replace

// Microsoft.AspNet.Identity.Core
PasswordHasher hasher = new PasswordHasher(); 

with something that looks like

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

// Microsoft.AspNetCore.Identity
PasswordHasher<YourUserTypeActuallyImmaterial> hasher 
= new PasswordHasher<YourUserTypeActuallyImmaterial>(
                Options.Create(new PasswordHasherOptions()
                {
                    CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV2,
                }));

Thereafter the difference in API is that the newer PasswordHasher<T> requires you to pass in an instance of T when hashing or verifying.

This will hash plaintext in a compatible way with the old method and will successfully verify valid passwords hashed with the previous method.