How to Store Password in Hash Format in ASP.NET Core Web API

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 the ASP.NET Core Web API Application and show you the different 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 Password in Hash Format?

A Password in Hash Format refers to the output produced by applying a cryptographic hash function (HMACSHA256 or HMACSHA512) to a password. Cryptographic hash functions are designed to take an input (or ‘message’) and return a fixed-size string of bytes. The output, known as the hash, is typically a string of hexadecimal characters that appears random. The following are the key characteristics of hashed passwords:

  • The same password will always produce the same hash when the same hash function and the same salt (if used) are applied.
  • Regardless of the original input (password) length, the hash output will have a fixed length, which depends on the hash function.
  • Even a small change in the password will result in a completely different hash, which makes hashes useful for ensuring data integrity.
Example to Understand Password in Hash Format in ASP.NET Core Web API:

Let us see an example of how to store and use a password in Hash Format in an ASP.NET Core Web API application. I will show you the use of SHA256 and SHA512 hashing algorithms and then show you the differences between these cryptographic hashing algorithms and when to use which one in real-time application. Then, we will discuss why we need to use PBKDF2 over SHA-2 Algorithms, which are currently used in the Industry.

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 to store the password in Hash format and Entity Framework Core as the Data access technology. So, please install the following two packages either using Package Manager Console by executing the below command or using NuGet Package Manager for solution.

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Define User Model

First, create a folder named Models in the Project root directory, where we will create all our Models and DTOs. Then, 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 critical on DTOs (which handle user input), adding Data Annotations to the User model ensures database-level constraints and data integrity.

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 DTOs for registration and login to encapsulate the required data.

RegistrationDTO

Create a class file named RegistrationDTO.cs within the Models folder and copy and paste the following code. The Registration DTO is used to capture user details during registration. The RegistrationDTO handles user input during registration. Applying Data Annotations here ensures that incoming data meets the required criteria before any processing occurs.

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

CreCreate a class file named ate LoginDTO.cs within the Models folder and copy and paste the following code. The LoginDTO is used during user login. Validation here ensures that the user provides both email and 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 AppSetings.json File

Instead of hardcoding the connection string with the DbContext class, we will store the connection string in the appsettings.json file. So, 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;"
  }
}
Create Db Context:

Define a context class that inherits from the DbContext class. So, add a class file named UserDbContext.cs within the Models folder and copy and paste the following code. Here, you can see we are using DbSet<User> as a property for which the entity framework will create a database table named Users, 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 the Connection String and DbContext Instance in Program Class:

Next, we need to configure the connection string and register the context class to use the connection string in the Program class. So, please modify the Program class as follows. Here, we also tell the Framework to use the property names defined in the C# model classes.

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.

How to Store Password in Hash Format in ASP.NET Core Web API

With this, our Database with Users table is created, as shown in the below image:

How to Store Password in Hash Format in ASP.NET Core Web API

Implement Password Hashing in ASP.NET Core Web API

To securely hash and verify passwords, we will use HMACSHA512 for better security (SHA256 is faster, but SHA512 provides higher security). So, 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).
            // The 'using' statement ensures that the HMACSHA512 instance is disposed of correctly after use.
            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.
            // The 'using' statement ensures that the HMACSHA512 instance is disposed of correctly after use.
            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;
            }
        }
    }
}
Integrating Password Hashing in ASP.NET Core Web API

Now, we can use PasswordHasher to hash passwords before storing them in the database. Create a controller that handles both registration and login actions. Before processing, check if the incoming model adheres to the validation rules defined by Data Annotations. If not, return a BadRequest with detailed error messages. 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." });
        }
    }
}
Code Explanation:

As you can see in the Registration action method, the Password Hasher utility generates the Password hash and salt and then stores them in the database. In this case, if two users try sending the same password, it will create two different password hashs. This is possible because of the dynamically generated salt. On the other hand, the Login action method also uses the Password Hasher utility to create and compare the password.

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.

how to Store a Password in Hash Format in the ASP.NET Core Web API Application

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.

Example to Understand Password in Hash Format in ASP.NET Core Web API

Verifying the Database:

Now, create a few users and then verify the database. You can see for each user, it is creating a different password hash and salt, as shown in the below image:

Password in Hash Format in ASP.NET Core Web API

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 and lower computational overhead are priorities.
HMACSHA512 in ASP.NET Core:
  • Hash Size: Generates a hash that is 512 bits (64 bytes) long.
  • Security: It offers a higher level of security than HMACSHA256 because of its larger hash size.
  • Performance: Generally slower than HMACSHA256 because it deals with larger data blocks 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 the 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.
Recommendations for Enhanced Security

While HMACSHA256 and HMACSHA512 are secure cryptographic hash functions, password hashing benefits from algorithms specifically designed to be computationally intensive, thereby reducing brute-force attacks. Industry standards recommend using Key Derivation Functions (KDFs) like PBKDF2, BCrypt, or Argon2.

Implementing PBKDF2 in ASP.NET Core

Let us implement Password Hashing using PBKDF2 in ASP.NET Core. The good thing is that .NET provides built-in support through Rfc2898DeriveBytes. For a better understanding, please modify the PasswordHasher utility class as follows.

using System.Security.Cryptography;

namespace PasswordHashingDemo.Models
{
    public static class PasswordHasher
    {
        // Configurable parameters for PBKDF2
        private const int SaltSize = 16; // 128-bit salt
        private const int HashSize = 32; // 256-bit hash
        private const int Iterations = 100000; // The higher, the more secure but slower

