Skip to content

Commit 8b82aa3

Browse files
driesvintstaylorotwell
authored andcommitted
[6.x] Implement new password rule and password confirmation (#30214)
* Implement new password rule * Implement password confirmation * Import classes * Use time function
1 parent cfe6b42 commit 8b82aa3

File tree

5 files changed

+262
-0
lines changed

5 files changed

+262
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Illuminate\Auth\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Routing\Redirector;
7+
8+
class RequirePassword
9+
{
10+
/**
11+
* The Redirector instance.
12+
*
13+
* @var \Illuminate\Routing\Redirector
14+
*/
15+
protected $redirector;
16+
17+
/**
18+
* Create a new middleware instance.
19+
*
20+
* @param \Illuminate\Routing\Redirector $redirector
21+
* @return void
22+
*/
23+
public function __construct(Redirector $redirector)
24+
{
25+
$this->redirector = $redirector;
26+
}
27+
28+
/**
29+
* Handle an incoming request.
30+
*
31+
* @param \Illuminate\Http\Request $request
32+
* @param \Closure $next
33+
* @param string $redirectToRoute
34+
* @return mixed
35+
*/
36+
public function handle($request, Closure $next, $redirectToRoute = null)
37+
{
38+
if ($this->shouldConfirmPassword($request)) {
39+
return $this->redirector->guest(
40+
$this->redirector->getUrlGenerator()->route($redirectToRoute ?? 'password.confirm')
41+
);
42+
}
43+
44+
return $next($request);
45+
}
46+
47+
/**
48+
* Determine if the confirmation timeout has expired.
49+
*
50+
* @param \Illuminate\Http\Request $request
51+
* @return bool
52+
*/
53+
protected function shouldConfirmPassword($request)
54+
{
55+
$confirmedAt = time() - $request->session()->get('auth.password_confirmed_at', 0);
56+
57+
return $confirmedAt > config('auth.password_timeout', 10800);
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace Illuminate\Foundation\Auth;
4+
5+
use Illuminate\Http\Request;
6+
7+
trait ConfirmsPasswords
8+
{
9+
use RedirectsUsers;
10+
11+
/**
12+
* Display the password confirmation view.
13+
*
14+
* @return \Illuminate\Http\Response
15+
*/
16+
public function showConfirmForm()
17+
{
18+
return view('auth.passwords.confirm');
19+
}
20+
21+
/**
22+
* Confirm the given user's password.
23+
*
24+
* @param \Illuminate\Http\Request $request
25+
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
26+
*/
27+
public function confirm(Request $request)
28+
{
29+
$request->validate($this->rules(), $this->validationErrorMessages());
30+
31+
$this->resetPasswordConfirmationTimeout($request);
32+
33+
return redirect()->intended($this->redirectPath());
34+
}
35+
36+
/**
37+
* Reset the password confirmation timeout.
38+
*
39+
* @param \Illuminate\Http\Request $request
40+
* @return void
41+
*/
42+
protected function resetPasswordConfirmationTimeout(Request $request)
43+
{
44+
$request->session()->put('auth.password_confirmed_at', time());
45+
}
46+
47+
/**
48+
* Get the password confirmation validation rules.
49+
*
50+
* @return array
51+
*/
52+
protected function rules()
53+
{
54+
return [
55+
'password' => 'required|password',
56+
];
57+
}
58+
59+
/**
60+
* Get the password confirmation validation error messages.
61+
*
62+
* @return array
63+
*/
64+
protected function validationErrorMessages()
65+
{
66+
return [];
67+
}
68+
}

src/Illuminate/Routing/Router.php

+16
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,11 @@ public function auth(array $options = [])
11641164
$this->resetPassword();
11651165
}
11661166

1167+
// Password Confirmation Routes...
1168+
if ($options['confirm'] ?? true) {
1169+
$this->confirmPassword();
1170+
}
1171+
11671172
// Email Verification Routes...
11681173
if ($options['verify'] ?? false) {
11691174
$this->emailVerification();
@@ -1183,6 +1188,17 @@ public function resetPassword()
11831188
$this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
11841189
}
11851190

1191+
/**
1192+
* Register the typical confirm password routes for an application.
1193+
*
1194+
* @return void
1195+
*/
1196+
public function confirmPassword()
1197+
{
1198+
$this->get('password/confirm', 'Auth\ConfirmPasswordController@showConfirmForm')->name('password.confirm');
1199+
$this->post('password/confirm', 'Auth\ConfirmPasswordController@confirm');
1200+
}
1201+
11861202
/**
11871203
* Register the typical email verification routes for an application.
11881204
*

src/Illuminate/Validation/Concerns/ValidatesAttributes.php

+22
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,28 @@ public function validateNumeric($attribute, $value)
12791279
return is_numeric($value);
12801280
}
12811281

1282+
/**
1283+
* Validate that the current logged in user's password matches the given value.
1284+
*
1285+
* @param string $attribute
1286+
* @param mixed $value
1287+
* @param array $parameters
1288+
* @return bool
1289+
*/
1290+
protected function validatePassword($attribute, $value, $parameters)
1291+
{
1292+
$auth = $this->container->make('auth');
1293+
$hasher = $this->container->make('hash');
1294+
1295+
$guard = $auth->guard(Arr::first($parameters));
1296+
1297+
if ($guard->guest()) {
1298+
return false;
1299+
}
1300+
1301+
return $hasher->check($value, $guard->user()->getAuthPassword());
1302+
}
1303+
12821304
/**
12831305
* Validate that an attribute exists even if not filled.
12841306
*

tests/Validation/ValidationValidatorTest.php

+97
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
use DateTime;
66
use DateTimeImmutable;
77
use Illuminate\Container\Container;
8+
use Illuminate\Contracts\Auth\Authenticatable;
9+
use Illuminate\Contracts\Auth\Guard;
10+
use Illuminate\Contracts\Hashing\Hasher;
811
use Illuminate\Contracts\Translation\Translator as TranslatorContract;
912
use Illuminate\Contracts\Validation\ImplicitRule;
1013
use Illuminate\Contracts\Validation\Rule;
@@ -686,6 +689,100 @@ public function testValidationStopsAtFailedPresenceCheck()
686689
$this->assertEquals(['validation.present'], $v->errors()->get('name'));
687690
}
688691

692+
public function testValidatePassword()
693+
{
694+
// Fails when user is not logged in.
695+
$auth = m::mock(Guard::class);
696+
$auth->shouldReceive('guard')->andReturn($auth);
697+
$auth->shouldReceive('guest')->andReturn(true);
698+
699+
$hasher = m::mock(Hasher::class);
700+
701+
$container = m::mock(Container::class);
702+
$container->shouldReceive('make')->with('auth')->andReturn($auth);
703+
$container->shouldReceive('make')->with('hash')->andReturn($hasher);
704+
705+
$trans = $this->getTranslator();
706+
$trans->shouldReceive('get');
707+
708+
$v = new Validator($trans, ['password' => 'foo'], ['password' => 'password']);
709+
$v->setContainer($container);
710+
711+
$this->assertFalse($v->passes());
712+
713+
// Fails when password is incorrect.
714+
$user = m::mock(Authenticatable::class);
715+
$user->shouldReceive('getAuthPassword');
716+
717+
$auth = m::mock(Guard::class);
718+
$auth->shouldReceive('guard')->andReturn($auth);
719+
$auth->shouldReceive('guest')->andReturn(false);
720+
$auth->shouldReceive('user')->andReturn($user);
721+
722+
$hasher = m::mock(Hasher::class);
723+
$hasher->shouldReceive('check')->andReturn(false);
724+
725+
$container = m::mock(Container::class);
726+
$container->shouldReceive('make')->with('auth')->andReturn($auth);
727+
$container->shouldReceive('make')->with('hash')->andReturn($hasher);
728+
729+
$trans = $this->getTranslator();
730+
$trans->shouldReceive('get');
731+
732+
$v = new Validator($trans, ['password' => 'foo'], ['password' => 'password']);
733+
$v->setContainer($container);
734+
735+
$this->assertFalse($v->passes());
736+
737+
// Succeeds when password is correct.
738+
$user = m::mock(Authenticatable::class);
739+
$user->shouldReceive('getAuthPassword');
740+
741+
$auth = m::mock(Guard::class);
742+
$auth->shouldReceive('guard')->andReturn($auth);
743+
$auth->shouldReceive('guest')->andReturn(false);
744+
$auth->shouldReceive('user')->andReturn($user);
745+
746+
$hasher = m::mock(Hasher::class);
747+
$hasher->shouldReceive('check')->andReturn(true);
748+
749+
$container = m::mock(Container::class);
750+
$container->shouldReceive('make')->with('auth')->andReturn($auth);
751+
$container->shouldReceive('make')->with('hash')->andReturn($hasher);
752+
753+
$trans = $this->getTranslator();
754+
$trans->shouldReceive('get');
755+
756+
$v = new Validator($trans, ['password' => 'foo'], ['password' => 'password']);
757+
$v->setContainer($container);
758+
759+
$this->assertTrue($v->passes());
760+
761+
// We can use a specific guard.
762+
$user = m::mock(Authenticatable::class);
763+
$user->shouldReceive('getAuthPassword');
764+
765+
$auth = m::mock(Guard::class);
766+
$auth->shouldReceive('guard')->with('custom')->andReturn($auth);
767+
$auth->shouldReceive('guest')->andReturn(false);
768+
$auth->shouldReceive('user')->andReturn($user);
769+
770+
$hasher = m::mock(Hasher::class);
771+
$hasher->shouldReceive('check')->andReturn(true);
772+
773+
$container = m::mock(Container::class);
774+
$container->shouldReceive('make')->with('auth')->andReturn($auth);
775+
$container->shouldReceive('make')->with('hash')->andReturn($hasher);
776+
777+
$trans = $this->getTranslator();
778+
$trans->shouldReceive('get');
779+
780+
$v = new Validator($trans, ['password' => 'foo'], ['password' => 'password:custom']);
781+
$v->setContainer($container);
782+
783+
$this->assertTrue($v->passes());
784+
}
785+
689786
public function testValidatePresent()
690787
{
691788
$trans = $this->getIlluminateArrayTranslator();

0 commit comments

Comments
 (0)