phplaravelsession

Session inconsistency in database after login on LoginController::authenticated()?


I have the SESSION_DRIVER set to database. After login, when I try to get the session()->getId() in the LoginController::authenticated() method, I seem to get the session id that was before regeneration. The database seems to contain this session id as well.

This causes problems when I try to log accesses to my application. I can't update the logout time against a session id because when I log the access in the LogSuccessfulLogin listener, the access is logged against the session id before regeneration and on logout I get the session id after regeneration for which a record doesn't exist in the access logs table.

Moreover, when I try to get $request->user()->sessions()->count() on the LoginController::authenticated() method, I always seem to get activeSessions-1 as the count. For example, I login on Chrome, I get the count as 0. I concurrently login from FireFox and I get the count as 1. The session seems to be inserted in database after the authenticated method. Following is the code for my LoginController:

<?php

namespace App\Http\Controllers\Auth;

use App\Facades\Settings;
use App\Http\Controllers\Controller;
use App\Models\SamlTenant;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Authy\AuthyApi;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    protected $decayMinutes = 5;

    protected $authy;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');

        $this->authy = new AuthyApi(config('auth.authy_key'));
    }

    /*
     * https://laravel.com/docs/6.x/authentication#authenticating-users
     */
    protected function credentials(Request $request)
    {
        return array_merge($request->only($this->username(), 'password'), ['status_id' => User::STATUS_ACTIVE]);
    }

    /**
     * @param Request $request
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
     */
    public function showLoginForm(Request $request)
    {
        if(SamlTenant::isSamlConfigured() && $tenant = SamlTenant::getSamlTenant()) {
            return redirect($tenant->idp_login_url);
        }

        return $this->extLogin($request);
    }

    public function extLogin(Request $request)
    {
        return view('auth.login');
    }

    /**
     * @param Request $request
     * @return \Illuminate\Http\Response|void
     * @throws ValidationException
     */
    public function login(Request $request)
    {
        $this->validateLogin($request);

        // We can automatically throttle the login attempts for this application.
        // We'll key this by the username and the IP address of the client making these requests into this application.
        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            //return $this->sendLockoutResponse($request);
            $this->sendLockoutResponse($request);
        }

        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

        // If the login attempt was unsuccessful we will increment the number of attempts
        // to login and redirect the user back to the login form. Of course, when this
        // user surpasses their maximum number of attempts they will get locked out.
        $this->incrementLoginAttempts($request);

        return $this->sendFailedLoginResponse($request);
    }

    protected function sendFailedLoginResponse(Request $request)
    {
        $message = trans('auth.failed');
        $user = User::where($this->username(), $request->{$this->username()})->first();

        if ($user && \Hash::check($request->password, $user->password)) {
            if ($user->status_id === User::STATUS_INACTIVE) {
                $message = trans('auth.account.inactive');
            } else if ($user->status_id === User::STATUS_PENDING) {
                $message = 'Account is pending.';
            } else if ($user->status_id === User::STATUS_LOCKED) {
                $message = 'Account is locked.';
            }
        }

        // other  user validation checks

        throw ValidationException::withMessages([
            $this->username() => [$message]
        ]);
    }

    /**
     * @param Request $request
     * @param User $user
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    protected function authenticated(Request $request, User $user)
    {
        // session id and count inconsistent
    }

    /**
     * @param Request $request
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
     * @throws \Exception
     */
    public function app(Request $request)
    {
        if (\Auth::check()) {
            return redirect()->intended(RouteServiceProvider::HOME);
        }

        return $this->showLoginForm($request);
    }

    /**
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     * @throws \Exception
     */
    public function logout(Request $request)
    {
        $isLoginTypeSso = $request->user() !== null
            ? $request->user()->isLoginTypeSso()
            : false;

        $this->guard()->logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();

        if ($isLoginTypeSso && Settings::isSsoEnabled()) {

            if (($tenant = SamlTenant::getSamlTenant()) !== null) {
                //return redirect(url('/sso/' . $tenant->uuid . '/logout'));
                return redirect(url('/auth/sso-logout'));
            }

            throw new \Exception('SAML tenant not setup');
        }

        return $this->loggedOut($request) ?: redirect('/');
    }

    public function ssoLogout()
    {
        return view('auth.logout');
    }
}

Am I missing something or is this supposed to be intented behaviour? Thank you in advance.


Solution

  • I found the problem. Session saving is written in StartSession middleware and will kick in only on each request. In this case, we are not redirecting after $request->session()->regenerate();, rather we are calling the ->authenticated() method.

    The session is not saved due to the middleware not being triggered yet, hence the inconsistency in session id. User's session count is also inconsistent due to this, since the latest session is not yet saved, the count will always be returned as actualCount - 1.

    A workaround to this is to override ->sendLoginResponse($request) method and manually save the session after regeneration like so:

        protected function sendLoginResponse(Request $request)
        {
            $request->session()->regenerate();
            $request->session()->save(); // this solves the issue
    
            $this->clearLoginAttempts($request);
    
            return $this->authenticated($request, $this->guard()->user())
                ?: redirect()->intended($this->redirectPath());
        }