Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

skovmand's avatar

Random TokenMismatchExceptions

Hi!

We have just deployed our new app into the wild, where it gets a lot more traffic. That is a lot of fun, and Laravel is performing good as expected.

However we are struggling with some TokenMismatchExceptions being thrown, maybe around 10 each day out of several thousand pageviews.

They are thrown from various places of the application, all POST routes. There is seemingly no system to it. It is from our login-form and also when entering data into forms elsewhere. Today alone the bug has been seen with IE 11, Chrome 44 and Safari.

I can see in my debug logs that the token they sent was simply different from the expected one in Laravel. Could it be that a new session was started meanwhile?

For example: Recieved token in POST request: 'KJS9roE1z8AZbWI2U6I4zagKZD4Ima8XjmDe0cES' Expected token was 'RbUpm9kYCsF4S6Kpq5ImewW1haHvRCvSwtSYKyTg'

Of course this might be because of attempted cross site request forgery. However I have seen it happen to people when I was sitting next to them, so I think it is not always related to that, and I can see that the data that was in the request was coming from the expected user.

Can anyone help me find the cause of the problem?

Regards, Niels.

0 likes
13 replies
thomaskim's avatar

If I were to guess, I'd say their session expired. My suggestion would be open your app/Exceptions/Handler.php file and do something like this:

use Illuminate\Session\TokenMismatchException;
    public function render($request, Exception $e)
    {
        if ($e instanceof TokenMismatchException){
            return redirect()->back()->withInput()->with('error', 'Your session has expired');
        }
        return parent::render($request, $e);
    }
2 likes
skovmand's avatar

I have recently set

'lifetime' => 1440, in config/session.php

It does not help, so I think it is not related to expiration of the session.

skovmand's avatar

The last two days I have done an experiment and put a hidden input field into my login-page. This field contains the time of the page load, so I can see how old the session is.

I just got a fresh TokenMismatchException thrown - and the session was only about 20 seconds old. This means that for some reason a new session had been initialised on the server, since the csrf_token has changed.

So, the error doesn't have anything to do with session expiration.

Any ideas? Any help would be very appeiciated!

EdwinL's avatar

It could be your server setup, if you are behind a load balancer with multiple nodes. When the balancer dispatches your request to the other node, your session is invalid because it existed on the other node. Try storing your session using the database driver.

I forgot to mention someting important. The database sessions only work when the 2 nodes/code connect to the same database. You also could install/create a memcache server and store your sessions on that.

1 like
skovmand's avatar

Thanks, I'll try out the database driver today. It's a very annoying issue! Crossing my fingers when viewing my Bugsnag status page tonight.

jimmck's avatar

When you are Posting and get a Token Mismatch error on the server. Get a new token and resend. Its not the browser, its not the driver... Set your Session timeout to 2 mins in Session.php and see...

Maybe a picture. All POSTS, except the GET to Get a new token.

\Route::get('token', function () {
       return csrf_token();
   });

    \Route::post('verifyToken', function() {
        return '{"_token": "' + csrf_token() + '"}';
    });
    this.lastError = NetError.ok;
    this.urlExists(url, this.exists);

    if (this.urlOk == false || url == ""  || arguments.length == 0) {
        fyi("Bad URL or Token [" + url + "]");
        this.getToken();
        if (this.getLastError() == NetError.ok) {
            // check token is ok.
            this.verifyToken();
            if (this.getLastError() == NetError.ok) {
                fyi("New Token [" + this.csrf_token + "]");
            } else {
                fyi("Network Error...");
            }
        }
    }

    if (this.getLastError() != NetError.ok) {
        return;
    }
skovmand's avatar

Thanks for your input. I consider it an option to get a new token via javascript, however it is very strange that the server initiates a new session on its own. However, I would definately prefer not to get new tokens dynamically, and that the server session would just persist. "Keep it simple!"

This github thread seems related: https://github.com/laravel/framework/issues/8172

skovmand's avatar

I have had some luck with different strategies and now I have only a few mismatchs per day. Though I am not sure which one of these are more effective - and maybe some of them are not related at all. However I see a big difference now.

For info: I am running my site on a week old Laravel Forge provisioned Ubuntu 14.04 server on DigitalOcean. It is the default setup.

Strategies:

  1. Editing the CRON-job which clears out old sessions in PHP to only run once every night at 04:01 GMT+2 instead of every 30 mins. All my clients are Danish, so no one is active there. I think this plays a big role. You can't disable it entirely since that will slowly eat up your disk space.

From the terminal on the server, type:

sudo nano /etc/cron.d/php5

changing the crontab line to

