Storing passwords safely (C#)

Karen Payne - Nov 3 - - Dev Community

Introduction

Learn and easy way to stored passwords in a SQL-Server database table using an NuGet package BCrypt.Net-Next and Microsoft EF Core using a value converter.

Many new developers will store passwords in a text file or a database table which leaves these passwords open to prying eyes. Passwords should never be stored as plain text, instead they should be hashed.

Note
What is presented will work with other databases such as SQLite, Oracle, PostgreSQL and others. Also, data operations are not tied to EF Core, the base operations can be done for instance with Dapper.

Important

There are more secure methods to secure passwords. What is presented here an attacker that gains access to password hashes can still try to use brute force on them. With that known what is presented will not satisfy security of passwords for an enterprise like Amazon on a bank for instance.

BCrypt.Net-Next is a great start over no security such as plain text passwords in a database table.

For ASP.NET Core an option is PasswordHasher<TUser> Class and a great article Exploring the ASP.NET Core Identity PasswordHasher on PasswordHasher that even Microsoft references.

Projects

Project name Description
BCrypt.Net-Next
AdminApplication Used to create mocked users
DapperSample Dapper example
EF_Core_ValueConversionsEncryptProperty Creates database
HashingNoDatabaseApp Password hashing no database
RazorPagesSample ASP.NET Core example
Other
GeneratePasswordApp Example to get random/secure passwords
NetDevPackHasherArgonApp Uses NetDevPack.Security.PasswordHasher.Argon2 NuGet package

Source code

Requires

Microsoft Visual Studio 2022 or higher with .NET Core 8 available. Other editors/IDE like Rider and Microsoft VS-Code will work except for the Windows Forms project

Table structure

The base structure has enough columns to show password hashing, a primary key, username and password. More columns may be appropriate for business requirements.

structure for table

EF Power tools

EF Power Tools Visual Studio extension was used to reverse engineer the database, yes, no migrations were used.

Sample project

The project is an ASP.NET Core project which has two pages performing a login with a hashed password. The only difference between the two pages is one provide a toggle reveal of password and the other does not.

ASP.NET Core web page for login

Setup

<ItemGroup>
   <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
   <Using Include="BCrypt.Net.BCrypt" Alias="BC" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

To keep the code simple, this project works against a single user in the database which is created and populated in a console project named EF_Core_ValueConversionsEncryptProperty.

Running this project will use EF Core to:

  • Create the database, if the database already exists it will be recreated.
  • AuthenticateUser validate the new entry works with the correct plain text password
  • NotAuthenticateUser validate the new entry works with the incorrect plain text password

Code

internal partial class Program
{
    private static async Task Main(string[] args)
    {

        await Examples.CreateDatabase();
        Console.WriteLine();
        await Examples.AuthenticateUser();
        Console.WriteLine();
        await Examples.NotAuthenticateUser();

        ExitPrompt();
    }
}
Enter fullscreen mode Exit fullscreen mode

The important aspect is using a value converter in OnModelCreating as shown below.

  • First part of HasConversion hashes the given plain text password into the database table.
  • The second part of HasConversion retrieves the hashed password from the table
public class Context : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
            v => BC.HashPassword(v),
            v => v);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .LogTo(new DbContextToFileLogger().Log,
                [DbLoggerCategory.Database.Command.Name],
                LogLevel.Information)
            .UseSqlServer(ConnectionString())
            .EnableSensitiveDataLogging();
}
Enter fullscreen mode Exit fullscreen mode

Note
If not using ASP.NET Core, the code provided above should be enough to hash passwords. And if not using EF Core, perhaps Dapper simply BC.HashPassword(plain text password).

Back to the ASP.NET Core project.

For dependency injection, add the following interface.

/// <summary>
/// Defines methods for authenticating users within the application.
/// </summary>
public interface IAuthentication
{
    /// <summary>
    /// Validates the specified user against the provided context.
    /// </summary>
    /// <param name="user">The user to validate, containing user credentials.</param>
    /// <param name="context">The database context used to retrieve user information.</param>
    /// <returns>
    /// A tuple where the first element indicates whether the user is valid, 
    /// and the second element is the user's ID if validation is successful, or -1 if not.
    /// </returns>
    (bool, int) ValidateUser(User user, Context context);
}
Enter fullscreen mode Exit fullscreen mode

