From 4e44c23570b41884adf4026c3f648bd2731d352e Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 12 Apr 2021 20:08:52 +0100 Subject: [PATCH 01/17] Adds password rule --- .../Contracts/Validation/DataAwareRule.php | 14 ++ .../Validation/NotCompromisedVerifier.php | 14 ++ .../Validation/NotPwnedVerifier.php | 90 +++++++ src/Illuminate/Validation/Rules/Password.php | 233 ++++++++++++++++++ .../Validation/ValidationServiceProvider.php | 17 ++ src/Illuminate/Validation/Validator.php | 5 + .../ValidationNotPwnedVerifierTest.php | 86 +++++++ .../Validation/ValidationPasswordRuleTest.php | 209 ++++++++++++++++ tests/Validation/ValidationValidatorTest.php | 61 +++++ 9 files changed, 729 insertions(+) create mode 100644 src/Illuminate/Contracts/Validation/DataAwareRule.php create mode 100644 src/Illuminate/Contracts/Validation/NotCompromisedVerifier.php create mode 100644 src/Illuminate/Validation/NotPwnedVerifier.php create mode 100644 src/Illuminate/Validation/Rules/Password.php create mode 100644 tests/Validation/ValidationNotPwnedVerifierTest.php create mode 100644 tests/Validation/ValidationPasswordRuleTest.php diff --git a/src/Illuminate/Contracts/Validation/DataAwareRule.php b/src/Illuminate/Contracts/Validation/DataAwareRule.php new file mode 100644 index 000000000000..7ec7ab5a9fd9 --- /dev/null +++ b/src/Illuminate/Contracts/Validation/DataAwareRule.php @@ -0,0 +1,14 @@ +factory = $factory; + } + + /** + * Verify that the given value has not been compromised in public breaches. + * + * @param string $value + * @return bool + */ + public function verify($value) + { + if (empty($value = (string) $value)) { + return false; + } + + [$hash, $hashPrefix] = $this->getHash($value); + + return ! $this->search($hashPrefix) + ->contains(function ($line) use ($hash, $hashPrefix) { + [$hashSuffix, $count] = explode(':', $line); + + return $hashPrefix.$hashSuffix == $hash && $count > 1; + }); + } + + /** + * Get the hash and its first 5 chars. + * + * @param string $value + * @return array + */ + protected function getHash($value) + { + $hash = strtoupper(sha1((string) $value)); + $hashPrefix = substr($hash, 0, 5); + + return [$hash, $hashPrefix]; + } + + /** + * Search by the given hash prefix and returns all occurrences. + * + * @param string $hashPrefix + * @return \Illuminate\Support\Collection + */ + protected function search($hashPrefix) + { + try { + $response = $this->factory->get( + 'https://api.pwnedpasswords.com/range/'.$hashPrefix + ); + } catch (Exception $e) { + report($e); + } + + $body = (isset($response) && $response->successful()) + ? $response->body() + : ''; + + return Str::of($body)->trim()->explode("\n")->filter(function ($line) { + return Str::contains($line, ':'); + }); + } +} diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php new file mode 100644 index 000000000000..c6ba2b463c4d --- /dev/null +++ b/src/Illuminate/Validation/Rules/Password.php @@ -0,0 +1,233 @@ +min = max((int) $min, 1); + } + + /** + * Set the data under validation. + * + * @param array $data + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } + + /** + * Sets the minimum size of the password. + * + * @param int $size + * @return $this + */ + public static function min($size) + { + return new static($size); + } + + /** + * Ensures the password has not been compromised in data leaks. + * + * @return $this + */ + public function ensureNotCompromised() + { + $this->notCompromised = true; + + return $this; + } + + /** + * Makes the password require at least one uppercase and one lowercase letter. + * + * @return $this + */ + public function requireCaseDiff() + { + $this->caseDiff = true; + + return $this; + } + + /** + * Makes the password require at least one letter. + * + * @return $this + */ + public function requireLetters() + { + $this->letters = true; + + return $this; + } + + /** + * Makes the password require at least one number. + * + * @return $this + */ + public function requireNumbers() + { + $this->numbers = true; + + return $this; + } + + /** + * Makes the password require at least one symbol. + * + * @return $this + */ + public function requireSymbols() + { + $this->symbols = true; + + return $this; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + $validator = Validator::make($this->data, [ + $attribute => 'required|string|confirmed|min:'.$this->min, + ]); + + if ($validator->fails()) { + return $this->fail($validator->messages()->all()); + } + + $value = (string) $value; + + if ($this->caseDiff && ! preg_match('/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u', $value)) { + $this->fail('The :attribute must contain at least one uppercase and one lowercase letter.'); + } + + if ($this->letters && ! preg_match('/\pL/', $value)) { + $this->fail('The :attribute must contain at least one letter.'); + } + + if ($this->symbols && ! preg_match('/\p{Z}|\p{S}|\p{P}/', $value)) { + $this->fail('The :attribute must contain at least one symbol.'); + } + + if ($this->numbers && ! preg_match('/\pN/', $value)) { + $this->fail('The :attribute must contain at least one number.'); + } + + if (! empty($this->messages)) { + return false; + } + + if ($this->notCompromised && ! app('validation.not_compromised')->verify($value)) { + return $this->fail( + 'The given :attribute has appeared in a data leak. Please choose a different :attribute.' + ); + } + + return true; + } + + /** + * Get the validation error message. + * + * @return array + */ + public function message() + { + return $this->messages; + } + + /** + * Adds the given failures, and return false. + * + * @param array|string $message + * @return bool + */ + protected function fail($messages) + { + $this->messages = array_merge($this->messages, Arr::wrap($messages)); + + return false; + } +} diff --git a/src/Illuminate/Validation/ValidationServiceProvider.php b/src/Illuminate/Validation/ValidationServiceProvider.php index ce04447e58e0..5d10b965ec7e 100755 --- a/src/Illuminate/Validation/ValidationServiceProvider.php +++ b/src/Illuminate/Validation/ValidationServiceProvider.php @@ -3,6 +3,7 @@ namespace Illuminate\Validation; use Illuminate\Contracts\Support\DeferrableProvider; +use Illuminate\Http\Client\Factory as HttpFactory; use Illuminate\Support\ServiceProvider; class ValidationServiceProvider extends ServiceProvider implements DeferrableProvider @@ -16,6 +17,8 @@ public function register() { $this->registerPresenceVerifier(); + $this->registerNotCompromisedVerifier(); + $this->registerValidationFactory(); } @@ -52,6 +55,20 @@ protected function registerPresenceVerifier() }); } + /** + * Register the not compromise verifier. + * + * @return void + */ + protected function registerNotCompromisedVerifier() + { + $this->app->singleton('validation.not_compromised', function ($app) { + return new NotPwnedVerifier( + $app[HttpFactory::class] + ); + }); + } + /** * Get the services provided by the provider. * diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 70b8c17a6f3d..258ab19f540c 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -5,6 +5,7 @@ use BadMethodCallException; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Translation\Translator; +use Illuminate\Contracts\Validation\DataAwareRule; use Illuminate\Contracts\Validation\ImplicitRule; use Illuminate\Contracts\Validation\Rule as RuleContract; use Illuminate\Contracts\Validation\Validator as ValidatorContract; @@ -748,6 +749,10 @@ protected function validateUsingCustomRule($attribute, $value, $rule) $value = is_array($value) ? $this->replacePlaceholders($value) : $value; + if ($rule instanceof DataAwareRule) { + $rule->setData($this->data); + } + if (! $rule->passes($attribute, $value)) { $this->failedRules[$attribute][get_class($rule)] = []; diff --git a/tests/Validation/ValidationNotPwnedVerifierTest.php b/tests/Validation/ValidationNotPwnedVerifierTest.php new file mode 100644 index 000000000000..d294cd43d568 --- /dev/null +++ b/tests/Validation/ValidationNotPwnedVerifierTest.php @@ -0,0 +1,86 @@ +assertFalse($verifier->verify($password)); + } + } + + public function testApiResponseGoesWrong() + { + $httpFactory = m::mock(HttpFactory::class); + $response = m::mock(Response::class); + + $httpFactory->shouldReceive('get') + ->andReturn($response); + + $response->shouldReceive('successful') + ->andReturn(true); + + $response->shouldReceive('body') + ->andReturn(''); + + $verifier = new NotPwnedVerifier($httpFactory); + + $this->assertTrue($verifier->verify(123123123)); + } + + public function testApiGoesDown() + { + $httpFactory = m::mock(HttpFactory::class); + $response = m::mock(Response::class); + + $httpFactory->shouldReceive('get') + ->andReturn($response); + + $response->shouldReceive('successful') + ->andReturn(false); + + $verifier = new NotPwnedVerifier($httpFactory); + + $this->assertTrue($verifier->verify(123123123)); + } + + public function testDnsDown() + { + $container = Container::getInstance(); + $exception = new ConnectionException(); + + $exceptionHandler = m::mock(ExceptionHandler::class); + $exceptionHandler->shouldReceive('report')->once()->with($exception); + $container->bind(ExceptionHandler::class, function () use ($exceptionHandler) { + return $exceptionHandler; + }); + + $httpFactory = m::mock(HttpFactory::class); + $httpFactory->shouldReceive('get') + ->andThrow($exception); + + $verifier = new NotPwnedVerifier($httpFactory); + $this->assertTrue($verifier->verify(123123123)); + } +} diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php new file mode 100644 index 000000000000..a3ed6a9bd438 --- /dev/null +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -0,0 +1,209 @@ +fails(Password::min(3), [null], [ + 'validation.required', + ]); + + $this->passes(Password::min(3), ['1234', 'abcd', 'a z s']); + } + + public function testString() + { + $this->fails(Password::min(3), [['foo' => 'bar'], ['foo']], [ + 'validation.string', + 'validation.min.string', + ]); + + $this->fails(Password::min(3), [1234567, 545], [ + 'validation.string', + ]); + + $this->passes(Password::min(3), ['abcd', '454qb^']); + } + + public function testMin() + { + $this->fails(Password::min(3), ['a', 'ff', '12'], [ + 'validation.min.string', + ]); + + $this->passes(Password::min(3), ['1234', 'abcd']); + } + + public function testCaseDiff() + { + $this->fails(Password::min(2)->requireCaseDiff(), ['nn', 'MM'], [ + 'The my password must contain at least one uppercase and one lowercase letter.', + ]); + + $this->passes(Password::min(2)->requireCaseDiff(), ['Nn', 'Mn', 'âA']); + } + + public function testLetters() + { + $this->fails(Password::min(2)->requireLetters(), ['11', '22', '^^', '``', '**'], [ + 'The my password must contain at least one letter.', + ]); + + $this->passes(Password::min(2)->requireLetters(), ['1a', 'b2', 'â1']); + } + + public function testNumbers() + { + $this->fails(Password::min(2)->requireNumbers(), ['aa', 'bb', ' a'], [ + 'The my password must contain at least one number.', + ]); + + $this->passes(Password::min(2)->requireNumbers(), ['1a', 'b2', '00']); + } + + public function testSymbols() + { + $this->fails(Password::min(2)->requireSymbols(), ['ab', '1v'], [ + 'The my password must contain at least one symbol.', + ]); + + $this->passes(Password::min(2)->requireSymbols(), ['n^d', 'd^!', 'âè']); + } + + public function testNotCompromised() + { + $this->fails(Password::min(2)->ensureNotCompromised(), [ + '123456', + 'password', + 'welcome', + 'ninja', + 'abc123', + '123456789', + '12345678', + 'nuno', + ], [ + 'The given my password has appeared in a data leak. Please choose a different my password.', + ]); + + $this->passes(Password::min(2)->ensureNotCompromised(), [ + '!p8VrB', + '&xe6VeKWF#n4', + '%HurHUnw7zM!', + 'rundeliekend', + '7Z^k5EvqQ9g%c!Jt9$ufnNpQy#Kf', + 'NRs*Gz2@hSmB$vVBSPDfqbRtEzk4nF7ZAbM29VMW$BPD%b2U%3VmJAcrY5eZGVxP%z%apnwSX', + ]); + } + + public function testMessages() + { + $makeRule = function () { + return Password::min(8) + ->requireCaseDiff() + ->requireLetters() + ->requireNumbers() + ->requireSymbols() + ->ensureNotCompromised(); + }; + + $this->fails($makeRule(), ['foo', 'azdazd', '1231231'], [ + 'validation.min.string', + ]); + + $this->fails($makeRule(), ['aaaaaaaaa', 'TJQSJQSIUQHS'], [ + 'The my password must contain at least one uppercase and one lowercase letter.', + 'The my password must contain at least one symbol.', + 'The my password must contain at least one number.', + ]); + + $this->fails($makeRule(), ['4564654564564'], [ + 'The my password must contain at least one uppercase and one lowercase letter.', + 'The my password must contain at least one letter.', + 'The my password must contain at least one symbol.', + ]); + } + + public function testConfirmed() + { + $v = new Validator( + resolve('translator'), + ['my_password' => '1234', 'my_password_confirmation' => '5678'], + ['my_password' => Password::min(3)] + ); + + $this->assertSame(false, $v->passes()); + $this->assertSame(['my_password' => ['validation.confirmed']], $v->messages()->toArray()); + + $v = new Validator( + resolve('translator'), + ['my_password' => '1234'], + ['my_password' => Password::min(3)] + ); + + $this->assertSame(false, $v->passes()); + $this->assertSame(['my_password' => ['validation.confirmed']], $v->messages()->toArray()); + } + + protected function passes($rule, $values) + { + $this->testRule($rule, $values, true, []); + } + + protected function fails($rule, $values, $messages) + { + $this->testRule($rule, $values, false, $messages); + } + + protected function testRule($rule, $values, $result, $messages) + { + foreach ($values as $value) { + $v = new Validator( + resolve('translator'), + ['my_password' => $value, 'my_password_confirmation' => $value], + ['my_password' => clone $rule] + ); + + $this->assertSame($result, $v->passes()); + + $this->assertSame( + $result ? [] : ['my_password' => $messages], + $v->messages()->toArray() + ); + } + } + + protected function setUp(): void + { + $container = Container::getInstance(); + + $container->bind('translator', function () { + return new Translator( + new ArrayLoader, 'en' + ); + }); + + Facade::setFacadeApplication($container); + + (new ValidationServiceProvider($container))->register(); + } + + protected function tearDown(): void + { + Container::setInstance(null); + + Facade::clearResolvedInstances(); + + Facade::setFacadeApplication(null); + } +} diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 7d592324e47c..661d913d8d60 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -10,6 +10,7 @@ use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Translation\Translator as TranslatorContract; +use Illuminate\Contracts\Validation\DataAwareRule; use Illuminate\Contracts\Validation\ImplicitRule; use Illuminate\Contracts\Validation\Rule; use Illuminate\Database\Eloquent\Model; @@ -5244,6 +5245,66 @@ public function message() $this->assertSame('name must be taylor', $v->errors()->get('name')[0]); $this->assertSame('name must be a first name', $v->errors()->get('name')[1]); $this->assertSame('validation.string', $v->errors()->get('name')[2]); + + // Test access to the validator data + $v = new Validator( + $this->getIlluminateArrayTranslator(), + ['password' => 'foo', 'password_confirmation' => 'foo'], + [ + 'password' => [ + new class implements Rule, DataAwareRule { + protected $data; + + public function setData($data) + { + $this->data = $data; + } + + public function passes($attribute, $value) + { + return $value === $this->data['password_confirmation']; + } + + public function message() + { + return ['The :attribute confirmation does not match.']; + } + }, 'string', + ], + ] + ); + + $this->assertTrue($v->passes()); + + $v = new Validator( + $this->getIlluminateArrayTranslator(), + ['password' => 'foo', 'password_confirmation' => 'bar'], + [ + 'password' => [ + new class implements Rule, DataAwareRule { + protected $data; + + public function setData($data) + { + $this->data = $data; + } + + public function passes($attribute, $value) + { + return $value === $this->data['password_confirmation']; + } + + public function message() + { + return ['The :attribute confirmation does not match.']; + } + }, 'string', + ], + ] + ); + + $this->assertTrue($v->fails()); + $this->assertSame('The password confirmation does not match.', $v->errors()->get('password')[0]); } public function testCustomValidationObjectWithDotKeysIsCorrectlyPassedValue() From 34a78cbe23dfc69728e9a5de800e12077b393842 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 12 Apr 2021 20:11:31 +0100 Subject: [PATCH 02/17] Typo --- src/Illuminate/Validation/ValidationServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/ValidationServiceProvider.php b/src/Illuminate/Validation/ValidationServiceProvider.php index 5d10b965ec7e..552b58730a62 100755 --- a/src/Illuminate/Validation/ValidationServiceProvider.php +++ b/src/Illuminate/Validation/ValidationServiceProvider.php @@ -56,7 +56,7 @@ protected function registerPresenceVerifier() } /** - * Register the not compromise verifier. + * Register the not compromised verifier. * * @return void */ From aa74d01aee74e5f845a28efaa48ed6fb76c43226 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 13 Apr 2021 10:44:56 +0100 Subject: [PATCH 03/17] Fixes default compromised number --- src/Illuminate/Validation/NotPwnedVerifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/NotPwnedVerifier.php b/src/Illuminate/Validation/NotPwnedVerifier.php index e076939425a0..5aeb7de8988f 100644 --- a/src/Illuminate/Validation/NotPwnedVerifier.php +++ b/src/Illuminate/Validation/NotPwnedVerifier.php @@ -45,7 +45,7 @@ public function verify($value) ->contains(function ($line) use ($hash, $hashPrefix) { [$hashSuffix, $count] = explode(':', $line); - return $hashPrefix.$hashSuffix == $hash && $count > 1; + return $hashPrefix.$hashSuffix == $hash && $count > 0; }); } From 8d4e5a07f19175210098d35ed46574a844c77fce Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 13 Apr 2021 10:59:18 +0100 Subject: [PATCH 04/17] Adds "Add-Padding" header to not pwned verifier --- .../Validation/NotPwnedVerifier.php | 4 +++- .../ValidationNotPwnedVerifierTest.php | 21 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/NotPwnedVerifier.php b/src/Illuminate/Validation/NotPwnedVerifier.php index 5aeb7de8988f..2a2c24dd3b52 100644 --- a/src/Illuminate/Validation/NotPwnedVerifier.php +++ b/src/Illuminate/Validation/NotPwnedVerifier.php @@ -72,7 +72,9 @@ protected function getHash($value) protected function search($hashPrefix) { try { - $response = $this->factory->get( + $response = $this->factory->withHeaders([ + 'Add-Padding' => true, + ])->get( 'https://api.pwnedpasswords.com/range/'.$hashPrefix ); } catch (Exception $e) { diff --git a/tests/Validation/ValidationNotPwnedVerifierTest.php b/tests/Validation/ValidationNotPwnedVerifierTest.php index d294cd43d568..d44e51f04928 100644 --- a/tests/Validation/ValidationNotPwnedVerifierTest.php +++ b/tests/Validation/ValidationNotPwnedVerifierTest.php @@ -35,6 +35,13 @@ public function testApiResponseGoesWrong() $httpFactory = m::mock(HttpFactory::class); $response = m::mock(Response::class); + $httpFactory = m::mock(HttpFactory::class); + + $httpFactory + ->shouldReceive('withHeaders') + ->with(['Add-Padding' => true]) + ->andThrow($httpFactory); + $httpFactory->shouldReceive('get') ->andReturn($response); @@ -54,6 +61,11 @@ public function testApiGoesDown() $httpFactory = m::mock(HttpFactory::class); $response = m::mock(Response::class); + $httpFactory + ->shouldReceive('withHeaders') + ->with(['Add-Padding' => true]) + ->andThrow($httpFactory); + $httpFactory->shouldReceive('get') ->andReturn($response); @@ -77,7 +89,14 @@ public function testDnsDown() }); $httpFactory = m::mock(HttpFactory::class); - $httpFactory->shouldReceive('get') + + $httpFactory + ->shouldReceive('withHeaders') + ->with(['Add-Padding' => true]) + ->andThrow($httpFactory); + + $httpFactory + ->shouldReceive('get') ->andThrow($exception); $verifier = new NotPwnedVerifier($httpFactory); From 8c085018a13269ce3bf6cc148d535d238cacdb33 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 13 Apr 2021 11:02:08 +0100 Subject: [PATCH 05/17] Improves testing --- .../Validation/ValidationNotPwnedVerifierTest.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/Validation/ValidationNotPwnedVerifierTest.php b/tests/Validation/ValidationNotPwnedVerifierTest.php index d44e51f04928..782ccf7dddb5 100644 --- a/tests/Validation/ValidationNotPwnedVerifierTest.php +++ b/tests/Validation/ValidationNotPwnedVerifierTest.php @@ -39,16 +39,20 @@ public function testApiResponseGoesWrong() $httpFactory ->shouldReceive('withHeaders') + ->once() ->with(['Add-Padding' => true]) - ->andThrow($httpFactory); + ->andReturn($httpFactory); $httpFactory->shouldReceive('get') + ->once() ->andReturn($response); $response->shouldReceive('successful') + ->once() ->andReturn(true); $response->shouldReceive('body') + ->once() ->andReturn(''); $verifier = new NotPwnedVerifier($httpFactory); @@ -63,13 +67,16 @@ public function testApiGoesDown() $httpFactory ->shouldReceive('withHeaders') + ->once() ->with(['Add-Padding' => true]) - ->andThrow($httpFactory); + ->andReturn($httpFactory); $httpFactory->shouldReceive('get') + ->once() ->andReturn($response); $response->shouldReceive('successful') + ->once() ->andReturn(false); $verifier = new NotPwnedVerifier($httpFactory); @@ -92,11 +99,13 @@ public function testDnsDown() $httpFactory ->shouldReceive('withHeaders') + ->once() ->with(['Add-Padding' => true]) - ->andThrow($httpFactory); + ->andReturn($httpFactory); $httpFactory ->shouldReceive('get') + ->once() ->andThrow($exception); $verifier = new NotPwnedVerifier($httpFactory); From 4fb92f582675ba6b865d29b1b988604c645cd2d7 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 19 Apr 2021 14:08:16 -0500 Subject: [PATCH 06/17] work on rule --- ...Verifier.php => UncompromisedVerifier.php} | 2 +- .../Validation/NotPwnedVerifier.php | 8 +-- src/Illuminate/Validation/Rules/Password.php | 58 ++++++++++-------- .../Validation/ValidationServiceProvider.php | 15 ++--- .../Validation/ValidationPasswordRuleTest.php | 60 +++++-------------- 5 files changed, 58 insertions(+), 85 deletions(-) rename src/Illuminate/Contracts/Validation/{NotCompromisedVerifier.php => UncompromisedVerifier.php} (87%) diff --git a/src/Illuminate/Contracts/Validation/NotCompromisedVerifier.php b/src/Illuminate/Contracts/Validation/UncompromisedVerifier.php similarity index 87% rename from src/Illuminate/Contracts/Validation/NotCompromisedVerifier.php rename to src/Illuminate/Contracts/Validation/UncompromisedVerifier.php index 491abde78268..b9c124e2cddf 100644 --- a/src/Illuminate/Contracts/Validation/NotCompromisedVerifier.php +++ b/src/Illuminate/Contracts/Validation/UncompromisedVerifier.php @@ -2,7 +2,7 @@ namespace Illuminate\Contracts\Validation; -interface NotCompromisedVerifier +interface UncompromisedVerifier { /** * Verify that the given value has not been compromised in data leaks. diff --git a/src/Illuminate/Validation/NotPwnedVerifier.php b/src/Illuminate/Validation/NotPwnedVerifier.php index 2a2c24dd3b52..51df12507817 100644 --- a/src/Illuminate/Validation/NotPwnedVerifier.php +++ b/src/Illuminate/Validation/NotPwnedVerifier.php @@ -3,11 +3,11 @@ namespace Illuminate\Validation; use Exception; -use Illuminate\Contracts\Validation\NotCompromisedVerifier; +use Illuminate\Contracts\Validation\UncompromisedVerifier; use Illuminate\Http\Client\Factory; use Illuminate\Support\Str; -class NotPwnedVerifier implements NotCompromisedVerifier +class NotPwnedVerifier implements UncompromisedVerifier { /** * The http factory instance. @@ -64,9 +64,9 @@ protected function getHash($value) } /** - * Search by the given hash prefix and returns all occurrences. + * Search by the given hash prefix and returns all occurrences of leaked passwords. * - * @param string $hashPrefix + * @param string $hashPrefix * @return \Illuminate\Support\Collection */ protected function search($hashPrefix) diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index c6ba2b463c4d..7d75e9d73c22 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -2,20 +2,15 @@ namespace Illuminate\Validation\Rules; +use Illuminate\Container\Container; use Illuminate\Contracts\Validation\DataAwareRule; use Illuminate\Contracts\Validation\Rule; +use Illuminate\Contracts\Validation\UncompromisedVerifier; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator; class Password implements Rule, DataAwareRule { - /** - * If the password requires at least one uppercase and one lowercase letter. - * - * @var bool - */ - protected $caseDiff = false; - /** * The data under validation. * @@ -24,25 +19,25 @@ class Password implements Rule, DataAwareRule protected $data; /** - * If the password requires at least one letter. + * The minimum size of the password. * - * @var bool + * @var int */ - protected $letters = false; + protected $min = 8; /** - * The minimum size of the password. + * If the password requires at least one uppercase and one lowercase letter. * - * @var int + * @var bool */ - protected $min = 8; + protected $mixedCase = false; /** - * If the password should has not been compromised in data leaks. + * If the password requires at least one letter. * * @var bool */ - protected $notCompromised = false; + protected $letters = false; /** * If the password requires at least one number. @@ -58,6 +53,13 @@ class Password implements Rule, DataAwareRule */ protected $symbols = false; + /** + * If the password should has not been compromised in data leaks. + * + * @var bool + */ + protected $uncompromised = false; + /** * The failure messages, if any. * @@ -105,9 +107,9 @@ public static function min($size) * * @return $this */ - public function ensureNotCompromised() + public function uncompromised() { - $this->notCompromised = true; + $this->uncompromised = true; return $this; } @@ -117,9 +119,9 @@ public function ensureNotCompromised() * * @return $this */ - public function requireCaseDiff() + public function mixedCase() { - $this->caseDiff = true; + $this->mixedCase = true; return $this; } @@ -129,7 +131,7 @@ public function requireCaseDiff() * * @return $this */ - public function requireLetters() + public function letters() { $this->letters = true; @@ -141,7 +143,7 @@ public function requireLetters() * * @return $this */ - public function requireNumbers() + public function numbers() { $this->numbers = true; @@ -153,7 +155,7 @@ public function requireNumbers() * * @return $this */ - public function requireSymbols() + public function symbols() { $this->symbols = true; @@ -170,7 +172,7 @@ public function requireSymbols() public function passes($attribute, $value) { $validator = Validator::make($this->data, [ - $attribute => 'required|string|confirmed|min:'.$this->min, + $attribute => 'string|min:'.$this->min, ]); if ($validator->fails()) { @@ -179,7 +181,7 @@ public function passes($attribute, $value) $value = (string) $value; - if ($this->caseDiff && ! preg_match('/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u', $value)) { + if ($this->mixedCase && ! preg_match('/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u', $value)) { $this->fail('The :attribute must contain at least one uppercase and one lowercase letter.'); } @@ -199,7 +201,7 @@ public function passes($attribute, $value) return false; } - if ($this->notCompromised && ! app('validation.not_compromised')->verify($value)) { + if ($this->uncompromised && ! Container::getInstance()->make(UncompromisedVerifier::class)->verify($value)) { return $this->fail( 'The given :attribute has appeared in a data leak. Please choose a different :attribute.' ); @@ -226,7 +228,11 @@ public function message() */ protected function fail($messages) { - $this->messages = array_merge($this->messages, Arr::wrap($messages)); + $messages = collect(Arr::wrap($messages))->map(function ($message) { + return __($message); + })->all(); + + $this->messages = array_merge($this->messages, $messages); return false; } diff --git a/src/Illuminate/Validation/ValidationServiceProvider.php b/src/Illuminate/Validation/ValidationServiceProvider.php index 552b58730a62..936235f9e7bb 100755 --- a/src/Illuminate/Validation/ValidationServiceProvider.php +++ b/src/Illuminate/Validation/ValidationServiceProvider.php @@ -3,6 +3,7 @@ namespace Illuminate\Validation; use Illuminate\Contracts\Support\DeferrableProvider; +use Illuminate\Contracts\Validation\UncompromisedVerifier; use Illuminate\Http\Client\Factory as HttpFactory; use Illuminate\Support\ServiceProvider; @@ -16,9 +17,7 @@ class ValidationServiceProvider extends ServiceProvider implements DeferrablePro public function register() { $this->registerPresenceVerifier(); - - $this->registerNotCompromisedVerifier(); - + $this->registerUncompromisedVerifier(); $this->registerValidationFactory(); } @@ -56,16 +55,14 @@ protected function registerPresenceVerifier() } /** - * Register the not compromised verifier. + * Register the uncompromised password verifier. * * @return void */ - protected function registerNotCompromisedVerifier() + protected function registerUncompromisedVerifier() { - $this->app->singleton('validation.not_compromised', function ($app) { - return new NotPwnedVerifier( - $app[HttpFactory::class] - ); + $this->app->singleton(UncompromisedVerifier::class, function ($app) { + return new NotPwnedVerifier($app[HttpFactory::class]); }); } diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index a3ed6a9bd438..2611817e3f4f 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -13,15 +13,6 @@ class ValidationPasswordRuleTest extends TestCase { - public function testRequired() - { - $this->fails(Password::min(3), [null], [ - 'validation.required', - ]); - - $this->passes(Password::min(3), ['1234', 'abcd', 'a z s']); - } - public function testString() { $this->fails(Password::min(3), [['foo' => 'bar'], ['foo']], [ @@ -47,43 +38,43 @@ public function testMin() public function testCaseDiff() { - $this->fails(Password::min(2)->requireCaseDiff(), ['nn', 'MM'], [ + $this->fails(Password::min(2)->mixedCase(), ['nn', 'MM'], [ 'The my password must contain at least one uppercase and one lowercase letter.', ]); - $this->passes(Password::min(2)->requireCaseDiff(), ['Nn', 'Mn', 'âA']); + $this->passes(Password::min(2)->mixedCase(), ['Nn', 'Mn', 'âA']); } public function testLetters() { - $this->fails(Password::min(2)->requireLetters(), ['11', '22', '^^', '``', '**'], [ + $this->fails(Password::min(2)->letters(), ['11', '22', '^^', '``', '**'], [ 'The my password must contain at least one letter.', ]); - $this->passes(Password::min(2)->requireLetters(), ['1a', 'b2', 'â1']); + $this->passes(Password::min(2)->letters(), ['1a', 'b2', 'â1']); } public function testNumbers() { - $this->fails(Password::min(2)->requireNumbers(), ['aa', 'bb', ' a'], [ + $this->fails(Password::min(2)->numbers(), ['aa', 'bb', ' a'], [ 'The my password must contain at least one number.', ]); - $this->passes(Password::min(2)->requireNumbers(), ['1a', 'b2', '00']); + $this->passes(Password::min(2)->numbers(), ['1a', 'b2', '00']); } public function testSymbols() { - $this->fails(Password::min(2)->requireSymbols(), ['ab', '1v'], [ + $this->fails(Password::min(2)->symbols(), ['ab', '1v'], [ 'The my password must contain at least one symbol.', ]); - $this->passes(Password::min(2)->requireSymbols(), ['n^d', 'd^!', 'âè']); + $this->passes(Password::min(2)->symbols(), ['n^d', 'd^!', 'âè']); } public function testNotCompromised() { - $this->fails(Password::min(2)->ensureNotCompromised(), [ + $this->fails(Password::min(2)->uncompromised(), [ '123456', 'password', 'welcome', @@ -96,7 +87,7 @@ public function testNotCompromised() 'The given my password has appeared in a data leak. Please choose a different my password.', ]); - $this->passes(Password::min(2)->ensureNotCompromised(), [ + $this->passes(Password::min(2)->uncompromised(), [ '!p8VrB', '&xe6VeKWF#n4', '%HurHUnw7zM!', @@ -110,11 +101,11 @@ public function testMessages() { $makeRule = function () { return Password::min(8) - ->requireCaseDiff() - ->requireLetters() - ->requireNumbers() - ->requireSymbols() - ->ensureNotCompromised(); + ->mixedCase() + ->letters() + ->numbers() + ->symbols() + ->uncompromised(); }; $this->fails($makeRule(), ['foo', 'azdazd', '1231231'], [ @@ -134,27 +125,6 @@ public function testMessages() ]); } - public function testConfirmed() - { - $v = new Validator( - resolve('translator'), - ['my_password' => '1234', 'my_password_confirmation' => '5678'], - ['my_password' => Password::min(3)] - ); - - $this->assertSame(false, $v->passes()); - $this->assertSame(['my_password' => ['validation.confirmed']], $v->messages()->toArray()); - - $v = new Validator( - resolve('translator'), - ['my_password' => '1234'], - ['my_password' => Password::min(3)] - ); - - $this->assertSame(false, $v->passes()); - $this->assertSame(['my_password' => ['validation.confirmed']], $v->messages()->toArray()); - } - protected function passes($rule, $values) { $this->testRule($rule, $values, true, []); From a0fe44c2b09e78890f2d93d1c9c35b43bae00d92 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 11:28:37 +0100 Subject: [PATCH 07/17] Adds uncompromised threshold --- .../Validation/UncompromisedVerifier.php | 6 +++--- .../Validation/NotPwnedVerifier.php | 13 +++++++----- src/Illuminate/Validation/Rules/Password.php | 17 ++++++++++++++-- .../ValidationNotPwnedVerifierTest.php | 20 +++++++++++++++---- .../Validation/ValidationPasswordRuleTest.php | 4 ++++ 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/Illuminate/Contracts/Validation/UncompromisedVerifier.php b/src/Illuminate/Contracts/Validation/UncompromisedVerifier.php index b9c124e2cddf..d4bd597d1a66 100644 --- a/src/Illuminate/Contracts/Validation/UncompromisedVerifier.php +++ b/src/Illuminate/Contracts/Validation/UncompromisedVerifier.php @@ -5,10 +5,10 @@ interface UncompromisedVerifier { /** - * Verify that the given value has not been compromised in data leaks. + * Verify that the given data has not been compromised in data leaks. * - * @param string $value + * @param array $data * @return bool */ - public function verify($value); + public function verify($data); } diff --git a/src/Illuminate/Validation/NotPwnedVerifier.php b/src/Illuminate/Validation/NotPwnedVerifier.php index 51df12507817..a5edab988c08 100644 --- a/src/Illuminate/Validation/NotPwnedVerifier.php +++ b/src/Illuminate/Validation/NotPwnedVerifier.php @@ -28,13 +28,16 @@ public function __construct($factory) } /** - * Verify that the given value has not been compromised in public breaches. + * Verify that the given data has not been compromised in public breaches. * - * @param string $value + * @param array $data * @return bool */ - public function verify($value) + public function verify($data) { + $value = $data['value']; + $threshold = $data['threshold']; + if (empty($value = (string) $value)) { return false; } @@ -42,10 +45,10 @@ public function verify($value) [$hash, $hashPrefix] = $this->getHash($value); return ! $this->search($hashPrefix) - ->contains(function ($line) use ($hash, $hashPrefix) { + ->contains(function ($line) use ($hash, $hashPrefix, $threshold) { [$hashSuffix, $count] = explode(':', $line); - return $hashPrefix.$hashSuffix == $hash && $count > 0; + return $hashPrefix.$hashSuffix == $hash && $count > $threshold; }); } diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index 7d75e9d73c22..8e0ba8431a50 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -60,6 +60,13 @@ class Password implements Rule, DataAwareRule */ protected $uncompromised = false; + /** + * The number of times a password can appear in data leaks before being consider compromised. + * + * @var int + */ + protected $uncompromisedThreshold = 0; + /** * The failure messages, if any. * @@ -105,12 +112,15 @@ public static function min($size) /** * Ensures the password has not been compromised in data leaks. * + * @param int $threshold * @return $this */ - public function uncompromised() + public function uncompromised($threshold = 0) { $this->uncompromised = true; + $this->uncompromisedThreshold = $threshold; + return $this; } @@ -201,7 +211,10 @@ public function passes($attribute, $value) return false; } - if ($this->uncompromised && ! Container::getInstance()->make(UncompromisedVerifier::class)->verify($value)) { + if ($this->uncompromised && ! Container::getInstance()->make(UncompromisedVerifier::class)->verify([ + 'value' => $value, + 'threshold' => $this->uncompromisedThreshold, + ])) { return $this->fail( 'The given :attribute has appeared in a data leak. Please choose a different :attribute.' ); diff --git a/tests/Validation/ValidationNotPwnedVerifierTest.php b/tests/Validation/ValidationNotPwnedVerifierTest.php index 782ccf7dddb5..fb50b61a81fa 100644 --- a/tests/Validation/ValidationNotPwnedVerifierTest.php +++ b/tests/Validation/ValidationNotPwnedVerifierTest.php @@ -26,7 +26,10 @@ public function testEmptyValues() $verifier = new NotPwnedVerifier($httpFactory); foreach (['', false, 0] as $password) { - $this->assertFalse($verifier->verify($password)); + $this->assertFalse($verifier->verify([ + 'value' => $password, + 'threshold' => 0, + ])); } } @@ -57,7 +60,10 @@ public function testApiResponseGoesWrong() $verifier = new NotPwnedVerifier($httpFactory); - $this->assertTrue($verifier->verify(123123123)); + $this->assertTrue($verifier->verify([ + 'value' => 123123123, + 'threshold' => 0, + ])); } public function testApiGoesDown() @@ -81,7 +87,10 @@ public function testApiGoesDown() $verifier = new NotPwnedVerifier($httpFactory); - $this->assertTrue($verifier->verify(123123123)); + $this->assertTrue($verifier->verify([ + 'value' => 123123123, + 'threshold' => 0, + ])); } public function testDnsDown() @@ -109,6 +118,9 @@ public function testDnsDown() ->andThrow($exception); $verifier = new NotPwnedVerifier($httpFactory); - $this->assertTrue($verifier->verify(123123123)); + $this->assertTrue($verifier->verify([ + 'value' => 123123123, + 'threshold' => 0, + ])); } } diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 2611817e3f4f..92a427c65f34 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -87,6 +87,10 @@ public function testNotCompromised() 'The given my password has appeared in a data leak. Please choose a different my password.', ]); + $this->passes(Password::min(2)->uncompromised(9999999), [ + 'nuno', + ]); + $this->passes(Password::min(2)->uncompromised(), [ '!p8VrB', '&xe6VeKWF#n4', From 2622a2647eba582a61e9db75d53fb2bc4ed35531 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 12:12:44 +0100 Subject: [PATCH 08/17] Updates docs --- src/Illuminate/Validation/NotPwnedVerifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/NotPwnedVerifier.php b/src/Illuminate/Validation/NotPwnedVerifier.php index a5edab988c08..9b7e3bfea037 100644 --- a/src/Illuminate/Validation/NotPwnedVerifier.php +++ b/src/Illuminate/Validation/NotPwnedVerifier.php @@ -17,7 +17,7 @@ class NotPwnedVerifier implements UncompromisedVerifier protected $factory; /** - * Create a new not compromised verifier. + * Create a new uncompromised verifier. * * @param \Illuminate\Http\Client\Factory $factory * @return void From 44fc91653b6bcf59df85401afcdbb9810c2eca18 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 12:25:02 +0100 Subject: [PATCH 09/17] Removes non used import --- src/Illuminate/Validation/NotPwnedVerifier.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Validation/NotPwnedVerifier.php b/src/Illuminate/Validation/NotPwnedVerifier.php index 9b7e3bfea037..498a1c2740a3 100644 --- a/src/Illuminate/Validation/NotPwnedVerifier.php +++ b/src/Illuminate/Validation/NotPwnedVerifier.php @@ -4,7 +4,6 @@ use Exception; use Illuminate\Contracts\Validation\UncompromisedVerifier; -use Illuminate\Http\Client\Factory; use Illuminate\Support\Str; class NotPwnedVerifier implements UncompromisedVerifier From fee96317e46d82a7fb1d11e6a3a36632d74c289a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 12:25:12 +0100 Subject: [PATCH 10/17] Updates property name --- src/Illuminate/Validation/Rules/Password.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index 8e0ba8431a50..2ff32522d2bb 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -65,7 +65,7 @@ class Password implements Rule, DataAwareRule * * @var int */ - protected $uncompromisedThreshold = 0; + protected $compromisedThreshold = 0; /** * The failure messages, if any. @@ -119,7 +119,7 @@ public function uncompromised($threshold = 0) { $this->uncompromised = true; - $this->uncompromisedThreshold = $threshold; + $this->compromisedThreshold = $threshold; return $this; } @@ -213,7 +213,7 @@ public function passes($attribute, $value) if ($this->uncompromised && ! Container::getInstance()->make(UncompromisedVerifier::class)->verify([ 'value' => $value, - 'threshold' => $this->uncompromisedThreshold, + 'threshold' => $this->compromisedThreshold, ])) { return $this->fail( 'The given :attribute has appeared in a data leak. Please choose a different :attribute.' From 18fe5c021bca120fb1b374608da8e60a01a9e357 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 12:26:00 +0100 Subject: [PATCH 11/17] Fixes docs --- src/Illuminate/Validation/Rules/Password.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index 2ff32522d2bb..439c30b04ca5 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -236,7 +236,7 @@ public function message() /** * Adds the given failures, and return false. * - * @param array|string $message + * @param array|string $messages * @return bool */ protected function fail($messages) From fa65baf70b9a0319f9fa978524dd297106f762db Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 13:15:04 +0100 Subject: [PATCH 12/17] Updates test methods --- tests/Validation/ValidationPasswordRuleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 92a427c65f34..63b4fba233c8 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -36,7 +36,7 @@ public function testMin() $this->passes(Password::min(3), ['1234', 'abcd']); } - public function testCaseDiff() + public function testMixedCase() { $this->fails(Password::min(2)->mixedCase(), ['nn', 'MM'], [ 'The my password must contain at least one uppercase and one lowercase letter.', @@ -72,7 +72,7 @@ public function testSymbols() $this->passes(Password::min(2)->symbols(), ['n^d', 'd^!', 'âè']); } - public function testNotCompromised() + public function testUncompromised() { $this->fails(Password::min(2)->uncompromised(), [ '123456', From 0782273481a7c844d5909874d5539d9aa336f797 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 13:28:16 +0100 Subject: [PATCH 13/17] Adds more tests --- src/Illuminate/Validation/Rules/Password.php | 6 +++--- tests/Validation/ValidationPasswordRuleTest.php | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index 439c30b04ca5..eb3cdcb1c465 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -195,15 +195,15 @@ public function passes($attribute, $value) $this->fail('The :attribute must contain at least one uppercase and one lowercase letter.'); } - if ($this->letters && ! preg_match('/\pL/', $value)) { + if ($this->letters && ! preg_match('/\pL/u', $value)) { $this->fail('The :attribute must contain at least one letter.'); } - if ($this->symbols && ! preg_match('/\p{Z}|\p{S}|\p{P}/', $value)) { + if ($this->symbols && ! preg_match('/\p{Z}|\p{S}|\p{P}/u', $value)) { $this->fail('The :attribute must contain at least one symbol.'); } - if ($this->numbers && ! preg_match('/\pN/', $value)) { + if ($this->numbers && ! preg_match('/\pN/u', $value)) { $this->fail('The :attribute must contain at least one number.'); } diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 63b4fba233c8..2f6bfe1935fb 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -38,11 +38,11 @@ public function testMin() public function testMixedCase() { - $this->fails(Password::min(2)->mixedCase(), ['nn', 'MM'], [ + $this->fails(Password::min(2)->mixedCase(), ['nn', 'MM', '京都府'], [ 'The my password must contain at least one uppercase and one lowercase letter.', ]); - $this->passes(Password::min(2)->mixedCase(), ['Nn', 'Mn', 'âA']); + $this->passes(Password::min(2)->mixedCase(), ['Nn', 'Mn', 'âA', '京都府']); } public function testLetters() @@ -51,16 +51,16 @@ public function testLetters() 'The my password must contain at least one letter.', ]); - $this->passes(Password::min(2)->letters(), ['1a', 'b2', 'â1']); + $this->passes(Password::min(2)->letters(), ['1a', 'b2', 'â1', '1 京都府']); } public function testNumbers() { - $this->fails(Password::min(2)->numbers(), ['aa', 'bb', ' a'], [ + $this->fails(Password::min(2)->numbers(), ['aa', 'bb', ' a', '京都府'], [ 'The my password must contain at least one number.', ]); - $this->passes(Password::min(2)->numbers(), ['1a', 'b2', '00']); + $this->passes(Password::min(2)->numbers(), ['1a', 'b2', '00', '京都府 1']); } public function testSymbols() @@ -69,7 +69,7 @@ public function testSymbols() 'The my password must contain at least one symbol.', ]); - $this->passes(Password::min(2)->symbols(), ['n^d', 'd^!', 'âè']); + $this->passes(Password::min(2)->symbols(), ['n^d', 'd^!', 'âè$']); } public function testUncompromised() From 80078f7666406233d7ffc40b310d51fefa1303ed Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 13:30:41 +0100 Subject: [PATCH 14/17] Removes mixed case test --- tests/Validation/ValidationPasswordRuleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 2f6bfe1935fb..213501cf74c2 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -38,11 +38,11 @@ public function testMin() public function testMixedCase() { - $this->fails(Password::min(2)->mixedCase(), ['nn', 'MM', '京都府'], [ + $this->fails(Password::min(2)->mixedCase(), ['nn', 'MM'], [ 'The my password must contain at least one uppercase and one lowercase letter.', ]); - $this->passes(Password::min(2)->mixedCase(), ['Nn', 'Mn', 'âA', '京都府']); + $this->passes(Password::min(2)->mixedCase(), ['Nn', 'Mn', 'âA']); } public function testLetters() From e806449d179d360b256c3335f507f794d351270a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Apr 2021 14:10:23 +0100 Subject: [PATCH 15/17] Adds more tests --- tests/Validation/ValidationPasswordRuleTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 213501cf74c2..5197976410fd 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -24,7 +24,7 @@ public function testString() 'validation.string', ]); - $this->passes(Password::min(3), ['abcd', '454qb^']); + $this->passes(Password::min(3), ['abcd', '454qb^', '接2133手田']); } public function testMin() @@ -69,7 +69,7 @@ public function testSymbols() 'The my password must contain at least one symbol.', ]); - $this->passes(Password::min(2)->symbols(), ['n^d', 'd^!', 'âè$']); + $this->passes(Password::min(2)->symbols(), ['n^d', 'd^!', 'âè$', '金廿土弓竹中;']); } public function testUncompromised() @@ -92,6 +92,7 @@ public function testUncompromised() ]); $this->passes(Password::min(2)->uncompromised(), [ + '手田日尸Z難金木水口火女月土廿卜竹弓一十山', '!p8VrB', '&xe6VeKWF#n4', '%HurHUnw7zM!', From f1916077cadd97a545bd3d1c1c22bc1c54a51ecf Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 23 Apr 2021 12:29:29 +0100 Subject: [PATCH 16/17] Adds tests --- .../Validation/ValidationPasswordRuleTest.php | 77 +++++++++++++++---- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 5197976410fd..e397bb8d9033 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -29,11 +29,16 @@ public function testString() public function testMin() { + $this->fails(new Password(8), ['a', 'ff', '12'], [ + 'validation.min.string', + ]); + $this->fails(Password::min(3), ['a', 'ff', '12'], [ 'validation.min.string', ]); - $this->passes(Password::min(3), ['1234', 'abcd']); + $this->passes(Password::min(3), ['333', 'abcd']); + $this->passes(new Password(8), ['88888888']); } public function testMixedCase() @@ -63,6 +68,14 @@ public function testNumbers() $this->passes(Password::min(2)->numbers(), ['1a', 'b2', '00', '京都府 1']); } + public function testDefaultRules() + { + $this->fails(Password::min(3), [null], [ + 'validation.string', + 'validation.min.string', + ]); + } + public function testSymbols() { $this->fails(Password::min(2)->symbols(), ['ab', '1v'], [ @@ -102,32 +115,66 @@ public function testUncompromised() ]); } - public function testMessages() + public function testMessagesOrder() { - $makeRule = function () { - return Password::min(8) - ->mixedCase() - ->letters() - ->numbers() - ->symbols() - ->uncompromised(); + $makeRules = function () { + return ['required', Password::min(8)->mixedCase()->numbers()]; }; - $this->fails($makeRule(), ['foo', 'azdazd', '1231231'], [ + $this->fails($makeRules(), [null], [ + 'validation.required', + ]); + + $this->fails($makeRules(), ['foo', 'azdazd', '1231231'], [ 'validation.min.string', ]); - $this->fails($makeRule(), ['aaaaaaaaa', 'TJQSJQSIUQHS'], [ + $this->fails($makeRules(), ['4564654564564'], [ 'The my password must contain at least one uppercase and one lowercase letter.', - 'The my password must contain at least one symbol.', - 'The my password must contain at least one number.', ]); - $this->fails($makeRule(), ['4564654564564'], [ + $this->fails($makeRules(), ['aaaaaaaaa', 'TJQSJQSIUQHS'], [ 'The my password must contain at least one uppercase and one lowercase letter.', + 'The my password must contain at least one number.', + ]); + + $this->passes($makeRules(), ['4564654564564Abc']); + + $makeRules = function () { + return ['nullable', 'confirmed', Password::min(8)->letters()->symbols()->uncompromised()]; + }; + + $this->passes($makeRules(), [null]); + + $this->fails($makeRules(), ['foo', 'azdazd', '1231231'], [ + 'validation.min.string', + ]); + + $this->fails($makeRules(), ['aaaaaaaaa', 'TJQSJQSIUQHS'], [ + 'The my password must contain at least one symbol.', + ]); + + $this->fails($makeRules(), ['4564654564564'], [ 'The my password must contain at least one letter.', 'The my password must contain at least one symbol.', ]); + + $this->fails($makeRules(), ['abcabcabc!'], [ + 'The given my password has appeared in a data leak. Please choose a different my password.', + ]); + + $v = new Validator( + resolve('translator'), + ['my_password' => 'Nuno'], + ['my_password' => ['nullable', 'confirmed', Password::min(3)->letters()]] + ); + + $this->assertFalse($v->passes()); + + $this->assertSame( + ['my_password' => ['validation.confirmed']], + $v->messages()->toArray() + ); } protected function passes($rule, $values) @@ -146,7 +193,7 @@ protected function testRule($rule, $values, $result, $messages) $v = new Validator( resolve('translator'), ['my_password' => $value, 'my_password_confirmation' => $value], - ['my_password' => clone $rule] + ['my_password' => is_object($rule) ? clone $rule : $rule] ); $this->assertSame($result, $v->passes()); From c5d57a7dbad9e3495e2e569d1aad17bb797ee969 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 23 Apr 2021 08:28:48 -0500 Subject: [PATCH 17/17] Update NotPwnedVerifier.php --- src/Illuminate/Validation/NotPwnedVerifier.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/NotPwnedVerifier.php b/src/Illuminate/Validation/NotPwnedVerifier.php index 498a1c2740a3..a178f8c14010 100644 --- a/src/Illuminate/Validation/NotPwnedVerifier.php +++ b/src/Illuminate/Validation/NotPwnedVerifier.php @@ -9,7 +9,7 @@ class NotPwnedVerifier implements UncompromisedVerifier { /** - * The http factory instance. + * The HTTP factory instance. * * @var \Illuminate\Http\Client\Factory */ @@ -60,6 +60,7 @@ public function verify($data) protected function getHash($value) { $hash = strtoupper(sha1((string) $value)); + $hashPrefix = substr($hash, 0, 5); return [$hash, $hashPrefix];