Search code examples
asp.netasp.net-web-apipassword-protectionpassword-hashbcrypt.net

hash and salt problem when using IPasswordHasher<User> and BCrypt algorhitm


I faced problem with hashing, salting and verifying password in ASP.NET. I am creating a new User and then using hashing method. But when I try to get some resources which requires Authorization and I enter the same username and password as I saved in database the result is failed.

Here is my password hasher class:

using Microsoft.AspNetCore.Identity;

namespace FlowerShop.ApplicationServices.Components.PasswordHasher
{
    public class BCryptPasswordHasher<User> : IPasswordHasher<User> where User : class
{
    
    public string HashPassword(User user, string password)
    {  
        return BCrypt.Net.BCrypt.HashPassword(password, 12);
    }

    public PasswordVerificationResult VerifyHashedPassword(User user, string hashedPassword, string providedPassword)
    {
        var isValid = BCrypt.Net.BCrypt.Verify(providedPassword, hashedPassword);

        if (isValid && BCrypt.Net.BCrypt.PasswordNeedsRehash(hashedPassword, 12))
        {
            return PasswordVerificationResult.SuccessRehashNeeded;
        }

        return isValid ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
    }
}

This is my authentication class:

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly IQueryExecutor queryExecutor;
    private readonly IPasswordHasher<User> passwordHasher;

    public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, 
        IQueryExecutor queryExecutor, IPasswordHasher<User> passwordHasher)
        : base(options, logger, encoder, clock)
    {
        this.queryExecutor = queryExecutor;
        this.passwordHasher = passwordHasher;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var endpoint = Context.GetEndpoint();
        if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
        {
            return AuthenticateResult.NoResult();
        }

        if (!Request.Headers.ContainsKey("Authorization"))
        {
            return AuthenticateResult.Fail("Missing Authorization Header");
        }

        User user = null;

        try
        {
            var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
            var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
            var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
            var username = credentials[0];
            var providedPassword = passwordHasher.HashPassword(user, credentials[1]);

            var query = new GetUserQuery()
            {
                UserName = username
            };
            user = await this.queryExecutor.Execute(query);
                           
            if (user == null || passwordHasher.VerifyHashedPassword(user, user.PasswordHash, providedPassword) 
                == PasswordVerificationResult.Failed)
            {
                return AuthenticateResult.Fail("Invalid Authorization Header");
            }
        }
        catch
        {
            return AuthenticateResult.Fail("Invalid Authorization Header");
        }

        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.Role, user.Role.ToString()),
            new Claim(ClaimTypes.Email, user.Email),
        };
        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);
        
        return AuthenticateResult.Success(ticket);
    }
}

And in this place I am creating a new User:

        using MediatR;
        using Microsoft.AspNetCore.Identity;
        using System.Threading;
        using System.Threading.Tasks;

        public class AddUserHandler : IRequestHandler<AddUserRequest,
    AddUserResponse>
        {
        private readonly ICommandExecutor commandExecutor;
        private readonly IQueryExecutor queryExecutor;
        private readonly IMapper mapper;
        private readonly IPasswordHasher<User> passwordHasher;

            public AddUserHandler(ICommandExecutor commandExecutor,
    IQueryExecutor queryExecutor, 
                IMapper mapper, IPasswordHasher<User> passwordHasher)
            {
                this.commandExecutor = commandExecutor;
                this.queryExecutor = queryExecutor;
                this.mapper = mapper;
                this.passwordHasher = passwordHasher;
            }

            public async Task<AddUserResponse> Handle(AddUserRequest
    request, CancellationToken cancellationToken)
            {
            var query = new GetUserQuery()
            {                
                UserName = request.UserName,
                Email = request.Email
            };

            var getUser = await this.queryExecutor.Execute(query);
            if (getUser != null)
            {
                if (getUser.UserName == request.UserName)      

              {
                        return new AddUserResponse()
                        {
                            Error = new ErrorModel(ErrorType.ValidationError +
    "! The name is already taken.")
                        };
                    }
                if (getUser.Email == request.Email)
                {
         

               return new AddUserResponse()
                        {
                            Error = new ErrorModel(ErrorType.ValidationError +
    "! Email address is in use.")
                        };
                    }
                return new AddUserResponse()
                {
                    Error = new ErrorModel(ErrorType.Conflict)
                };
            }

            request.PasswordHash = passwordHasher.HashPassword(getUser,
request.Password);

            var user = this.mapper.Map<User>(request);
            var command = new AddUserCommand() 
            { 
                Parameter = user 
            };
            var addedUser = await this.commandExecutor.Execute(command);
            var response = new AddUserResponse()
            {
                Data =
this.mapper.Map<Domain.Models.UserDTO>(addedUser)
            };

            return response;
        }
    }

This is my Startup.cs :

      public void ConfigureServices(IServiceCollection services)
            {
                services.AddAuthentication("BasicAuthentication")
                        .AddScheme<AuthenticationSchemeOptions,
    BasicAuthenticationHandler>("BasicAuthentication", null);
    
                services.AddScoped<IPasswordHasher<User>,
    BCryptPasswordHasher<User>>();
}

Maybe first of all, is it all correct implemented?

  1. Is hash in AddUserHandler correct assigned to request.PasswordHash?
  2. How to retrieve salt and assign to request.PasswordSalt? Sorry for any unclear things if they occur.

Any feedback and help will be appreciated.

Thanks in advance :)

Edit:

for example if I add user with password "pass123" and it is stored in database as 'user.PasswordHash = "$2a$12$Iqpy7FyQh/pt2O8upTtG5eOQKzo1V395wRNdAXPpp5Qf.NQ.KxUyy"' and provided password after hashing is 'providedPassword = "$2a$12$9vSz8Sw/WtmqGY6jyDiTleN/btZ0wXJkXdoB3sDpANVIIDGBpaqT."'


Solution

  • I fixed the bug if anyone needs to use it in thee future. The problem was in my authentication class. In place of:

    var username = credentials[0];
    var providedPassword = passwordHasher.HashPassword(user, credentials[1]);
    

    Should be:

    var username = credentials[0];
    var providedPassword = credentials[1];
    

    I am sure that I have checked it a few times but somehow didn't work then. Anyway, it finally works properly.