Handling expired token in Laravel
What is the best way to handle expired tokens in laravel 5.
I mean I have a page and it has some links which perform ajax requests. They work fine when the page is loaded but when I wait for sometime then I get a TOKEN MISMATCH error.
Now, I have to refresh the page to make it work again. BUT, I don't want to refresh the page. I want some way to refresh the token or some other work around to make it fix.
I hope you got my point.
Solution 1:
I think the answer by @UX Labs is misleading. And then the comment from @jfadich seems completely incorrect.
For Laravel 5.4 in May 2017, I solved the problem this way:
Here Is an Answer That Works
In web.php
:
Route::post('keep-token-alive', function() {
return 'Token must have been valid, and the session expiration has been extended.'; //https://stackoverflow.com/q/31449434/470749
});
In javascript in your view:
$(document).ready(function () {
setInterval(keepTokenAlive, 1000 * 60 * 15); // every 15 mins
function keepTokenAlive() {
$.ajax({
url: '/keep-token-alive', //https://stackoverflow.com/q/31449434/470749
method: 'post',
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
}).then(function (result) {
console.log(new Date() + ' ' + result + ' ' + $('meta[name="csrf-token"]').attr('content'));
});
}
});
Note that you must not list 'keep-token-alive'
in the exclusions within VerifyCsrfToken.php
. As @ITDesigns.eu implied in a comment, it's important for this route to verify that there is a valid token currently and that it just needs to have its expiration extended.
Why this approach solves my problem
My Laravel site allows users to watch a video (an hour long), and it uses ajax to post their progress every minute.
But many users load the page and then don't start the video until many hours later.
I don't know why they leave their browser tab open so long before watching, but they do.
And then I'd get a ton of TokenMismatch exceptions in my logs (and would miss out on the data of their progress).
In session.php
, I changed 'lifetime'
from 120 to 360 minutes, but that still wasn't enough. And I didn't want to make it longer than 6 hours. So I needed to enable this one page to frequently extend the session via ajax.
How you can test it and get a sense for how the tokens work:
In web.php
:
Route::post('refresh-csrf', function() {//Note: as I mentioned in my answer, I think this approach from @UX Labs does not make sense, but I first wanted to design a test view that used buttons to ping different URLs to understand how tokens work. The "return csrf_token();" does not even seem to get used.
return csrf_token();
});
Route::post('test-csrf', function() {
return 'Token must have been valid.';
});
In javascript in your view:
<button id="tryPost">Try posting to db</button>
<button id="getNewToken">Get new token</button>
(function () {
var $ = require("jquery");
$(document).ready(function () {
$('body').prepend('<div>' + new Date() + ' Current token is: ' + $('meta[name="csrf-token"]').attr('content') + '</div>');
$('#getNewToken').click(function () {
$.ajax({
url: '/refresh-csrf',
method: 'post',
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
}).then(function (d) {
$('meta[name="csrf-token"]').attr('content', d);
$('body').prepend('<div>' + new Date() + ' Refreshed token is: ' + $('meta[name="csrf-token"]').attr('content') + '</div>');
});
});
$('#tryPost').click(function () {
$.ajax({
url: '/test-csrf',
method: 'post',
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
}).then(function (d) {
$('body').prepend('<div>' + new Date() + ' Result of test: ' + d + '</div>');
});
});
});
})();
In session.php
, temporarily change 'lifetime'
to something very short for testing purposes.
Then play around.
This is how I learned how the Laravel token works and how we really just need to successfully POST to a CSRF-protected route frequently so that the token continues to be valid.
Solution 2:
Update 2021:
Hello Stackoverflow! It seems that the answer we've posted a few years ago has sparked some controversy.
To sum it up, the approach we've posted does solve the technical aspect of the problem. However, from web security standpoint it seems to be debatable.
With our limited expertise, we still believe our solution is viable, but to reduce doubt please make sure to go through the comments section as well as the answer posted by Ryan since they think otherwise before you make your decision. Thanks.
Original Answer From 2015
a work around for it, is to actually get the new token every certain time, otherwise you are defeating the purpose of the csrf token:
<html>
<head>
<meta name="csrf_token" content="{{ csrf_token() }}">
</head>
<body>
<script type="text/javascript">
var csrfToken = $('[name="csrf_token"]').attr('content');
setInterval(refreshToken, 3600000); // 1 hour
function refreshToken(){
$.get('refresh-csrf').done(function(data){
csrfToken = data; // the new token
});
}
setInterval(refreshToken, 3600000); // 1 hour
</script>
</body>
</html>
In laravel routes
Route::get('refresh-csrf', function(){
return csrf_token();
});
I apologize in case of any syntax errors, haven't used jquery for long time, but i guess you get the idea
Solution 3:
I combine 2 things for this case:
1. Increase session lifetime
//In config/session.php replace this:
'lifetime' => 120
//with:
'lifetime' => 360
Laravel 5 default lifetime is 120 (minutes), you can change it to whatever value you like, for example 360 (6 hours)
2. Catch the exception and display an error message
//In app/Exceptions/Handler.php replace this:
public function render($request, Exception $e)
{
if ($e instanceof ModelNotFoundException) {
$e = new NotFoundHttpException($e->getMessage(), $e);
}
return parent::render($request, $e);
}
//with:
public function render($request, Exception $e)
{
if ($e instanceof ModelNotFoundException) {
$e = new NotFoundHttpException($e->getMessage(), $e);
}
if ($e instanceof \Illuminate\Session\TokenMismatchException) {
return redirect('/')->withErrors(['token_error' => 'Sorry, your session seems to have expired. Please try again.']);
}
return parent::render($request, $e);
}
So basicaly you redirect the user to the root "/" (you can change this to any path you want) with an error message and on that page you have to do this to display the error message:
@if ($errors->has('token_error'))
{{ $errors->first('token_error') }}
@endif
Solution 4:
According to the docs:
Laravel automatically generates a CSRF "token" for each active user session managed by the application.
This means, for any individual the csrf code is the same for any page that the user visits. It becomes invalid once your session expires. Thus if you set the lifetime to 1 week, CSRF token will only expire after 1 week.
This can achieved like this in config/session.php
:
/*
|--------------------------------------------------------------------------
| 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' => 60 * 24 * 7, // Set session lifetime to 1 week
'expire_on_close' => true,
Why I dont like any of the above answers:
- Answer from UX Labs:
Keeps the session forever alive and recreates a new CSRF token
after a fixed time. This is an issue if the user has multiple taps open. Everytime one tap refreshes the CSRF
token, all the other tabs become invalid.
- Answer from Ryan
This answer is better, because it does not change the CSRF token
, so multiple tabs are not effected. It simply keeps the session alive with making a js call after a fixed time with setInterval
. However, setInterval
does not work while the PC goes to sleep. So the session may expire when the PC goes to sleep, which is also a likly scenario. Therefore, instead of trying to keep the session alive by js-calls,just increase lifetime.
- Answer from paulalexandru
Displaying an error when session is timed out is okay, but it would be better if the issue never happens. Setting lifetime to 6h is not enough, because its likly that a tab may be open for a couple of days.
- Other answers
All the other answers propose to disable CSRF for the route in questions, but this is of course no option, because it creates a big security risk.
Solution 5:
Best way to handle this Exception is with App\Exceptions\Handler.php
.
public function render($request, Exception $e) {
if ($e instanceof \Illuminate\Session\TokenMismatchException) {
return Redirect::back()->withErrors(['session' => 'Désolé, votre session semble avoir expiré. Veuillez réessayer.']);
}
return parent::render($request, $e);
}
and where ever you wanna show this message (in all your pages that contains csrf_token
), add this piece:
<div>
@if(count($errors)>0)
@foreach($errors->all() as $error)
<ul>
<li>{{$error}}</li>
</ul>
@endforeach
@endif
</div>