Share, Read & Learn

Laravel JWT Authentication – Vue Js SPA (Part 2)

14 7,254

In this article, we will discuss the Laravel JWT Authentication – Vue Js SPA (Part 2). In this part, we will continue from where we leave in the tutorial (part 1).

Still, if you are not reading the previous part then please go and check it once for better understanding. In the previous part, I’m describing the Laravel JWT Authentication in brief. You can learn how to make the API authentication using JWT.

Today, I will describe how we can setup Vue Js in Laravel application and how we can connect JWT API authentication with Vue Js frontend.

Setup Vue Js

Vue Js is already part of a Laravel application. You just need to execute the following command in your terminal and install the required dependencies.

npm install

After executing this command, you will see the node_modules folder in your Laravel application. This folder contains all the necessary node modules. You can read here the full process of Vue Js setup.

Then open your “resource/views/welcome.blade.php” file and replace the file content.

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- CSRF Token -->
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <title>{{ config('app.name', 'Laravel') }}</title>
  <!-- Fonts -->
  <link rel="dns-prefetch" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600" rel="stylesheet" type="text/css">
  <!-- Styles -->
  <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
  <div id="app">
    <index></index>
  </div>
  <!-- Scripts -->
  <script src="{{ asset('js/app.js') }}" defer></script>
</body>
</html>

 

Create Default Component

Create a default component at “resources/js/Index.vue”. Open Index.vue and add the following code snippet into the file. It’s like a default component, which is loaded first then other components can be added in the content div as per the app functionality.

<template>
    <div id="main">
        <header id="header">
            <h1>Laravel Vue SPA with JWT Authentication</h1>
        </header>
        <div id="content">
            <router-view></router-view>
        </div>
    </div>
</template>
<script>
  export default {
    data() {
      return {
        //
      }
    },
    components: {
        //
    }
  }
</script>

 

Update app.js

Open app.js located at “resources/js” and update the following code snippet.

import './bootstrap'
import Vue from 'vue'
import Index from './Index'

// Set Vue globally
window.Vue = Vue

// Load Index
Vue.component('index', Index)

const app = new Vue({
  el: '#app'
});

After that, you need to execute the following NPM command. This command can watch the changes in your app at the time of development.

npm run watch

Laravel JWT Vue Js

 

Setup Vue Js Packages for Authentication

Now, the basic Vue Js setup is ready with our Laravel application. Time to install the necessary package “websanova/vue-auth” and some of its dependencies such as “vue-router” , “vue-axios”, “axiosand “es6-promise” for our authentication with Laravel JWT.

You can use the following command to install all these packages.

npm i @websanova/vue-auth vue-router vue-axios axios es6-promise

After successful installation, you need to create an auth configuration file at “resources/auth.js” and add the following code.

import bearer from '@websanova/vue-auth/drivers/auth/bearer'
import axios from '@websanova/vue-auth/drivers/http/axios.1.x'
import router from '@websanova/vue-auth/drivers/router/vue-router.2.x'

/**
 * Authentication configuration, some of the options can be override in method calls
 */
const config = {
  auth: bearer,
  http: axios,
  router: router,
  tokenDefaultName: 'laravel-jwt-auth',
  tokenStore: ['localStorage'],
  
  // API endpoints used in Vue Auth.
  registerData: {
    url: 'auth/register', 
    method: 'POST', 
    redirect: '/login'
  },
  loginData: {
    url: 'auth/login', 
    method: 'POST', 
    redirect: '', 
    fetchUser: true
  },
  logoutData: {
    url: 'auth/logout', 
    method: 'POST', 
    redirect: '/', 
    makeRequest: true
  },
  fetchData: {
    url: 'auth/user', 
    method: 'GET', 
    enabled: true
  },
  refreshData: {
    url: 'auth/refresh', 
    method: 'GET', 
    enabled: true, 
    interval: 30
  }
}
export default config

In this configuration, we create some of the basic config variables. These configurations used for “vue-auth”. You can read more on Vue-Auth configuration on the official documentation.

 

Create Route File

Create a file router.js under the resources directory and add the following code.

import VueRouter from 'vue-router'

// Pages
import Home from './pages/Home'
import About from './pages/About'
import Register from './pages/Register'
import Login from './pages/Login'
import Dashboard from './pages/user/Dashboard'
import AdminDashboard from './pages/admin/Dashboard'

// Routes
const routes = [
  {
    path: '/',
    name: 'home',
    component: Home,
    meta: {
      auth: undefined
    }
  },
  {
    path: '/about',
    name: 'about',
    component: About,
    meta: {
      auth: undefined
    }
  },
  {
    path: '/register',
    name: 'register',
    component: Register,
    meta: {
      auth: false
    }
  },
  {
    path: '/login',
    name: 'login',
    component: Login,
    meta: {
      auth: false
    }
  },
  // USER ROUTES
  {
    path: '/dashboard',
    name: 'dashboard',
    component: Dashboard,
    meta: {
      auth: true
    }
  },
]
const router = new VueRouter({
  history: true,
  mode: 'history',
  routes,
})
export default router

