How to check is a password was compromised in PHP
IMPORTANT
This article is based on the Symfony in-built constraint.
For non-Symfony environments, you can copy the implementation from source code and use it in your project.
Introduction
Starting from version 4.3, Symfony has a new feature to check if a password is compromised. This feature is based on the Have I Been Pwned API.
I'm sure most of you missed this constraint, so I decided to write this article to show you how to use it.
TIP
If you are curious about password strength validation, visit my other article Extended implementation of PasswordStrengthValidator
Setup
Good news: if you are using Symfony 4.3 or higher, you already have everything you need installed. If not, it's time to upgrade.
How does it work?
Example:
echo sha1('My$upp3rP@ssw0rd'); //e4168877f1be8b69fcc86c5bbdf5cfc9e7b60e3c
The constraint sends only the first 5 characters of the SHA-1 hash of the password to the API:
e4168
The API returns a list of hashes that match the prefix, BUT without the first 5 characters:
877f1be8b69fcc86c5bbdf5cfc9e7b60e3c:12
38c27b10b195b32e6e878054be6d92cb523:70
This means that the prefix e4168
was found in 2 hashes (hashed passwords) a total of 82 times, where:
877f1be8b69fcc86c5bbdf5cfc9e7b60e3c
is the rest of the hash.
12
is the number of times those 5 characters were found in the database.
The constraint then checks if the full hash e4168877f1be8b69fcc86c5bbdf5cfc9e7b60e3c
matches one of the hashes from the list e4168 + 877f1be8b69fcc86c5bbdf5cfc9e7b60e3c
and if the number of times is greater than or equal to the threshold
option value.
Is that secure?
Yes.
Instead of sending the full password to the API, the constraint sends only the first 5 characters of the SHA-1 hash of the password.
Usage
Entity
namespace App\Entity;
+ use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
class User
{
+ #[NotCompromisedPassword]
private string $password;
}
You can also use the NotCompromisedPassword
constraint in the form type.
Form
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
+ use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('password', PasswordType::class, [
'constraints' => [
+ new NotCompromisedPassword()
]
]);
}
}
Limitation
Rate limiting: https://haveibeenpwned.com/API/v2#RateLimiting
TL;DR: There is no rate limit on the Pwned Passwords API.
Tricks
A few tricks that might be useful:
framework:
validation:
not_compromised_password:
enabled: false # Disable the check. Will always pass.
endpoint: 'https://yourimplementation/%s' # Change the endpoint to your own implementation.
# It must be fully compatible with the original API
Or
use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
new NotCompromisedPassword(
'enabled' => false, // Skip the check.
'endpoint' => 'https://yourimplementation/%s' // Change the endpoint to your own implementation.
)
That's it! Hope it was helpful for you.