DEV Community

Godfrey Mpuya
Godfrey Mpuya

Posted on

Secure User Authentication in Laravel 8 + Vue.js: Implementing JWT-Auth

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 [];
    }
}

Enter fullscreen mode Exit fullscreen mode

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',
        ],
    ],
...
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
  1. 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,
    ];
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
  }
] 

Enter fullscreen mode Exit fullscreen mode

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')
        },
    }
})

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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', '.*');

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Finally run php artisan serve and you are done.

Top comments (0)