In this file, we will be defined as the required routes. The “meta” parameter is used to define the access rules for each route.

Each of the routes contains path, name, component and meta parameter.

  • “path” is a route path, which accesses in our application.
  • “name” is a route name, we use route name to call the specific route.
  • “component” is loaded at a time for a route called.
  • “meta” is used to define the access rules for each route.
    • auth: undefined, it’s a public route.
    • auth: true, it’s available only for the authenticated users and auth: false, is available only for the unauthenticated users.

 

Create Components

Now, time to create required components such as home, register, login, dashboard, and etc as given below.

Home Component

Create home component at “resources/js/pages/home.vue”. This component is the home page of our application.

<template>
    <div class="container">
        <div class="card card-default">
            <div class="card-header">Home</div>
            <div class="card-body">
                <p>
                    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt amet tempora sint dolor nam quam quos inventore odio hic, enim beatae nulla in tenetur odit natus facere voluptas excepturi deleniti? Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur sit eligendi rem et minus dolor hic, placeat eum sequi ipsa, debitis ex magni. Hic laudantium consectetur aliquid eos fuga cumque.
                </p>
                <p>
                    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt amet tempora sint dolor nam quam quos inventore odio hic, enim beatae nulla in tenetur odit natus facere voluptas excepturi deleniti? Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur sit eligendi rem et minus dolor hic, placeat eum sequi ipsa, debitis ex magni. Hic laudantium consectetur aliquid eos fuga cumque.
                </p>
            </div>
        </div>
    </div>
</template>

 

Register Component

Registration form – Create register component at “resources/js/pages/register.vue”.

<template>
  <div class="container">
    <div class="row justify-content-md-center">
      <div class="col-6">
        <div class="card card-default">
          <div class="card-header">Register</div>
          <div class="card-body">
            <div class="alert alert-danger" v-if="has_error && !success">
                <p v-if="error == 'registration_validation_error'">Validation Errors.</p>
                <p v-else>Error, can not register at the moment. If the problem persists, please contact an administrator.</p>
            </div>
            <form autocomplete="off" @submit.prevent="register" v-if="!success" method="post">
                <div class="form-group" v-bind:class="{ 'has-error': has_error && errors.name }">
                    <label for="name">Name</label>
                    <input type="text" id="name" class="form-control" placeholder="Full Name" v-model="name">
                    <span class="help-block" v-if="has_error && errors.name">{{ errors.name }}</span>
                </div>
                <div class="form-group" v-bind:class="{ 'has-error': has_error && errors.email }">
                    <label for="email">E-mail</label>
                    <input type="email" id="email" class="form-control" placeholder="user@example.com" v-model="email">
                    <span class="help-block" v-if="has_error && errors.email">{{ errors.email }}</span>
                </div>
                <div class="form-group" v-bind:class="{ 'has-error': has_error && errors.password }">
                    <label for="password">Password</label>
                    <input type="password" id="password" class="form-control" v-model="password">
                    <span class="help-block" v-if="has_error && errors.password">{{ errors.password }}</span>
                </div>
                <div class="form-group" v-bind:class="{ 'has-error': has_error && errors.password }">
                    <label for="password_confirmation">Password confirmation</label>
                    <input type="password" id="password_confirmation" class="form-control" v-model="password_confirmation">
                </div>
                <button type="submit" class="btn btn-primary">Submit</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        name: '',
        email: '',
        password: '',
        password_confirmation: '',
        has_error: false,
        error: '',
        errors: {},
        success: false
      }
    },
    methods: {
      register() {
        var app = this
        this.$auth.register({
          data: {
            name: app.name,
            email: app.email,
            password: app.password,
            password_confirmation: app.password_confirmation
          },
          success: function () {
            app.success = true
            this.$router.push({name: 'login', params: {successRegistrationRedirect: true}})
          },
          error: function (res) {
            // console.log(res.response.data.errors)
            app.has_error = true
            app.error = res.response.data.error
            app.errors = res.response.data.errors || {}
          }
        })
      }
    }
  }
</script>

 

Login Component

Login form – Create login component at “resources/js/pages/Login.vue”.