Followed by the class to validate a password.

public class Authentication : IAuthentication
{
    /// <summary>
    /// Validates the specified user by comparing the provided password with the stored password in the database.
    /// </summary>
    /// <param name="user">The user object containing the credentials to validate.</param>
    /// <param name="context">The database context used to access the stored user data.</param>
    /// <returns>
    /// A tuple containing a boolean and an integer:
    /// <list type="bullet">
    /// <item>
    /// <description><c>true</c> if the provided password matches the stored password for the user; otherwise, <c>false</c>.</description>
    /// </item>
    /// <item>
    /// <description>The user's ID if the password matches; otherwise, -1.</description>
    /// </item>
    /// </list>
    /// </returns>
    /// <remarks>
    /// This method utilizes the BCrypt library to verify the password and logs the result of the authentication attempt.
    /// </remarks>      
    public (bool, int) ValidateUser(User user, Context context)
    {
        var current = context.Users.FirstOrDefault(x => x.Name == user.Name);
        return current is null ? 
            (false, -1) : 
            (BC.Verify(user.Password, current.Password), current.Id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Add the following to Program.cs

builder.Services.AddScoped<IAuthentication, Authentication>();
Enter fullscreen mode Exit fullscreen mode

For the login page

A primary constructor is used for the DbContext and authentication work.

public class IndexModel(Context context, IAuthentication authentication) : PageModel
Enter fullscreen mode Exit fullscreen mode

A property for the mocked user.

[BindProperty]
public User CurrentUser { get; set; }
Enter fullscreen mode Exit fullscreen mode

A property which is displayed to indicate success or failure. Of course a dialog may be used.

public string Message { get; set; } = "";
Enter fullscreen mode Exit fullscreen mode

Frontend, there is a button with an event handler.

<button type="submit" class="btn btn-primary mb-3" asp-page-handler="ValidateUser">
    Login
</button>
Enter fullscreen mode Exit fullscreen mode

OnPost event for the above button which performs validation and uses SeriLog to show results along with setting text for a Bootstrap 5.3 alert @Html.Raw(Model.Message).

public void OnPostValidateUser()
{

    var (authenticated, id) = authentication.ValidateUser(CurrentUser!, context);

    Log.Information(authenticated ?
        "{Id,-3} User {Name} authenticated" :
        "User {Name} not authenticated", id,CurrentUser.Name);

    Message = authenticated ? "Authenticated" : "Not authenticated";
}
Enter fullscreen mode Exit fullscreen mode

Note that is the table configuration, for this sample reading one mocked user the HasConversion is not needed but would be needed for a real application accepting new users.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RazorPagesSample.Models;

namespace RazorPagesSample.Data.Configurations;

public partial class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> entity)
    {
        entity.ToTable("User");

        entity.Property(e => e.Name).IsRequired();
        entity.Property(e => e.Password).IsRequired();

        entity.Property(e => e.Password).HasConversion(
            v => BC.HashPassword(v),
            v => v);

        OnConfigurePartial(entity);
    }

    partial void OnConfigurePartial(EntityTypeBuilder<User> entity);

}
Enter fullscreen mode Exit fullscreen mode

Adding more users

In the project AdminApplication (Windows forms) done cheaply using NuGet package Bogus to generate users.

  • Keeps the first record
  • If the database does not exists the app stops with a notice and ends gracefully.

Form to create mocked users

To get at the plain text passwords used so they can be used UsersExposed.json is created in the application folder.

[
  {
    "Id": 2,
    "Name": "Arthur_Anderson37",
    "Password": "wee98uD_Yj"
  },
  {
    "Id": 3,
    "Name": "Allen51",
    "Password": "CwjTmtAVwz"
  },
  {
    "Id": 4,
    "Name": "Inez_Skiles26",
    "Password": "3p0jAJh8IV"
  },
  {
    "Id": 5,
    "Name": "Marlon.Kreiger1",
    "Password": "LMa_iXHiMW"
  },
  {
    "Id": 6,
    "Name": "Cody_Davis",
    "Password": "z4_qWZWs7p"
  }
]
Enter fullscreen mode Exit fullscreen mode

See also

Summary

With the code samples provided there is zero reasons to store plain text passwords in a database.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .