Refreshing authentication tokens for a Vue.js SPA using Laravel for the backend
Considering that you are using an api for authentication, I would suggest using Passport or JWT Authentication to handle authentication tokens.
Finally fixed it!
By returning the UserResource directly in the LoginControllers authenticated
method, it is not a valid Laravel Response (but I guess raw JSON data?) so probably things like cookies are not attached. I had to attach a call to response() on the resource and now everything seems to work fine (though I need to do more extensive testing).
So:
protected function authenticated(\Illuminate\Http\Request $request, $user){ ... return (new UserResource($user))->additional( ['permissions' => $user->getUIPermissions()] );}
becomes
protected function authenticated(\Illuminate\Http\Request $request, $user){ ... return (new UserResource($user))->additional( ['permissions' => $user->getUIPermissions()] )->response(); // Add response to Resource}
Hurray for the Laravel docs on attributing a section to this:https://laravel.com/docs/5.5/eloquent-resources#resource-responses
Additionally, the laravel_token is not set by the POST request to login, and the call to refreshCsrfToken() also didn't do the trick, probably because it was protected by the guest middleware.
What worked for me in the end is to perform a dummy call to '/' right after the login function returned (or the promise was fulfilled).
In the end, my login function in the component was as follows:
login(){ // Copy the user object const data = {...this.user}; // If remember is false, don't send the parameter to the server if(data.remember === false){ delete data.remember; } this.authenticating = true; this.authenticate(data) .then( csrf_token => { window.Laravel.csrfToken = csrf_token; window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrf_token; // Perform a dummy GET request to the site root to obtain the larevel_token cookie // which is used for authentication. Strangely enough this cookie is not set with the // POST request to the login function. axios.get('/') .then( () => { this.authenticating = false; this.$router.replace(this.$route.query.redirect || '/'); }) .catch(e => this.showErrorMessage(e.message)); }) .catch( error => { this.authenticating = false; if(error.response && [422, 423].includes(error.response.status) ){ this.validationErrors = error.response.data.errors; this.showErrorMessage(error.response.data.message); }else{ this.showErrorMessage(error.message); } });
and the authenticate()
action in my vuex store is as follows:
authenticate({ dispatch }, data){ return new Promise( (resolve, reject) => { axios.post(LOGIN, data) .then( response => { const {csrf_token, ...user} = response.data; // Set Vuex state dispatch('setUser', user ); // Store the user data in local storage Vue.ls.set('user', user ); return resolve(csrf_token); }) .catch( error => reject(error) ); });},
Because I didn't want to make an extra call to refreshTokens
in addition to the dummy call to /
, I attached the csrf_token to the response of the /login route of the backend:
protected function authenticated(\Illuminate\Http\Request $request, $user){ $user->last_login = \Carbon\Carbon::now(); $user->timestamps = false; $user->save(); $user->timestamps = true; return (new UserResource($user))->additional([ 'permissions' => $user->getUIPermissions(), 'csrf_token' => csrf_token() ])->response();}
You should use Passports CreateFreshApiToken middleware in your web middleware passport consuming-your-api
web => [..., \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,],
this attaches attach the right csrftoken()
to all your Request headers as request_cookies