From a4c037f539e282491496995925159463edec6629 Mon Sep 17 00:00:00 2001 From: Alexis Saettler Date: Mon, 28 Jun 2021 23:15:37 +0200 Subject: [PATCH] feat: revoke session from other browser after a password change (#5328) --- .../Controllers/Auth/InvitationController.php | 3 +- .../Auth/PasswordChangeController.php | 5 +- .../Controllers/Auth/RegisterController.php | 3 +- .../Auth/ResetPasswordController.php | 15 +++ app/Http/Requests/PasswordChangeRequest.php | 4 +- app/Listeners/LogoutUserDevices.php | 97 +++++++++++++++++++ app/Providers/AppServiceProvider.php | 5 + app/Providers/EventServiceProvider.php | 4 +- 8 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 app/Listeners/LogoutUserDevices.php diff --git a/app/Http/Controllers/Auth/InvitationController.php b/app/Http/Controllers/Auth/InvitationController.php index 15426cb072e..e0e8dcd6c4e 100644 --- a/app/Http/Controllers/Auth/InvitationController.php +++ b/app/Http/Controllers/Auth/InvitationController.php @@ -12,6 +12,7 @@ use Illuminate\Auth\Events\Registered; use Illuminate\Support\Facades\Validator; use Illuminate\Foundation\Auth\RedirectsUsers; +use Illuminate\Validation\Rules\Password as PasswordRules; class InvitationController extends Controller { @@ -68,7 +69,7 @@ protected function validator(array $data) 'first_name' => 'required|max:255', 'email' => 'required|email|max:255|unique:users', 'email_security' => 'required', - 'password' => 'required|min:6|confirmed', + 'password' => ['required', 'confirmed', PasswordRules::defaults()], 'policy' => 'required', ]); } diff --git a/app/Http/Controllers/Auth/PasswordChangeController.php b/app/Http/Controllers/Auth/PasswordChangeController.php index 127e016de4e..c1d593bfb6f 100644 --- a/app/Http/Controllers/Auth/PasswordChangeController.php +++ b/app/Http/Controllers/Auth/PasswordChangeController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Auth\Events\PasswordReset; use App\Http\Requests\PasswordChangeRequest; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Foundation\Auth\RedirectsUsers; @@ -45,7 +46,7 @@ public function passwordChange(PasswordChangeRequest $request) $response = $this->validateAndPasswordChange($credentials); - return $response == 'passwords.changed' + return $response === 'passwords.changed' ? $this->sendChangedResponse($response) : $this->sendChangedFailedResponse($response); } @@ -128,6 +129,8 @@ protected function setNewPassword($user, $password) $user->save(); + event(new PasswordReset($user)); + Auth::guard()->login($user); } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 36f3df1c330..f55f392ce01 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -13,6 +13,7 @@ use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Validator; use Illuminate\Foundation\Auth\RegistersUsers; +use Illuminate\Validation\Rules\Password as PasswordRules; class RegisterController extends Controller { @@ -75,7 +76,7 @@ protected function validator(array $data) 'last_name' => 'required|max:255', 'first_name' => 'required|max:255', 'email' => 'required|email|max:255|unique:users', - 'password' => 'required|min:6|confirmed', + 'password' => ['required', 'confirmed', PasswordRules::defaults()], 'policy' => 'required', ]); } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index 87785f3a45a..634be7ed843 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ResetsPasswords; +use Illuminate\Validation\Rules\Password as PasswordRules; class ResetPasswordController extends Controller { @@ -24,4 +25,18 @@ class ResetPasswordController extends Controller * @var string */ protected $redirectTo = '/dashboard'; + + /** + * Get the password reset validation rules. + * + * @return array + */ + protected function rules() + { + return [ + 'token' => 'required', + 'email' => 'required|email', + 'password' => ['required', 'confirmed', PasswordRules::defaults()], + ]; + } } diff --git a/app/Http/Requests/PasswordChangeRequest.php b/app/Http/Requests/PasswordChangeRequest.php index bba5480c642..da8bd8aff04 100644 --- a/app/Http/Requests/PasswordChangeRequest.php +++ b/app/Http/Requests/PasswordChangeRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests; +use Illuminate\Validation\Rules\Password as PasswordRules; + class PasswordChangeRequest extends AuthorizedRequest { /** @@ -13,7 +15,7 @@ public function rules() { return [ 'password_current' => 'required', - 'password' => 'required|confirmed|min:6', + 'password' => ['required', 'confirmed', PasswordRules::defaults()], ]; } } diff --git a/app/Listeners/LogoutUserDevices.php b/app/Listeners/LogoutUserDevices.php new file mode 100644 index 00000000000..9e2e5a750f1 --- /dev/null +++ b/app/Listeners/LogoutUserDevices.php @@ -0,0 +1,97 @@ +user instanceof User) { + $this->logoutOtherDevices($event->user); + + $this->deleteOtherSessionRecords($event->user); + } + } + + /** + * Invalidate other sessions for the current user. + * + * The application must be using the AuthenticateSession middleware. + * + * @param User $user + * @throws \Illuminate\Auth\AuthenticationException + */ + public function logoutOtherDevices($user) + { + $guard = $this->guard(); + $cookieJar = $guard->getCookieJar(); + + if ($this->recaller($guard) || + $cookieJar->hasQueued($guard->getRecallerName())) { + $cookieJar->queue($cookieJar->forever($guard->getRecallerName(), + $user->getAuthIdentifier().'|'.$user->getRememberToken().'|'.$user->getAuthPassword() + )); + } + } + + /** + * Get the guard. + * + * @return SessionGuard + */ + protected function guard(): SessionGuard + { + $guard = Auth::guard('web'); + if (! $guard instanceof SessionGuard) { + throw new \LogicException('guard is not a SessionGuard kind'); + } + + return $guard; + } + + /** + * Get the decrypted recaller cookie for the request. + * + * @param SessionGuard $guard + * @return \Illuminate\Auth\Recaller|null + */ + protected function recaller($guard): ?Recaller + { + if ($recaller = request()->cookies->get($guard->getRecallerName())) { + return new Recaller($recaller); + } + + return null; + } + + /** + * Delete the other browser session records from storage. + * + * @param User $user + * @return void + */ + protected function deleteOtherSessionRecords($user) + { + if (config('session.driver') !== 'database') { + return; + } + + DB::connection(config('session.connection'))->table(config('session.table', 'sessions')) + ->where('user_id', $user->id) + ->where('id', '!=', request()->session()->getId()) + ->delete(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6c3e772f029..c99d3b66a54 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -14,6 +14,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Validation\Rules\Password; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Auth\Notifications\VerifyEmail; use Werk365\EtagConditionals\EtagConditionals; @@ -55,6 +56,10 @@ public function boot() 'partials.check', 'App\Http\ViewComposers\InstanceViewComposer' ); + Password::defaults(function () { + return Password::min(6); + }); + if (config('database.use_utf8mb4') && DBHelper::connection()->getDriverName() == 'mysql' && ! DBHelper::testVersion('5.7.7')) { diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 393b53df8ad..93866dd87d8 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,7 +2,6 @@ namespace App\Providers; -use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider @@ -16,6 +15,9 @@ class EventServiceProvider extends ServiceProvider \Illuminate\Auth\Events\Registered::class => [ \Illuminate\Auth\Listeners\SendEmailVerificationNotification::class, ], + \Illuminate\Auth\Events\PasswordReset::class => [ + \App\Listeners\LogoutUserDevices::class, + ], ]; /**