<template>
  <div class="container">
    <div class="row justify-content-md-center">
      <div class="col-6">
        <div class="card card-default">
          <div class="card-header">Login</div>
          <div class="card-body">
            <div class="alert alert-danger" v-if="has_error && !success">
              <p v-if="error == 'login_error'">Validation Errors.</p>
              <p v-else>Error, unable to connect with these credentials.</p>
            </div>
            <form autocomplete="off" @submit.prevent="login" method="post">
              <div class="form-group">
                <label for="email">E-mail</label>
                <input type="email" id="email" class="form-control" placeholder="user@example.com" v-model="email" required>
              </div>
              <div class="form-group">
                <label for="password">Password</label>
                <input type="password" id="password" class="form-control" v-model="password" required>
              </div>
              <button type="submit" class="btn btn-primary">Signin</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        email: null,
        password: null,
        success: false,
        has_error: false,
        error: ''
      }
    },
    mounted() {
      //
    },
    methods: {
      login() {
        // get the redirect object
        var redirect = this.$auth.redirect()
        var app = this

        this.$auth.login({
          data: {
            email: app.email,
            password: app.password
          },
          success: function() {
            // handle redirection
            app.success = true
            const redirectTo = 'dashboard'
            this.$router.push({name: redirectTo})
          },
          error: function() {
            app.has_error = true
            app.error = res.response.data.error
          },
          rememberMe: true,
          fetchUser: true
        })
      }
    }
  }
</script>

 

Dashboard Component

Dashboard.vue – Create dashboard component at “resources/js/pages/user/Dashboard.vue”.

<template>
    <div class="container">
        <div class="card card-default">
            <div class="card-header">Dashboard</div>
            <div class="card-body">
                User Dashboard
            </div>
        </div>
    </div>
</template>
<script>
  export default {
    data() {
      return {
        //
      }
    },
    components: {
      //
    }
  }
</script>

 

Menu Component

Menu.vue – Create menu component at “resources/js/components/Menu.vue”. It’s a reuseable component. We are going to use this component in Index.vue.

<template>
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <router-link :to="{name: 'home'}" class="navbar-brand">Laravel + JWT + Vue JS</router-link>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav mr-auto" v-if="$auth.check(1)">
          <li class="nav-item" v-for="(route, key) in routes.user" v-bind:key="route.path">
            <router-link :to="{ name : route.path }" :key="key" class="nav-link">{{route.name}}</router-link>
          </li>
      </ul>
      <ul class="navbar-nav mr-auto" v-if="$auth.check(2)">
          <li class="nav-item" v-for="(route, key) in routes.user" v-bind:key="route.path">
            <router-link :to="{ name : route.path }" :key="key" class="nav-link">{{route.name}}</router-link>
          </li>
      </ul>
      <ul class="navbar-nav ml-auto" v-if="!$auth.check()">
          <li class="nav-item" v-for="(route, key) in routes.unlogged" v-bind:key="route.path">
            <router-link :to="{ name : route.path }" :key="key" class="nav-link">{{route.name}}</router-link>
          </li>
      </ul>
      <ul class="navbar-nav ml-auto" v-if="$auth.check()">
        <li class="nav-item">
          <a class="nav-link" href="#" @click.prevent="$auth.logout()">Logout</a>
        </li>
      </ul>
    </div>
  </nav>
</template>
<script>
  export default {
    data() {
      return {
        routes: {
          // UNLOGGED
          unlogged: [
            { name: 'Register', path: 'register' },
            { name: 'Login', path: 'login'}
          ],
          // LOGGED USER
          user: [
            { name: 'Dashboard', path: 'dashboard' }
          ],
          // LOGGED ADMIN
          admin: [
            { name: 'Dashboard', path: 'admin.dashboard' }
          ]
        }
      }
    },
    mounted() {
      //
    }
  }
</script>

<style>
.navbar {
  margin-bottom: 30px;
}
</style>

 

Index Component

Index.vue – component is the main component which is loaded by default. You can control this in your resources/js/app.js.

<template>
    <div id="main">
        <header id="header">
            <Menu></Menu>
        </header>
        <div id="content">
            <router-view></router-view>
        </div>
    </div>
</template>
<script>
  import Menu from './components/Menu.vue'
  export default {
    data() {
      return {
        //
      }
    },
    components: {
      Menu
    }
  }
</script>

 

Update App.js

Finally, time to update resources/js/app.js. So open your app.js file and replace the code with the following code.

require('./bootstrap');

import 'es6-promise/auto'
import axios from 'axios'
import Vue from 'vue'
import VueAuth from '@websanova/vue-auth'
import VueAxios from 'vue-axios'
import VueRouter from 'vue-router'
import Index from './Index'
import auth from './auth'
import router from './router'

// Set Vue globally
window.Vue = Vue

// Set Vue router
Vue.router = router
Vue.use(VueRouter)

// Set Vue authentication
Vue.use(VueAxios, axios)
axios.defaults.baseURL = `${process.env.MIX_APP_URL}/api/v1`

Vue.use(VueAuth, auth)

// Load Index
Vue.component('index', Index)
const app = new Vue({
  el: '#app',
  router
});

