SPA Authentication using Laravel Sanctum and Vue.js - TechvBlogs

SPA Authentication using Laravel Sanctum and Vue.js

Laravel Sanctum provides a featherweight authentication system for SPAs, mobile applications, and simple, token-based APIs.


Suresh Ramani - Author - TechvBlogs
Suresh Ramani
 

2 years ago

TechvBlogs - Google News

Laravel Sanctum provides a lightweight authentication system relying on Laravel's built-in cookie-based session authentication services.

How Laravel Sanctum works

Before we start blindly mashing away without an understanding of what's happening behind the scenes, let's run over how Sanctum works.

Laravel Sanctum uses Laravel's cookie-based session authentication to authenticate users from your client. Here's the flow.

  1. You request a CSRF cookie from Sanctum on the client, which allows you to make CSRF-protected requests to normal endpoints like / login.
  2. You make a request to the normal Laravel / login endpoint.
  3. Laravel issues a cookie holding the user's session.
  4. Any requests to your API now include this cookie, so your user is authenticated for the lifetime of that session.

In this blog, together we will create a complete register and login feature for a single page application in Vue.js and Laravel Sanctum.

Step 1: Create Laravel Project

First, open Terminal and run the following command to create a fresh Laravel project:

composer create-project --prefer-dist laravel/laravel larasanctum-authvue

or, if you have installed the Laravel Installer as a global composer dependency:

laravel new larasanctum-authvue

Step 2: Configure Database Detail

open .env and update database detail

DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=<DATABASE NAME>
DB_USERNAME=<DATABASE USERNAME>
DB_PASSWORD=<DATABASE PASSWORD>

Step 3: Install laravel/ui

composer require laravel/ui
php artisan ui vue
npm install && npm run dev

These commands do three things:

  1. Install the Laravel UI package with Composer.

  2. Generate the JS / UI files, auth boilerplate, and package.json modifications.

  3. Install the frontend dependencies and compile development JS / CSS assets.

Step 4: Migrate Database

php artisan migrate

Read Also:  Firebase Push Notification Laravel Tutorial

Step 5: Install Laravel Sanctum

You can find documentation on the Official  Laravel   Website.

composer require laravel/sanctum

Step 6: Configure Laravel Sanctum

Open config/sanctum.php and update the following code:

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),

You will need to change this when deploying to production, so adding  SANCTUM_STATEFUL_DOMAINS to your  .env file with a comma-separated list of allowed domains is a great idea.

Open .env file and add this line

SANCTUM_STATEFUL_DOMAINS=localhost:<PORT NUMBER>

Change the session driver

In .env, update session driver file to cookie.

SESSION_DRIVER=cookie

Configure CORS

Open config/cors.php and update the following code into the file:

'paths' => [
    'api/*',
    '/login',
    '/logout',
    '/sanctum/csrf-cookie'
],

Also set supports_credentials option to true:

'supports_credentials' => true,

Let's create our Vue component that will hold our login form and display some secrets.

Step 7: Setup Frontend

When we generated our frontend code earlier using  php artisan ui vue , an example component was generated under  resources/js/components/ExampleComponent.vue. Let's create other components for Login, Register, and Dashboard Page.

What is Vuex?

Vuex is a state management pattern + library for Vue. js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.

Well, since we want to hold an overall authenticated 'state' in our client, using a state management library like Vuex makes sense here. It'll also allow us to easily check within any component if we're authenticated or not (e.g. our navigation).

Let's Install Vuex

npm install vuex --save

First, create a resources/js/store/auth.js file with the following.

import axios from 'axios'
import router from '../router'

export default {
    namespaced: true,
    state:{
        authenticated:false,
        user:{}
    },
    getters:{
        authenticated(state){
            return state.authenticated
        },
        user(state){
            return state.user
        }
    },
    mutations:{
        SET_AUTHENTICATED (state, value) {
            state.authenticated = value
        },
        SET_USER (state, value) {
            state.user = value
        }
    },
    actions:{
        login({commit}){
            return axios.get('/api/user').then(({data})=>{
                commit('SET_USER',data)
                commit('SET_AUTHENTICATED',true)
                router.push({name:'dashboard'})
            }).catch(({response:{data}})=>{
                commit('SET_USER',{})
                commit('SET_AUTHENTICATED',false)
            })
        },
        logout({commit}){
            commit('SET_USER',{})
            commit('SET_AUTHENTICATED',false)
        }
    }
}

The state the property holds whether we're authenticated or not, and holds the user details we'll be fetching once authenticated.

Our getters return to us that state.

Our mutations update our state. For example, once we're successfully authenticated, we'll commit a mutation to set authenticated to true and commit another mutation to set the user's details.

