-
-
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. | |
@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']);
}
});
}
Thank you! You saved my day.