camilovietnam's avatar

Logging out users in other devices

Hello Laravel friends.

I have a small request from a customer, to log out users from a website when they log in from different browsers or pcs. I saw that there was a logoutOtherDevices method in Laravel so I decided to use it.

I overloaded the login method in my LoginController, as:

protected function authenticated(Request $request)
    {
        Auth::guard(get_guard())->logoutOtherDevices($request->password);
    }

I uncommented the AuthenticateSession line in the Kernel.php:

   protected $middlewareGroups = [
        'web' => [
		...
            \Illuminate\Session\Middleware\AuthenticateSession::class,
		...
        ],

	...
    ];

The problem is , when I debug this code, I find that inside the call to logoutOtherDevices method in SessionGuard, the user is null, so it always returns without resetting the hash.

public function logoutOtherDevices($password, $attribute = 'password')
    {
        if (! $this->user()) {
            return;  // execution always comes here when I log-in
        }

	...

I know we have more than one guard, so it might be an issue with the route and the middlewares applied. I have been using Laravel for a few months so I don't understand the entire logic of the middlewares and the guards. Can someone help me resolve this issue? Here is my auth config:

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'staff' => [
            'driver' => 'session',
            'provider' => 'staff'
        ],
       'partner' => [
           'driver' => 'session',
           'provider' => 'partner'
       ],
        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

    'providers' => [
       'partner' => [
           'driver' => 'eloquent',
           'model' => \App\Models\Partner::class,
       ],
        'staff' => [
            'driver' => 'eloquent',
            'model' => \App\Models\Staff::class
        ],
        'users' => [
            'driver' => 'database',
            'table' => 'users',
        ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
        ],
        'partner' => [
            'provider' => 'partner',
            'table' => 'password_resets',
            'expire' => 60,
        ],
        'staff' => [
            'provider' => 'staff',
            'table' => 'password_resets',
            'expire' => 60,
        ]
    ],

];

Why is it that I'm not able to fetch the user from the Session Guard?

Thanks for your help

0 likes
11 replies
camilovietnam's avatar

Forgot to mention, get_guard() is a helper method we have to return either 'partner' or 'staff' guard. Here is the code for that method:

function get_guard() : ?string
{
    if (!Route::current()) {
        return null;
    }

    //Check if guard was set in route group
    $guard = Route::current()->getAction('guard');
    if ($guard) {
        return $guard;
    }

    //Get guard from subdomain (common used routes)
    $guards = [
        env('STAFF_SUBDOMAIN', 'staff') => 'staff',
        env('PARTNER_SUBDOMAIN', 'partner') => 'partner'
    ];
    $subdomain = Route::current()->parameter('subdomain', null);
    if ($subdomain && isset($guards[$subdomain])) {
        $guard = $guards[$subdomain];
    }

    return $guard;
}
Snapey's avatar

Cant you just use the User object?

protected function authenticated(Request $request, User $user)
{
    $user->logoutOtherDevices($request->password);
}
Snapey's avatar

I checked one of my projects where I did this, and I just used the Auth facade, although I only have the one guard so may not be relevant

Auth::logoutOtherDevices($request->password);
1 like
camilovietnam's avatar

Thank you Snapey, at least the problem about the user being null I was able to fix using the specific guard:

    protected function authenticated(Request $request, $user)
    {
        Auth::guard(get_guard())->logoutOtherDevices($request->password);
    }

And this seems to be working, the hash in the database is being updated, but the user is still logged-in when I check the page in a different browser. I can see that both tabs use a different session cookie.

I don't know where is the logic to check every request for authentication, which files should I check to make sure the auth logic is considering the password hash?

1 like
camilovietnam's avatar

Bump

I still don't fully understand Laravel's session system, can somebody enlighten me a little bit? Here's what we have:

auth.php

 'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'staff' => [
            'driver' => 'session',
            'provider' => 'staff'
        ],
       'partner' => [
           'driver' => 'session',
           'provider' => 'partner'
       ],
        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

This is our sessionconfig

 'driver' => env('SESSION_DRIVER', 'file'),
    'lifetime' => env('SESSION_LIFETIME', 120),
    'expire_on_close' => false,
    'encrypt' => false,
    'files' => storage_path('framework/sessions'),
    'connection' => env('SESSION_CONNECTION', null),
    'table' => 'sessions',
    'store' => env('SESSION_STORE', null),
    'lottery' => [2, 100],
    'cookie' => env(
        'SESSION_COOKIE',
        Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
    ),
    'path' => '/',
    'domain' => env('SESSION_DOMAIN', null),
    'secure' => env('SESSION_SECURE_COOKIE', false),
    'http_only' => true,
    'same_site' => null,

I understand that Laravel has their own session system, but I'm just not sure where in the code is the user authenticated. I debugged a request to our system and I saw that when using the AuthenticateSession middleware,we are always hitting this part of the code:

  public function handle($request, Closure $next)
    {
        if (! $request->user() || ! $request->session()) {
            return $next($request);  // <-- all requests hit this line
        }
	…
}

I don't understand this, so basically our request has no user, and has no session? How are we doing authentication then? I'm so confused...

camilovietnam's avatar

Bump

This is not working

I open an incognito window, log-in again, and the hash in the Database changes but the user is still logged in, in both of the browser windows.

I have multiple guards, could this be the reason

camilovietnam's avatar

If the user is not being logged out, it means the code is failing to detect that the password hash in the database has changed

Where in the code do we get the hash from the cookie and compare to the hash in the user pass? Am I correct assuming that the password hash is stored in the cookie and sent with every request?

camilovietnam's avatar

Bump

I am debugging DatabaseUserProvider.php and I was expecting the code to hit one of the functions used to retrieve the user

    public function retrieveById($identifier)
    {
      …
    }

    public function retrieveByToken($identifier, $token)
    {
        …
    }

    public function updateRememberToken(UserContract $user, $token)
    {
        …
    }

    public function retrieveByCredentials(array $credentials)
    {
         … 
    }

    protected function getGenericUser($user)
    {
        …
    }
}

However, none of these are being used, and the code finishes without triggering the debugger. I was assuming this was how the user was being retrieved, but it seems not.

reza305's avatar

@camilovietnam This event is fired when you use Auth::logoutOtherDevices method. Here is the method:

/**
 * Invalidate other sessions for the current user.
 *
 * The application must be using the AuthenticateSession middleware.
 *
 * @param  string  $password
 * @param  string  $attribute
 * @return bool|null
 */
public function logoutOtherDevices($password, $attribute = 'password')
{
    if (! $this->user()) {
        return;
    }

    $result = tap($this->user()->forceFill([
        $attribute => Hash::make($password),
    ]))->save();

    $this->queueRecallerCookie($this->user());

    $this->fireOtherDeviceLogoutEvent($this->user());

    return $result;
}
HiwaB41's avatar

@reza305 how to logout a device that is trying to log into an active(being used by another device) account?

Please or to participate in this conversation.