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.
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.