73 lines
2.2 KiB
PHP
73 lines
2.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Domain\User;
|
|
|
|
/**
|
|
* Applies the same password rules to initial credentials and password resets.
|
|
* It validates only; callers remain responsible for hashing accepted passwords.
|
|
*/
|
|
final class PasswordPolicy
|
|
{
|
|
public const MINIMUM_LENGTH = 12;
|
|
|
|
/** @return array{valid: bool, errors: list<string>} */
|
|
public function validate(mixed $password): array
|
|
{
|
|
$errors = [];
|
|
if (!is_string($password)) {
|
|
return ['valid' => false, 'errors' => ['password must be a string']];
|
|
}
|
|
|
|
if (mb_strlen($password) < self::MINIMUM_LENGTH) {
|
|
$errors[] = 'minimum length';
|
|
}
|
|
if (preg_match('/\p{Lu}/u', $password) !== 1) {
|
|
$errors[] = 'uppercase letter';
|
|
}
|
|
if (preg_match('/\p{Ll}/u', $password) !== 1) {
|
|
$errors[] = 'lowercase letter';
|
|
}
|
|
if (preg_match('/\p{N}/u', $password) !== 1) {
|
|
$errors[] = 'number';
|
|
}
|
|
if (preg_match('/[^\p{L}\p{N}\s]/u', $password) !== 1) {
|
|
$errors[] = 'symbol';
|
|
}
|
|
if ($this->isCommonPlaceholder($password)) {
|
|
$errors[] = 'common placeholder';
|
|
}
|
|
|
|
return ['valid' => $errors === [], 'errors' => $errors];
|
|
}
|
|
|
|
/** @return array{valid: bool, errors: list<string>} */
|
|
public function validateInitial(mixed $password): array
|
|
{
|
|
return $this->validate($password);
|
|
}
|
|
|
|
/** @return array{valid: bool, errors: list<string>} */
|
|
public function validateReset(mixed $password): array
|
|
{
|
|
return $this->validate($password);
|
|
}
|
|
|
|
public function isValid(mixed $password): bool
|
|
{
|
|
return $this->validate($password)['valid'];
|
|
}
|
|
|
|
private function isCommonPlaceholder(string $password): bool
|
|
{
|
|
$canonical = strtolower($password);
|
|
$canonical = strtr($canonical, ['@' => 'a', '$' => 's', '0' => 'o']);
|
|
$canonical = preg_replace('/[^a-z0-9]/', '', $canonical) ?? '';
|
|
|
|
return preg_match(
|
|
'/^(?:password|changeme|welcome|admin|administrator|temporary|temppassword|qwerty|letmein)[0-9]*$/',
|
|
$canonical,
|
|
) === 1;
|
|
}
|
|
}
|