Skip to content

Commit

Permalink
feat: revoke session from other browser after a password change (#5328)
Browse files Browse the repository at this point in the history
  • Loading branch information
asbiin committed Jun 28, 2021
1 parent 9cbc2dd commit a4c037f
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 5 deletions.
3 changes: 2 additions & 1 deletion app/Http/Controllers/Auth/InvitationController.php
Expand Up @@ -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
{
Expand Down Expand Up @@ -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',
]);
}
Expand Down
5 changes: 4 additions & 1 deletion app/Http/Controllers/Auth/PasswordChangeController.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -128,6 +129,8 @@ protected function setNewPassword($user, $password)

$user->save();

event(new PasswordReset($user));

Auth::guard()->login($user);
}

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/Auth/RegisterController.php
Expand Up @@ -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
{
Expand Down Expand Up @@ -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',
]);
}
Expand Down
15 changes: 15 additions & 0 deletions app/Http/Controllers/Auth/ResetPasswordController.php
Expand Up @@ -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
{
Expand All @@ -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()],
];
}
}
4 changes: 3 additions & 1 deletion app/Http/Requests/PasswordChangeRequest.php
Expand Up @@ -2,6 +2,8 @@

namespace App\Http\Requests;

use Illuminate\Validation\Rules\Password as PasswordRules;

class PasswordChangeRequest extends AuthorizedRequest
{
/**
Expand All @@ -13,7 +15,7 @@ public function rules()
{
return [
'password_current' => 'required',
'password' => 'required|confirmed|min:6',
'password' => ['required', 'confirmed', PasswordRules::defaults()],
];
}
}
97 changes: 97 additions & 0 deletions app/Listeners/LogoutUserDevices.php
@@ -0,0 +1,97 @@
<?php

namespace App\Listeners;

use App\Models\User\User;
use Illuminate\Auth\Recaller;
use Illuminate\Auth\SessionGuard;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Auth\Events\PasswordReset;

class LogoutUserDevices
{
/**
* Handle the event.
*
* @param \Illuminate\Auth\Events\PasswordReset $event
* @return void
*/
public function handle(PasswordReset $event)
{
if ($event->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();
}
}
5 changes: 5 additions & 0 deletions app/Providers/AppServiceProvider.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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')) {
Expand Down
4 changes: 3 additions & 1 deletion app/Providers/EventServiceProvider.php
Expand Up @@ -2,7 +2,6 @@

namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
Expand All @@ -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,
],
];

/**
Expand Down

0 comments on commit a4c037f

Please sign in to comment.