Secure authentication in Nuxt SPA with Laravel as back-end
Tags: php, javascript, laravel, nuxt
Published: 2020-01-15 14:17:51

---
title: Secure authentication in Nuxt SPA with Laravel as back-end
published: true
description: Learn how to securely set authentication system in SPA with Laravel as back-end. This post is written for Nuxt as front-end and Laravel as back-end, but the concepts can be applied to any SPA that has different back-end.
tags: php, javascript, laravel, nuxt
cover_image: https://thepracticaldev.s3.amazonaws.com/i/4audo1fftw4ufaqpak3x.png
series: SPA authentication with Laravel
---

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
+ [Laravel API](https://github.com/StefanT123/auth-api)
+ [Nuxt SPA](https://github.com/StefanT123/auth-spa-frontend)

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
...
```

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}
...
```

Then we will add the "password client" id and secret to the `config/services.php` so we can use them later in our code:
```php
...
'passport' => [
    'password_client_id' => env('PASSWORD_CLIENT_ID'),
    'password_client_secret' => env('PASSWORD_CLIENT_SECRET'),
],
```

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

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

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

...
```

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

```php
<?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;

    ...
}
```
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.
```php
public function boot()
{
    $this->registerPolicies();

    Passport::routes(function ($router) {
        $router->forAccessTokens();
        $router->forPersonalAccessTokens();
        $router->forTransientTokens();
    });
}
```
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](https://thepracticaldev.s3.amazonaws.com/i/o1thdbzp8p9jpqubexf8.png)

**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:
```php
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));
}
```

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
```php
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);
}
```

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
```php
...

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,
];

...
```

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
<?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');
});
```
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
<?php

namespace App\Http\Controllers;

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

    public function login()
    {
    }

    public function refreshTo()
    {
    }

    public function logout()
    {
    }
}
```

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
<?php

namespace App\Utilities;

class ProxyRequest
{

}
```

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

namespace App\Http\Controllers;

use App\Utilities\ProxyRequest;

class AuthController extends Controller
{
    protected $proxy;

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

...
```

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
<?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
        );
    }
}
```

`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
```php
...

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,
];

...
```

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

In the `register` method, add this
```php
...

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);
}

...
```

In the `login` method, add this
```php
...

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);
 }

...
```

The `refreshToken` method
```php
...

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

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

...
```

The `logout` method
```php
...

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);
}

...
```

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`
```javascript
export default {
  ...

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

  ...
}
```

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
```javascript
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');
  }
};
```

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
```vue
<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>
```

`pages/login.vue` is pretty similar to register, we just need to make some slight changes
```vue
<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>
```

In the `pages/secret.vue` add this
```vue
<template>
  <h2>THIS IS SOME SECRET PAGE</h2>
</template>

<script>
  export default {
    middleware: 'auth',
  }
</script>
```

We must make route middleware for auth, in the middleware folder add new `auth.js` file, and add this
```javascript
export default function ({ store, redirect }) {
  if (! store.state.token) {
    return redirect('/');
  }
}
```

Now we will make the navbar. Change `layouts/deafult.vue` like this
```vue
<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>

...
```

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
```javascript
export default {
  ...

  router: {
    middleware: 'refreshToken',
  },

  ...
}
```

And create that middleware. In the middleware folder add new file `refreshToken.js` and add this
```javascript
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');
      });
  }
}
```

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.