How to check for old passwords using Spring "matches" method?

Peter Penzov :
@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.

Manuel :

I think you code has several problems.

1. The type of 'passwordEncoder'

There are different types of password encoder, depending on the actual used encoding algorithm. If the type of 'passwordEncoder' is e.g., 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 e.g., "topSecret123", and another user has the same password "topSecret123", you method

oldPasswordsService.findEncryptedPassword(...)

will return multiple entries, instead of one.

That will result in e.g., a NonUniqueResultException or something.

1.1 A possible solution:

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.

1.2 Another possible solution

Use a e.g., 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.

2. The actual problem

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);
}

3. OldPasswords entities

You neither have to make the @Id column unique=true, nor nullable=false no updateable=false.

4. Mixing layers

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.

5. Exposing information

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.

Edit:

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();

}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=417330&siteId=1