Building Vue.js Client SPA Token-Based Authentication with Laravel Sanctum
Tags: vue, laravel, sanctum, auth
Published: 2020-03-23 11:02:07

---
title: Building Vue.js Client SPA Token-Based Authentication with Laravel Sanctum
published: true
description: Vue.js Token-Based Authentication with Laravel Sanctum
tags: #vue, #laravel, #sanctum, #auth
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/tww0oj723dsm3xsqgk2l.png
---

Authentication systems are a vital part of most modern applications, and should thus be appropriately implemented.

In this article, you will learn how to build an authentication system using [Vue.js](https://vuejs.org) and [Laravel Sanctum](https://laravel.com/docs/7.x/sanctum) (former Airlock).

We are going to create separate projects for the front end, and for the back end, that will interact with one another through a REST API.

Let's dive in!

### Back end (Laravel)

#### Step #1
For Laravel installation instructions, visit an [Official Documentation](https://laravel.com/docs/7.x#installing-laravel) page.

Create a new Laravel project by running in the terminal
```
laravel new my-app
```
or 
```
composer create-project --prefer-dist laravel/laravel my-app
```

I'm using Laravel Valet, which automatically allows us to access our site at the `http://my-app.test` domain.

On your machine, it will be accessible according to your local development environmental settings.

#### Step #2

Create a new database called `my-app` and set `DB_DATABASE=my-app` in `.env` file in your app's directory.

#### Step #3

Install Laravel Sanctum.
```
composer require laravel/sanctum
```

Publish the Sanctum configuration and migration files using the `vendor:publish` Artisan command. The `sanctum` configuration file will be placed in your `config` directory:

```
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
```

Run your database migrations to create a database table in which to store API tokens:

```
php artisan migrate
```

Add the Sanctum's middleware to your `api` middleware group within your *app/Http/Kernel.php*

```php
../app/Http/Kernel.php

use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

...

    protected $middlewareGroups = [
        ...

        'api' => [
            EnsureFrontendRequestsAreStateful::class,
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    ...
],
```

#### Step #4

To use tokens for users, we have to add `HasApiTokens` to the `User` model in *app/User.php*.

```php
../app/User.php

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}
```

#### Step #5

Let's create the seeder for the `User` model. We'll need that a bit later to test the login proccess.

```
php artisan make:seeder UsersTableSeeder
```

Now let's insert 

```php
DB::table('users')->insert([
    'name' => 'John Doe',
    'email' => 'john@doe.com',
    'password' => Hash::make('password')
]);
```
into the `run()` function in *database/seeds/UsersTableSeeder.php*

To seed `users` table with user, let's run: 
```
php artisan db:seed --class=UsersTableSeeder
```
So now we have a new user in our database called `John Doe` with email `john@doe.com` and password `password`.

#### Step #6

Let's create a `/login` route in the *routes/api.php* file:

```php
../routes/api.php

use App\User;
use Illuminate\Support\Facades\Hash;

Route::post('/login', function (Request $request) {
    $data = $request->validate([
        'email' => 'required|email',
        'password' => 'required'
    ]);

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

    if (!$user || !Hash::check($request->password, $user->password)) {
        return response([
            'message' => ['These credentials do not match our records.']
        ], 404);
    }

    $token = $user->createToken('my-app-token')->plainTextToken;

    $response = [
        'user' => $user,
        'token' => $token
    ];

    return response($response, 201);
});
```

#### Step #7

Let's send a POST request with email `john@doe.com` and password `password` as parameters to the `http://my-app.test/api/login` route. You can use the [Postman](https://www.postman.com/) or [Insomnia](https://insomnia.rest/) software packages to accomplish this.

If everything is working well, we will receive a JSON object as response to our request:

```php
{
    "user": {
        "id": 1,
        "name": "John Doe",
        "email": "john@doe.com",
        "email_verified_at": null,
        "created_at": null,
        "updated_at": null
    },
    "token": "AbQzDgXa..."
}
```

#### Step #8

Next, we need to change some middleware. We do this in the */routes/api.php* file by replacing `auth:api` with `auth:sanctum`:

```php
../routes/api.php

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});
```

#### Step #9

Before we continue to the front end, we have to setup cross-origin requests [CORS](https://developer.mozilla.org/en-US/docs/Glossary/CORS) handling.

```php
../config/cors.php

    'paths' => ['api/*', 'login', 'logout'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,
```

```php
../.env

SANCTUM_STATEFUL_DOMAINS=127.0.0.1
```

### Front end (Vue.js)

We'll use [Vuex](https://vuex.vuejs.org/) for state management, [Vue Router](https://router.vuejs.org/) for routing and [axios](https://github.com/axios/axios) to make HTTP requests.

#### Step #1

We are going to use [Vue CLI](https://cli.vuejs.org/) to create a new Vue project. If you are not familiar with this standard tooling for Vue.js development, please, read this [guide](https://cli.vuejs.org/guide/).

In the directory we are using for our projects, let's run the following command:

```
vue create my-vue-app
```

Choose to `Manually select features` and then opt for `Router` and `Vuex`

![vue_create](https://dev-to-uploads.s3.amazonaws.com/i/5qgzsug6e7gq6cwrlqva.png)

After successfully creating the `my-vue-app` project, run the following commands:

```
cd my-vue-app
npm run serve
```
Now our app should be available at the `http://localhost:8080/` domain.

#### Step #2

Let's create a new file for a `Login` view.

```javascript
..src/views/Login.vue

<template>
  <div>
    <h1>Login</h1>
    <form @submit.prevent="login">
      <input type="email" name="email" v-model="email">
      <input type="password" name="password" v-model="password">
      <button type="submit">Login</button>
    </form>
  </div>
</template>

<script>
export default {
  data () {
    return {
      email: '',
      password: ''
    }
  },

  methods: {
    login () {
      this.$store
        .dispatch('login', {
          email: this.email,
          password: this.password
        })
        .then(() => {
          this.$router.push({ name: 'About' })
        })
        .catch(err => {
          console.log(err)
        })
    }
  }
}
</script>
```

In the `Vue Router`, we have to implement a route for the `Login` view.

```javascript
../src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

```

Now, if we navigate to `http://localhost:8080/login` in a browser, we can see a login page.

![login](https://dev-to-uploads.s3.amazonaws.com/i/jejj3vag7mhjnkufp8pe.png)

#### Step #3

We have to install [axios](https://github.com/axios/axios) in our frontend directory to make HTTP requests:

```
npm install axios
```

#### Step #4

Let's implement some user authentication actions `(login/logout)` in Vuex.

```javascript
../src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

axios.defaults.baseURL = 'http://app-backend.test/api'

export default new Vuex.Store({
  state: {
    user: null
  },

  mutations: {
    setUserData (state, userData) {
      state.user = userData
      localStorage.setItem('user', JSON.stringify(userData))
      axios.defaults.headers.common.Authorization = `Bearer ${userData.token}`
    },

    clearUserData () {
      localStorage.removeItem('user')
      location.reload()
    }
  },

  actions: {
    login ({ commit }, credentials) {
      return axios
        .post('/login', credentials)
        .then(({ data }) => {
          commit('setUserData', data)
        })
    },

    logout ({ commit }) {
      commit('clearUserData')
    }
  },

  getters : {
    isLogged: state => !!state.user
  }
})

```

After successfully logging in, we are going to store some user data in the `user` variable and the `localStorage`.

#### Step #5

Let's define routes for authenticated and unauthenticated pages.

We can make an `About` page accessible for authenticated users only.

For this purpose let's add the `meta` field to the `About` route.

Let's use Vue Router's `beforeEach` method to check if the user is logged in. If the user is not authenticated, we'll redirect them back to the login page.

```javascript
../src/router.index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    meta: {
      auth: true
    },
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

router.beforeEach((to, from, next) => {
  const loggedIn = localStorage.getItem('user')

  if (to.matched.some(record => record.meta.auth) && !loggedIn) {
    next('/login')
    return
  }
  next()
})

export default router

```

#### Step #6

What if the user refreshes a page? Should we ask him to log in again? 

Of course not!

Let's add a `created()` method to the `Vue` instance to handle that scenario.

```javascript
created () {
  const userInfo = localStorage.getItem('user')
  if (userInfo) {
    const userData = JSON.parse(userInfo)
    this.$store.commit('setUserData', userData)
  }
}
```

#### Step #7

We also need to handle cases when a token is expired or if the user is unauthorized.

Let's do that in the `created()` method by using interceptors.

So our updated `main.js` file looks like this:

```javascript
../src/main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  created () {
    const userInfo = localStorage.getItem('user')
    if (userInfo) {
      const userData = JSON.parse(userInfo)
      this.$store.commit('setUserData', userData)
    }
    axios.interceptors.response.use(
      response => response,
      error => {
        if (error.response.status === 401) {
          this.$store.dispatch('logout')
        }
        return Promise.reject(error)
      }
    )
  },
  render: h => h(App)
}).$mount('#app')

```

#### Step #8

We haven't implemented a `Logout` feature yet. Let's do that in the `App.vue` file.

Also, let's show the `About` and `Logout` buttons only when a user is logged in.

```javascript
../src/App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about" v-if="isLogged">About</router-link>
      <router-link to="/login" v-else>Login</router-link>
      <button type="button" @click="logout()" v-if="isLogged">
        Logout
      </button>
    </div>
    <router-view/>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters([
      'isLogged'
    ])
  },

  methods: {
    logout () {
      this.$store.dispatch('logout')
    }
  }
}
</script>
```

Ok, our tutorial is done.

I hope you found this info helpful!

View source code
[Front End](https://gitlab.com/paprotsky/my-vue-app)
[Back End](https://gitlab.com/paprotsky/my-app)