diff --git a/src/Illuminate/Auth/Middleware/RequirePassword.php b/src/Illuminate/Auth/Middleware/RequirePassword.php new file mode 100644 index 000000000000..062eabdf0407 --- /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 = 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 new file mode 100644 index 000000000000..655c4e5bef2d --- /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', time()); + } + + /** + * 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. * 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..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; @@ -686,6 +689,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(Guard::class); + $auth->shouldReceive('guard')->andReturn($auth); + $auth->shouldReceive('guest')->andReturn(true); + + $hasher = m::mock(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(Authenticatable::class); + $user->shouldReceive('getAuthPassword'); + + $auth = m::mock(Guard::class); + $auth->shouldReceive('guard')->andReturn($auth); + $auth->shouldReceive('guest')->andReturn(false); + $auth->shouldReceive('user')->andReturn($user); + + $hasher = m::mock(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(Authenticatable::class); + $user->shouldReceive('getAuthPassword'); + + $auth = m::mock(Guard::class); + $auth->shouldReceive('guard')->andReturn($auth); + $auth->shouldReceive('guest')->andReturn(false); + $auth->shouldReceive('user')->andReturn($user); + + $hasher = m::mock(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(Authenticatable::class); + $user->shouldReceive('getAuthPassword'); + + $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(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();