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('/');
}