Snapey's avatar
Level 122

CRSF checked before Auth

I seem to be hitting a scenario where the CSRF token is validated before checking that the user is logged in.

So, if the user goes away for more than 2 hours then comes back and hits a link that requires them to be logged in, the Authenticate middleware runs and redirects them to the Auth\login page.

If however, they are on a form page, and they go away for more than two hours, the csrf token and the session token expire. Now they come back and fill in the form and press submit. This time, the csrf token is checked first and instead of being passed to the login page they see a Token Mismatch error.

I'm assuming that this is because VerifyCSRFToken is a global middleware and the Authenticate middleware is route. This affects the order that they are evaluated.

HOW can I handle this gracefully? What do others do?

0 likes
26 replies
bestmomo's avatar

There is no difference between your guy that goes away for more than 2 hours and someone that try to send a post request with a random token. So it's logical to send a Token Mismatch error.

bashy's avatar

Catch the exception and maybe redirect back (this should then activate the redirect for login).

bashy's avatar

You can make a global handler for it in app/Exceptions/Handler.php?

Snapey's avatar
Level 122

So I added

    public function render($request, Exception $e)
    {
        if ($e instanceof \Illuminate\Session\TokenMismatchException) {
            
            return redirect('/')->with('message', 'Sorry, your session seems to have expired. Please login again.');

        }

        return parent::render($request, $e);
    }

but I can't seem to get the message to display?

1 like
bashy's avatar

Does it redirect you?

Pick the message up with session

{{ Session::get('message') }}
JacobBennett's avatar

Had this same problem not too long ago. Chose to display a javascript pop-up letting the user know they had an expired session. The two options were to renew their session, or to log out. In the case that they renewed the session, I simply refreshed the page, which would catch an expired session on route that is protected by the auth middleware, so the user would be prompted to log in again. In my Sessions Controller, I made sure that my store method was using a Redirect::intended('default.route') after logging in to send users that had chosen to refresh back to the page they were just on. Those who chose to log out of course were just sent to the logout page.

The way you are doing it seems like it would work just fine as well, but would be a good idea to use the Redirect::intended() to send users back to the location they were at previously.

1 like
Snapey's avatar
Level 122

@JacobBennett the problem with the JavaScript as you have described is that the user can quite easily be working away in another tab, keeping their session going. At the same time, the token expires on the form. Altering your idea, better wording might be "this form has expired, click ok to refresh the page or logout to end your session.

The other thread was looking for a way to renew and replace the token in the background.

JacobBennett's avatar

@Snapey that makes sense, and you're right, that is better wording for the problem the user is having. Might have to revisit my code :)

garethdaine's avatar

Just had someone ask a similar question in IRC, does this work for you guys.

public function render($request, Exception $e)
{
    if ($e instanceof \Illuminate\Session\TokenMismatchException) {
        return redirect()->back()->withInput()->with('token', csrf_token());
    }

    return parent::render($request, $e);
}

Then check in your view for the new token and replace the old, as well as using Request::old() on your fields.

Not tested but I imagine it will get someone on the right track.

mmguide's avatar

I tried both of the above and it just stayed on the upload page with a blank screen. I'm a bit of a noobie so maybe a rookie mistake. Any ideas please?

ClearanceJobsDev's avatar

Putting something like this in App::error() works for me in Laravel 4.x

    if ($exception instanceof \Illuminate\Session\TokenMismatchException) {
        return Redirect::back()->withInput(Input::except('_token'))
            ->withMessage('Your session has expired. Please try again.');
    }
1 like
mistermat's avatar

It seems we cannot put or flash to a session after it has expired, so withInput() or with() is out of the question. I also tried to session()->regenerate() within the catch block, without any success.

Anyone else with any luck?

jimmck's avatar

All I do is get a new token and send the request again. Works everytime.

jonnywilliamson's avatar

@jimmck - Could you expand on that?

Do you catch the exception, then get a new token and resubmit the same data? Isn't there a risk of an infinite loop there?

lafortuna18's avatar

You can set the session lifetime in the /app/config/session.php file, where 120 represents minutes.

/* |-------------------------------------------------------------------------- | Session Lifetime |-------------------------------------------------------------------------- | | Here you may specify the number of minutes that you wish the session | to be allowed to remain idle before it expires. If you want them | to immediately expire on the browser closing, set that option. | */