01 04     * * *     root   [ -x /usr/lib/php5/sessionclean ] && /usr/lib/php5/sessionclean
  1. Setting the garbage collection maxlifetime in php.ini located in /etc/php5/fpm/php.ini from 1440 seconds (=24 minutes) to 86400 seconds (24 hours).
session.gc_maxlifetime = 86400
  1. Disabling CSRF checking on routes where it's not necessary, e.g. on the login-form. In VerifyCsrfToken.php:
class VerifyCsrfToken extends BaseVerifier
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'sessions/store'
    ];
    ...
  1. Detecting whether the request is a JSON-request. In that case my site would normally redirect to the login page and then the ajax request would generate 4-5 requests all generating TokenMismatchExceptions. But for JSON-requests it will now respond with HTTP_FORBIDDEN. In Handler.php:
        if ($e instanceof TokenMismatchException) {
            if($request->isJson() || $request->ajax()) {
                return Response::json(['Session expired. Please log in again.'], HttpCodes::HTTP_FORBIDDEN);
            }

            return redirect(route('login'))
                ->with('flash_message', 'Session expired. Please log in again.');

        } 
  1. Setting my cookies on the root domain. I found out that if my cookies were set on www.mysite.dk, they would not be set on the root domain mysite.dk. The same of course goes for the sessions. I solved it by changing this line in Session.php. Note that when you do this, you should also change the SESSION_COOKIE_NAME to something else, or your client will get two may potentially conflicting cookies (the old + the new).
    'domain' => env('SESSION_COOKIE_DOMAIN', ".mysite.dk"),
arkhipov's avatar

We had the same problem (random TokenMismatchException) while using the built-in PHPStorm webserver on Windows. After we moved on Homestead, the problem has gone.

1 like
wiedem's avatar

There are several reasons why the token verification may fail:

  • Session expiration. The CSRF token is saved in your session, when your session expires the token is lost and the verification done by the VerifyCsrfToken will fail.
  • Session cookie is deleted. A user who blocks cookies or deletes the browser cookies will get a new session leading to the same behavior as if the session has expired.
  • An actual cross site request is made and the token really is invalid or missing.

You should be aware that there might be some randomness involved in the session expiration - to be more specific in the session garbage collection (depending on your session config). Unless you set the following config option 'lottery' => [100, 100], in config/session.php.

Session expiration is certainly a problem if a user is not even logged in. Laravel needs to create a session even for a logged out area in order for tokens to work. That means if a user opens your landing page, leaves the site open for a while and returns some time later the session might have expired which will cause a login to fail with a TokenMismatchException exception. The only thing you can do here is to handle that case and ask the user to try again.

If a token mismatch happens in your logged in area and no user exists in your session (=no valid logged in user) you should simply show a warning that the session might have expired and that the user should login again.

Another thing you should do on your site is to check if a user is blocking cookies, that should prevent some of the token mismatches.

@skovmand I know this posting is rather old and maybe you've already solved your problems. But my advice would be to check the logs to see which requests (URLs) caused the token mismatch exception.

Btw. changing php.ini session settings doesn't have any effect since Laravel doesn't use those settings at all. Laravel uses its own session handling which has nothing to do with the PHP session handling.

One general advice regarding sessions in Laravel:
Do not use any other session driver than a cache based one in Laravel. I.e. you should either use redis or memcached.

Some parts of Laravel's implementation of the StartSession middleware are straight up bad and have several flaws, yet the Laravel core developers are not really interested in fixing those:

  • garbage collection of session data takes place as part of a request cycle. Whenever the garbage collection takes place the system might need to delete dozens of expired sessions. And this means that the request of the unlucky user who triggered the GC will be delayed for no obvious reason and might even time out. That can be especially problematic if you're using the file session driver since each session is a file which needs to be deleted.
  • even if you set the GC lottery to 100% sessions might not time out as expected. This issue describes pretty much everything regarding this.
  • GC works completely different for cache based sessions compared to all other drivers. That's sth. that isn't event documented in Laravel. GC is still called for cache based drivers but it's simply a NOP and has no effect at all. Caches invalidate their entries (and thus their sessions) on their own and automatically delete expired entries. Which of course means that the whole GC lottery thing doesn't have any effect on them but it's still being used by the StartSession middleware.
3 likes
MikeHopley's avatar

@wiedem thanks for the detailed advice. I've always felt something weird was going on in the session with all these TokenMismatchExceptions, now I have a better idea. I will try Redis and see if that makes it better.

fahmi's avatar

@wiedem thank you. I might not get everything you said but at least I now have better understanding on what's the cause.

sholeem's avatar

I've recently had this problem and my issue was that I didn't have right permissions in storage/framework/sessions on homestead-7 box.

Maybe this answer can help someone in the future.

Cheers!

Please or to participate in this conversation.