How to check is a password was compromised in PHP

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
  1. The constraint sends only the first 5 characters of the SHA-1 hash of the password to the API: e4168

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

src/Entity/User.php
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

src/Form/UserType.php
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:

config/packages/framework.yaml

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.