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
a short and fast way.... for handling ajax requests,when token expire : add this script to the end of master layout or your document
$(window).load(function(){
$.ajaxSetup({
statusCode: {
419: function(){
location.reload();
}
}
});
});
and for handling http requests when token expires, create 419.blade.php in this path: \resources\views\errors and add this script to it:
<script type="text/javascript">
//reload on current page
window.location = '';
</script>
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:
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.
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.
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.
Increase the lifetime
of your sessions. You can do so by editing the config/session.php
file in your laravel configuration.
/*
|--------------------------------------------------------------------------
| 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' => 120,
This are all workarounds that I dont like.. (but I admit they can work) I dont know since witch version this exists on Laravel, but there is a way to exclude pages from CSRF token validation:
https://laravel.com/docs/5.5/csrf
Simply adding a record on $except array on VerifyCsrfToken Middleware with your uri that you want to exclude. Please take in account, this must only be done on specific cases.
This is the best solution for me... Simple and just like (almost) everything on Laravel, they already thought about it. ;)
I have a simple solution that:
in /routes/web.php:
$router->get('csrf-token', function() {
return request()->session()->token();
});
This simply returns the current csrf token.
Because this only returns a new token when necessary, there are no problems when having multiple tabs open as described by @Adam.
You just need to make sure to call the above route every X minutes (where X is your session lifetime - 5 minutes), and update any _token
inputs. I do this as follows (i use momentjs and axios here):
handleNewCsrfToken();
// Use visbility API to make sure the token gets updated in time, even when the device went to sleep.
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
setTimeoutToRefreshCsrfToken();
} else if (document.visibilityState === 'hidden') {
clearTimeout(refreshCsrfTokenTimeout);
}
});
function handleNewCsrfToken() {
updateCsrfTokenTimeoutTarget();
setTimeoutToRefreshCsrfToken();
}
function updateCsrfTokenTimeoutTarget() {
csrfTokenTimeoutTarget = moment().add(2, 'hour').subtract(5, 'minute');
}
function setTimeoutToRefreshCsrfToken() {
refreshCsrfTokenTimeout = setTimeout(refreshCsrfToken, csrfTokenTimeoutTarget.diff());
}
function refreshCsrfToken() {
axios.get('/csrf-token').then(function(response) {
document.getElementsByName('_token').forEach(function(element) {
element.value = response.data;
handleNewCsrfToken();
});
});
}
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>