Introduction
ASP.NET Core Identity uses IPasswordHasher<TUser> to hash and verify passwords. The default implementation uses PBKDF2 with HMAC-SHA256. When password verification fails for valid credentials, it is usually due to a hash format mismatch (e.g., migrating from ASP.NET Identity 2 to ASP.NET Core Identity), custom seeding with incorrect hash format, or a changed password hasher implementation.
Symptoms
SignInManager.PasswordSignInAsyncreturnsSignInResult.Failedfor correct passwordPasswordHasher.VerifyHashedPasswordreturnsPasswordVerificationResult.Failed- User can log in on old system but not after migration
- Seeded admin user cannot log in
- Works for newly registered users but not for migrated users
Example debugging: ```csharp var hasher = new PasswordHasher<ApplicationUser>();
// Check the hash format var result = hasher.VerifyHashedPassword(user, user.PasswordHash, enteredPassword); // Returns Failed when hash format does not match
// Identity v3 hash format: // AQAAAAEAACcQAAAAE... (base64, starts with specific prefix) ```
Common Causes
- Migrated from ASP.NET Identity v2 (different hash format) to v3
- Seeded users with plain text passwords instead of hashed passwords
- Custom
IPasswordHasherimplementation incompatible with default - Password hash column truncated in database migration
- Different iteration count between hasher versions
Step-by-Step Fix
- 1.Verify hash format in the database:
- 2.```sql
- 3.-- Identity v3 (ASP.NET Core) hash starts with: AQAAAAEAACcQAAAAE
- 4.-- Identity v2 (ASP.NET 4.x) hash starts with: ABAAAAAAAACQAAAAQB
- 5.SELECT Id, Email, SUBSTRING(PasswordHash, 1, 30) as HashPrefix
- 6.FROM AspNetUsers;
- 7.
` - 8.Seed users with properly hashed passwords:
- 9.```csharp
- 10.public static async Task SeedAdminUserAsync(IServiceProvider services)
- 11.{
- 12.var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var admin = new ApplicationUser { UserName = "admin@example.com", Email = "admin@example.com", EmailConfirmed = true };
// Let Identity hash the password correctly var result = await userManager.CreateAsync(admin, "SecureP@ssw0rd!"); if (!result.Succeeded) { throw new Exception( $"Failed to create admin: {string.Join(", ", result.Errors.Select(e => e.Description))}"); }
await userManager.AddToRoleAsync(admin, "Admin"); } ```
- 1.Implement version-aware password hasher for migration:
- 2.```csharp
- 3.public class MigratingPasswordHasher<TUser> : PasswordHasher<TUser> where TUser : class
- 4.{
- 5.public override PasswordVerificationResult VerifyHashedPassword(
- 6.TUser user, string hashedPassword, string providedPassword)
- 7.{
- 8.// Try the default (v3) hasher first
- 9.var result = base.VerifyHashedPassword(user, hashedPassword, providedPassword);
- 10.if (result != PasswordVerificationResult.Failed)
- 11.return result;
// Fall back to v2 hash verification (ASP.NET Identity 2) if (VerifyV2Hash(providedPassword, hashedPassword)) { // Re-hash with v3 format var newHash = HashPassword(user, providedPassword); // Update would need to be saved by the caller return PasswordVerificationResult.SuccessRehashNeeded; }
return PasswordVerificationResult.Failed; }
private bool VerifyV2Hash(string password, string hashedPassword) { // Implement ASP.NET Identity v2 verification // Uses HMAC-SHA1 with 1000 iterations // ... return false; } }
// Register in DI builder.Services.AddSingleton<IPasswordHasher<ApplicationUser>, MigratingPasswordHasher<ApplicationUser>>(); ```
- 1.Reset password for affected users:
- 2.```csharp
- 3.// Generate a reset token and set new password
- 4.var user = await userManager.FindByEmailAsync("user@example.com");
- 5.var token = await userManager.GeneratePasswordResetTokenAsync(user);
- 6.var result = await userManager.ResetPasswordAsync(user, token, "NewP@ssw0rd!");
if (!result.Succeeded) { // Log the errors foreach (var error in result.Errors) { _logger.LogError("Password reset error: {Code} - {Description}", error.Code, error.Description); } } ```
Prevention
- Always use
UserManager.CreateAsyncorUserManager.AddPasswordAsyncto set passwords - Never insert raw password hashes into the database manually
- Test login with seeded users after every database migration
- Add integration tests that verify password hashing round-trip
- Document the password hash version in use
- Use
PasswordVerificationResult.SuccessRehashNeededto upgrade old hashes transparently