javaspring-boothashbcrypt

Built in Spring-Boot BCrypt matches method doesn't work


I have a UserController that receives a UserDTO and creates/updates the user in the DB. The problem I'm getting is that I also have a login, and when I insert the username and password on the login form, I always get the 'Wrong Password.' exception, despite the credentials being correctly inserted.
One thing I suspect is that BCrypt is to blame, since due to the fact that it generates random salt while encoding, maybe, just maybe, the cipher text ends up being different and stuff, which is weird, since I assume that it should work. I want to know how can I fix this problem of the hashing being different & not being able to validate the userCredentials

I have tried for example encoding the received password and using the matches method via my autowired passwordEncoder, and I'm using my own authProvider.

Here's the code, let me know if you need anything else.

CustomAuthProvider.java

@Service
public class CustomAuthProvider implements AuthenticationProvider {

        private final UserServiceImpl userServiceImpl;
        private final BCryptPasswordEncoder passwordEncoder;

        @Autowired
        public CustomAuthProvider(UserServiceImpl userServiceImpl, BCryptPasswordEncoder passwordEncoder) {
            this.userServiceImpl = userServiceImpl;
            this.passwordEncoder = passwordEncoder;
        }

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            String username = authentication.getName();
            String password = authentication.getCredentials().toString();
            UserDetails userDetails = userServiceImpl.loadUserByUsername(username);

            if (!passwordEncoder.matches(password, userDetails.getPassword())) { //The problem is here evidently.
                throw new BadCredentialsException("Wrong password.");
            }

            return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return authentication.equals(UsernamePasswordAuthenticationToken.class);
        }
    }

Also, here's the loadUserByUsername method:

UserServiceImpl.java

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   UserDTO user = this.getUserByUsername(username);
    User anUser = convertToUser(user);
    ModelMapper modelMapper = new ModelMapper();
    return modelMapper.map(anUser,UserPrincipal.class);
    }
}

And here is the save method I use to save and update users, as well as the LoginController;


@Override     

public void save(UserDTO user) {         

User aUser = this.convertToUser(user);         

aUser.setPassword(passwordEncoder.encode(aUser.getPassword()));       

this.userRepository.save(aUser); } 

LoginController.java:

@RestController
public class LoginController{


    private final CustomAuthProvider providerManager;

    @Autowired
    public LoginController(CustomAuthProvider providerManager) {
        this.providerManager = providerManager;
    }

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @PostMapping("/login")
    public String login(@RequestParam("username") @NotBlank String username,
                        @RequestParam("password") @NotBlank String password, Model model) {
        if(username == null || password == null) { //This is probably not necessary
            model.addAttribute("error", "Invalid credentials");
            return "login";
        }
        try {
            Authentication auth = providerManager.authenticate(
                    new UsernamePasswordAuthenticationToken(username, password)
            );
            SecurityContextHolder.getContext().setAuthentication(auth);
            return "redirect:/notes";
        } catch (AuthenticationException e) {
            model.addAttribute("error", "Invalid credentials");
            return "login";
        }
    }

}

UserPrincipal.java

@Data
    public class UserPrincipal implements Serializable , UserDetails {
        int id;
        private String username;
        private String password;
        private Date accountCreationDate = new Date();
    
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return null;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return false;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return false;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return false;
        }
    
        @Override
        public boolean isEnabled() {
            return false;
        }
    }

UserDTO.java

@Data
public class UserDTO implements Serializable {
    int id;
    private String username;
    private String password;
    private List<Note> notes = new ArrayList<>();
}

I read several issues related to this topic, like
Spring Boot PasswordEncoder.matches always false
Spring Security - BcryptPasswordEncoder

Inconsistent hash with Spring Boot BCryptPasswordEncoder matches() method

How can bcrypt have built-in salts?

Decode the Bcrypt encoded password in Spring Security to deactivate user account

but none of those helped me solve my issue and there was no real solution to the problem since most of them don't even have an accepted answer.

EDIT: Found out that the 'matches' method only works if I insert the hashed password, not the raw password.


Solution

  • Found out my mistake:

    The setPassword method in the User class was re-hashing the hashed password which was already being hashed on the save method, thus the modelMapper.map() method used that setPassword method, therefore the passwords never matched and the password I got from the user class never matched the actual password I could see on my database.