loading...
Cover image for Secure authentication in Nuxt SPA with Laravel as back-end

Secure authentication in Nuxt SPA with Laravel as back-end

stefant123 profile image StefanT123 Updated on ・11 min read

This past period I was working on some project that included building Single Page Application in Nuxt that was on one domain, and building API in Laravel that was on some other sub-domain. When the API was built, and it was time to make the front-end I was trying to make the authentication system properly and with security in mind. There are many articles out there on this subject, but I couldn't find any that was touching security of the application.

TL;DR Please don't store your tokens in LocalStorage, or any other sensitive information, as it can be accessed by any javascript code on your page and that makes you vulnerable to XSS attack.

TL;DR If you just want to see the code, here are github links

The authentication flow will be as follow:

  1. The user enters his username and password.
  2. If the credentials are valid, we are saving the refresh token in an httponly cookie.
  3. The user sets the access token in the cookie, please note that this is normal cookie, which has expiration time of 5 minutes.
  4. After the access token has been expired, we will refresh the access token if the user has the valid refresh token set.
  5. Access token is refreshed, and new access token and refresh token are assigned to the user.

In this post I will give you a complete guidance on how to make securely authentication system for Single Page Applications.

Making the Laravel back-end

I assume that you have composer and laravel installed on your machine, if you don't, just follow their documentation.

Setting Laravel Passport

Create new laravel project and cd into it laravel new auth-api && cd auth-api.

We will use Laravel Passport which provides a full OAuth2 server implementation for your Laravel application. I know that Passport might be overkill for some small to medium applications, but I think it's worth it.

Next we'll install Passport with composer composer require laravel/passport.

Set your .env variables for the database. For this example I'll use sqlite.

If you follow along, change the DB_CONNECTION variable to use the sqlite in .env like this:

...
DB_CONNECTION=sqlite
...
Enter fullscreen mode Exit fullscreen mode

Make the database.sqlite file with touch database/database.sqlite.

Run the migrations with php artisan migrate. The Passport migrations will create the tables your application needs to store clients and access tokens.

Next, run the php artisan passport:install command. This command will create the encryption keys needed to generate secure access tokens. After you run this command you will see that "personal access" and "password grant" clients are created and you can see their Client ID and Client secret, we will store these in .env file. In this post we will use only the password grant client, but we will store both of them for convenience.

...
PERSONAL_CLIENT_ID=1
PERSONAL_CLIENT_SECRET={your secret}

PASSWORD_CLIENT_ID=2
PASSWORD_CLIENT_SECRET={your secret}
...
Enter fullscreen mode Exit fullscreen mode

Then we will add the "password client" id and secret to the config/services.php so we can use them later in our code:

...
'passport' => [
    'password_client_id' => env('PASSWORD_CLIENT_ID'),
    'password_client_secret' => env('PASSWORD_CLIENT_SECRET'),
],
Enter fullscreen mode Exit fullscreen mode

In the config/auth.php set the api guard driver as passport

...

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
        'hash' => false,
    ],
],

...
Enter fullscreen mode Exit fullscreen mode

Next step is to add Laravel\Passport\HasApiTokens trait to your App\User model

<?php

namespace App;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable, HasApiTokens;

    ...
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to import the trait at the top.

The last step is to register passport routes. In the AuthServiceProvider in the boot method add this and import Laravel\Passport\Passport at the top.

public function boot()
{
    $this->registerPolicies();

    Passport::routes(function ($router) {
        $router->forAccessTokens();
        $router->forPersonalAccessTokens();
        $router->forTransientTokens();
    });
}
Enter fullscreen mode Exit fullscreen mode

We are only registering the routes that we need, if for some reason you want to register all passport routes, don't pass a closure, just add Passport::routes().

If you run php artisan route:list | grep oauth you should see the oauth routes. It should look like this
routes

Now this is very important, we're going to set the expiration time for the tokens. In order to properly secure our app, we'll set the access token expiration time to 5 minutes, and the refresh token expiration time to 10 days.

In the AuthServiceProvider in boot method we add the expirations. Now the boot method should look like this:

public function boot()
{
    $this->registerPolicies();

    Passport::routes(function ($router) {
        $router->forAccessTokens();
        $router->forPersonalAccessTokens();
        $router->forTransientTokens();
    });
    Passport::tokensExpireIn(now()->addMinutes(5));
    Passport::refreshTokensExpireIn(now()->addDays(10));
}
Enter fullscreen mode Exit fullscreen mode

That's all we have to do regarding the Passport. Next thing we are going to do, is we are going to set our API.

Setting CORS

In order to access our API from our front-end that is on different domain, we need to set CORS middleware.

Run php artisan make:middleware Cors.

Then in app/Http/Middleware/Cors.php change the handle method like this

public function handle($request, Closure $next)
{
    $allowedOrigins = [
        'http://localhost:3000',
    ];
    $requestOrigin = $request->headers->get('origin');

    if (in_array($requestOrigin, $allowedOrigins)) {
        return $next($request)
            ->header('Access-Control-Allow-Origin', $requestOrigin)
            ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
            ->header('Access-Control-Allow-Credentials', 'true')
            ->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    }

    return $next($request);
}
Enter fullscreen mode Exit fullscreen mode

Here we are checking if the request origin is in the array of the allowed origins, if it is, we are setting the proper headers.

Now we just need to register this middleware. In app/Http/Kernel.php add the middleware

...

protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
    \App\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \App\Http\Middleware\Cors::class,
];

...
Enter fullscreen mode Exit fullscreen mode

That's it, pretty simple.

Making the API

In the routes/api.php file we are going to register the routes that we are going to use. Delete everything there, and add this:

<?php

Route::middleware('guest')->group(function () {
    Route::post('register', 'AuthController@register')->name('register');
    Route::post('login', 'AuthController@login')->name('login');
    Route::post('refresh-token', 'AuthController@refreshToken')->name('refreshToken');
});

Route::middleware('auth:api')->group(function () {
    Route::post('logout', 'AuthController@logout')->name('logout');
});
Enter fullscreen mode Exit fullscreen mode

We need to create the AuthController run php artisan make:controller AuthController.

In the App\Http\Controllers\AuthController we will add the methods that we need. It should look like this:

<?php

namespace App\Http\Controllers;

class AuthController extends Controller
{
    public function register()
    {
    }

    public function login()
    {
    }

    public function refreshTo()
    {
    }

    public function logout()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

In order for this to work we need to make a proxy that will make request to our own API. It might seems confusing at first but once we're done it will make perfect sense.

We'll make new folder in the app directory called Utilities. In the app/Utilities make new php file ProxyRequest.php

<?php

namespace App\Utilities;

class ProxyRequest
{

}
Enter fullscreen mode Exit fullscreen mode

Now we need to inject the App\Utilities\ProxyRequest in the constructor of the App\Http\Controllers\AuthController

<?php

namespace App\Http\Controllers;

use App\Utilities\ProxyRequest;

class AuthController extends Controller
{
    protected $proxy;

    public function __construct(ProxyRequest $proxy)
    {
        $this->proxy = $proxy;
    }

...
Enter fullscreen mode Exit fullscreen mode

In the App\Utilities\ProxyRequest we will add some methods for granting token and for refreshing the token. Add the following and then I'll explain what each method does

<?php

namespace App\Utilities;

class ProxyRequest
{
    public function grantPasswordToken(string $email, string $password)
    {
        $params = [
            'grant_type' => 'password',
            'username' => $email,
            'password' => $password,
        ];

        return $this->makePostRequest($params);
    }

    public function refreshAccessToken()
    {
        $refreshToken = request()->cookie('refresh_token');

        abort_unless($refreshToken, 403, 'Your refresh token is expired.');

        $params = [
            'grant_type' => 'refresh_token',
            'refresh_token' => $refreshToken,
        ];

        return $this->makePostRequest($params);
    }

    protected function makePostRequest(array $params)
    {
        $params = array_merge([
            'client_id' => config('services.passport.password_client_id'),
            'client_secret' => config('services.passport.password_client_secret'),
            'scope' => '*',
        ], $params);

        $proxy = \Request::create('oauth/token', 'post', $params);
        $resp = json_decode(app()->handle($proxy)->getContent());

        $this->setHttpOnlyCookie($resp->refresh_token);

        return $resp;
    }

