From e4584d3c4f4877e9019d7ef12ace16586902dab7 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Fri, 4 Oct 2019 18:21:14 +0200 Subject: [PATCH 1/4] Implement new password rule --- .../Concerns/ValidatesAttributes.php | 22 +++++ tests/Validation/ValidationValidatorTest.php | 94 +++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index ef2f31a2d622..70ccf76678df 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1279,6 +1279,28 @@ public function validateNumeric($attribute, $value) return is_numeric($value); } + /** + * Validate that the current logged in user's password matches the given value. + * + * @param string $attribute + * @param mixed $value + * @param array $parameters + * @return bool + */ + protected function validatePassword($attribute, $value, $parameters) + { + $auth = $this->container->make('auth'); + $hasher = $this->container->make('hash'); + + $guard = $auth->guard(Arr::first($parameters)); + + if ($guard->guest()) { + return false; + } + + return $hasher->check($value, $guard->user()->getAuthPassword()); + } + /** * Validate that an attribute exists even if not filled. * diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 4ccd308cdea7..80b7ade7e162 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -686,6 +686,100 @@ public function testValidationStopsAtFailedPresenceCheck() $this->assertEquals(['validation.present'], $v->errors()->get('name')); } + public function testValidatePassword() + { + // Fails when user is not logged in. + $auth = m::mock(\Illuminate\Contracts\Auth\Guard::class); + $auth->shouldReceive('guard')->andReturn($auth); + $auth->shouldReceive('guest')->andReturn(true); + + $hasher = m::mock(\Illuminate\Contracts\Hashing\Hasher::class); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('auth')->andReturn($auth); + $container->shouldReceive('make')->with('hash')->andReturn($hasher); + + $trans = $this->getTranslator(); + $trans->shouldReceive('get'); + + $v = new Validator($trans, ['password' => 'foo'], ['password' => 'password']); + $v->setContainer($container); + + $this->assertFalse($v->passes()); + + // Fails when password is incorrect. + $user = m::mock(\Illuminate\Contracts\Auth\Authenticatable::class); + $user->shouldReceive('getAuthPassword'); + + $auth = m::mock(\Illuminate\Contracts\Auth\Guard::class); + $auth->shouldReceive('guard')->andReturn($auth); + $auth->shouldReceive('guest')->andReturn(false); + $auth->shouldReceive('user')->andReturn($user); + + $hasher = m::mock(\Illuminate\Contracts\Hashing\Hasher::class); + $hasher->shouldReceive('check')->andReturn(false); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('auth')->andReturn($auth); + $container->shouldReceive('make')->with('hash')->andReturn($hasher); + + $trans = $this->getTranslator(); + $trans->shouldReceive('get'); + + $v = new Validator($trans, ['password' => 'foo'], ['password' => 'password']); + $v->setContainer($container); + + $this->assertFalse($v->passes()); + + // Succeeds when password is correct. + $user = m::mock(\Illuminate\Contracts\Auth\Authenticatable::class); + $user->shouldReceive('getAuthPassword'); + + $auth = m::mock(\Illuminate\Contracts\Auth\Guard::class); + $auth->shouldReceive('guard')->andReturn($auth); + $auth->shouldReceive('guest')->andReturn(false); + $auth->shouldReceive('user')->andReturn($user); + + $hasher = m::mock(\Illuminate\Contracts\Hashing\Hasher::class); + $hasher->shouldReceive('check')->andReturn(true); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('auth')->andReturn($auth); + $container->shouldReceive('make')->with('hash')->andReturn($hasher); + + $trans = $this->getTranslator(); + $trans->shouldReceive('get'); + + $v = new Validator($trans, ['password' => 'foo'], ['password' => 'password']); + $v->setContainer($container); + + $this->assertTrue($v->passes()); + + // We can use a specific guard. + $user = m::mock(\Illuminate\Contracts\Auth\Authenticatable::class); + $user->shouldReceive('getAuthPassword'); + + $auth = m::mock(\Illuminate\Contracts\Auth\Guard::class); + $auth->shouldReceive('guard')->with('custom')->andReturn($auth); + $auth->shouldReceive('guest')->andReturn(false); + $auth->shouldReceive('user')->andReturn($user); + + $hasher = m::mock(\Illuminate\Contracts\Hashing\Hasher::class); + $hasher->shouldReceive('check')->andReturn(true); + + $container = m::mock(Container::class); + $container->shouldReceive('make')->with('auth')->andReturn($auth); + $container->shouldReceive('make')->with('hash')->andReturn($hasher); + + $trans = $this->getTranslator(); + $trans->shouldReceive('get'); + + $v = new Validator($trans, ['password' => 'foo'], ['password' => 'password:custom']); + $v->setContainer($container); + + $this->assertTrue($v->passes()); + } + public function testValidatePresent() { $trans = $this->getIlluminateArrayTranslator(); From 1eeb4eebc8ca615ca48d8dbb212ba5bb2b114a26 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Tue, 8 Oct 2019 13:45:10 +0200 Subject: [PATCH 2/4] Implement password confirmation --- .../Auth/Middleware/RequirePassword.php | 59 ++++++++++++++++ .../Foundation/Auth/ConfirmsPasswords.php | 68 +++++++++++++++++++ src/Illuminate/Routing/Router.php | 16 +++++ 3 files changed, 143 insertions(+) create mode 100644 src/Illuminate/Auth/Middleware/RequirePassword.php create mode 100644 src/Illuminate/Foundation/Auth/ConfirmsPasswords.php diff --git a/src/Illuminate/Auth/Middleware/RequirePassword.php b/src/Illuminate/Auth/Middleware/RequirePassword.php new file mode 100644 index 000000000000..ab65a173a692 --- /dev/null +++ b/src/Illuminate/Auth/Middleware/RequirePassword.php @@ -0,0 +1,59 @@ +redirector = $redirector; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string $redirectToRoute + * @return mixed + */ + public function handle($request, Closure $next, $redirectToRoute = null) + { + if ($this->shouldConfirmPassword($request)) { + return $this->redirector->guest( + $this->redirector->getUrlGenerator()->route($redirectToRoute ?? 'password.confirm') + ); + } + + return $next($request); + } + + /** + * Determine if the confirmation timeout has expired. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + protected function shouldConfirmPassword($request) + { + $confirmedAt = strtotime('now') - $request->session()->get('auth.password_confirmed_at', 0); + + return $confirmedAt > config('auth.password_timeout', 10800); + } +} diff --git a/src/Illuminate/Foundation/Auth/ConfirmsPasswords.php b/src/Illuminate/Foundation/Auth/ConfirmsPasswords.php new file mode 100644 index 000000000000..9e35d6e8eeb6 --- /dev/null +++ b/src/Illuminate/Foundation/Auth/ConfirmsPasswords.php @@ -0,0 +1,68 @@ +validate($this->rules(), $this->validationErrorMessages()); + + $this->resetPasswordConfirmationTimeout($request); + + return redirect()->intended($this->redirectPath()); + } + + /** + * Reset the password confirmation timeout. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function resetPasswordConfirmationTimeout(Request $request) + { + $request->session()->put('auth.password_confirmed_at', strtotime('now')); + } + + /** + * Get the password confirmation validation rules. + * + * @return array + */ + protected function rules() + { + return [ + 'password' => 'required|password', + ]; + } + + /** + * Get the password confirmation validation error messages. + * + * @return array + */ + protected function validationErrorMessages() + { + return []; + } +} diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index 198b3c778db6..04a37b3b1d2b 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -1164,6 +1164,11 @@ public function auth(array $options = []) $this->resetPassword(); } + // Password Confirmation Routes... + if ($options['confirm'] ?? true) { + $this->confirmPassword(); + } + // Email Verification Routes... if ($options['verify'] ?? false) { $this->emailVerification(); @@ -1183,6 +1188,17 @@ public function resetPassword() $this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update'); } + /** + * Register the typical confirm password routes for an application. + * + * @return void + */ + public function confirmPassword() + { + $this->get('password/confirm', 'Auth\ConfirmPasswordController@showConfirmForm')->name('password.confirm'); + $this->post('password/confirm', 'Auth\ConfirmPasswordController@confirm'); + } + /** * Register the typical email verification routes for an application. * From c28db23f3c302fa7b60bd9a3387debdddda6f840 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Tue, 8 Oct 2019 14:20:47 +0200 Subject: [PATCH 3/4] Import classes --- tests/Validation/ValidationValidatorTest.php | 25 +++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 80b7ade7e162..69ca23084501 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -5,6 +5,9 @@ use DateTime; use DateTimeImmutable; use Illuminate\Container\Container; +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Contracts\Auth\Guard; +use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Translation\Translator as TranslatorContract; use Illuminate\Contracts\Validation\ImplicitRule; use Illuminate\Contracts\Validation\Rule; @@ -689,11 +692,11 @@ public function testValidationStopsAtFailedPresenceCheck() public function testValidatePassword() { // Fails when user is not logged in. - $auth = m::mock(\Illuminate\Contracts\Auth\Guard::class); + $auth = m::mock(Guard::class); $auth->shouldReceive('guard')->andReturn($auth); $auth->shouldReceive('guest')->andReturn(true); - $hasher = m::mock(\Illuminate\Contracts\Hashing\Hasher::class); + $hasher = m::mock(Hasher::class); $container = m::mock(Container::class); $container->shouldReceive('make')->with('auth')->andReturn($auth); @@ -708,15 +711,15 @@ public function testValidatePassword() $this->assertFalse($v->passes()); // Fails when password is incorrect. - $user = m::mock(\Illuminate\Contracts\Auth\Authenticatable::class); + $user = m::mock(Authenticatable::class); $user->shouldReceive('getAuthPassword'); - $auth = m::mock(\Illuminate\Contracts\Auth\Guard::class); + $auth = m::mock(Guard::class); $auth->shouldReceive('guard')->andReturn($auth); $auth->shouldReceive('guest')->andReturn(false); $auth->shouldReceive('user')->andReturn($user); - $hasher = m::mock(\Illuminate\Contracts\Hashing\Hasher::class); + $hasher = m::mock(Hasher::class); $hasher->shouldReceive('check')->andReturn(false); $container = m::mock(Container::class); @@ -732,15 +735,15 @@ public function testValidatePassword() $this->assertFalse($v->passes()); // Succeeds when password is correct. - $user = m::mock(\Illuminate\Contracts\Auth\Authenticatable::class); + $user = m::mock(Authenticatable::class); $user->shouldReceive('getAuthPassword'); - $auth = m::mock(\Illuminate\Contracts\Auth\Guard::class); + $auth = m::mock(Guard::class); $auth->shouldReceive('guard')->andReturn($auth); $auth->shouldReceive('guest')->andReturn(false); $auth->shouldReceive('user')->andReturn($user); - $hasher = m::mock(\Illuminate\Contracts\Hashing\Hasher::class); + $hasher = m::mock(Hasher::class); $hasher->shouldReceive('check')->andReturn(true); $container = m::mock(Container::class); @@ -756,15 +759,15 @@ public function testValidatePassword() $this->assertTrue($v->passes()); // We can use a specific guard. - $user = m::mock(\Illuminate\Contracts\Auth\Authenticatable::class); + $user = m::mock(Authenticatable::class); $user->shouldReceive('getAuthPassword'); - $auth = m::mock(\Illuminate\Contracts\Auth\Guard::class); + $auth = m::mock(Guard::class); $auth->shouldReceive('guard')->with('custom')->andReturn($auth); $auth->shouldReceive('guest')->andReturn(false); $auth->shouldReceive('user')->andReturn($user); - $hasher = m::mock(\Illuminate\Contracts\Hashing\Hasher::class); + $hasher = m::mock(Hasher::class); $hasher->shouldReceive('check')->andReturn(true); $container = m::mock(Container::class); From c0ff4999438bd83f426bfbcaac344f6b5489a816 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Tue, 8 Oct 2019 14:23:10 +0200 Subject: [PATCH 4/4] Use time function --- src/Illuminate/Auth/Middleware/RequirePassword.php | 2 +- src/Illuminate/Foundation/Auth/ConfirmsPasswords.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Auth/Middleware/RequirePassword.php b/src/Illuminate/Auth/Middleware/RequirePassword.php index ab65a173a692..062eabdf0407 100644 --- a/src/Illuminate/Auth/Middleware/RequirePassword.php +++ b/src/Illuminate/Auth/Middleware/RequirePassword.php @@ -52,7 +52,7 @@ public function handle($request, Closure $next, $redirectToRoute = null) */ protected function shouldConfirmPassword($request) { - $confirmedAt = strtotime('now') - $request->session()->get('auth.password_confirmed_at', 0); + $confirmedAt = time() - $request->session()->get('auth.password_confirmed_at', 0); return $confirmedAt > config('auth.password_timeout', 10800); } diff --git a/src/Illuminate/Foundation/Auth/ConfirmsPasswords.php b/src/Illuminate/Foundation/Auth/ConfirmsPasswords.php index 9e35d6e8eeb6..655c4e5bef2d 100644 --- a/src/Illuminate/Foundation/Auth/ConfirmsPasswords.php +++ b/src/Illuminate/Foundation/Auth/ConfirmsPasswords.php @@ -41,7 +41,7 @@ public function confirm(Request $request) */ protected function resetPasswordConfirmationTimeout(Request $request) { - $request->session()->put('auth.password_confirmed_at', strtotime('now')); + $request->session()->put('auth.password_confirmed_at', time()); } /**