In this file, we need to import the necessary Vue.js packages and other dependencies which we need at the time to application load.

 

Update Laravel “.env”

As you notify, I’m using “MIX_APP_URL” variable for Axios base URL. But before using this you need to add this variable in your “Laravel .env” file.

// VARIABLE USE IN VUR APP

MIX_APP_URL="${APP_URL}"

// Laravel Default APP_URL VARIABLE

APP_URL=http://127.0.0.1:8000

Now, time to test the application. So I’m using two different terminals or command prompt window to execute our Vue Laravel App.

 

Test Laravel Vue App

Open the first terminal and execute the following artisan command.

php artisan serve --port=9090

In the second terminal, we need to execute the following NPM command.

npm run watch

The above mention “npm run watch” command will detect all the file changes and compile those file immediately. You just need to refresh the browser.

Let’s time to check the working of our Laravel Vue application.

Home Page

Laravel Vue Js Home Page

Login Page

Laravel Vue Js Login Page

Register Page

Laravel Vue Js Register Page

Everything works fine, but when you trying to reload any specific route such as login or register then we face error. It because of Laravel is currently configured only for the index route. You have to update the “route/web.php” route file and replace the following code with the existing.

// Route to handle page reload in Vue except for api routes
Route::get('/{any?}', function (){
    return view('welcome');
})->where('any', '^(?!api\/)[\/\w\.-]*');

After that, each route works fine.

When a user logged in the application then the user will redirect to its dashboard.

Laravel Vue Js Dashboard Page

Conclusion

In this tutorial, we will be discussing the Vue Js Setup and connect Laravel JWT Authentication with Vue App. This is the second part of the series, so you need to check part 1 for better understanding. In part 1 I’m explaining the Laravel JWT Authentication.

It’s just a start, still not enough in Vue Js. We will discuss more on Vue Js such as protecting route, error handling, user role management and many more.

You can get the complete source code on GitHub.

Please feel free to ask questions, and also send me your feedback so I can make improvement in this tutorial.

🙂 😉

Leave A Reply

Your email address will not be published.

14 Comments
  1. Maria Smith says

    Thanks Pankaj
    Great tutorial. More appreciated if you share this tutorial via GitHub repository.

  2. James says

    Hi Pankaj,

    As far as I know I followed your tutorial to the letter but I am getting the following console error:

    POST http://localhost:8000/undefined/api/v1/auth/login 405 (Method Not Allowed)

    Any suggestions? Do you have a working version of the code on Github?

    1. Code Briefly says

      Hi James,

      Please recheck all the step once and try again.
      Yes, I have a working version and soon I will create a GitHub repository for this.

    2. mr nice guy says

      I have the exact same problem… Hmm,

      1. Bonno says

        I have the exact same error: http://localhost:8080/undefined/api/v1/auth/login 404 (Not Found)
        I’m using vue cli 3 project with laravel.
        http://localhost:8080 is my vue project.
        http://localhost:8000 is my laravel project.
        So my baseURL is supposed to be http://localhost:8000/api/v1/auth/login

        When I try to hardcode i.e: axios.defaults.baseURL = ‘http:/localhost:8000/api/v1’ , I get error:
        POST http://localhost:8080/localhost:8000/api/v1/auth/login 404 (Not Found)

        1. Alvaro González says

          Hi, you need to leave it like this
          axios.defaults.baseURL = ‘/api/v1’

          That worked for me

  3. d says

    Thanks!

  4. David says

    Hi. Where do you save the token sent from server?
    As i see, upon successful login, it is sent in header.

    return response()->json([‘status’ => ‘success’], 200)->header(‘Authorization’, $token);

    Shouldn’t we save it in local storage or in cookie??

    Thanks.

  5. Joan says

    Thanks for your great tuto!

    My issue is that I can register and login with postman. ok part 1. But when I try to “refresh” with postman (/api/auth/refresh/), from a logged user (authentication: Bearer tokenkey…) I get an “undefined variable ‘token'” from the refresh method.

    How does this method get the token to refresh?

    Thank you!

    1. Joan says

      I should add to my comment that the problem seems to be that token doesn’t persist. Even having set the option as ‘ttl’ => env(‘JWT_TTL’, 1440) in config/jwt.php

      Thanks!

    2. Shawn says

      Author does not include that when using /api/auth/refresh you must pass the token as a a header. In Postman:

      Key: Authorization
      Value: Bearer

  6. alex says

    NOT WORKING IN 5.8

  7. Cleber says

    Don’t work in Laravel 5.8,*:
    Method Illuminate\Auth\SessionGuard::refresh does not exist.

  8. Sakakido Kou says

    Thanks for sharing Pankaj!
    It works with the latest Laravel and VueJS.
    But the errors.name which is showed in span element is “[…]”. Can you help me to remove the double quotes and the square brackets, pls?