@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private OldPasswordsService oldPasswordsService;
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches)
{
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
else
{
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
}
Table for old passwords:
@Table(name = "old_passwords")
public class OldPasswords implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", unique = true, updatable = false, nullable = false)
private int id;
@Column(name = "encrypted_password", length = 255)
private String encryptedPassword;
@Column(name = "password_owner_id", length = 4)
private Integer passwordOwnerId;
@Column(name = "created_at")
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime createdAt;
But I get java.util.NoSuchElementException: No value present
. Do you know how I can implement a logic which compares old and new passwords?
EDIT: I tried this design:
SQL query:
public List<OldPasswords> findByOwnerId(Integer ownerId) {
String hql = "select e from " + OldPasswords.class.getName() + " e where e.passwordOwnerId = :passwordOwnerId ORDER BY e.createdAt DESC";
TypedQuery<OldPasswords> query = entityManager.createQuery(hql, OldPasswords.class).setMaxResults(3).setParameter("passwordOwnerId", ownerId);
List<OldPasswords> list = query.getResultList();
return list;
}
Endpoint:
@PostMapping("reset_password")
public ResponseEntity<?> reset(@RequestBody PasswordResetDTO resetDTO) {
return this.userService.findByLogin(resetDTO.getName()).map(user -> {
Integer userId = user.getId();
List<OldPasswords> list = oldPasswordsService.findByOwnerId(userId);
if(!list.isEmpty() && !list.isEmpty()) {
for (int i = 0; i<list.size(); i++){
OldPasswords value = list.get(i);
boolean matches = passwordEncoder.matches(resetDTO.getPassword(), value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
}
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswords.setPasswordOwnerId(userId);
oldPasswordsService.save(oldPasswords);
user.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
user.setResetPasswordToken(null);
userService.save(user);
return ok().build();
}).orElseGet(() -> notFound().build());
}
But when I change the code several times with the same password the error PASSWORD_ALREADY_USED
is not shown.
I think you code has several problems.
There are different types of password encoder, depending on the actual used encoding algorithm. If the type of 'passwordEncoder' is eg, MD5, SHA1 you will most likely have a password collision, as you expect the passwords to be unique.
Means if a user has a weak password eg, "topSecret123", and another user has the same password "topSecret123", you method
oldPasswordsService.findEncryptedPassword(...)
will return multiple entries, instead of one.
That will result in eg, a NonUniqueResultException
or something.
Associated the password with the username. Fetch the user given by the userId (or something similar) and make the password checks with that user's password.
Use a eg, BCryptPasswordEncoder
. This type PasswordEncoder
takes care of adding a salt to your has. This avoids having possible duplicate entries in your database. These types of password encoders are unable to calculate the password or check if a password matches if only the "password" is provided. As they are using a "salt" with your encoded password, these types of password encoders need the (salt+hashed) password as input, in order to check if a provided password matches.
The code
OldPasswords value = list.get();
is the problem. The Optional<OldPasswords>
may contain a null
value. A call .get()
on an Optional
will null
value will result in java.util.NoSuchElementException: No value present
.
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
if (!list.isPresent()) {
return new ResponseEntity<>("The old password value you've entered is incorrect", HttpStatus.UNAUTHORIZED);
}
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches)
{
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
else
{
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
}
You neither have to make the @Id
column unique=true
, nor nullable=false
no updateable=false
.
The code you've posted uses services and updates domain objects. And it does return a ResponseEntity
. You clearly mix different layers of the application into one.
You expose the information, that the (new) password chosen is already used by another user! Don't do that! That adds up because of point 1. I've listed.
After the question was updated, I want to update my answer as well. As the code snipped in the updated question does not compile, I wanted to make a very simple, basic example based on what I know from the code snippets.
I do not comment on the concept of the "reset password" design as showed in the question, as lots of code is missing in between.
The whole code including tests can be found here: https://github.com/mschallar/so_oldpasswords_example
The code for the funtion requested in the question is:
@PostMapping("reset_password")
public ResponseEntity<?> reset(@RequestBody PasswordResetDTO resetDTO) {
Optional<User> findByLogin = this.userService.findByLogin(resetDTO.getName());
if (!findByLogin.isPresent()) {
return ResponseEntity.notFound().build();
}
User user = findByLogin.get();
Integer userId = user.getUserId();
String encodedPassword = passwordEncoder.encode(resetDTO.getPassword());
for (OldPasswords oldPasswords : oldPasswordsService.findByOwnerId(userId)) {
if (passwordEncoder.matches(resetDTO.getPassword(), oldPasswords.getEncryptedPassword())) {
// Information: Don't do that! Don't reveal that another user already has such a password!
log.info("Password already used.");
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(encodedPassword));
oldPasswords.setPasswordOwnerId(userId);
oldPasswordsService.save(oldPasswords);
user.setEncryptedPassword(encodedPassword);
user.setResetPasswordToken(null);
userService.save(user);
return ResponseEntity.ok().build();
}
check that the if the list of optional type contains valid required value
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
OldPasswords value = list.orElse(null);
if(value != null) {
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
return new ResponseEntity<>("New Password Saved" , HttpStatus.OK);
}
return new ResponseEntity<>("Incorrect Password Provided" , HttpStatus.BAD_REQUEST);
or like @Manuel suggested
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
if(list.isPresent()) {
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
return new ResponseEntity<>("New Password Saved" , HttpStatus.OK);
}
return new ResponseEntity<>("Incorrect Password Provided" , HttpStatus.BAD_REQUEST);
I have resolved this using a simple solution.
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
if (encoder.matches(newPassword, oldPassword))
{
System.out.println("Successfully logged in!");
}
else
{
System.out.println("Incorrect Password.");
}
newPassword
and oldPassword
must be in String so that it matches both passwords.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.