        // Creates a PBKDF2 hash and salt for the given password.
        // Parameters:
        // - password: The plaintext password provided by the user.
        // - passwordHash: An output parameter that will hold the PBKDF2 hash of the password.
        // - passwordSalt: An output parameter that will hold the randomly generated salt.
        public static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
        {
            // Initialize a byte array to hold the salt with the specified SaltSize
            passwordSalt = new byte[SaltSize];

            // Create an instance of a cryptographically secure random number generator
            // RandomNumberGenerator.Create() provides a way to generate secure random bytes
            using (var rng = RandomNumberGenerator.Create())
            {
                // Fill the passwordSalt array with cryptographically strong random bytes
                rng.GetBytes(passwordSalt);
                // At this point, passwordSalt contains a unique salt for this password
            }

            // Create an instance of Rfc2898DeriveBytes to perform the PBKDF2 hashing
            // Parameters:
            // - password: The plaintext password to hash
            // - passwordSalt: The unique salt generated above
            // - Iterations: The number of iterations to perform (increases computational difficulty)
            // - HashAlgorithmName.SHA256: Specifies that SHA256 is used as the underlying hash algorithm
            using (var pbkdf2 = new Rfc2898DeriveBytes(password, passwordSalt, Iterations, HashAlgorithmName.SHA256))
            {
                // Derive the hash bytes from the password and salt
                // The GetBytes method returns a byte array with the length specified by HashSize
                passwordHash = pbkdf2.GetBytes(HashSize);
                // At this point, passwordHash contains the derived key (hash) for the password
            }

            // After execution:
            // - passwordSalt contains the unique salt used for hashing
            // - passwordHash contains the derived hash of the password
            // These should be stored securely in the database for future password verifications

        }

        // Verifies the given password against the stored hash and salt.
        // Parameters:
        // - password: The plaintext password to be verified.
        // - storedHash: The hash previously generated and stored in the database.
        // - storedSalt: The salt used to generate the original hash.
        // Returns:
        // - A boolean indicating whether the password matches the stored hash and salt.
        public static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
        {
            // Derive a hash from the provided password and stored salt
            using (var pbkdf2 = new Rfc2898DeriveBytes(password, storedSalt, Iterations, HashAlgorithmName.SHA256))
            {
                byte[] computedHash = pbkdf2.GetBytes(HashSize);

                // Compare the computed hash with the stored hash byte-by-byte
                return CryptographicOperations.FixedTimeEquals(computedHash, storedHash);
            }
        }
    }
}

The above is the modified version of the PasswordHasher class that uses PBKDF2 (via Rfc2898DeriveBytes) for password hashing. This approach is more widely recommended for password storage due to its built-in support for key stretching and the ability to configure the number of iterations. Now, run the application, and it should work as expected.

What is Key Stretching?

Key stretching is a technique used to make password hashing computationally expensive and time-consuming, thereby slowing down brute-force attacks. The idea is to apply the hashing function multiple times (iterations) to make each attempt significantly slower for attackers.

How PBKDF2 Implements Key Stretching

PBKDF2 (Password-Based Key Derivation Function 2) is designed for password hashing with built-in key stretching. It repeatedly applies a hashing algorithm (e.g., SHA-256) on the password and salt combination. Each iteration takes time, multiplying the total computational cost. With 100,000 iterations, the hashing operation is repeated 100,000 times, making brute-force attacks 100,000 times slower.

How Iterations Work:
  • In the first iteration, the password and salt are combined and hashed.
  • The output hash is then hashed again, repeatedly, for the configured number of iterations.
  • The final hash is the result of all these iterations.

Goal: Increase time per password guess for attackers, forcing them to spend significantly more resources.

Layman’s Example to Understand Key Stretching: Unlocking a Phone

Imagine you’re trying to unlock your smartphone with a passcode.

Without Key Stretching (No Extra Delay):

If someone guesses your passcode, they can try 1,000 combinations very quickly because there’s no delay between each attempt. For instance, they might try:

  • 0000
  • 0001
  • 0002 … and so on.
With Key Stretching (Extra Delay):

Imagine your phone is programmed to intentionally wait 3 seconds before processing each passcode attempt. If someone wants to guess 1,000 combinations, they’ll need 3,000 seconds (over 50 minutes) instead of a few seconds. This extra delay makes it frustrating and impractical for someone to brute-force the passcode.

Why PBKDF2 over SHA-2 for Password Hashing?

SHA-2 (including SHA-256 or SHA-512) is a general-purpose cryptographic hash function. While it is fast and secure for data integrity checks, it lacks features needed for secure password hashing:

  • Speed: SHA-2 is designed to be computationally efficient, making it fast for attackers to compute millions of hashes per second.
  • No Key Stretching: It applies the hashing algorithm once, making it unsuitable for password storage without additional mechanisms like manual iteration loops.
  • No Salt Integration: SHA-2 does not inherently incorporate salt to protect against precomputed attacks (e.g., rainbow tables).
PBKDF2 overcomes the limitations of SHA-2:

Why PBKDF2 over SHA-2 for Password Hashing?

Example: PBKDF2 Iterations vs. SHA-2

Imagine hashing a password with a salt using both methods:

Using SHA-2:
  • Password: “Test@1234”
  • Salt: “RandomSalt”
  • Hash: SHA256(“Test@1234” + “RandomSalt”)
  • Single operation, fast computation.
Using PBKDF2:
  • Password: “Test@1234”
  • Salt: “RandomSalt”
  • Iterations: 100,000
  • Steps:
    1. Combine password and salt.
    2. Hash the result.
    3. Use the output as input for the next hash.
    4. Repeat 100,000 times.
  • Final Hash: The output after 100,000 iterations.

Storing passwords securely is crucial to safeguarding user data and maintaining trust in your application. Always use PBKDF2 (or similar algorithms like Argon2 or BCrypt) for password hashing in modern applications.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *