Back to: ASP.NET Core Web API Tutorials
How to Store Password in Hash Format in ASP.NET Core Web API
In this article, I will discuss how to store a Password in Hash Format in an ASP.NET Core Web API Application and demonstrate the various cryptographic algorithms for storing the Password hash in the database using Entity Framework Core. Storing passwords securely is mandatory for a web application to protect user data and maintain the integrity of your application.Â
What is a Password in Hash Format?
A Password in Hash Format is the output of applying a cryptographic hash function (such as HMACSHA256 or HMACSHA512) to a password (plus salt). Cryptographic hash functions are designed to take an input (or ‘message’) and return a fixed-size string of bytes. It’s not an encrypted password (which can be decrypted); it’s a one-way transformation. The following are the Key Characteristics of a Password in Hash Format:
- Deterministic: The same password + salt + algorithm always results in the same hash.
- Fixed Size: The output size (e.g., 256 bits for SHA256, 512 bits for SHA512) is always the same, regardless of input length. Hash output typically looks like a random string of hexadecimal characters.
- Small Changes = Big Differences: Changing one character of the password completely changes the hash.
- Salting: Adding a random salt (stored with the hash) ensures that identical passwords get different hashes.
For a better understanding, please refer to the following diagram.
Example: If you hash Test@1234 with SHA512 and salt X, you will get a unique string of bytes. Store this hash and the salt in your database. During login, you hash the entered password with the stored salt and compare the hashes.
Why Do We Need to Store Passwords in Hash Format in a Database?
Storing passwords in hash format is a critical security measure. Here’s why:
- Security of User Data: Storing passwords as plain text is a huge security risk. If your database is ever compromised, attackers can see all user passwords directly and misuse them.
- One-Way Protection: Hashing converts the password into a fixed-length string that cannot be reversed into the original password, protecting users even if the data is leaked.
- Data Integrity: Hash functions ensure that even a slight change in the input (password) significantly alters the output (hash), thereby preventing tampering.
- Salting: Using a salt (random data combined with the password) before hashing prevents attackers from using precomputed tables (rainbow tables) to crack passwords.
- Prevents Password Reuse Attacks: Even if hackers obtain the hashed passwords, they must expend significant time and computing power to crack them, especially if you use proper hashing and salt (a random value added to each password before hashing).
- Follows Best Practices and Legal Requirements: Regulatory standards (like GDPR, PCI-DSS, HIPAA, etc.) require secure password handling.
Bottom Line: Hashing is the industry standard for storing passwords securely. Never store or log plaintext passwords.
Example to Understand Password in Hash Format in ASP.NET Core Web API:
Let us see an example to understand how to store and use passwords in a Hashed Format in an ASP.NET Core Web API Application. I will demonstrate the use of SHA256 and SHA512 hashing algorithms, and then discuss the differences between these two cryptographic hashing algorithms, as well as when to use each one in a real-time application.
Creating a New ASP.NET Core Web API Project:
First, create a new ASP.NET Core Web API Project named PasswordHashingDemo. We will use SQL Server as the database where we will store the password in Hash format, and Entity Framework Core (Code First Approach) as the Data access technology.
Installing the Required Packages:
Please install the following two packages using either the Visual Studio Package Manager Console by executing the command below or the NuGet Package Manager for a solution.
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
Define User Model
First, create a folder named ‘Models’ at the Project root directory, where we will store all our Models and DTOs. Create a class file named User.cs within the Models folder and then copy and paste the following code. The User model represents the database entity. While validation is more crucial on DTOs (which handle user input), adding Data Annotations to the User model ensures database-level constraints and data integrity. We need to store PasswordHash and PasswordSalt as byte[]. Email is marked unique with [Index] to prevent duplicate registrations.
using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace PasswordHashingDemo.Models { [Index(nameof(Email), Name = "Index_Email", IsUnique = true)] public class User { [Key] public int Id { get; set; } [Required(ErrorMessage = "First name is required.")] [StringLength(50, ErrorMessage = "First name cannot exceed 50 characters.")] public string FirstName { get; set; } [Required(ErrorMessage = "Last name is required.")] [StringLength(50, ErrorMessage = "Last name cannot exceed 50 characters.")] public string LastName { get; set; } [Required(ErrorMessage = "Email is required.")] [EmailAddress(ErrorMessage = "Invalid email address format.")] [StringLength(100, ErrorMessage = "Email cannot exceed 100 characters.")] public string Email { get; set; } [Required] public byte[] PasswordHash { get; set; } [Required] public byte[] PasswordSalt { get; set; } } }
Define DTOs for User Registration and Login
Next, we need to create separate Data Transfer Objects (DTOs) for registration and login to encapsulate the required data.
RegistrationDTO
The Registration DTO is used for capturing user details at registration, including validations such as:
- Password complexity (minimum eight characters, includes uppercase, lowercase, numbers, and special characters).
- Email format validation.
- First and last name letters only.
So, create a class file named RegistrationDTO.cs within the Models folder and copy and paste the following code.
using System.ComponentModel.DataAnnotations; namespace PasswordHashingDemo.Models { public class RegistrationDTO { [Required(ErrorMessage = "First name is required.")] [StringLength(50, ErrorMessage = "First name cannot exceed 50 characters.")] [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "First name can only contain letters.")] public string FirstName { get; set; } [Required(ErrorMessage = "Last name is required.")] [StringLength(50, ErrorMessage = "Last name cannot exceed 50 characters.")] [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "Last name can only contain letters.")] public string LastName { get; set; } [Required(ErrorMessage = "Email is required.")] [EmailAddress(ErrorMessage = "Invalid email address format.")] [StringLength(100, ErrorMessage = "Email cannot exceed 100 characters.")] public string Email { get; set; } [Required(ErrorMessage = "Password is required.")] [StringLength(100, ErrorMessage = "Password must be between 8 and 100 characters.", MinimumLength = 8)] [DataType(DataType.Password)] [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&]).{8,}$", ErrorMessage = "Password must be at least 8 characters long and include uppercase, lowercase, number, and special character.")] public string Password { get; set; } } }
LoginDTO
Next, create a class file named LoginDTO.cs within the Models folder and copy and paste the following code. The LoginDTO is used during the user login process. Validation ensures that the user provides both an email and a password in the correct format.
using System.ComponentModel.DataAnnotations; namespace PasswordHashingDemo.Models { public class LoginDTO { [Required(ErrorMessage = "Email is required.")] [EmailAddress(ErrorMessage = "Invalid email address format.")] [StringLength(100, ErrorMessage = "Email cannot exceed 100 characters.")] public string Email { get; set; } [Required(ErrorMessage = "Password is required.")] [DataType(DataType.Password)] public string Password { get; set; } } }
Configure the Database Connection String in AppSettings.json File
Instead of hard-coding the connection string within the DbContext class, we will store it in the appsettings.json file. So, please update the appsettings.json file as follows.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=UserDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Creating Db Context:
Create a class file named UserDbContext.cs within the Models folder and copy and paste the following code. Here, you can see that we are using DbSet<User> as a property, for which the Entity Framework will create a database table named Users, which will be mapped to the User entity.
using Microsoft.EntityFrameworkCore; namespace PasswordHashingDemo.Models { public class UserDbContext : DbContext { public UserDbContext(DbContextOptions<UserDbContext> options) : base(options) { } public DbSet<User> Users { get; set; } } }
Registering DbContext in Program Class:
Next, we need to register the context class in the Program class. So, please modify the Program class as follows.
using PasswordHashingDemo.Models; using Microsoft.EntityFrameworkCore; namespace PasswordHashingDemo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() .AddJsonOptions(options => { // This will use the property names as defined in the C# model options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); //Configure the ConnectionString and DbContext class builder.Services.AddDbContext<UserDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection")); }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
Database Migration
Next, we need to generate the Migration and update the database schema. So, open the Package Manager Console and execute the Add-Migration and Update-Database commands as follows.
With this, our Database with the Users table is created, as shown in the image below:
Implement Password Hashing
To securely hash and verify passwords, we will use HMAC-SHA512 for enhanced security (HMAC-SHA256 is faster, but HMAC-SHA512 provides higher security). Let us first understand how it works:
When Registering:
- The API receives the plain password from the user.
- The password is hashed using a secure hashing algorithm (e.g., HMAC-SHA512) with a randomly generated salt.
- Both the PasswordHash and PasswordSalt are stored in the database.
When Logging In:
- API retrieves the stored salt and hash for the email.
- The entered password is hashed with the stored salt.
- If the hashes match, the password is correct.
Create a class file named PasswordHasher.cs within the Models folder and copy and paste the following code. The following utility class is used for password hashing and verification.
using System.Security.Cryptography; using System.Text; namespace PasswordHashingDemo.Models { // Provides methods for creating and verifying password hashes using HMACSHA512. public static class PasswordHasher { // Creates a hashed version of the provided password along with a unique salt. // password: The plaintext password to be hashed. // passwordHash: Outputs the resulting password hash as a byte array. // passwordSalt: Outputs the unique salt used in hashing as a byte array. public static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) { // Instantiate HMACSHA512 to generate a cryptographic hash and a unique key (salt). using (var hmac = new HMACSHA512()) { // The Key property of HMACSHA512 provides a randomly generated salt. passwordSalt = hmac.Key; // Assign the generated salt to the output parameter. // Convert the plaintext password into a byte array using UTF-8 encoding. byte[] passwordBytes = Encoding.UTF8.GetBytes(password); // Compute the hash of the password bytes using the HMACSHA512 instance. passwordHash = hmac.ComputeHash(passwordBytes); // Assign the computed hash to the output parameter. } } // Verifies whether the provided password matches the stored hash using the stored salt. // password: The plaintext password to verify. // storedHash: The stored password hash to compare against. // storedSalt: The stored salt used during the original hashing process. // Return: True if the password is valid and matches the stored hash; otherwise, false. public static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt) { // Instantiate HMACSHA512 with the stored salt as the key to ensure the same hashing parameters. using (var hmac = new HMACSHA512(storedSalt)) { // Convert the plaintext password into a byte array using UTF-8 encoding. byte[] passwordBytes = Encoding.UTF8.GetBytes(password); // Compute the hash of the password bytes using the HMACSHA512 instance initialized with the stored salt. byte[] computedHash = hmac.ComputeHash(passwordBytes); // Compare the computed hash with the stored hash byte by byte. // SequenceEqual ensures that both byte arrays are identical in sequence and value. bool hashesMatch = computedHash.SequenceEqual(storedHash); // Return the result of the comparison. return hashesMatch; } } } }
Using Password Hashing
Now, we can use PasswordHasher to hash passwords before storing them in the database. Create a controller that handles both registration and login actions.
Register:
- Validates the registration data.
- Checks for duplicate email.
- Uses PasswordHasher to hash and salt the password.
- Saves the new user.
Login:
- Validates login data.
- Looks up the user by email.
- Uses PasswordHasher to check if the password is correct.
- Return appropriate responses. Never return the hash or salt to the user. On successful login, you’d typically generate a JWT or similar token.
So, create an Empty API Controller named UserController within the Controllers folder and then copy and paste the following code:
using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PasswordHashingDemo.Models; namespace PasswordHashingDemo.Controllers { [Route("api/[controller]")] [ApiController] public class UserController : ControllerBase { private readonly UserDbContext _context; public UserController(UserDbContext context) { _context = context; } // POST: api/user/register [HttpPost("register")] public async Task<IActionResult> Register([FromBody] RegistrationDTO model) { // Validate the incoming model if (!ModelState.IsValid) { return BadRequest(ModelState); } // Check if the email already exists if (await _context.Users.AnyAsync(u => u.Email == model.Email)) return BadRequest("Email is already in use."); // Create password hash and salt PasswordHasher.CreatePasswordHash(model.Password, out byte[] passwordHash, out byte[] passwordSalt); // Create user entity var user = new User { FirstName = model.FirstName, LastName = model.LastName, Email = model.Email, PasswordHash = passwordHash, PasswordSalt = passwordSalt }; // Add user to the database _context.Users.Add(user); await _context.SaveChangesAsync(); // For security reasons, do not return password hash and salt return Ok(new { Message = "User registered successfully." }); } // POST: api/user/login [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginDTO model) { // Validate the incoming model if (!ModelState.IsValid) { return BadRequest(ModelState); } // Retrieve the user by email var user = await _context.Users.FirstOrDefaultAsync(u => u.Email == model.Email); if (user == null) return Unauthorized("Invalid email or password."); // Verify the password if (!PasswordHasher.VerifyPasswordHash(model.Password, user.PasswordHash, user.PasswordSalt)) return Unauthorized("Invalid email or password."); // TODO: Generate JWT or other token as needed return Ok(new { Message = "User logged in successfully." }); } } }
Test the Registration Endpoint
URL: https://localhost:<port>/api/user/register
Method: POST
Headers: Content-Type: application/json
Body (JSON):
{ "FirstName": "Pranaya", "LastName": "Rout", "Email": "Pranaya@Example.com", "Password": "Test@1234" }
Expected Response:
On success, you should see a 200 OK response with a success message (e.g., “Registration successful.”) as shown in the image below.
Test the Login Endpoint
URL: https://localhost:<port>/api/user/login
Method: POST
Headers: Content-Type: application/json
Body (JSON):
{ "Email": "Pranaya@Example.com", "Password": "Test@1234" }
Expected Response:
If the credentials are incorrect, you should see a 401 Unauthorized response with an appropriate message (e.g., “Invalid email or password.”). If the credentials are correct, you should see a 200 OK response with a success message (e.g., “User logged in successfully”), as shown in the image below.
Verifying the Database:
Now, create a few users and then verify the database. You can see that for each user, a different password hash and salt are produced, as shown in the image below. Each user has a different password hash and salt, even if the same password is used, thanks to unique salts.
What are the Differences Between HMACSHA256 and HMACSHA512?
SHA256 and SHA512 are both cryptographic hash functions from the SHA-2 (Secure Hash Algorithm 2) family, designed by the National Security Agency (NSA). In ASP.NET Core, both HMACSHA256 and HMACSHA512 are part of the System.Security.Cryptography namespace. They differ primarily in the length of their output and computational requirements.
HMACSHA256 in ASP.NET Core:
- Hash Size: Generates a hash that is 256 bits (32 bytes) long.
- Security: Provides good security and is widely adopted.
- Performance: Faster than HMACSHA512 due to its smaller hash size, which can be advantageous in scenarios where performance is a priority.
HMACSHA512 in ASP.NET Core:
- Hash Size: Generates a hash that is 512 bits (64 bytes) long.
- Security: Offers a higher level of security compared to HMACSHA256 because of its larger hash size.
- Performance: Generally slower than HMACSHA256 because it deals with larger blocks of data and produces a larger hash. However, on systems with 64-bit processors, the performance difference may be less noticeable.
Choosing Between HMACSHA256 and HMACSHA512
- Security Needs: If you require higher security and are less concerned about performance, HMACSHA512 might be a better choice. For general purposes, HMACSHA256 provides a balanced approach.
- Performance Considerations: In performance-sensitive applications, the quicker computation of HMACSHA256 might be more suitable.
- Compliance Requirements: Some regulations or industry standards might specify the use of a particular hash function size.
Storing passwords as hashed values (with a salt) is mandatory for all production applications. ASP.NET Core makes it easy with secure hashing functions. Use either HMACSHA256 or HMACSHA512, always salt your hashes, and never store plaintext passwords.
In the next article, I will discuss how to implement HMAC Authentication in an ASP.NET Core Web API Application. In this article, I explain how to store a Password in Hash Format in an ASP.NET Core Web API Application with examples. I hope you enjoy this article.
Want a quick and clear walkthrough?
Check out our detailed YouTube video tutorial on Password Hashing in ASP.NET Core Web API for step-by-step guidance with practical code examples.
Watch it here: https://www.youtube.com/watch?v=qxBR-YTg9Dc
Enhance your API authentication security by adopting best hashing practices.