-
-
Save jrmadsen67/bd0f9ad0ef1ed6bb594e to your computer and use it in GitHub Desktop.
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. | |
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."
Great explanation and thanks for the comments about the explanations for the end user!
Thx for this
Great work, thank you..
Thank you! You saved my day.
@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);
}
When I use this, the redirect is preformed. But I do not see the error message or the old form input.
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');
}
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.
does it works in Laravel 8?
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'); } });
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']);
}
});
}
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."