Sometimes we need our VueJS Web App to persist some information in browser local storage. It could be local settings, account info, or some tokens. We definitely don’t want to lose them once the page is refreshed. That's why we need to use vuex-persistedstate.

Install vuex-persistedstate

npm i vuex-persistedstate

Now add the auth module to Vuex in resources/js/store/index.js.

import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import auth from './auth'

Vue.use(Vuex)

export default new Vuex.Store({
    plugins:[
        createPersistedState()
    ],
    modules:{
        auth
    }
})

Add Vuex into resources/js/app.js

/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

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

import store from './store'

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

Read Also: Implement Passport In Laravel

What is Vue Router?

Vue Router helps link between the browser's URL / History and Vue's components allowing for certain paths to render whatever view is associated with it.

Features Of Vue Router

  • Nested Routes
  • Route params, query
  • Dynamic Routes Matching
  • Links with automatic active CSS classes
  • and many more

Let's install vue-router

npm install vue-router

Now, Create Components For Login and Register.

Create a File inside resources/js/components folder name with Login.vue .

resources/js/components/Login.vue

<template>
    <div class="container h-100">
        <div class="row h-100 align-items-center">
            <div class="col-12 col-md-6 offset-md-3">
                <div class="card shadow sm">
                    <div class="card-body">
                        <h1 class="text-center">Login</h1>
                        <hr/>
                        <form action="javascript:void(0)" class="row" method="post">
                            <div class="form-group col-12">
                                <label for="email" class="font-weight-bold">Email</label>
                                <input type="text" v-model="auth.email" name="email" id="email" class="form-control">
                            </div>
                            <div class="form-group col-12">
                                <label for="password" class="font-weight-bold">Password</label>
                                <input type="password" v-model="auth.password" name="password" id="password" class="form-control">
                            </div>
                            <div class="col-12 mb-2">
                                <button type="submit" :disabled="processing" @click="login" class="btn btn-primary btn-block">
                                    {{ processing ? "Please wait" : "Login" }}
                                </button>
                            </div>
                            <div class="col-12 text-center">
                                <label>Don't have an account? <router-link :to="{name:'register'}">Register Now!</router-link></label>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import { mapActions } from 'vuex'
export default {
    name:"login",
    data(){
        return {
            auth:{
                email:"",
                password:""
            },
            processing:false
        }
    },
    methods:{
        ...mapActions({
            signIn:'auth/login'
        }),
        async login(){
            this.processing = true
            await axios.get('/sanctum/csrf-cookie')
            await axios.post('/login',this.auth).then(({data})=>{
                this.signIn()
            }).catch(({response:{data}})=>{
                alert(data.message)
            }).finally(()=>{
                this.processing = false
            })
        },
    }
}
</script>

Create a File inside  resources/js/components  folder name with  Register.vue.

<template>
    <div class="container h-100">
        <div class="row h-100 align-items-center">
            <div class="col-12 col-md-6 offset-md-3">
                <div class="card shadow sm">
                    <div class="card-body">
                        <h1 class="text-center">Register</h1>
                        <hr/>
                        <form action="javascript:void(0)" @submit="register" class="row" method="post">
                            <div class="form-group col-12">
                                <label for="name" class="font-weight-bold">Name</label>
                                <input type="text" name="name" v-model="user.name" id="name" placeholder="Enter name" class="form-control">
                            </div>
                            <div class="form-group col-12">
                                <label for="email" class="font-weight-bold">Email</label>
                                <input type="text" name="email" v-model="user.email" id="email" placeholder="Enter Email" class="form-control">
                            </div>
                            <div class="form-group col-12">
                                <label for="password" class="font-weight-bold">Password</label>
                                <input type="password" name="password" v-model="user.password" id="password" placeholder="Enter Password" class="form-control">
                            </div>
                            <div class="form-group col-12">
                                <label for="password_confirmation" class="font-weight-bold">Confirm Password</label>
                                <input type="password_confirmation" name="password_confirmation" v-model="user.password_confirmation" id="password_confirmation" placeholder="Enter Password" class="form-control">
                            </div>
                            <div class="col-12 mb-2">
                                <button type="submit" :disabled="processing" class="btn btn-primary btn-block">
                                    {{ processing ? "Please wait" : "Register" }}
                                </button>
                            </div>
                            <div class="col-12 text-center">
                                <label>Already have an account? <router-link :to="{name:'login'}">Login Now!</router-link></label>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import { mapActions } from 'vuex'
export default {
    name:'register',
    data(){
        return {
            user:{
                name:"",
                email:"",
                password:"",
                password_confirmation:""
            },
            processing:false
        }
    },
    methods:{
        ...mapActions({
            signIn:'auth/login'
        }),
        async register(){
            this.processing = true
            await axios.post('/register',this.user).then(response=>{
                this.signIn()
            }).catch(({response:{data}})=>{
                alert(data.message)
            }).finally(()=>{
                this.processing = false
            })
        }
    }
}
</script>

