Skip to content

Instantly share code, notes, and snippets.

@jrmadsen67
Last active August 19, 2024 10:37
Show Gist options
  • Save jrmadsen67/bd0f9ad0ef1ed6bb594e to your computer and use it in GitHub Desktop.
Save jrmadsen67/bd0f9ad0ef1ed6bb594e to your computer and use it in GitHub Desktop.
Laravel Quick Tip: Handling CsrfToken Expiration gracefully
Quick tip for handling CSRF Token Expiration - common issue is when you use csrf protection is that if
a form sits there for a while (like a login form, but any the same) the csrf token in the form will
expire & throw a strange error.
Handling it is simple, and is a good lesson for dealing with other types of errors in a custom manner.
In Middleware you will see a file VerifyCsrfToken.php and be tempted to handle things there. DON'T!
Instead, look at your app/Exceptions/Handler.php, at the render($request, Exception $e) function.
All of your exceptions go through here, unless you have excluded them in the $dontReport array at the
top of this file. You can see we have the $request and also the Exception that was thrown.
Take a quick look at the parent of VerifyCsrfToken - Illuminate\Foundation\Http\Middleware\VerifyCsrfToken.
You can see from VerifyCsrfToken.php that handle() is the function called to do the token check. In the
parent class, if the token fails, a `TokenMismatchException` is thrown.
So back in the Handler class, let's specifically handle that type of exception:
public function render($request, Exception $e)
{
if ($e instanceof \Illuminate\Session\TokenMismatchException)
{
return redirect()
->back()
->withInput($request->except('password'))
->with([
'message' => 'Validation Token was expired. Please try again',
'message-type' => 'danger']);
}
return parent::render($request, $e);
}
The code is simple - if the exception is a `TokenMismatchException` we will handle it just like
a validation error in a controller. In our forms(s), we need to be sure to use the
$request->old('field_name') (or the old('field_name') helper function) to repopulate. Simply going
"back" will refresh the form with a new token so they can re-submit.
CAREFUL! - I found that using the http://laravelcollective.com/ Form::open() tag seemed to be
incompatible with the token - redirect()->back() was not refresh the token for me. This may just be
something in my code, but when I used a regular html tag it was fine. If this is happening to you,
try that.
@ChrisReid
Copy link

I have an improvement on this for when you're using the http://laravelcollective.com/ Form::open() tag. You need to also ignore the _token field when redirecting and re-rendering the form.

if ($e instanceof \Illuminate\Session\TokenMismatchException)
    {
        return redirect()
            ->back()
            ->withInput($request->except('password', '_token'))
            ->withError('Validation Token has expired. Please try again');
    }

Have tested this and it works.

@shangsunset
Copy link

what about sending form via ajax?

@danielbaylis
Copy link

Thank you! Just what I was looking for.

@justin-cook
Copy link

This method is good but don't use, "Validation Token was expired. Please try again." That is not a helpful explanation to an enduser.

I'd suggest using something more simple/familiar such as, "Your session timed out."

@Wendtly
Copy link

Wendtly commented May 26, 2016

Except that it isn't the session that's timing out, it's the token. The session has a duration you set is config/Sessions.php. The token always seems to have a 2 hour duration. Perhaps "Your form has expired. Please refresh the page and try again."

@martijnimhoff
Copy link

Great explanation and thanks for the comments about the explanations for the end user!

@davispeixoto
Copy link

Thx for this

@waqas-mehmood-pk
Copy link

Great work, thank you..

@tjanuki
Copy link

tjanuki commented Sep 29, 2017

Thank you! You saved my day.

@troccoli
Copy link

troccoli commented Oct 19, 2017

@jrmadsen67 Thank you, this is awesome

@Wendtly I don't think the message should say "refresh the page" as there is no need to refresh the page.

@ChrisReid, I don't know if I had to, but I also added the password_confirmation field to the list, so now my method looks like

public function render($request, Exception $e)
    {
        if ($e instanceof TokenMismatchException) {
            return redirect()
                ->back()
                ->withInput($request->except('password', 'password_confirmation', '_token'))
                ->with(['error' => 'Your form has expired. Please try again']);
        }

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

@megawubs
Copy link

megawubs commented May 1, 2018

When I use this, the redirect is preformed. But I do not see the error message or the old form input.

@ryancwalsh
Copy link

This was super helpful. Thank you.

Here is a modern version:

public function render($request, Exception $exception) {
    if ($exception instanceof \Illuminate\Session\TokenMismatchException) {//https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e
        //https://laravel.com/docs/5.6/validation#quick-displaying-the-validation-errors
        $errors = new \Illuminate\Support\MessageBag(['password' => 'For security purposes, the form expired after sitting idle for too long. Please try again.']);
        return redirect()
                        ->back()
                        ->withInput($request->except($this->dontFlash))
                        ->with(['errors' => $errors]);
    }
    return parent::render($request, $exception);
}

And to prove to yourself that $request->except($this->dontFlash) is successfully stripping out the password, you can temporarily add this to your LoginController on your local dev machine and play around:

public function showLoginForm(\Illuminate\Http\Request $request)
{
    Log::debug(json_encode($request->session()->all()));
    return view('auth.login');
}

@zhegwood
Copy link

zhegwood commented Oct 31, 2018

I have a REST/single page application and found this. For the ajax questions, this worked for me... Not super graceful, but it does the trick.
`
public function render($request, Exception $e)
{

    if ($e instanceof \Illuminate\Session\TokenMismatchException) {
        if ($request->ajax()) {
            return response([
                'error' => 'expired',
                'success' => false,
            ], 302);
        } else {
            return redirect('/');
        }
    }

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

`
Of course, your code that handles 302 in an ajax request needs to listen for this and manually redirect... location.href = "/" or something similar, but it works.

@dagpacket
Copy link

does it works in Laravel 8?

@dont-panic-42
Copy link

dont-panic-42 commented Jul 21, 2021

This is great, thank you.

Regarding how to handle AJAX requests, here's what I did.

  • First, add the code above to Handler.php, so normal requests are handled gracefully.

  • Next, tweak that code a bit to ignore AJAX requests. If it is an AJAX request, we don't want to redirect back with an error, so exclude them like so:

    if ($e instanceof \Illuminate\Session\TokenMismatchException && ! $request->expectsJson()) {
    
  • Now AJAX requests will throw an exception just as before, and we need to handle those in JS. I'm using jQuery, so for eg with $.post():

    $.post(...)
    .fail(function xhr, status, error) {
        if (xhr.status === 419) {
            // Token error - handle however you want
            alert('The page has expired due to inactivity. Please refresh and try again');
        }
    });
    

@alikuro
Copy link

alikuro commented Aug 15, 2021

does it works in Laravel 8?

@dagpacket you can try this code, it woks on 8

    use Exception;

    . . . 

    public function register() {

        $this->reportable(function (Throwable $e) {
            // ..
        });

        $this->renderable(function (Exception $e, $request) {
            if ($e->getPrevious() instanceof \Illuminate\Session\TokenMismatchException) {
                return redirect()
                ->back()
                ->withInput($request->except('password', 'password_confirmation', '_token'))
                ->withErrors(['Your form has expired, please try again']);
            }
        });
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment