Password hashing in register with Dapper

Atilla Rüstəmli 60 Reputation points
2025-05-28T13:13:31.1933333+00:00

Hi, I switched from EF to Dapper in a days and this is first time I'm writing register and login.

The thing is that, in Dapper we have to hash Password manually and GPT recommended to use BCrypt. That is okay, but I'm suspicious of is that a best case. We are writing on Clean Architecture and using Repository patter with CQRS. I'm curious about in which layer I should write hashing part? Again, GPT said that it is best to write in Service layer and that seemed appropriate at first look, but this'll violate SRP rule.

Can you share best cases about writing register/login? Should I add hashing part and all those things to service layer, or you have even better way to handle these API?

Every advice appreciated. Thanks from now!🙏

Developer technologies | ASP.NET | ASP.NET API
0 comments No comments
{count} votes

Accepted answer
  1. Bruce (SqlWork.com) 77,686 Reputation points Volunteer Moderator
    2025-05-28T21:33:10.2166667+00:00

    as passwords are a one way hash, it typically just data passed the database layer. normally you store the hash and the seed.

    most of the code just uses the hashed data, even the compare. the only module that needs to know how to hash is the password input. the input module will need to read the seed value and calculate the hash.

    the authentication service layer should supply the hash function. it certainly does not belong in the repository. the repo should just store and retrieve the hash and seed.

    0 comments No comments

1 additional answer

Sort by: Most helpful
  1. SurferOnWww 4,706 Reputation points
    2025-05-29T02:15:17.19+00:00

    I'm curious about in which layer I should write hashing part?

    In the ASP.NET Core Identity, password hashing (PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations) is taken care by the Identity Manager Layer.

    enter image description here

    See the Microsoft document Custom storage providers for ASP.NET Core Identity.

    Why don't you consider customizing the ASP.NET Core Identity? As mentioned in the above document you will be able to use a different data access approach, such as Dapper.

    You will have to create only the data source, the data access layer, and the store classes that interact with this data access layer (the green and grey boxes in the diagram above).

    Other than password hashing, the functionalities available in the ASP.NET Core Identity such as issue of authentication cookie, redirect to login page, validation of user name and password, prevention of duplicated user name, persistence and sliding expiration will be provided by the Identity Manager and ASP.NET Core App layers.

    Blow is a sample code of UserStore in Identity Store layer. The code uses EF Core. You will be able to rewrite the code to use Dapper instead of EF Core.

    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using MvcIdCustom.Models;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
     
    namespace MvcIdCustom.DAL
    {
        public class UserStore : IUserStore<User>, 
                                 IUserPasswordStore<User>, 
                                 IQueryableUserStore<User>
        {
            private DataContext db;
     
            public UserStore(DataContext db)
            {
                this.db = db;
            }
     
            public void Dispose()
            {
                Dispose(true);
            }
     
            protected virtual void Dispose(bool disposing)
            {
                if (disposing)
                {
                    if (db != null)
                    {
                        db.Dispose();
                        db = null;
                    }                
                }
            }
     
            public IQueryable<User> Users
            {
                get { return db.Users.Select(u => u); }
            }
     
            // IUserStore<TUser>
            public Task<string> GetUserIdAsync(User user, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
     
                return Task.FromResult(user.Id.ToString());
            }
     
            // IUserStore<TUser>
            public Task<string> GetUserNameAsync(User user, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
     
                return Task.FromResult(user.UserName);
            }
     
            // IUserStore<TUser>
            public Task SetUserNameAsync(User user, 
                string userName, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
                if (string.IsNullOrEmpty(userName)) 
                    throw new ArgumentException("userName");
     
                user.UserName = userName;
     
                return Task.CompletedTask;
            }
     
            // IUserStore<TUser>
            public Task<string> GetNormalizedUserNameAsync(User user, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
     
                return Task.FromResult(user.UserName);
            }
     
            // IUserStore<TUser>
            public Task SetNormalizedUserNameAsync(User user, 
                string normalizedName, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
                if (string.IsNullOrEmpty(normalizedName)) 
                    throw new ArgumentException("normalizedName");
     
                return Task.CompletedTask;
            }
     
            // IUserStore<TUser>
            public async Task<IdentityResult> CreateAsync(User user, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
     
                db.Add(user);
     
                int rows = await db.SaveChangesAsync(cancellationToken);
     
                if (rows > 0)
                { 
                    return IdentityResult.Success;
                }
     
                return IdentityResult.Failed(
                    new IdentityError { Description = $"{user.UserName} Register Failed" });
            }
     
            // IUserStore<TUser>
            public async Task<IdentityResult> UpdateAsync(User user, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
     
                db.Update(user);
     
                int rows = await db.SaveChangesAsync(cancellationToken);
     
                if (rows > 0)
                {
                    return IdentityResult.Success;
                }
     
                return IdentityResult.Failed(
                    new IdentityError { Description = $"{user.UserName} Update Failed" });
            }
     
            // IUserStore<TUser>
            public async Task<IdentityResult> DeleteAsync(User user, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
     
                db.Remove(user);
     
                int rows = await db.SaveChangesAsync(cancellationToken);
     
                if (rows > 0)
                {
                    return IdentityResult.Success;
                }
     
                return IdentityResult.Failed(
                    new IdentityError { Description = $"{user.UserName} Deletion Failed" });
            }
     
            // IUserStore<TUser>
            public async Task<User> FindByIdAsync(string userId, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
     
                if (int.TryParse(userId, out int id))
                {
                    return await db.Users.SingleOrDefaultAsync(u => u.Id == id, 
                        cancellationToken);
                }
                else
                {
                    return await Task.FromResult<User>(null);
                }
            }
     
            // IUserStore<TUser>
            public async Task<User> FindByNameAsync(string normalizedUserName, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (string.IsNullOrEmpty(normalizedUserName))
                    throw new ArgumentException("normalizedUserName");
     
                User user = await db.Users.SingleOrDefaultAsync(
                    u => u.UserName.Equals(normalizedUserName.ToLower()), 
                    cancellationToken);
     
                return user;
            }
     
            // IUserPasswordStore<TUser>
            public Task SetPasswordHashAsync(User user, 
                string passwordHash, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
                if (string.IsNullOrEmpty(passwordHash))
                    throw new ArgumentException("passwordHash");
     
                user.PasswordHash = passwordHash;
     
                return Task.CompletedTask;
            }
     
            // IUserPasswordStore<TUser>
            public Task<string> GetPasswordHashAsync(User user, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
     
                return Task.FromResult(user.PasswordHash);
            }
     
            // IUserPasswordStore<TUser>
            public Task<bool> HasPasswordAsync(User user, 
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                if (user == null) throw new ArgumentNullException(nameof(user));
     
                return Task.FromResult(
                    !string.IsNullOrWhiteSpace(user.PasswordHash));
            }
        }
    }
    
    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.