When building any web application that handles user accounts, security is paramount. Storing passwords in plain text is one of the most dangerous vulnerabilities a system can have. If your database is ever compromised, plain-text passwords expose every single user instantly.
Even standard hashing algorithms like MD5 or SHA-256 are no longer sufficient for passwords because they are too fast to compute, making them vulnerable to brute-force and rainbow table attacks.
Fortunately, ASP.NET Core provides a robust, built-in mechanism via the PasswordHasher<TUser> class from the Microsoft.AspNetCore.Identity namespace. It implements PBKDF2 (Password-Based Key Derivation Function 2) with a cryptographically secure random salt, iteration counts, and constant-time comparison out of the box.
In this comprehensive guide, we will explore how to implement secure password hashing and verification in your ASP.NET Core application.
Why Standard Hashing Falls Short (And Why Salting Matters)
A hash is a one-way mathematical function that converts data into a fixed-size string. However, if two users have the same password (e.g., "Password123!"), a basic hash will produce the exact same output. Attackers use precomputed tables of hashes (rainbow tables) to crack millions of passwords in seconds.
Salting solves this by appending a unique, random string (the salt) to every user's password before it is hashed. Because every salt is unique, two identical passwords will result in completely different stored hashes, rendering rainbow tables useless.
Step 1: Create a Dedicated Password Service
To keep your controllers clean and follow the Single Responsibility Principle, it is best practice to encapsulate password hashing and verification inside a dedicated service.
Create a class named PasswordService.cs:
C#
using Microsoft.AspNetCore.Identity;
namespace YourNamespace.Services
{
public class PasswordService
{
private readonly PasswordHasher<object> _passwordHasher = new();
/// <summary>
/// Hashes a plain-text password using PBKDF2 with a unique random salt.
/// </summary>
public string HashPassword(string password)
{
// Pass null for the user object since we are managing hashes independently of EF Core Identity
return _passwordHasher.HashPassword(null!, password);
}
/// <summary>
/// Verifies a plain-text password against a stored secure hash.
/// </summary>
public bool VerifyPassword(string hashedPassword, string providedPassword)
{
var result = _passwordHasher.VerifyHashedPassword(null!, hashedPassword, providedPassword);
// Returns Success or SuccessRehashNeeded (if the hashing algorithm parameters were upgraded)
return result == PasswordVerificationResult.Success ||
result == PasswordVerificationResult.SuccessRehashNeeded;
}
}
}
Step 2: Register the Service in Dependency Injection
Register your PasswordService in Program.cs so it can be injected into your controllers or API endpoints wherever registration and authentication occur.
C#
builder.Services.AddScoped<PasswordService>();
Step 3: Implement Hashing During User Registration
When a new user registers, you must take their plain-text password from the request body, pass it through your PasswordService, and store only the resulting hash in your database. Never store the plain text.
C#
[HttpPost("register")]
public IActionResult Register([FromBody] RegisterModel model, [FromServices] PasswordService passwordService)
{
// 1. Hash the user's password securely
var hashedPassword = passwordService.HashPassword(model.Password);
// 2. Save the user and the hashedPassword to your database
// var user = new User { Username = model.Username, PasswordHash = hashedPassword };
// _dbContext.Users.Add(user);
// _dbContext.SaveChanges();
return Ok(new { message = "User registered successfully!" });
}
Step 4: Implement Verification During Login
When a user attempts to log in, fetch their user record from the database using their username, retrieve their stored password hash, and use the PasswordService to verify the incoming password.
C#
[HttpPost("login")]
public IActionResult Login([FromBody] LoginModel model, [FromServices] PasswordService passwordService)
{
// 1. Retrieve the user from your database
// var user = _dbContext.Users.FirstOrDefault(u => u.Username == model.Username);
// if (user == null) return Unauthorized(new { message = "Invalid credentials." });
// (Mock retrieved hash for demonstration purposes)
var storedPasswordHash = "AQAAAAIAAYagAAAAEP...";
// 2. Verify the provided password against the stored hash
bool isPasswordValid = passwordService.VerifyPassword(storedPasswordHash, model.Password);
if (!isPasswordValid)
{
return Unauthorized(new { message = "Invalid username or password." });
}
// 3. Credentials are valid — generate and return your JWT token here
return Ok(new { message = "Login successful! Token generated." });
}
Key Security Features of ASP.NET Core's PasswordHasher
Automatic Salting: Every password hash generated includes a cryptographically secure random salt embedded directly within the returned hash string.
Timing Attack Protection: The
VerifyHashedPasswordmethod compares hashes in constant time, preventing attackers from timing how long the comparison takes to guess character patterns.Future-Proofing (
SuccessRehashNeeded): The hash string contains a version header. If computational standards evolve and Microsoft updates the underlying algorithm in a future framework version, ASP.NET Core can recognize older secure hashes and automatically prompt a rehash when the user successfully logs in next.

Join the conversation! Your thoughts help the community grow.