How to use remember me for multiple devices in Laravel
How to avoid being logged out when using remember me.
How does remember me work?
There is a string column in the users table which is called remember_token
.
When user tries to login using username/password, then Auth:attempt
is called which received the credentials, as well the
info if user should be remembered:
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
If you take a look into the framework at Illuminate/Auth/SessionGuard
, then this is where attempt
be called by default:
public function attempt(array $credentials = [], $remember = false)
{
$this->fireAttemptEvent($credentials, $remember);
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
// If an implementation of UserInterface was returned, we'll ask the provider
// to validate the user against the given credentials, and if they are in
// fact valid we'll log the users into the application and return true.
if ($this->hasValidCredentials($user, $credentials)) {
$this->login($user, $remember);
return true;
}
// If the authentication attempt fails we will fire an event so that the user
// may be notified of any suspicious attempts to access their account from
// an unrecognized user. A developer may listen to this event as needed.
$this->fireFailedEvent($user, $credentials);
return false;
}
If you continue following the path of $this->login(..)
you will see that it will create a new remember token
public function login(AuthenticatableContract $user, $remember = false)
{
$this->updateSession($user->getAuthIdentifier());
// If the user should be permanently "remembered" by the application we will
// queue a permanent cookie that contains the encrypted copy of the user
// identifier. We will then decrypt this later to retrieve the users.
if ($remember) {
$this->ensureRememberTokenIsSet($user);
$this->queueRecallerCookie($user);
}
// If we have an event dispatcher instance set we will fire an event so that
// any listeners will hook into the authentication events and run actions
// based on the login and logout events fired from the guard instances.
$this->fireLoginEvent($user, $remember);
$this->setUser($user);
}
The method ensureRemeberTokenIsSet
will update the column remember_token
on the users row in the users
table,
if and only if the column is null. Otherwise the existing will stay.
The queueRecallerCookie
method will write the remember_me
token from the user in the clients cookie. If you need
a refresher about how laravel session and cookies work, checkout my Medium article: How Do Laravel Sessions Work?.
Everytime a page with auth protection is called, the user()
method from the Guard will be called.
For the SessionGuard
, it looks like this:
public function user()
{
if ($this->loggedOut) {
return;
}
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
$id = $this->session->get($this->getName());
// First we will try to load the user using the identifier in the session if
// one exists. Otherwise we will check for a "remember me" cookie in this
// request, and if one exists, attempt to retrieve the user using that.
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
$this->user = $this->userFromRecaller($recaller);
if ($this->user) {
$this->updateSession($this->user->getAuthIdentifier());
$this->fireLoginEvent($this->user, true);
}
}
return $this->user;
}
The recaller will simply get the remember_me
token from the cookie and will try to find a user in the users
table who has the same
token in the remember_me
column. If one is found, he will be logged in.
The many device issue
It is possible to use the remember_me
function from many devices. In this case, all devices have the same remember_token
stored in their cookie.
If you logout using $user->logout()
, the remeber_token
from the row in the users
table will be removed. This means, the remember_me functionality will stop working for all
other devices, as the there stored remember_token
in the cookie won't find a match in the users
table.
Calling the logout()
method on the guard is the default behaviour:
public function destroy(Request $request): RedirectResponse
{
Auth::guard('admin')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
There is the function logoutOtherDevices
aswell, which will basically recreate a new remeber_token
. This means all other devices will be logged out, but you current stays logged it.
Since Laravel 6, a new function has been introduced, callend $user->logoutCurrentDevice()
. This will cleanup the cookie for the current device and
logout the user, but keeps the remember_me
token in the database. To use it, you would need to customize the logout method from your Auth/AuthenticatedSessionController.php
.
In our case, we would have to change it like this:
public function destroy(Request $request): RedirectResponse
{
Auth::guard('admin')->logoutCurrentDevice(); // <---
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}