    protected function setHttpOnlyCookie(string $refreshToken)
    {
        cookie()->queue(
            'refresh_token',
            $refreshToken,
            14400, // 10 days
            null,
            null,
            false,
            true // httponly
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

ProxyRequest methods:

  • grantPasswordToken - not much happens in this method, we are just setting the parameters needed for Passport "password grant" and make POST request.
  • refreshAccessToken - we are checking if the request contains refresh_token if it does we are setting the parameters for refreshing the token and make POST request, if the refresh_token does not exist we abort with 403 status.
  • makePostRequest - this is the key method of this class.
    • We are setting client_id and client_secret from the config, and we are merging additional parameters that are passed as argument
    • Then we are making internal POST request to the Passport routes with the needed parameters
    • We are json decoding the response
    • Set the httponly cookie with refresh_token
    • Return the response
  • setHttpOnlyCookie - set the httponly cookie with refresh_token in the response.

In order to queue the cookies for the response we need to add middleware. In app/Http/Kernel.php add \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class like this

...

protected $middleware = [
    \App\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \App\Http\Middleware\TrustProxies::class,
    \App\Http\Middleware\Cors::class,
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
];

...
Enter fullscreen mode Exit fullscreen mode

Now to make the App\Http\Controllers\AuthController methods. Don't forget to import the App\User.

In the register method, add this

...

public function register()
{
    $this->validate(request(), [
        'name' => 'required',
        'email' => 'required|email',
        'password' => 'required',
    ]);

    $user = User::create([
        'name' => request('name'),
        'email' => request('email'),
        'password' => bcrypt(request('password')),
    ]);

    $resp = $this->proxy->grantPasswordToken(
        $user->email,
        request('password')
    );

    return response([
        'token' => $resp->access_token,
        'expiresIn' => $resp->expires_in,
        'message' => 'Your account has been created',
    ], 201);
}

...
Enter fullscreen mode Exit fullscreen mode

In the login method, add this

...

public function login()
{
    $user = User::where('email', request('email'))->first();

    abort_unless($user, 404, 'This combination does not exists.');
    abort_unless(
        \Hash::check(request('password'), $user->password),
        403,
        'This combination does not exists.'
    );

    $resp = $this->proxy
        ->grantPasswordToken(request('email'), request('password'));

    return response([
        'token' => $resp->access_token,
        'expiresIn' => $resp->expires_in,
        'message' => 'You have been logged in',
    ], 200);
 }

...
Enter fullscreen mode Exit fullscreen mode

The refreshToken method

...

public function refreshToken()
{
    $resp = $this->proxy->refreshAccessToken();

    return response([
        'token' => $resp->access_token,
        'expiresIn' => $resp->expires_in,
        'message' => 'Token has been refreshed.',
    ], 200);
}

...
Enter fullscreen mode Exit fullscreen mode

The logout method

...

public function logout()
{
    $token = request()->user()->token();
    $token->delete();

    // remove the httponly cookie
    cookie()->queue(cookie()->forget('refresh_token'));

    return response([
        'message' => 'You have been successfully logged out',
    ], 200);
}

...
Enter fullscreen mode Exit fullscreen mode

Ok, that's everything we have to do in our back-end. I think that the methods in the AuthController are self explanatory.

Making the Nuxt front-end

Nuxt is, as stated in the official documentation, a progressive framework based on Vue.js to create modern web applications. It is based on Vue.js official libraries (vue, vue-router and vuex) and powerful development tools (webpack, Babel and PostCSS). Nuxt goal is to make web development powerful and performant with a great developer experience in mind.

To create nuxt project run npx create-nuxt-app auth-spa-frontend. If you don't have npm install it first.

It will ask you some questions like project name, description, package manager, etc. Enter and choose whatever you like. Just make sure that custom server framework is set to none and you add axios nuxt module. Note that I will be using bootstrap-vue.

We will also install additional package js-cookie, run npm install js-cookie.

I won't bother you with structuring the front-end and how the things should look like. The front-end will be pretty simple but functional.

In the nuxt.config.js set the axios baseUrl

export default {
  ...

  axios: {
    baseURL: 'http://auth-api.web/api/',
    credentials: true, // this says that in the request the httponly cookie should be sent
  },

  ...
}
Enter fullscreen mode Exit fullscreen mode

Next we will activate the vue state management library vuex. In order to do that we only need to make new js file in store folder.

If you are not familiar how vuex works, I would suggest to read the documentation, it's pretty straightforward.

Add index.js file in the store folder, and add the following

import cookies from 'js-cookie';

export const state = () => ({
  token: null,
});

export const mutations = {
  SET_TOKEN(state, token) {
    state.token = token;
  },

  REMOVE_TOKEN(state) {
    state.token = null;
  }
};

export const actions = {
  setToken({commit}, {token, expiresIn}) {
    this.$axios.setToken(token, 'Bearer');
    const expiryTime = new Date(new Date().getTime() + expiresIn * 1000);
    cookies.set('x-access-token', token, {expires: expiryTime});
    commit('SET_TOKEN', token);
  },

  async refreshToken({dispatch}) {
    const {token, expiresIn} = await this.$axios.$post('refresh-token');
    dispatch('setToken', {token, expiresIn});
  },

  logout({commit}) {
    this.$axios.setToken(false);
    cookies.remove('x-access-token');
    commit('REMOVE_TOKEN');
  }
};
Enter fullscreen mode Exit fullscreen mode

I will explain the actions one by one:

  1. setToken - it sets the token in axios, in the cookie and calls the SET_TOKEN commit
  2. refreshToken - it send POST request to the API to refresh the token and dispatches setToken action
  3. logout - it removes the token form axios, cookie and from the state

In the pages folder, add these vue files: register.vue, login.vue, secret.vue.

Then in the pages/register.vue add this

<template>
  <div class="container">
    <b-form @submit.prevent="register">
      <b-form-group
        id="input-group-1"
        label="Email address:"
        label-for="input-1"
      >
        <b-form-input
          id="input-1"
          v-model="form.email"
          type="email"
          required
          placeholder="Enter email"
        ></b-form-input>
      </b-form-group>

      <b-form-group id="input-group-2" label="Your Name:" label-for="input-2">
        <b-form-input
          id="input-2"
          v-model="form.name"
          required
          placeholder="Enter name"
        ></b-form-input>
      </b-form-group>

      <b-form-group id="input-group-3" label="Password:" label-for="input-3">
        <b-form-input
          id="input-3"
          type="password"
          v-model="form.password"
          required
          placeholder="Enter password"
        ></b-form-input>
      </b-form-group>

      <b-button type="submit" variant="primary">Submit</b-button>
    </b-form>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        form: {
          email: '',
          name: '',
        },
      }
    },
    methods: {
      register() {
        this.$axios.$post('register', this.form)
          .then(({token, expiresIn}) => {
            this.$store.dispatch('setToken', {token, expiresIn});
            this.$router.push({name: 'secret'});
          })
          .catch(errors => {
            console.dir(errors);
          });
      },
    }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

pages/login.vue is pretty similar to register, we just need to make some slight changes

<template>
  <div class="container">
    <b-form @submit.prevent="login">
      <b-form-group
        id="input-group-1"
        label="Email address:"
        label-for="input-1"
      >
        <b-form-input
          id="input-1"
          v-model="form.email"
          type="email"
          required
          placeholder="Enter email"
        ></b-form-input>
      </b-form-group>

      <b-form-group id="input-group-3" label="Password:" label-for="input-3">
        <b-form-input
          id="input-3"
          type="password"
          v-model="form.password"
          required
          placeholder="Enter password"
        ></b-form-input>
      </b-form-group>

