Password Reuse Prevention: How to Implement Secure Password History

Password security is a critical concern that can’t be overlooked in today’s digital age. With 70% password reuse rate among users who had their data exposed in breaches¹, it’s evident that more needs to be done. This article focuses on implementing a basic password reuse prevention feature as part of your authentication system.

Goce Bo
5 min readSep 14, 2023

--

The Problem with Password Reuse

Password reuse amplifies the consequences of data breaches, turning a single security incident into a potential gateway for multiple points of unauthorized access. When a set of credentials is compromised, it’s not just one account that’s at risk. If the same password is used across multiple platforms, attackers can exploit this vulnerability to gain widespread access. This creates a domino effect, making password reuse an urgent issue to address in any authentication system.

What is Password Reuse Prevention?

Password reuse prevention is a security feature implemented within authentication systems to prevent users from reusing a set of previously used passwords. The primary goal is to mitigate the risks associated with password reuse, which can lead to unauthorized access across multiple accounts when one is compromised.

In practical terms, when a user tries to change or reset their password, the system compares the new password against a stored list of the user’s previous passwords. If the new password matches any in the list, the system will prevent the password change and prompt the user to try a different password.

This feature is beneficial for several reasons:

  • By ensuring that users are regularly creating unique passwords, you diminish the risk of a security breach affecting multiple accounts.
  • This system implicitly educates users about good security hygiene by enforcing a practice of varied, unique credentials.
  • Many regulatory frameworks require companies to enforce strong password policies, including not allowing the reuse of previous passwords.

Implementing password history is a proactive step toward better security hygiene and is often a recommended or required practice in various industry security frameworks and regulations. However, it’s crucial to acknowledge that storing this history does introduce additional security considerations, as you’re increasing the amount of sensitive data that needs to be securely stored. As such, it’s essential that these historical hashes be given the same level of protection as current user credentials.

Implementing Secure Password History

Implementing a secure password history involves several components, each of which plays a crucial role in enhancing your authentication system’s security.

Password Hashing

The PasswordHasher ensures that any implementing class can securely hash and validate passwords. Its primary functions are to transform a plaintext password into a secure hash and to verify if a given plaintext password corresponds to a stored hash.

interface PasswordHasher
{
public function hashPassword(string $plaintextPassword) : string;
public function isPasswordValid(string $plaintextPassword, string $hashedPassword) : bool;
}

Secure hashing algorithms like bcrypt or Argon2 are recommended to ensure optimal security. For more details, you can consult the OWASP Password Storage Cheat Sheet.

The Password History

The PasswordHistory class encapsulates the logic needed to prevent password reuse. By maintaining a list of a user's most recent passwords, it can effectively stop the reuse of older, possibly compromised, passwords.

Each instance of the password history is tied to a specific user, ensuring that each one has its own isolated history of previously used passwords.

Adding a new password to the list is a straightforward operation. If the list of stored hashes exceeds a predefined limit, the class will automatically remove the oldest hash to make room for the new one. This constant turnover ensures that only the most recent passwords are considered during any password change or reset action.

Another important feature is the ability to validate new passwords against the stored history. By iterating over its list of previous hashed passwords and using the provided PasswordHasher for validation, it can check if the password is reused.

final class PasswordHistory
{
private const PASSWORD_HISTORY_LENGTH = 10;

public function __construct(
public readonly UserId $userId,
private array $passwords
) {
}

public function add(string $hashedPassword) : void
{
$this->passwords[] = $hashedPassword;
$this->passwords = array_slice($this->passwords, -self::PASSWORD_HISTORY_LENGTH, self::PASSWORD_HISTORY_LENGTH);
}

public function contains(string $plaintextPassword, PasswordHasher $passwordHasher) : bool
{
foreach ($this->passwords as $historicalPasswordHash) {
if ($passwordHasher->isPasswordValid($plaintextPassword, $historicalPasswordHash)) {
return true;
}
}
return false;
}
}

When deciding on the number of previous passwords to store in the history, it’s crucial to find a balance that aligns with your organization’s data retention policies. While an extensive history can improve security by preventing the reuse of many old passwords, storing too many could introduce increased risks, especially in the event that the password history itself is compromised.

Storage

It’s crucial to ensure that the storage containing old password hashes maintains the same level of access control and encryption as your main user data. Just because they’re “old” doesn’t mean they are less sensitive. Any compromise here can defeat the purpose of implementing a secure password history. To keep things flexible and maintainable, the storage is abstracted behind a PasswordHistoryRepository interface, allowing you to plug in any secure storage mechanism.

interface PasswordHistoryRepository
{
public function store(PasswordHistory $passwordHistory) : void;
public function find(UserId $userId) : PasswordHistory;
}

Putting Everything Together

With the fundamental components in place for managing password history, it’s time to turn our attention to integrating this functionality into the application.

In the example below, the ChangePasswordHandler runs a series of checks: verifying the old password, ensuring the new one hasn't been used before, and then safely updating the current password—all while maintaining a secure record of previous passwords.

final class ChangePasswordHandler
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly PasswordHistoryRepository $passwordHistoryRepository,
private readonly PasswordHasher $passwordHasher
) {
}

public function handle(ChangePassword $command) : void
{
$user = $this->userRepository->getById($command->userId);
if (!$this->passwordHasher->isPasswordValid($command->currentPassword, $user->hashedPassword())) {
throw new PasswordDoesNotMatchException();
}

$passwordHistory = $this->passwordHistoryRepository->find($user->id());
if ($passwordHistory->contains($command->newPassword, $this->passwordHasher)) {
throw new NewPasswordIsSameAsOneOfOldException();
}

$hashedPassword = $this->passwordHasher->hashPassword($command->newPassword);
$user->changePassword($hashedPassword);
$this->userRepository->store($user);

$passwordHistory->add($hashedPassword);
$this->passwordHistoryRepository->store($passwordHistory);
}
}

This logic should be applied uniformly, whether the user is changing their password or resetting it. Ensuring this consistency across different password-related actions further strengthens the system’s resilience against security risks associated with password reuse.

UI Considerations

When a user attempts to reuse a password, the system should provide a clear but secure error message. While it’s essential to keep users informed, it’s equally important to not divulge too much information that could be a security risk.

By carefully crafting your frontend messages and cues, you can provide a seamless user experience that aligns with your backend security measures.

Beyond the Core Implementation

While a robust password history system offers substantial security benefits, going the extra mile can make your system even more secure. For instance, adding a list of passwords known to be compromised in public breaches provides an extra safeguard. Likewise, creating a list of organization-specific disallowed passwords, such as those using the company name, can prevent easily guessable combinations.

When a user’s password is rejected, it’s important to provide clear but secure feedback, so they understand why it was rejected. By enhancing the core security features of your password history system with these additional measures, you not only strengthen your system’s defenses but also increase user trust.

Sources

¹ 70% Password Reuse: Password Security Needs a Forced Reset [source]

--

--