Create Layout Component For All Authenticated Pages. So we don't need to add header, footer, and any other component in all pages component so here we created a layout component named  Dashboard.vue. Here in the component, We add header, footer, and router-view so every component will render in this router-view.

resources/js/components/layouts/Dashboard.vue

<template>
    <div>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
            <a href="https://techvblogs.com/?ref=project" target="_blank" class="navbar-brand">TechvBlogs</a>
            <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">
                    <li class="nav-item active">
                        <router-link :to="{name:'dashboard'}" class="nav-link">Home <span class="sr-only">(current)</span></router-link>
                    </li>
                </ul>
                <div class="ml-auto">
                    <ul class="navbar-nav">
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                {{ user.name }}
                            </a>
                            <div class="dropdown-menu dropdown-menu-lg-right" aria-labelledby="navbarDropdownMenuLink">
                                <a class="dropdown-item" href="javascript:void(0)" @click="logout">Logout</a>
                            </div>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
        <main class="mt-3">
            <router-view></router-view>
        </main>
    </div>
</template>

<script>
import {mapActions} from 'vuex'
export default {
    name:"dashboard-layout",
    data(){
        return {
            user:this.$store.state.auth.user
        }
    },
    methods:{
        ...mapActions({
            signOut:"auth/logout"
        }),
        async logout(){
            await axios.post('/logout').then(({data})=>{
                this.signOut()
                this.$router.push({name:"login"})
            })
        }
    }
}
</script>

resources/js/components/Dashboard.vue

<template>
    <div class="container">
        <div class="row">
            <div class="col-12">
                <div class="card shadow-sm">
                    <div class="card-header">
                        <h3>Dashboard</h3>
                    </div>
                    <div class="card-body">
                        <p class="mb-0">You are logged in as <b>{{user.email}}</b></p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    name:"dashboard",
    data(){
        return {
            user:this.$store.state.auth.user
        }
    }
}
</script>

Now add this page component to the router.

Create a new file resources/js/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '../store'

Vue.use(VueRouter)

/* Guest Component */
const Login = () => import('../components/Login.vue' /* webpackChunkName: "resource/js/components/login" */)
const Register = () => import('../components/Register.vue' /* webpackChunkName: "resource/js/components/register" */)
/* Guest Component */

/* Layouts */
const DahboardLayout = () => import('../components/Layouts/Dashboard.vue' /* webpackChunkName: "resource/js/components/layouts/dashboard" */)
/* Layouts */

/* Authenticated Component */
const Dashboard = () => import('../components/Dashboard.vue' /* webpackChunkName: "resource/js/components/dashboard" */)
/* Authenticated Component */


const Routes = [
    {
        name:"login",
        path:"/login",
        component:Login,
        meta:{
            middleware:"guest",
            title:`Login`
        }
    },
    {
        name:"register",
        path:"/register",
        component:Register,
        meta:{
            middleware:"guest",
            title:`Register`
        }
    },
    {
        path:"/",
        component:DahboardLayout,
        meta:{
            middleware:"auth"
        },
        children:[
            {
                name:"dashboard",
                path: '/',
                component: Dashboard,
                meta:{
                    title:`Dashboard`
                }
            }
        ]
    }
]

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

router.beforeEach((to, from, next) => {
    document.title = `${to.meta.title} - ${process.env.MIX_APP_NAME}`
    if(to.meta.middleware=="guest"){
        if(store.state.auth.authenticated){
            next({name:"dashboard"})
        }
        next()
    }else{
        if(store.state.auth.authenticated){
            next()
        }else{
            next({name:"login"})
        }
    }
})

export default router

Here we used lazy loading components. Vue JS handles loading components lazily with routes, so on the DOM, you can load components only when they are needed through routes.

Add router into resources/js/app.js

/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

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

import router from './router'
import store from './store'

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

Before we make these requests, we'll need to set a base URL for our API (notice these are not included in the requests we have right now) and also enable the  withCredentials option.

Open resources/js/bootstrap.jsand add the following code into that file:

window.axios.defaults.withCredentials = true

The  withCredentials an option is really important here. This Axios instructs to automatically send our authentication cookie along with every request.

Now, it's time to run our project.

php artisan serve
npm run dev

Open localhost:<PORT NUMBER> in the browser.

It'd be a good idea to follow along with the simple demo app that can be found in this GitHub repo.

Read Also:  API Authentication using Laravel Sanctum

If you have any queries or doubts about this topic please feel free to contact us. We will try to reach you.

Comments (2)

Usama Rauf Usama Rauf 1 year ago

Where is the blade side , I am unable to show the component on the browser

Comment


Note: All Input Fields are required.