      <b-button type="submit" variant="primary">Submit</b-button>
    </b-form>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        form: {
          email: '',
          name: '',
        },
      }
    },
    methods: {
      login() {
        this.$axios.$post('login', this.form)
          .then(({token, expiresIn}) => {
            this.$store.dispatch('setToken', {token, expiresIn});
            this.$router.push({name: 'secret'});
          })
          .catch(errors => {
            console.dir(errors);
          });
      },
    }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

In the pages/secret.vue add this

<template>
  <h2>THIS IS SOME SECRET PAGE</h2>
</template>

<script>
  export default {
    middleware: 'auth',
  }
</script>
Enter fullscreen mode Exit fullscreen mode

We must make route middleware for auth, in the middleware folder add new auth.js file, and add this

export default function ({ store, redirect }) {
  if (! store.state.token) {
    return redirect('/');
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we will make the navbar. Change layouts/deafult.vue like this

<template>
  <div>
    <div>
      <b-navbar toggleable="lg" type="dark" variant="info">
        <b-navbar-brand href="#">NavBar</b-navbar-brand>

        <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

        <b-collapse id="nav-collapse" is-nav>
          <b-navbar-nav class="ml-auto" v-if="isLoggedIn">
            <b-nav-item :to="{name: 'secret'}">Secret Page</b-nav-item>
            <b-nav-item href="#" right @click="logout">Logout</b-nav-item>
          </b-navbar-nav>

          <b-navbar-nav class="ml-auto" v-else>
            <b-nav-item :to="{name: 'login'}">Login</b-nav-item>
          </b-navbar-nav>
        </b-collapse>
      </b-navbar>
    </div>
    <nuxt />
  </div>
</template>

<script>
  export default {
    computed: {
      isLoggedIn() {
        return this.$store.state.token;
      }
    },

    methods: {
      logout() {
        this.$axios.$post('logout')
          .then(resp => {
            this.$store.dispatch('logout');
            this.$router.push('/');
          })
          .catch(errors => {
            console.dir(errors);
          });
      }
    }
  }
</script>

...
Enter fullscreen mode Exit fullscreen mode

And in order for the access token to be refreshed, we will add another middleware that will be applied to every route. To do this, in nuxt.config.js add this

export default {
  ...

  router: {
    middleware: 'refreshToken',
  },

  ...
}
Enter fullscreen mode Exit fullscreen mode

And create that middleware. In the middleware folder add new file refreshToken.js and add this

import cookies from 'js-cookie';

export default function ({ store, redirect }) {
  const token = cookies.get('x-access-token');

  if (! token) {
    store.dispatch('refreshToken')
      .catch(errors => {
        console.dir(errors);
        store.dispatch('logout');
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we check if the user has token in the cookies, if he doesn't, we will try to refresh his token, and assign him a new access token.

And that's it. Now we have authentication system that's secure, because even if someone is able to steal the access token of some user, he won't have much time to do anything with it.

This was a long post, but I hope that the concepts are clear and concise. If you have any questions or if you think that something can be improved, please comment below.

Discussion

pic
Editor guide
Collapse
bpedroza profile image
Bryan

That's a great tutorial! I did something very similar in a Vue app I'm working on. Good idea with the httponly cookie also.

The only thing I would say is, the hack on the laravel side is no longer required since the new version of passport supports PKCE now.

Collapse
stefant123 profile image
StefanT123 Author

Thanks for the comment. To be honest, I wasn't aware that the new version of Passport ships with PKCE, thank you for the info.

Collapse
bpedroza profile image
Bryan

No problem. Also, calling it a hack wasn't meant to be an insult to your code or anything. It's just easier with the new functionality.

Thread Thread
jameshull profile image
James R. Hull 🎬

To be clear for those of us new to Passport, the PKCE "hack" -- is that the Proxy bit? In other words, what part is no longer needed because of v.8?

Thread Thread
bpedroza profile image
Bryan

The proxy is the hack. PKCE is what you should use instead. See laravel passport docs for details.

Thread Thread
devondahon profile image
gvi

I would also be interested in knowing how to adapt this code to use the new PKCE.

Thread Thread
bpedroza profile image
Bryan

I actually just released an npm package to handle the client side npmjs.com/package/js-pkce There server side is just a matter of following the docs.

Additionally, I wouldn't recommend persisting the refresh or access token at all. Just send the user back to sign in again if they refresh the page or the token express. If they still have a session on the server side, it will all be behind the scenes, so no hassle for the user. Just store their requested route in session storage before sending them to log in.

I've been meaning to make my first post here. If I have some extra time I'll try to do a formal write up on this.

Thread Thread
efillman profile image
Evan Fillman

Any chance you were able to do the write up on not needing to persist the tokens client side and just using server side session?

Collapse
mkantautas profile image
Mantas Kantautas

Hey,

For some reason I keep getting message: "Your refresh token is expired."

I suspect that here:
const {token, expiresIn} = await this.$axios.$post('refresh-token');

I am not sending a request with a refresh_token cookie attached? Can you please explain how refresh_token should be being passed here?

Collapse
stefant123 profile image
StefanT123 Author

When your access_token has expired (because it's short-lived), we are sending a POST request to the refresh-token route, which calls the refreshToken method in the Controller. There we are checking if the httponly cookie is passed with the request, if it is, then we are getting a new access_token and new refresh_token.

Collapse
mkantautas profile image
Mantas Kantautas

Yeah, this is clear, but for some reason, the cookie is not being passed from nuxt in the first place in the await this.$axios.$post('refresh-token'); . Can this be because my front-end and back-end are on different domains? And how to overcome this?

Thread Thread
mkantautas profile image
Mantas Kantautas

dump(request()->cookie()) returns an empty array on Cors.php middleware on /api/refresh-token request

Thread Thread
mkantautas profile image
Mantas Kantautas

Will your guide work in case my laravel api is site.test and my nuxt is running on localhost:3000 for e.g.? Because I coming to the conclusion that top-level domains must be the same for this to work?

Thread Thread
stefant123 profile image
StefanT123 Author

No, your apps can be on different domains, and this would still work. I can't know for sure what went wrong in your case. Maybe the httponly cookie isn't set in the first place, or you're sending the request without the cookie. Until I see the errors or some code, I can't know what's wrong.

Thread Thread
brainuso profile image
brainuso

Was there a solution to this issue? I'm currently having the exact error when refresh token is called.

Update:
I ran this in the login after grantPasswordToken function:

Log::notice(cookie('refresh_token'));

and here is the response:

refresh_token=deleted; expires=Mon, 28-Oct-2019 22:11:22 GMT; Max-Age=0; path=/; httponly

Collapse
smzapp profile image
smzapp

Hi @stefan

I am also encountering this problem. I tested on Postman and I can see the refresh_token into the cookie so I can get it through request()->cookie('cookie')

i.imgur.com/YxbCYde.png

But on my browser, the refresh_token cookie is not existing. I already added AddQueuedCookiesToResponse in Kernel.php and tried to except the cookie in EncryptCookies but still, refresh_token is not existing.

i.imgur.com/WsGiaYi.png

Did I miss anything? THanks in advance.

Thread Thread
stefant123 profile image
StefanT123 Author

As the refresh_token is set to be HttpOnly cookie, you can't access it or see it in the browser cookies. The refresh_token is used only to renew the access_token when it expires.

Collapse
jes490 profile image
Aleksey Sesyolkin

Hi! Thanks for the article! One thing to note though -- will refreshing part work in universal mode (SSR) after page reload?

Let's imagine that you're authenticated and reading some page with auth middleware for 5 minutes (for access token to expire). Then you click refresh button in browser and several things will happen:

  1. You refreshToken.js middleware will not work (because it will be fired from node.js server which has no refresh_token cookie, this cookie only available in browser) -- so you'll get "unauthenticated"
  2. If you use asyncData or fetch to retrieve some data against auth middleware in laravel - you actually will not be able to do so either (also because there isn't refresh_token cookie in node.js)

Actually if user visits auth page after access_token expiration then he will be logged out too. (For example if user tries to visit auth page on the next day or event after one hour)

Any way to get around these things?

Collapse
stefant123 profile image
StefanT123 Author

Yes, I haven't covered that in this post, but I have posted a solution for this in the comments, here's a link. I think I should write a short post on this subject.

Collapse
jes490 profile image
Aleksey Sesyolkin

Thanks for the response!

Yes, I saw this comment, but I don't understand how it will solve the problems (okay, it may solve the first problem I described, but not the second -- which is most important).

I think if you store refresh_token in cookie -- there is no way to use asyncData or fetch on api routes protected by auth middleware, because we have to do requests from node.js server for asyncData and fetch to work and we will be unauthenticated because of expired access_token and having no way to refresh it from node.

Thread Thread
stefant123 profile image
StefanT123 Author

I never tested it like that, but I use SSR only for SEO optimization, so if some content needs authentication, then in my case it doesn't need to be server-side rendered. But I have an idea how to get it to work, I'll try it as soon as I can.

Thread Thread
jes490 profile image
Aleksey Sesyolkin

Hmm, you're right actually... It doesn't make much sense to use asyncData() on auth pages.

But it would be nice to hear your idea about this case anyway )

Thread Thread
harsha935 profile image
Harsha Sampath

Hi, I'm exactly stuck in this scenario. In this post Laravel httpOnly cookie is useless. because the author saving a cookie in frontend instead of using that httpOnly cookie. Ofcause, the author can't! Because when calling client-side HTTP request, httpOnly cookie which server sent, does not persistently save in the browser. Nuxt also cannot create httpOnly cookie even if it running on NodeJs! I have search about this issue and Nuxt authors are not capable to do that. github.com/nuxt-community/auth-mod...

Collapse
thond1st profile image
thond1st

This is a very usefull tutorial. I have implemented this in our prototype project but one of my colleague is complaining when the page is refreshed the user is being signed out so he implemented saving the token in the local storage. Am I right that the implementation is SPA so page reload is not needed but how did you circumvent when users try to refresh the page?
Thanks for this tutorial!

Collapse
stefant123 profile image
StefanT123 Author

Never save the token in the local storage, if you do, you are exposing your app to potential attacks. If you are using Nuxt.js, you should simply make plugin and put this

export default function ({store}) {
  window.onNuxtReady(() => {
    // refresh the token
  });
}

As stated in the Nuxt.js docs for the plugin:

Nuxt.js allows you to define JavaScript plugins to be run before instantiating the root Vue.js Application.

If you are using some other JS library, check their doocumentation and see how can you run some function before instantiating the root app.

Collapse
abronson07013635 profile image
ABronson

Hi, this is a really great tutorial, any chance you would go into this part in a bit more detail, with refreshing the token in window.onNuxtReady()? I did a version which checks if x-access-token is set similar to the middleware, and then does router push to '/', but it's quite choppy and not sure how secure. Would be a nice addition to the post. Thanks!!!

Thread Thread
stefant123 profile image
StefanT123 Author

I'm not sure if that requires whole new post, but maybe I'll do part 2 in addition to this post. Basically what you need to do is this:

  1. when the user log in, persist the user in the cookies (maybe use vuex-persisted state library)
  2. get the user from the store if it exists, if not, check in the cookies
  3. in the window.onNuxtReady() check if user is set in the vuex AND there isn't x-access-token in the cookies
  4. dispatch the action for refreshing the token
Thread Thread
orenlande profile image
orenlande

I'd love to see how you apply it on code - having the same issues right now more or less. I want the user to stay logged on refresh.

Thread Thread
binumathew profile image
Binu Mathew

I am also got the similar error, get logout everytime i do a refresh, any option ?

Thread Thread
stefant123 profile image
StefanT123 Author

Once a user has logged in, you should put user_id in the cookies. Then make a plugin that will check if the user exists and x-access-token does not exist, if these conditions are true, then you should dispatch an action to refresh the token.

export default function ({store}) {
  window.onNuxtReady(() => {
    let token = clientCookies.get('x-access-token');
    let user = clientCookies.get('user_id');

    if (user && ! token) {
      store.dispatch('auth/refreshToken')
        .catch(errors => {
          store.dispatch('auth/signUserOut');
        });
    }
  });
}
Thread Thread
alexwu66922308 profile image
Alex Wu

This doesn't seem to work for me, does this work for anyone else? It STILL signs me out every time I refresh the page. very annoying

Collapse
devondahon profile image
gvi

Why not using the laravel.passport strategy in nuxt.config.js, as documented below ?
auth.nuxtjs.org/providers/laravel-...

I'm actually struggling with it : from Nuxt.js, I'm redirected to Laravel login page and then back to Nuxt.js homepage with a code and state, but I don't know how to use it to get token.

Collapse
samkoto profile image
Sam

I am encountering the same problem. Have you figured out the solution? I posted this on stackoverflow but no reply yet. Maybe middleware:auth in nuxtjs for laravel passport has a problem. Very frustrating. Sigh!

stackoverflow.com/questions/634971...

Collapse
stefant123 profile image
StefanT123 Author

It's not flexible enough for my needs...

Collapse
jameshull profile image
James R. Hull 🎬

Just a couple things I noticed while working through this:

  • initial code of AuthController lists refreshTo() (should be refreshToken())
  • nuxt.config.js: credentials should be withCredentials: true, correct?
  • in the refreshToken milddleware, I assume when the cookie IS there, there should be a commit to the store to reset it (page refresh, direct access through URL). Like this?
  } else {
    store.commit('SET_TOKEN', token);
  }
Collapse
stefant123 profile image
StefanT123 Author
  1. Changed the refreshTo into refreshToken
  2. I saw that you've figured out yourself :D
  3. Well you can do that, but it will send a request on every route change and I don't like that. I'll only send a request when there the token is not there. That way, only one request will be sent every 5 minutes, and I think that's better approach.
Collapse
jameshull profile image
James R. Hull 🎬

It looks like the refresh_token part is not working, unless I missed something. I can login, x-access-token is there - but when it comes time to refresh - I get the 403 error.

Collapse
jameshull profile image
James R. Hull 🎬

I was totally wrong about the withCredentials key...everything kept failing till I put it back the way you had it with credentials instead...weird because it goes against everything else I see.

THANK YOU!

Thread Thread
kenegade profile image
Ken Hall

Hi there, I'm running into a CORS issue that I can't seem to get around. I'm using Laravel 7.5.2. Basically anytime I try to use the register function I get: Access to XMLHttpRequest at 'myapi.test/api/register' from origin 'localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute. If I change the 'credentials' to 'withCredentials', it passes the CORS preflight, but then the token part does not appear to be working.

***FOLLOW UP: I'm still using the 'withCredentials' parameter. The issue going in that direction was in the .env using CLIENT_ID as opposed to PASSWORD_CLIENT_ID. That was mentioned before in this thread. Thanks!

Collapse
jameshull profile image
James R. Hull 🎬

This is fantastic! Thanks so much for posting. Any thoughts on modifying this because of Laravel Airlock? I assume this approach works well for SSR Nuxt (Universal) too...

Collapse
stefant123 profile image
StefanT123 Author

Thanks. Yes, it works well, you just need to check for the cookie in the nuxtServerInit function. As for airlock, I haven't tried it, but I assume that same flow would be applied.

Collapse
jameshull profile image
James R. Hull 🎬

Trying to work this out with SSR (Universal) flow (everything worked great for SPA). When I make the original login call I get back the x-access-token, but the refresh_token is only listed in the Response/Request headers. I can set the x-access-token into cookie storage, but I'm not sure where the refresh_token ends up (can't seem to access it)

Once the time on the x-access-token expires, a page refresh or change triggers the refreshToken call, but the refresh_token is not set as a cookie for that call back to the server, and then I get the 403.

I suppose I'm supposed to store the refresh_token as a cookie - but not sure how to even get it when it comes in. It seemed like SPA just kept it around...

Thread Thread
stefant123 profile image
StefanT123 Author

You shouldn't store the refresh_token anywhere, it should be httponly cookie. As the name suggests, the refresh_token should be used for refreshing the access_token once it's expired.

Thread Thread
jameshull profile image
James R. Hull 🎬

OK - yeah, with SSR on the /api/refresh_token call, I'm getting a 500 - a Undefined property: stdClass::$refresh_token error in the ProxyRequest controller. It gets all the way to makePostRequest but then fails here: $this->setHttpOnlyCookie($resp->refresh_token);

With dev tools, I can see the refresh_token listed under Request Cookies, but it doesn't seem to be going up with the request (axios log doesn't list it)

Thread Thread
stefant123 profile image
StefanT123 Author

Hmm, if the refresh_token is in the Request Cookies, it should work fine. Do you json_decode the response?

Thread Thread
jameshull profile image
James R. Hull 🎬

Almost got this working - still Request is malformed - getting these two errors: Laravel\Passport\Exceptions\OAuthServerException
The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.
ErrorException
Undefined property: stdClass::$refresh_token

and yes - following your code above explicitly:
$proxy = \Request::create('oauth/token', 'post', $params);
$resp = json_decode(app()->handle($proxy)->getContent());

Thread Thread
stefant123 profile image
StefanT123 Author

I can't help you unless I see your code :(
However, you can check the github links:

and make sure that your code matches.

Thread Thread
jameshull profile image
James R. Hull 🎬

Got it....Clear those old cookies people, lest you spend hours chasing your tail. (Refresh tokens are set for longer periods...)

Thread Thread
orenlande profile image
orenlande

I'm having exact same issues now. Driving me mad - cannot figure out how to resolve it - any help?
And yes, I followed the tutorial 100% same.

EDIT: FOUND THE ISSUE!
seems like the .env wasn't configured properly - make sure the following exists:
PASSWORD_CLIENT_ID=2
PASSWORD_CLIENT_SECRET={secret_created_by_passport_in_step_1}

Thread Thread
stefant123 profile image
StefanT123 Author

Yes, I mention that in the post, I guess you missed that part :D

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
stefant123 profile image
StefanT123 Author

I didn't quite understand your question. Can you explain it a bit more?! :/

Collapse
dhanibaharzah profile image
dhanibaharzah

sorry, i have solved the problem 🙏

Collapse
klukiyan profile image
Kiril Lukiyan

I spent a lot of time following this tutorial, fixing all the errors, setting up both environments to finally find out that it's not persistent.
The user has to login every time when opening a page.
Who would use such app?

Collapse
stefant123 profile image
StefanT123 Author

You're doing something wrong

Collapse
klukiyan profile image
Kiril Lukiyan

Maybe. I ended up with a working middleware and working login function.
So when I access the site, it brings me to login page. I login and get redirected to the dashboard.
However if I F5 to refresh the dashboard, the api calls that were made, return 401 not authorized.

Anyway, while I researched further I realized that I'd be totally find with @nuxt/auth and jwt or token approach. In my case I'm working on migrating the existing Laravel+jquery application to Nuxt + Laravel backend. Thus I need something that is relatively simple to add without breaking the existing functionality.

Thank you for your tutorial. Even though I won't use it in the end. I really started to understand how the things work and will be able to implement the @nuxt/auth

Thread Thread
stefant123 profile image
StefanT123 Author

I'm glad it helped you :)

Collapse
soonseek profile image
soonseek

HEEEEY, I love this article so much. I finally could have a little of sense what AUTH is.
After a little of time spending to overcome page reload issue, I customized bit of your code.
Key issue was that I have to make my client read cookie from SSR part.

1) To get cookie from SSR, install 'cookieparser'
npm i cookieparser -S

2) @ store/index.js
const cookieparser = process.server ? require('cookieparser') : undefined
and action part,
async nuxtServerInit ({ commit, dispatch }, {req}) {
const token = cookieparser.parse(req.headers.cookie)['x-access-token']
await commit('SET_TOKEN', token);
},

3) @ middleware/refreshToken.js
if (! store.state.token) {
store.dispatch('refreshToken')
.catch(errors => {
store.dispatch('logout');
});
}else{
}

Hope this helps some people who are struggling to overcome page reload issue.

Collapse
abronson07013635 profile image
ABronson

Thanks for the great tutorial. Would it make sense to do another dispatch in the login() method in login.vue, to a '/me' Laravel endpoint, and store some user information in Vuex, to use throughout the app? Any thoughts / best practices on that? (Replicating the Nuxt auth module a bit)

Collapse
stefant123 profile image
StefanT123 Author

I would just directly return the user from the login route in Laravel along wiith the token, that way I wouldn't need to send two requests, one for the token and another for the user.

My response would look something like this

return response([
    'user' => $user, // or even better wrap it in resource
    'token' => $token,
    'expiresIn' => $expiresIn,
    'message' => 'You have been logged in'
], 200);
Collapse
abronson07013635 profile image
ABronson

Awesome, thanks for the advice. I'm a bit of a noob with Vue/Nuxt, maybe there's a cleaner way to do it, but something like this seems to work (setting user on login, clearing user on logout).

Authcontroller.php
return response([
'user' => $user->name,
'token' => $resp->access_token,
'expiresIn' => $resp->expires_in,
'message' => 'You have been logged in',
], 200);

Added the user in store/index.js
export const state = () => ({
token: null,
user: null,
});

Added Vuex mutations:
SET_USER(state, user) {
state.user = user;
}
REMOVE_USER(state, user) {
state.user = null;
},

Added Vuex actions:
setUser({commit}, user) {
commit('SET_USER', user);
},
removeUser({commit}) {
commit('REMOVE_USER');
}

Changed logout method
logout: function() {
this.$axios.post('/api/logout')
.then(resp => {
this.$store.dispatch('removeUser');
this.$store.dispatch('logout');
this.$router.push('/login');
})
.catch(errors => {
console.log(errors);
});
}

Login.vue, login() method:
login() {
this.$axios.$post('/api/login', this.form)
.then(({token, expiresIn, user}) => {
this.$store.dispatch('setToken', {token, expiresIn});
this.$store.dispatch('setUser', user);
this.$router.push('/');
})
.catch(errors => {
console.log(errors);
})
}

  • A computed value for the current user in my NavBar.vue component, to display logged in users name currentUser() { return this.$store.state.user; },
Collapse
smzapp profile image
smzapp

Hi, Thanks for this post. Does cookie()->queue work in you? I tried this but it can't retrieve the cookie. I already added AddQueuedCookiesToResponse in Kernel.php and also added the cookies in EncryptCookies.php. I think, I can't get cookie values in Laravel API. Is there anything I missed? Thanks for the help

Collapse
orenlande profile image
orenlande

Hey, Small suggestion to update your tutorial:
I've been stumbled upon the following issue (pretty common if we judge by comments) - once you refresh your SPA, you're being kicked out to main page.

After short research online and experimenting myself, I think the most elegant solution keep the login persist upon refresh would be using the nuxt.js plugins option:

  1. at nuxt.config.js:
    add to plugins array the following:
    plugins: [
    { ssr: false, src: '~plugins/app-bootstrap' },
    ...
    ]

  2. at root/plugins folder, add the file "app-bootstrap.js" and put inside the following:
    import cookies from 'js-cookie';

export default ({ store }) => {

const token = cookies.get('x-access-token');

if(token) {
    store.dispatch('setToken', {token});
}

}

this way, we ensure we are using the same functions and workflow as the rest of the application.

Collapse
stefant123 profile image
StefanT123 Author

I don't think this is the best solution, but it might work in your case. I've already addressed how to handle this in the comments, here's link. I think I'll do part 2 and 3 of this post, where in one I will go through the issue for the token on page reload, and the other will be for authenticating with PKCE (this may be a separate post, I haven't decided yet).

Collapse
orenlande profile image
orenlande

Imho - if we can resolve this without extra plugins and libs, it would be the best - reduce the overall package size and understand better what's going on.

What would be the advantages of including this plugin? I'm curious if I'm missing something on my solution.

Thread Thread
stefant123 profile image
StefanT123 Author

It's not a third-party plugin, it's just a custom nuxt plugin that we going to create. Nuxt plugin is just some function that will be called before the instantiation of the root app, nothing more.

Collapse
klukiyan profile image
Kiril Lukiyan

Sorry for newbs question. What is that proxy for?

In order for this to work we need to make a proxy that will make request to our own API. It might seems confusing at first but once we're done it will make perfect sense.

Still don't understand it.

Thank you

Collapse
stefant123 profile image
StefanT123 Author

Because we don't want our Passport client_secret exposed, so we are sending it with a proxy that's made in our backend along with the credentials that came from our frontend.

    [code]

    $params = array_merge([
        'client_id' => config('services.passport.password_client_id'),
        'client_secret' => config('services.passport.password_client_secret'),
        'scope' => '*',
    ], $params);

    [code]
Collapse
timothymarois profile image
Timothy Marois

So far everything works except for the refresh_token, always fails and logout happens every 5 minutes. I'll keep trying to see if I can get it working, but as of right now it does not based on the above details. Great tutorial however, just wish we could find a way to get refresh to actually work. My guess is, its not passing in the token to be refreshed.

Collapse
feliped97511974 profile image
Felipe Damasceno

Congratulations for this super tutorial! Keep Rocking!

Just a Q: In the middleware folder it should not be a new auth.JS file?

Collapse
stefant123 profile image
StefanT123 Author

Thank you, and you are right, it should be auth.js. I'll change it right away.

Collapse
devondahon profile image
gvi

Great article. Would be nice to have an update with Passport's PKCE :-)

Collapse
stefant123 profile image
StefanT123 Author

Here you go, a post for PKCE autheticaiton

Collapse
javierpomachagua profile image
Javier Pomachagua

Such a great post for the stack that I love.

Collapse
bawa_geek profile image
Lakhveer Bawa

This post is super incomplete, you gonna waste a lot of time trying to solve the problems, it has a lot of gotchas, I mean a lot. Don't waste your time on it, use nuxt-auth

Collapse
devondahon profile image
gvi

Laravel Sanctum and Laravel JWT have now been integrated to Nuxt Auth module.

Collapse
jespinosaonline profile image
Jonathan Espinosa

What about if I don't use Register or Login Users and is only the Frontend using the Client Credentials Grant Tokens from Laravel Passport?

Collapse
stefant123 profile image
StefanT123 Author

The flow will be more or less the same

Collapse
k90mirzaei profile image
k90mirzaei

Thank you for this great Article, one question: Is it possible to integrate nuxt/auth with your way?

Collapse
stefant123 profile image
StefanT123 Author

Yes, but I don't want to use it, because it's not flexible enough for my needs

Collapse
dhanibaharzah profile image
dhanibaharzah

hi stefant, how to implement the get methods in your tutorial?