Storing passwords safely (C#)

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 t…


This content originally appeared on DEV Community and was authored by Karen Payne

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.

Projects

Project name Description
AdminApplication Used to create mocked users
EF_Core_ValueConversionsEncryptProperty Creates database
HashingNoDatabaseApp Password hashing no database
RazorPagesSample ASP.NET Core example

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>

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();
    }
}

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();
}

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);
}

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);
    }
}

Add the following to Program.cs

builder.Services.AddScoped<IAuthentication, Authentication>();

For the login page

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

public class IndexModel(Context context, IAuthentication authentication) : PageModel

A property for the mocked user.

[BindProperty]
public User CurrentUser { get; set; }

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

public string Message { get; set; } = "";

Frontend, there is a button with an event handler.

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

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";
}

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);

}

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"
  }
]

Summary

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


This content originally appeared on DEV Community and was authored by Karen Payne


Print Share Comment Cite Upload Translate Updates
APA

Karen Payne | Sciencx (2024-11-03T16:45:04+00:00) Storing passwords safely (C#). Retrieved from https://www.scien.cx/2024/11/03/storing-passwords-safely-c/

MLA
" » Storing passwords safely (C#)." Karen Payne | Sciencx - Sunday November 3, 2024, https://www.scien.cx/2024/11/03/storing-passwords-safely-c/
HARVARD
Karen Payne | Sciencx Sunday November 3, 2024 » Storing passwords safely (C#)., viewed ,<https://www.scien.cx/2024/11/03/storing-passwords-safely-c/>
VANCOUVER
Karen Payne | Sciencx - » Storing passwords safely (C#). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/11/03/storing-passwords-safely-c/
CHICAGO
" » Storing passwords safely (C#)." Karen Payne | Sciencx - Accessed . https://www.scien.cx/2024/11/03/storing-passwords-safely-c/
IEEE
" » Storing passwords safely (C#)." Karen Payne | Sciencx [Online]. Available: https://www.scien.cx/2024/11/03/storing-passwords-safely-c/. [Accessed: ]
rf:citation
» Storing passwords safely (C#) | Karen Payne | Sciencx | https://www.scien.cx/2024/11/03/storing-passwords-safely-c/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.