Secure User Authentication in Laravel 8 + Vue.js: Implementing JWT-Auth
Tags: webdev, beginners, programming, tutorial
Published: 2023-07-13 09:46:45

In modern web development, user authentication is a crucial aspect of building secure applications. Laravel and Vue.js are powerful frameworks that, when combined, offer an excellent foundation for creating robust and secure web applications. In this post, we will explore the implementation of user authentication using the popular **JWT-Auth package** in a Laravel + Vue.js project. By the end of this guide, you'll have the knowledge and tools to build a secure authentication system that leverages JSON Web Tokens (JWT) for seamless communication between the Laravel backend and Vue.js frontend. This tutorial assumes you have already connected laravel with database.

Let's follow below steps to implement this:

**step 1: Install laravel project (laravel 8 in this tutorial)**
Below command will set up a new Laravel project.
`composer create-project laravel/laravel='8.*' --prefer-dist`

**Step 2: Install tymon/jwt-auth Package: In your Laravel project directory**
Follow the below steps to install tymon/jwt-auth package in your laravel project

1. Run below command to install the JWT authentication package:
`composer require tymon/jwt-auth`.
2. Publish Configuration Files: Publish the JWT configuration files using the following command.
`php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"`
3. Generate Secret Key: Generate the JWT secret key using the following command.
`php artisan jwt:secret`
4. Configure User Model: In your User model, implement the `Tymon\JWTAuth\Contracts\JWTSubject` interface and include the required methods.

```
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }
    
    public function getJWTCustomClaims()
    {
        return [];
    }
}

```

**Step 3: Configure JWT in Laravel:**
1. Update Configuration Files: Open **config/auth.php** and set the driver for the api guard to 'jwt'. Also, make sure the model is set to your User model.

```
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => 'api',
        'passwords' => 'users',
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],
...
```

**Step 4: Create JWTAuthentication middleware**
1. Use bellow command to create this midleware
`php artisan make:middleware JWTAuthentication`
2. Open the new created middleware file **(found in app/http/middleware/JWTAuthentication.php)** and add the below code

```
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Facades\JWTAuth;

class JWTAuthentication
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
     */
    public function handle(Request $request, Closure $next)
    {
        try{
            $user = JWTAuth::parseToken()->authenticate();
        } catch(Exception $e){
            if($e instanceof TokenExpiredException){
                $newToken = JWTAuth::parseToken()->refresh();
                return response()->json(['success' => false, 'token' => $newToken, 'status' => 'Expired'], 200);
            } else if($e instanceof TokenInvalidException){
                return response()->json(['success' => false, 'message' => 'token invalid'], 401);
            }
            else{
                return response()->json(['success' => false, 'message' => 'token not found'], 401);
            }
        }
        return $next($request);
    }
}

```
3. Update Middleware: Open **app/Http/Kernel.php** and add the jwtauth middleware to the middleware group.

```
 protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'jwtauth' => \App\Http\Middleware\JWTAuthentication::class,
    ];
```

**Step 5: Install laravel/ui package**
`composer requre laravel/ui:3.4`
**Step 6: Install Vuejs in Laravel**
`php artisan ui vue` and run `npm install && npm run dev`
**Step 7: Install Vue-Router and Vue-Vuex**
`npm install vue-router@3.5.3`
**Step 7: Install Vuex**
`npm install vuex@3.1.3`
**Step 8: Open the file resources/js/app.js and copy below codes**

```
require('./bootstrap');

import Vue from 'vue'
import store from './store'
import VueRouter from 'vue-router'
Vue.use(VueRouter)

import {routes} from './routes'

window.Vue = require('vue').default;

Vue.component('app-component', require('./components/AppComponent.vue').default);

const router = new VueRouter({
  routes,
  mode: 'history' 
})

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

```

**Step 9: Create file named as routes.js located in resources/js and add below codes**

```
// Pages
import Dashboard from './components/Dashboard'
import Login from './components/Login'
import Logout from './components/Logout'
import Home from './components/Home'

// Routes
export const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/login',
    name: 'login',
    component: Login
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: Dashboard,
    // meta: { requiresAuth: true } 
  },
  {
      path: '/logout',
      name: 'logout',
      component: Logout
  }
] 

```

**Step 10: Create file named as store.js located in resources/js and add below codes**

```
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        token: localStorage.getItem('auth') || ''
    },
    mutations: {
        setToken (state, token) {
            localStorage.setItem('auth', token)
            state.token = token
        },
        clearToken (state) {
            localStorage.removeItem('auth')
        },
    }
})

```

**Step 11: Create file named as Dashboard.vue located in resources/js/components and add below codes**

