-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #55 from programmatordev/YAPV-40-create-passwordst…
…rength-rule Create PasswordStrength rule
- Loading branch information
Showing
11 changed files
with
243 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# PasswordStrength | ||
|
||
Validates that the given password has reached the minimum strength required by the constraint. | ||
The strength is calculated by measuring the entropy of the password (in bits) based on its length and the number of unique characters. | ||
|
||
```php | ||
PasswordStrength( | ||
string $minStrength = 'medium', | ||
?string $minMessage = null | ||
); | ||
``` | ||
|
||
## Basic Usage | ||
|
||
```php | ||
Validator::passwordStrength()->validate('password'); // false | ||
Validator::passwordStrength()->validate('i8Kq*MBob~2W"=p'); // true | ||
Validator::passwordStrength(minStrength: 'very-strong')->validate('i8Kq*MBob~2W"=p'); // false | ||
``` | ||
|
||
> [!NOTE] | ||
> An `UnexpectedValueException` will be thrown when a `minStrength` option is invalid. | ||
> [!NOTE] | ||
> An `UnexpectedValueException` will be thrown when the input value is not a `string`. | ||
## Options | ||
|
||
### `minStrength` | ||
|
||
type: `string` default: `medium` | ||
|
||
Sets the minimum strength of the password in entropy bits. | ||
The entropy is calculated using the formula [here](https://www.pleacher.com/mp/mlessons/algebra/entropy.html). | ||
|
||
Available options are: | ||
|
||
- `weak` entropy between `64` and `79` bits. | ||
- `medium` entropy between `80` and `95` bits. | ||
- `strong` entropy between `96` and `127` bits. | ||
- `very-strong` entropy greater than `128` bits. | ||
|
||
All measurements less than `64` bits will fail. | ||
|
||
### `message` | ||
|
||
type: `?string` default: `The password strength is not strong enough.` | ||
|
||
Message that will be shown when the password is not strong enough. | ||
|
||
The following parameters are available: | ||
|
||
| Parameter | Description | | ||
|---------------------|---------------------------| | ||
| `{{ name }}` | Name of the invalid value | | ||
| `{{ minStrength }}` | Selected minimum strength | | ||
|
||
## Changelog | ||
|
||
- `0.8.0` Created |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<?php | ||
|
||
namespace ProgrammatorDev\Validator\Exception; | ||
|
||
class PasswordStrengthException extends ValidationException {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
<?php | ||
|
||
namespace ProgrammatorDev\Validator\Rule; | ||
|
||
use ProgrammatorDev\Validator\Exception\PasswordStrengthException; | ||
use ProgrammatorDev\Validator\Exception\UnexpectedOptionException; | ||
use ProgrammatorDev\Validator\Exception\UnexpectedTypeException; | ||
|
||
class PasswordStrength extends AbstractRule implements RuleInterface | ||
{ | ||
private const STRENGTH_VERY_WEAK = 'very-weak'; | ||
public const STRENGTH_WEAK = 'weak'; | ||
public const STRENGTH_MEDIUM = 'medium'; | ||
public const STRENGTH_STRONG = 'strong'; | ||
public const STRENGTH_VERY_STRONG = 'very-strong'; | ||
|
||
private const STRENGTH_OPTIONS = [ | ||
self::STRENGTH_WEAK, | ||
self::STRENGTH_MEDIUM, | ||
self::STRENGTH_STRONG, | ||
self::STRENGTH_VERY_STRONG | ||
]; | ||
|
||
private const STRENGTH_SCORE = [ | ||
self::STRENGTH_VERY_WEAK => 0, | ||
self::STRENGTH_WEAK => 1, | ||
self::STRENGTH_MEDIUM => 2, | ||
self::STRENGTH_STRONG => 3, | ||
self::STRENGTH_VERY_STRONG => 4 | ||
]; | ||
|
||
private string $message = 'The password strength is not strong enough.'; | ||
|
||
public function __construct( | ||
private readonly string $minStrength = self::STRENGTH_MEDIUM, | ||
?string $message = null | ||
) | ||
{ | ||
$this->message = $message ?? $this->message; | ||
} | ||
|
||
public function assert(#[\SensitiveParameter] mixed $value, ?string $name = null): void | ||
{ | ||
if (!\in_array($this->minStrength, self::STRENGTH_OPTIONS)) { | ||
throw new UnexpectedOptionException('minStrength', self::STRENGTH_OPTIONS, $this->minStrength); | ||
} | ||
|
||
if (!\is_string($value)) { | ||
throw new UnexpectedTypeException('string', get_debug_type($value)); | ||
} | ||
|
||
$minScore = self::STRENGTH_SCORE[$this->minStrength]; | ||
$score = self::STRENGTH_SCORE[$this->calcStrength($value)]; | ||
|
||
if ($minScore > $score) { | ||
throw new PasswordStrengthException( | ||
message: $this->message, | ||
parameters: [ | ||
'name' => $name, | ||
'minStrength' => $this->minStrength | ||
] | ||
); | ||
} | ||
} | ||
|
||
private function calcStrength(#[\SensitiveParameter] string $password): string | ||
{ | ||
$length = \strlen($password); | ||
$chars = \count_chars($password, 1); | ||
|
||
$control = $digit = $upper = $lower = $symbol = $other = 0; | ||
foreach ($chars as $char => $count) { | ||
match (true) { | ||
($char < 32 || $char === 127) => $control = 33, | ||
($char >= 48 && $char <= 57) => $digit = 10, | ||
($char >= 65 && $char <= 90) => $upper = 26, | ||
($char >= 97 && $char <= 122) => $lower = 26, | ||
($char >= 128) => $other = 128, | ||
default => $symbol = 33, | ||
}; | ||
} | ||
|
||
$pool = $control + $digit + $upper + $lower + $symbol + $other; | ||
$entropy = \log(\pow($pool, $length), 2); | ||
|
||
return match (true) { | ||
$entropy >= 128 => self::STRENGTH_VERY_STRONG, | ||
$entropy >= 96 => self::STRENGTH_STRONG, | ||
$entropy >= 80 => self::STRENGTH_MEDIUM, | ||
$entropy >= 64 => self::STRENGTH_WEAK, | ||
default => self::STRENGTH_VERY_WEAK | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<?php | ||
|
||
namespace ProgrammatorDev\Validator\Test; | ||
|
||
use ProgrammatorDev\Validator\Exception\PasswordStrengthException; | ||
use ProgrammatorDev\Validator\Rule\PasswordStrength; | ||
use ProgrammatorDev\Validator\Test\Util\TestRuleFailureConditionTrait; | ||
use ProgrammatorDev\Validator\Test\Util\TestRuleMessageOptionTrait; | ||
use ProgrammatorDev\Validator\Test\Util\TestRuleSuccessConditionTrait; | ||
use ProgrammatorDev\Validator\Test\Util\TestRuleUnexpectedValueTrait; | ||
|
||
class PasswordStrengthTest extends AbstractTest | ||
{ | ||
use TestRuleUnexpectedValueTrait; | ||
use TestRuleFailureConditionTrait; | ||
use TestRuleSuccessConditionTrait; | ||
use TestRuleMessageOptionTrait; | ||
|
||
public static function provideRuleUnexpectedValueData(): \Generator | ||
{ | ||
$unexpectedOptionMessage = '/Invalid (.*) "(.*)". Accepted values are: "(.*)"./'; | ||
$unexpectedTypeMessage = '/Expected value of type "string", "(.*)" given./'; | ||
|
||
yield 'invalid min strength' => [new PasswordStrength(minStrength: 'invalid'), 'password', $unexpectedOptionMessage]; | ||
yield 'invalid value type' => [new PasswordStrength(), 123, $unexpectedTypeMessage]; | ||
} | ||
|
||
public static function provideRuleFailureConditionData(): \Generator | ||
{ | ||
$value = 'password'; | ||
$exception = PasswordStrengthException::class; | ||
$message = '/The password strength is not strong enough./'; | ||
|
||
yield 'min strength weak' => [new PasswordStrength(minStrength: 'weak'), $value, $exception, $message]; | ||
yield 'min strength medium' => [new PasswordStrength(minStrength: 'medium'), $value, $exception, $message]; | ||
yield 'min strength strong' => [new PasswordStrength(minStrength: 'strong'), $value, $exception, $message]; | ||
yield 'min strength very strong' => [new PasswordStrength(minStrength: 'very-strong'), $value, $exception, $message]; | ||
} | ||
|
||
public static function provideRuleSuccessConditionData(): \Generator | ||
{ | ||
$value = 'tP}D+9_$?m&g<ZX[D-]}5`ou$}Y,G1'; | ||
|
||
yield 'min strength weak' => [new PasswordStrength(minStrength: 'weak'), $value]; | ||
yield 'min strength medium' => [new PasswordStrength(minStrength: 'medium'), $value]; | ||
yield 'min strength strong' => [new PasswordStrength(minStrength: 'strong'), $value]; | ||
yield 'min strength very strong' => [new PasswordStrength(minStrength: 'very-strong'), $value]; | ||
} | ||
|
||
public static function provideRuleMessageOptionData(): \Generator | ||
{ | ||
yield 'message' => [ | ||
new PasswordStrength( | ||
message: 'The {{ name }} value entropy is not high enough.' | ||
), | ||
'password', | ||
'The test value entropy is not high enough.' | ||
]; | ||
} | ||
} |