'lifetime' => 120,

'expire_on_close' => false,

jlrdw's avatar

A user should logout if leaving the computer even for short periods. If they pull that on a state of Texas computer they can actually be punished, fined.
Likewise they should have to login again if a session expired I don't even see no big deal here.

Snapey's avatar
Level 122

@lafortuna18 @jlrdw neither posting relate to the original issue. This was a year ago however, on 5.0 or 5.1. The new middleware arrangements in 5.2 may negate the issue.

Look at it this way. Your user does logout (heeding legal advice when in the state of Texas (whatever)). Perhaps you leave them on the login page, or perhaps the home page with embedded login form. That page has a timebomb csrf token. The user fills out the form three hours later and sees csrf token error. Its not clever and it's not nice.

There at least four solutions

  1. remove csrf requirement on login pages (it serves no useful purpose anyway)
  2. use caffeine to keep the session going (ties up resources)
  3. Catch the csrf error, apologise and reshow the form
  4. After logout, leave the user on a page that has no login form.

None of the above are resolved by tinkering with the session timeout.

1 like
jlrdw's avatar

@Snapey I wonder how PayPal, eBay, and some sites like that handle, if you are on eBay and left screen for 2 hours would you have to completely re- login or does it just hang for 2 hours I just don't have time to try that. Maybe we can get @jekinney to test that he he (big grin).

Snapey's avatar
Level 122

@jldrw - pretty sure you wont see 'an error has occurred' or 'csrf token mismatch'

1 like
jekinney's avatar

@jlrdw Lots of sites use a Javascript timer to pop up an alert when your session is about to time out, and on confirm resets the timer. When the timer runs out you can auto redirect or page refresh with out input from the user is one way that either removes the session or refreshes it. So a session time of two minutes javascript timer max is set at 1 1/2 minutes (for example). If the user clicks some type of confirmation in time will send an ajax call to the server to reset the time on the session with a partial page refresh (usually like some hidden div or something).

For the CSRF issue on 5.2 and the web middleware group:

// Kernal.php under Http
/**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
        ],
'auth-web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
      // Make new group and add auth check. BUT MUST be before the csrf check
       \App\Http\Middleware\Authenticate::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
        ],

        'api' => [
            'throttle:60,1',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    ];

The order it is fired is important.

Route::group(['middleware' => ['auth', 'web']], function () {
    // This way should fire the auth middleware first and as the web group has the csrf token should check that after auth is done,
});
Route::group(['middleware' => [ 'web']], function () {
    Route::group(['middleware' =>  'auth'], function () {
            // This will always fire the web (csrf) first then the auth,
    });
});

I separate my routes personally so all guest routes are in:

Route::group(['middleware' => [ 'web']], function () {
    Route::get('/', ['as' => 'home', 'uses' => 'PageController@home']);
});

All my routes that need auth protection

Route::group(['middleware' => ['auth', 'web']], function () {
    Route::get('profile/{user}/edit', ['as' => 'profile.edit', 'uses' => 'Account\ProfileController@edit'});
});
1 like
all1.ai's avatar

This is what I came up with that works for me!

public function render($request, Exception $e)
    {
         if ($e instanceof \Illuminate\Session\TokenMismatchException) {
            
              return redirect()
                  ->back()
                  ->withInput($request->except('_token'))
                  ->withMessage('Your explanation message depending on how much you want to dumb it down, lol! ');

        }

        return parent::render($request, $e);
    }
}
2 likes
janduano's avatar

I fix this, with this piece of code in Handler

public function render($request, Exception $exception)
{
    if  (!Auth::check()) {
            if ($exception instanceof \Illuminate\Session\TokenMismatchException) {
                return redirect()->back();
            }
    }

    return parent::render($request, $e);
}
1 like
froind's avatar

This is an issue that has been fixed in Laravel 5.3

I'm just posting this so that people using Laravel < 5.3 know how to fix it. You just need to paste the following method (found in above link)

addCookieToResponse

in

app/Http/Middleware/VerifyCsrfToken.php
ThinkingMan's avatar

@froind Can you confirm that your last response to paste addCookieToResponse to the VerifyCsrfToken.php file worked? If so, could you show it in the updated file?

Please or to participate in this conversation.