Search code examples
c#asp.net-coreasp.net-identityasp.net-core-identity

ASP .NET Core Identity - Microsoft.AspNetCore.Identity.SignInManager: Warning: User failed to provide the correct password


I have an existing .NET 4.x database with Identity running. I am able to login just fine with it. I am upgrading my application to .NET Core with Identity. I've been through a bunch of issues. I'm doing to getting the error message in identity:

Microsoft.AspNetCore.Identity.UserManager: Warning: Invalid password for user. Microsoft.AspNetCore.Identity.SignInManager: Warning: User failed to provide the correct password.

The code that I am using is:

var findUser = await signinManager.PasswordSignInAsync(userName, Password, false, false);

-- or -- var au = new AspNetUser() { UserName = userName, EmailConfirmed = true }; var res = await _userManager.CheckPasswordAsync(au, Password);

The exact error I get depends on whether or not I try to sign in via the UserManager or the SignInManager.

I have set the following in the Startup.cs file:

    services.AddIdentity<AspNetUser, AspNetRole>().AddEntityFrameworkStores<GolfGameContext>();
    services.Configure<IdentityOptions>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
        options.SignIn.RequireConfirmedEmail = false;
        options.SignIn.RequireConfirmedPhoneNumber = false;
    });
    services.Configure<PasswordHasherOptions>(options => options.CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV2);

I would think that the changing of the setting for the password hashing algo would allow my existing userid/password combo to connect to the database.

I have updated my db schema. I have set the NormalizedEmail and NormalizedUserName to the uppercase version of the email and the username. I have also directly set the confirmed phone number and the confirmed email values to true in the database.

Any ideas on how to correctly connect are appreciated.


Solution

  • Perhaps the issue is that: Asp.net Identity and Asp.net Core Identity uses the different hashing algorithm to generate the Hash Password. Even you have changed the set the Asp.net core PasswordHasherOptions to V2 version, the generate hash password is still not same with the Asp.net Identity generated hash password.

    For the Asp.net Identity, it uses the following code to hash the Password (refer the source code):

        private const int PBKDF2IterCount = 1000; // default for Rfc2898DeriveBytes
        private const int PBKDF2SubkeyLength = 256/8; // 256 bits
        private const int SaltSize = 128/8; // 128 bits
    
        /* =======================
         * HASHED PASSWORD FORMATS
         * =======================
         * 
         * Version 0:
         * 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 }
         */
    
        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);
        }
    

    In the Asp.net Core Identity, it will use the PasswordHasher to hash the password (refer the source code):

        public virtual string HashPassword(TUser user, string password)
        {
            if (password == null)
            {
                throw new ArgumentNullException(nameof(password));
            }
    
            if (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV2)
            {
                return Convert.ToBase64String(HashPasswordV2(password, _rng));
            }
            else
            {
                return Convert.ToBase64String(HashPasswordV3(password, _rng));
            }
        }
    
        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;
        }
    

    To solve this issue, since the existing user password is hashed using Asp.net Identity. When you login, you could query the database and get the HashedPassword based on the user name, and then use the Asp.net Identity VerifyHashedPassword method to verify the HashedPassword. Code as below:

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
    
            if (ModelState.IsValid)
            {
    
                // This doesn't count login failures towards account lockout
                // To enable password failures to trigger account lockout, set lockoutOnFailure: true
                //var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
    
                //get the hashedpassword from the database.
                var hashedpassword = _dbContext.Users.Where(c => c.UserName == Input.Email).FirstOrDefault().PasswordHash;
    
                var result = VerifyHashedPassword(hashedpassword, Input.Password);
    
               //if success, reditect to returnUrl, else show error message.
    

    Asp.net Identity VerifyHashedPassword method:

        private const int PBKDF2IterCount = 1000; // default for Rfc2898DeriveBytes
        private const int PBKDF2SubkeyLength = 256 / 8; // 256 bits
        private const int SaltSize = 128 / 8; // 128 bits
    
        // hashedPassword must be of the format of HashWithPassword (salt + Hash(salt+input)
        public static bool VerifyHashedPassword(string hashedPassword, string password)
        {
            if (hashedPassword == null)
            {
                return false;
            }
            if (password == null)
            {
                throw new ArgumentNullException("password");
            }
    
            var hashedPasswordBytes = Convert.FromBase64String(hashedPassword);
    
            // Verify a version 0 (see comment above) text hash.
    
            if (hashedPasswordBytes.Length != (1 + SaltSize + PBKDF2SubkeyLength) || hashedPasswordBytes[0] != 0x00)
            {
                // Wrong length or version header.
                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);
        }
    
        // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
        [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;
        }