I'm using Laravel's CSRF protection on my public site. However since Laravel uses a session to maintain this, I'm worried that a user might walk away from their computer and return to a page they have previously left open, only to find ajax requests don't work. The ajax requests don't work because the session has timed out (and the token no longer validates?). If these users were "logged in" users, then I could simply redirect them back to the login page. Since they are public users, then the user is forced to refresh the page to get it back working (awkward).
Or am I wrong about this? Would the CSRF token still get validated by Laravel (even after the session has timed out, the page will still send over the token...but what will Laravel do with it?). An optimal solution would be to have the tokens partially based on a timestamp so that we could give the tokens expiration limits apart from session time limits. I could make my CSRF tokens last for 2 days (so only those users that walk away for 2 days will return to a dead page).
Ultimately this brings me to my question: Where is the specific code in the Laravel framework that handles this? I'm currently trying to locate it. Also, is there an easy drop in replacement I can make, or am I left to create my own version of csrf_token(); to output to my pages and then I would need to create my own route filter to go with it.
Laravel just facilitates that for you by keeping the token stored in session, but the code is actually yours (to change as you wish). Take a look at filters.php you should see:
Route::filter('csrf', function()
{
if (Session::token() != Input::get('_token'))
{
throw new Illuminate\Session\TokenMismatchException;
}
});
It tells us that if you have a route:
Route::post('myform', ['before' => 'csrf', 'uses' => 'MyController#update']);
And the user session expires, it will raise an exception, but you can do the work yourself, keep your own token stored wherever you think is better, and instead of throwing that exception, redirect your user to the login page:
Route::filter('csrf', function()
{
if (MySession::token() != MyCSRFToken::get())
{
return Redirect::to('login');
}
});
And, yes, you can create your own csrf_token(), you just have to load it before Laravel does. If you look at the helpers.php file in Laravel source code, you`ll see that it only creates that function if it doesn't already exists:
if ( ! function_exists('csrf_token'))
{
function csrf_token()
{
...
}
}
Since this has become a popular question, I decided to post my specific solution that has been working quite nicely...
Most likely you will have a header.php or some partial view that you use at the top of all your pages, make sure this is in it in the <head> section:
<meta name="_token" content="<?=csrf_token(); ?>" />
In your filters.php:
Route::filter('csrf', function()
{
if (Request::ajax()) {
if(Session::token() != Request::header('X-CSRF-Token')){
throw new Illuminate\Session\TokenMismatchException;
}
}
});
And in your routes.php
Route::group(array('before' => 'csrf'), function(){
// All routes go in here, public and private
});
Related
I created an SPA with Laravel 5.6, Vue 2.5 and Laravel Passport which is working quite well. I really love Laravel and Vue as they make building SPAs with APIs very easy and a lot of fun.
After setting up Laravel Passport as described in the docs the login as well as calls to the API are working as expected based on the 'laravel_token' which is correctly returned and stored in the cookie.
However, my problem is that my users are using the app for a pretty long time, without reloading the page but only performing calls against the API with axios. Somehow Laravel does not refresh the 'laravel_token' (and the corresponding cookie) in API calls (it does so, when I call a 'web' route). Consequently, the 'laravel_token' expires t some point and the user needs to log in again.
How can I force Laravel to refresh the 'laravel_token' (and thereby prolong its validity) with every call of an API route from axios?
Any help is very much appreciated!
I solved a similar issues in the past by creating a simple route (in the web middleware group) to keep the session alive for as long as the browser tab is open.
In routes/web.php:
Route::get('/keep-alive', function () {
return response()->json(['ok' => true]);
})->middleware('auth');
And then ping this route periodically with javascript:
setInterval(() => {
axios.get('/keep-alive')
.then(() => {})
.catch(() => {})
}, 600000)
I go into a little more detail here: https://stackoverflow.com/a/57290268/6038111
Axios has a way to "intercept" / see if a call failed. Inside the error callback I am seeing if it was an unauthenticated error and simply reloading the page.
Admittedly, I would love to be able to write another Axios call inside the error caught block to grab another session token "laravel_token", but have yet to find a way to do it. Reloading the page will refresh the laravel_token though, so it solves my problem for now. ¯\_(ツ)_/¯
After-thought: I'm actually thinking you probably couldn't refresh the laravel_token through an Axios call because you'e already dropped the session. I'm guessing you have to do it this way.
// Refresh Laravel Session for Axios
window.axios.interceptors.response.use(
function(response) {
// Call was successful, don't do anything special.
return response;
},
function(error) {
if (error.response.status === 401) {
// Reload the page to refresh the laravel_token cookie.
location.reload();
}
// If the error is not related to being Unauthorized, reject the promise.
return Promise.reject(error);
}
);
I'm sending an ajax post request, and with Laravel it seems that is done by creating a post route for it. I've set it up so a csrf token is put in the header automaticaly for every ajax request using ajaxSetup. I'm attempting to then catch that header on the backend and verify the tokens match.
In my web routes (which automatically use the web middleware), this returns as expected:
Route::get('/test', function() {
return csrf_token();
});
However, when I post to a route via AJAX, like either of the below ways:
Attempt 1:
Route::post('/test', 'AjaxController#test');
In the AjaxController construct, followed by an alert in the view:
var_dump(csrf_token().',hi'); die;
Response: ',hi' (csrf_token was null).
Attempt 2:
Route::post('/test', ['test' => csrf_token().',hi', 'uses' => 'AjaxController#test']);
$test = $request->route()->getAction()['test'];
var_dump($test); die;
Response: ',hi' (csrf_token was null).
What I seem to be running into is, with get requests csrf_token() is populated, on my post request, it is not.
Any ideas?
check your route group it must apply the web middleware as
Route::group(['middleware' => 'web'], function () {
Route::get('/test', function() {
return csrf_token();
//or return $request->session()->token();
});
});
Finally figured this out.
CSRF can indeed be checked on an ajax post request. I wanted to make sure someone on their own site isn't hitting my ajax endpoint with any success of doing anything, especially for another user.
However, I ran into a Laravel order of operations issue, with the way Laravel sets up the session. I was trying to call a validation method (within in the same class) in the constructor, where I validated for CSRF and verified the requesting user all in one place. I wanted to do this so that any time someone hits this class, I didn't have to call the verification in each public method in the class, I'd only have to call it once.
However, csrf_token(), and the request session in general, is not available to me yet in my construct. It is, however, available to me in the method within the controller class that is called in the route.
For example, given the following route:
Route::post('/test', 'AjaxController#test');
If I injected Request into the construct and then tried to reference anything in the session (in the construct), or get the value of csrf_token(), it will throw an error, because Laravel hasn't set that stuff up yet. But if I reference either of those things in the test method, it'll be there and available just fine.
A bit of a weird Laravel order of operations issue.
csrf protections are managed by Laravel Forms. It won't be available when dealing with APIs.
You should have a look at how middlewares are used in Laravel
https://laravel.com/docs/5.4/middleware
Think using API middleware for your APIs ;)
If you run this command php artisan make:auth documented here https://laravel.com/docs/5.4/authentication#authentication-quickstart when going to resources/views/layouts/app.blade.php you'll see this:
<meta name="csrf-token" content="{{ csrf_token() }}">
And in app.js
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN':$('meta[name="csrf-token"]').attr('content')
}
});
In 5.3 there was this cool feature which looks as though it has since been removed in 5.4.
<script>
window.Laravel = <?php echo json_encode([
'csrfToken' => csrf_token(),
]); ?>
</script>
So what you need to do is add the csrf field to every request. Do the first 2 code snippets and you'll be fine. The 3rd I believe is probably for Vue.
Answer to your question: no, no, no and no. CSRF tokens I wouldn't believe are generated in POST requests, it's a Cross site Reference token, not an authentication token. If you're looking for something like authentication token refreshing then checkout JWT although the packages for JWT for laravel are a bit unfinished at the moment; with a little work you can get them working.
https://github.com/tymondesigns/jwt-auth 1.0.*#dev is pretty good. You can then use their refresh middleware to generate new tokens on request but this is quite advanced and unless it's for authentication then I wouldn't bother really.
I believe Dingo (another work in progress I believe) https://github.com/dingo/api uses the above package
Anything else let me know!
I am trying to implement the (in)famous improved persistent session as middleware in the Slim microframework.
There are some places in the algorithm described where the application should check the user's cookie and redirect the user if their cookie has expired or is invalid. Unfortunately, it is impossible to redirect a user from within middleware, for two reasons:
Slim's redirect can only be used within named routes;
redirect will create an entirely new request, thus restarting the Slim application. The same conditions that triggered the redirect before will be re-triggered, thus creating an infinite loop.
Problem 1 can be solved with clever use of hooks, but I am not sure what to do about problem 2. I notice that some middleware solves this by using a custom Exception, which they then catch with Slim's error handler, and then call the redirect:
// Handle the possible 403 the middleware can throw
$app->error(function (\Exception $e) use ($app) {
...
if ($e instanceof HttpUnauthorizedException) {
return $app->redirectTo('login');
}
...
});
But I am not certain that this is the best way to do it. Are there any other ways that I can accomplish this?
What you listed above is a perfectly fine way of doing it, and is generally how it's done. Assuming your login page doesn't check for HttpUnauthorizedExcepion, there would be no way it could ever redirect loop.
I'm attempting to route a single URI to multiple controllers, based on user authentication. Essentially, if the user is not logged in and they hit the root URI, show a generic landing page, otherwise, if they are logged in, and access the root URI, show their personalized content.
I am using the standard out-of-the-box filters (auth/guest) and some other routes (not shown here) that have been setup to quickly auth/de-auth for testing.
The idea seems straight-forward enough and seems to me like it should work, yet Laravel is not handling this correctly:
Route::group(array('before' => 'auth'), function() {
Route::get('/', function() { echo 'logged in'; });
});
Route::group(array('before' => 'guest'), function() {
Route::get('/', function() { echo 'logged out'; });
});
It doesn't matter what order I have these in, Laravel will not acknowledge the Auth filter when the user is authenticated. The first route gets skipped over and the Guest filter is running first, or solely (probably more accurately).
Did I mistakenly change something in one of the filters? Why is this happening? Shouldn't this work without a hitch?
It seems as if Laravel cannot handle the assignment of multiple actions to test for to a single URI. I don't particularly want to spend time digging through the codebase to figure out the problem. This seems to me to be a bad design decision with the framework itself, though if that is the case it would explain the problem here.
I need a sanity check, please.
It should be this
Route::group(array('before' => 'guest|auth'), function() {
Route::get('/', function() {
if(Auth::check()) {
return "logged in";
}
return 'logged out';
});
});
What is the point of multiple controller-action for one URI? I'm pretty confused. An URI is served by one Controller's action only. Otherwise, it gets DRY, IMO.
same as title, i want to all people when using trang web of me, must be login (look like FB or Twitter, ...) with some of the required as follows:
If such as the current URL is the '/' (home page), systems display the interfaces registered. (display the rather than the redirect)
If such as a different URL '/' (home page), systems redirection to login page.
somebody can help me? I'm using laravel framework.
Laravel uses power thing called filter.
You can use them in any Route::action you want.
But a little exemple may helps you.
Following your request :
// Check manualy if user is logged. If so, redirect to the dashboard.
// If not, redirect to the login page
Route::get('/', function()
{
if (Auth::check()) // If user is logged
return View::make('dashboard')
return View::make('/login');
}
// Each routes inside this Route::group will check if the user is logged
// Here, /example will only be accessible if you are logged
Route::group(array('before'=>'auth', function()
{
// All your routes will be here
Route::get('/example', function()
{
return View::make('contents.example');
}
});
Of course, the filter auth is built in Laravel. You can find this file in app/filters.php
and modifying it to your needs.
As follow:
Route::filter('auth', function()
{
if (Auth::guest()) return Redirect::guest('/login');
});