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);
}
);
Related
Building an application with Laravel, Passport and Vue. This question does not directly pertain to logging in with oAuth2, but rather consuming the api that's protected by Passport with your own javascript code, as per the docs.
When accessing the home page and using axios to get /oauth, I get a error 401, as expected.
After logging in using the default login provided by laravel (uses web auth), I can go back to the home page, and the axios request for /oauth works great; for example /oauth/clients returns the clients of the logged in user, as expected.
mounted() {
//works as expected: 401 when logged out and response when logged in via /login
axios.get('/oauth/clients')
.then(response => {
console.log(response.data)
})
//Always returns 400 error
axios.get('/api/user')
.then(response => {
console.log(response.data)
})
}
However, when I try with axios to get /api/user, I get a 400 error, with the message Unauthenticated (regardless if before or after login, same error).
//Returns a 400, always
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
If I'm correct, the fact that the /oauth routes work proves that the laravel_token, csrf, and any such things are being sent correctly. Therefore, I think that this is a server side issue, especially with the auth:api guard. In the auth config file, I've set it to use Passport, and followed all the docs.
I'm confused as to why I get a 400 error and not a 401 when calling the api, and why it's not authenticating in the first place.
Especially frustrating since according to this video (11:30 mark), it's pretty much plug 'n' play.
Same behaviour with Postman.
Q: Any solutions to this error?
Full code on GitHub.
Add this code in your app/Http/Kernel.php
'web' => [
// Other middleware...
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
I am building a REST user-microservice using Laravel 5.5 + Passport.
I am using the standard Passport::routes(), but I have had to modify the Auth::routes in order to make them return JSON responses, and to make them work with Passport.
I have added the following lines to my routes/web.php file:
Route::group(['middleware' => 'auth:api'], function () {
$this->post('logout', 'Auth\LoginController#logout')->name('logout');
});
This allows me to POST https://myapi/logout
If I make the call with the header "Authorization => Bearer TOKEN", I get a successful logout response.
If I provide no header at all, I get a "not authenticated" message (which is good)
However, if I provide the header with a revoked token, I get a recursive deadloop of the function: Illuminate\Auth\RequestGuard->user() (it keeps calling itself recursively until stack-overflow)
This is all done in the auth:api middleware, my logout code is not reached, but my LoginController constructor is called. Constructor code:
public function __construct(Application $app)
{
$this->apiConsumer = $app->make('apiconsumer');
$this->middleware('guest')
->except('logout');
}
I'm struggling to understand if it's my code causing this issue, or some combination of Laravel + passport + auth.
My first thought was that the auth:api middleware fails to authenticate the user, and as a result redirects the user to /home, where for some reason it's triggered again, recursively. But if that was the case, why would it work correctly with no header?
My current thinking is that the token in question does exist in the database, but Laravel is failing to figure out that it's revoked.
Any suggestions appreciated,
I found an answer (if not the answer) after a lot of research. It appears this is a Laravel bug (https://github.com/laravel/passport/issues/440). The solution is to add OAuthServerException to the $dontReport array in app/Exceptions/Handler.php:
class Handler extends ExceptionHandler
{
protected $dontReport = [
...
\League\OAuth2\Server\Exception\OAuthServerException::class,
];
}
This will avoid trying to log user information, thereby avoid the deadloop.
I have faced this in localhost. in my case, I have used xampp server and facing this issue
after creating a virtual host like "testlarave.test" then solve the 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!
Say I want to create an AuthService in vue-router to validate the current session before proceeding to the next route, like the example on:
http://vuejs.github.io/vue-router/en/api/before-each.html
router.beforeEach(function ({ to, next }) {
if (to.path === '/auth-required') {
// return a Promise that resolves to true or false
return AuthService.isLoggedIn()
} else {
next()
}
})
How would one approach this for Laravel 5.1 use without using JSON Web Tokens(JWT)?
Is there a "best-pratice" approach to SESSION_DRIVER in Laravel, ex. Redis, for this scenario?
I've searched around the web alot but never seen any attempt do authenticate a session with vue-router without JWT.
For your code, you want to move the isLoggedIn() auth check into the if statement. Your auth service should return a boolean if the user is logged in. Within the if, you would route the user to the appropriate path. beforeEach works like "What should we do before each route is processed?" so you do not need to return a truthy value inside the if statement.
router.beforeEach(function (transition) {
if (transition.to.auth && !AuthService.isLoggedIn()) {
// if route requires auth and user isn't authenticated
transition.redirect('/login')
} else {
transition.next()
}
})
If you want to "validate the current session before proceeding to the next route" each time, your isLoggedIn() would need to call your login API each time. It's usually not best practice because once you login, why do you need to check again? That's why tokens and JWT exist. After you login, you're given a token, you remember this token and send the token in upcoming requests.
How would one approach this for Laravel 5.1 use without using JSON Web
Tokens(JWT)?
Not technically JWTs, you can use API tokens. API tokens can be generated with Laravel's str_random() function. You'd associate 1 token per user and keep the tokens unique. You can put this token in 2 places: 1. in the URL for parameter ?api_token=XXX 2. in the header for "Authorization: Bearer XXX".
If you're going with headers, in Vue.js, you'd setup vue-resource as such:
Vue.http.headers.common['Authorization'] = 'Bearer ' + token;
and then all your requests now contain the API token.
Is there a "best-pratice" approach to SESSION_DRIVER in Laravel, ex.
Redis, for this scenario?
Not 100% sure what you mean here, but tokens are considered one of the best practices when interacting with APIs. You exchange the token with each web request so that you do not need to send a username/password each time.
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
});