```
<template>
    <div>
        <div v-if="loading">
            <div class="spinner-border" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
        </div>
        <div class="container" v-else>
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <h1>You're logged in</h1>
            </div>
        </div>
        <button class="btn btn-primary" @click="logout">logout</button>
    </div>
    </div>
</template>

<script>
    export default {
        data(){
            return{
                loading: true
            }
        },
        mounted(){
            if(this.$store.state.token !== ''){
                axios.post('api/auth/checktoken', {token:this.$store.state.token})
                    .then(res =>{
                        console.log(res.data)
                        if(!res.data.success){
                            this.$store.commit('setToken', res.data.token)
                        }
                            this.loading = false
                    })
                    .catch(err =>{
                        this.loading = false
                        this.$router.push('/login')
                    })
            } else {
                this.loading = false
                this.$router.push('/login')
            }
        },
        methods: {
            logout(){
                axios.post('api/auth/logout', {token:this.$store.state.token})
                    .then(res => {
                        this.$store.commit('clearToken')
                        this.$router.push('/login')
                    })
            }
        }       
    }
</script>

```

**Step 12: Create file named as Home.vue located in resources/js/components and add below codes**

```
<template>
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <h1>Home component</h1>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        
    }
</script>
```

**Step 13: Create file named as AppComponent.vue located in resources/js/components and add below codes**

```
<template>
   <div>
        <div class="d-flex justify-content-center">
            <router-link to='/'>Home</router-link>
            <router-link to='/dashboard'>Dashboard</router-link>
            <router-link to='/login'>Login</router-link>
        </div>
        <div>
            <router-view></router-view>
        </div>
   </div>
</template>

<script>
    export default {
    
    }
</script>
```

**Step 14: Create file named as Login.vue located in resources/js/components and add below codes**

```
<template>
   <div>
        <div v-if="loading">
            <div class="spinner-border" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
        </div>
        <div class="text-center form-wrapper" v-else>
            <form class="form-signin" v-on:submit.prevent="submitLogin">
                <h1 class="mb-3 font-weight-normal">Please sign in</h1>

                <label for="inputEmail" class="sr-only">Email address</label>
                <input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus v-model="form.email">

                <label for="inputPassword" class="sr-only">Password</label>
                <input type="password" id="inputPassword" class="form-control" placeholder="Password" required v-model="form.password">

                <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
            </form>
        </div>
   </div>
</template>

<script>
    export default {
        mounted(){
            if(this.$store.state.token !== ''){
                axios.post('api/auth/checktoken', {token : this.$store.state.token})
                    .then(res => {
                        this.loading = false
                        // console.log(this.$store.state.token)
                        if(res.data.success){
                            this.$router.push('/dashboard')
                        }else{
                            this.$store.commit('setToken', res.data.token)
                        }
                    })
                    .catch(err => {
                        this.loading = false
                        // this.$store.commit('clearToken')
                    })
            } else{
                this.loading = false
            }
        },
        data() {
            return {
               form: {
                    email: '',
                    password: ''
                },
                loading: true,
            }
        },
        methods: {
            submitLogin() {
                axios.post('/api/auth/login', this.form)
                    .then(res => {
                        if(res.data.success){
                            //update store
                            this.$store.commit('setToken', res.data.token)
                            this.$router.push('/dashboard')
                            // console.log(res.data)
                        }
                }).catch(error => {
                    console.log(error)
                });
            }
        }
    }
</script>

<style scoped>
    .form-wrapper {
        min-height: 100%;
        min-height: 100vh;
        display: flex;
        align-items: center;
    }
    .form-signin {
        width: 100%;
        max-width: 330px;
        padding: 15px;
        margin: 0 auto;
    }
    .form-signin .form-control {
        position: relative;
        box-sizing: border-box;
        height: auto;
        padding: 10px;
        font-size: 16px;
    }
    .form-signin .form-control:focus {
        z-index: 2;
    }
    .form-signin input[type="email"] {
        margin-bottom: -1px;
        border-bottom-right-radius: 0;
        border-bottom-left-radius: 0;
    }
    .form-signin input[type="password"] {
        margin-bottom: 10px;
        border-top-left-radius: 0;
        border-top-right-radius: 0;
    }
</style>
```

**Step 15: Open routes/web.php file and copy below codes**

```
Route::get('/', function () {
    return view('welcome');
});

Route::get('/{any}', function () {
    return view('welcome');
})->where('any', '.*');

```
The last route is a catch-all route that matches any URL

**Step 16: Create AuthController**
use command `php artisan make:controller AuthController` and copy below codes

```
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('jwtauth')->except('login');
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
        $credentials = $request->only(['email', 'password']);

        if (! $token = JWTAuth::attempt($credentials)) {
            return response()->json(['success' => false], 401);
        }

        return response()->json(['success' => true, 'token' => $token], 200);
    }

    public function checkToken(){
        return response()->json(['success' => true], 200);
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
       $logout =  auth()->logout();

        return response()->json(['success' => true], 200);
    }
}

```

**Step 17: Open routes/api.php file and copy below codes**

```
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\AdminController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::group(['namespace' => 'api', 'prefix' => 'auth'], function () {
    Route::post('login', [AuthController::class, 'login']);
    Route::post('checktoken', [AuthController::class, 'checkToken']);
    Route::post('admin', [AdminController::class, 'index']);
});

```
Finally run `php artisan serve` and you are done.