Extended implementation of PasswordStrengthValidator for Symfony
Table of Contents
NOTE
The following code does not claim to be the best solution. It is just one way to solve the problem. Feel free to suggest improvements, rewrite, etc.
Introduction
While building https://kit.domains, I focused heavily on security. One of the most important parts of security is password strength.
Symfony has the PasswordStrengthValidator, which is a great constraint for checking the strength of a password.
However, it does not meet all my needs. For example, you can't add requirements like:
- Password must be at least 8 characters long
- Password must contain at least one uppercase letter
- Password must contain at least one lowercase letter
- Password must contain at least one number
- Password must contain at least one special character
Let's fix this.
Source code
Requirements
My approach has dependencies on the php-intl extension. See the installation documentation: https://www.php.net/manual/en/intl.installation.php
Code
Constraint
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class PasswordStrengthExtended extends Constraint
{
public string $message = 'The password is too weak. Use at least {{ length }} characters, including {{ minUppercase }} uppercase letters, {{ minLowercase }} lowercase letters, {{ minNumbers }} numbers and {{ minSpec }} special characters.';
public function __construct(
public int $minUppercase = 1,
public int $minLowercase = 1,
public int $minNumbers = 1,
public int $minSpec = 1,
public int $minLength = 8,
?array $options = null,
?array $groups = null,
mixed $payload = null,
?string $message = null
) {
parent::__construct($options, $groups, $payload);
$this->message = $message ?? $this->message;
}
}
Validator
Add the validator class. There is no need to set it up in services.yaml
; Symfony will find it automatically.
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class PasswordStrengthExtendedValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof PasswordStrengthExtended) {
throw new UnexpectedTypeException($constraint, PasswordStrengthExtended::class);
}
if (null === $value || '' === $value) {
return;
}
if (!\is_scalar($value) && !$value instanceof \Stringable) {
throw new UnexpectedValueException($value, 'string');
}
$arrayValue = str_split($value);
$upper = count(array_filter($arrayValue, fn($l) => \IntlChar::isupper($l)));
$lower = count(array_filter($arrayValue, fn($l) => \IntlChar::islower($l)));
$isDigit = count(array_filter($arrayValue, fn($l) => \IntlChar::isdigit($l)));
$isSymbol = count(array_filter($arrayValue, fn($l) => !\IntlChar::isalpha($l) && !\IntlChar::isdigit($l)));
if ($constraint->minUppercase > $upper
|| $constraint->minLowercase > $lower
|| $constraint->minNumbers > $isDigit
|| $constraint->minSpec > $isSymbol
) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ length }}', $constraint->minLength)
->setParameter('{{ minUppercase }}', $constraint->minUppercase)
->setParameter('{{ minLowercase }}', $constraint->minLowercase)
->setParameter('{{ minNumbers }}', $constraint->minNumbers)
->setParameter('{{ minSpec }}', $constraint->minSpec)
->addViolation();
}
}
}
That's it! Hope it was helpful for you.