VueJS Auth Using Laravel Airlock

Laravel Airlock provides a featherweight authentication system for SPAs (single page applications), mobile applications, and simple, token-based APIs. Airlock allows each user of your application to generate multiple API tokens for their account. These tokens may be granted abilities/scopes which specify which actions the tokens are allowed to perform.

Airlock exists to offer a simple way to authenticate single-page applications (SPAs) that need to communicate with a Laravel powered API. These SPAs might exist in the same repository as your Laravel application or might be an entirely separate repository, such as a SPA created using Vue CLI. Laravel Documentation

Before we begin, Let me state that Laravel Airlock works for laravel 6.x and above. I consider it a perfect fit for the issues that currently exist with security for SPAs namely: Authentication and Session Tracking, Cross Site Scripting (XSS) Attacks and Cross Site Request Forgery (CSRF).

As a prerequisite to understanding this tutorial, you should have:

  • Knowledge of PHP.
  • Very basic knowledge of the Laravel framework.
  • Knowledge of JavaScript and the Vue framework.
  • Basic knowledge of Node.js **and NPM.

Install Laravel

Let’s begin by creating a fresh Laravel project via composer

`composer create-project --prefer-dist laravel/laravel laravel-airlock`

After installation cd laravel-airlock

Create a database and edit the .env DB config with details of the newly created database.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=airlock
DB_USERNAME=root
DB_PASSWORD=

Run Migration

This will create our database tables, also Airlock will create one database table in which to store API tokens:

php artisan migrate

For those running MariaDB or older versions of MySQL you may hit this error when trying to run migrations:

[Illuminate\Database\QueryException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table users add unique users_email_unique(email))
[PDOException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

As outlined in the Migrations guide to fix this all you have to do is edit your AppServiceProvider.php file and inside the boot method set a default string length:

use Illuminate\Support\Facades\Schema;
public function boot()
{
Schema::defaultStringLength(191);
}

Step 2

Install Laravel Airlock

We can install Laravel Airlock via composer, so on the terminal, we run

`composer require laravel/airlock`

Step 3

Publish Airlock config

Next, we publish the Airlock configuration and migration files using the vendor:publish Artisan command. The airlock configuration file will be placed in our config directory, Run:

php artisan vendor:publish — provider=”Laravel\Airlock\AirlockServiceProvider”

In this article, we aim to authenticate our SPA (Single Page Application) in this case a VueJS frontend. Hence, we don’t need to use API tokens to authenticate our routes. Laravel Airlock comes with a built-in SPA Authentication. We’ll leverage that on the next step.

Step 4

SPA Authentication

For this feature, Airlock does not use tokens of any kind. Instead, Airlock uses Laravel’s built-in cookie-based session authentication services. This provides the benefits of CSRF protection, session authentication, as well as protects against leakage of the authentication credentials via XSS. Airlock will only attempt to authenticate using cookies when the incoming request originates from our own SPA frontend.

We add Airlock’s middleware to our api middleware group withinapp/Http/Kernel.php file:

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

This middleware is responsible for ensuring that incoming requests from our SPA can authenticate using Laravel’s session cookies, while still allowing requests from third parties or mobile applications to authenticate using API tokens.

Step 5

Configuration

Remember our published airlock config in Step 3? The airlock config file consists of a stateful configuration option, this setting determines which domains will maintain “stateful” authentication using Laravel session cookies when making requests to our API.

‘stateful’ => explode(‘,’, env(‘AIRLOCK_STATEFUL_DOMAINS’, ‘localhost’)),

By default, localhost is set.

Step 6

Scaffold Laravel’s Authentication

Airlock uses Laravel auth methods to authenticate SPAs and as from Laravel 6.0, this can be done through laravel/ui package. We install this package via compser

composer require laravel/ui:2.0

php artisan ui vue –auth command will create all of the views we need for authentication and place them in the resources/views/auth directory.

To authenticate our SPA, our SPA’s login page should first make a request to the /airlock/csrf-cookie route to initialize CSRF protection for the application:

axios.get(‘/airlock/csrf-cookie’).then(response => {
// Login…
});

Once CSRF protection has been initialized, we should make a POST request to the typical Laravel /login route. This /login route is provided by the laravel/ui authentication scaffolding package.

If the login request is successful, we will be authenticated and subsequent requests to our API routes will automatically be authenticated via the session cookie that the Laravel backend issued to our client.

Step 7

Build APIs

So far, we only have User model, we are going to add the login, register and logout endpoints and we will create a simple Task model, migration and TaskController.

Run the command

php artisan make:controller UserController

And in the UserController, we add the register, login and logout methods like so:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class UserController extends Controller
{
    public function register(Request $request)
    {
        $this->validator($request->all())->validate();

        $user = $this->create($request->all());

        $this->guard()->login($user);

        return response()->json(['user'=> $user,
                                  'message'=> 'registration successful'
                                ], 200);
    }
      /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:4', 'confirmed'],
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return \App\User
     */
    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
    protected function guard()
    {
        return Auth::guard();
    }

    public function login(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            // Authentication passed...
            return response()->json(['message' => 'Login successful'], 200);
        }
    }

    public function logout()
    {
        Auth::logout();
        return response()->json(['message' => 'Logged Out'], 200);
    }
}

UserController.php

Note that the login method authenticates the user using the standard, session-based authentication services that Laravel provides.

Run the command

php artisan make:model Task -mc

This will create three files: Task.php, TaskController.php and 2020_02_28_054834_create_tasks_table.php

Next, we update the create_task_table migration file and add a task field to the table

public function up()
{
   Schema::create(‘tasks’, function (Blueprint $table) {
     $table->increments(‘id’);
     $table->string(‘task’);
     $table->timestamps();
  });
}

Now, in the Task.php we add task to fillable property

class Task extends Model
{
protected $fillable = [‘task’];
}

Run the command php artisan migrate to create the tasks table.

In the TaskController.php file, we create to functions

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Task;
class TaskController extends Controller
{
public function addTask(Request $request)
  {
      $task = Task::create([
      ‘task’ => $request->task
    ]);
    return response()->json([‘message’ => ‘task added!’], 200);
  }
public function getTask()
  {
    return response()->json([‘tasks’ => Task::all()], 200);
  }
}

The create function simple stores a new task to the database, while the getTask() function returns all created tasks.

Our aim is to retrieve this data through our API protected with Laravel Airlock, hence we are not going to add more functions. You could do more on your projects.

Next, in the routes/api.php file, we add the following endpoints

Route::post(‘/login’, ‘UserController@login’);
Route::post(‘/register’, ‘UserController@register’);
Route::get(‘/logout’, ‘UserController@logout’);
//Task Routes
Route::post(‘/add-task’, ‘TaskController@addTask’)->middleware(‘auth:airlock’);
Route::get(‘/get-task’, ‘TaskController@getTask’)->middleware(‘auth:airlock’);

Protecting Routes

To protect routes so that all incoming requests must be authenticated, we protected task routes with airlock middleware. This middleware is provided by the Laravel Airlock package. Now no unauthenticated user can consume these endpoints. This guard will ensure that incoming requests are authenticated as either a stateful authenticated requests from our SPA or contain a valid API token header if the request is from a third party.

Step 8

Create VueJs Application (SPA)

Next, we write our VueJs application

Laravel comes pre-packaged with Vue, this means we don’t have to use Vue-CLI for creating the Vue Project.

So we run npm install Or yarn install depending on your preferred package manager to get our project dependencies for Vuejs.

Also, run npm install vue vue-router jquery popper.js this adds Vue Router, Jquery, and Popper.js package to our dependencies.

Next, let’s edit the webpack.mix.js file so it compiles our assets. Just after the first line, add this:

mix.webpackConfig({
   resolve:{
     extensions:[‘.js’,’.vue’],
       alias:{
         ‘@’:__dirname + ‘/resources’
      }
   }
});

In resourses/js folder, we create routes.js file

import Vue from 'vue';
import VueRouter from 'vue-router';
import Login from '@/js/pages/Login'
import Register from '@/js/pages/Register'
import Dashboard from '@/js/pages/Dashboard'
import Home from '@/js/pages/Home';

Vue.use(VueRouter);

let router = new VueRouter({
    mode: "history",
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home,
        },
        {
            path: '/login',
            name: 'login',
            component: Login,
        },
        {
            path: '/register',
            name: 'register',
            component: Register,
        },
        {
            path: '/dashboard',
            name: 'dashboard',
            component: Dashboard,
        },
    ]
});

export default router;

routes.js

In resources/js/app.js file, we import components like so:

import './bootstrap';
import Vue from 'vue';
import Routes from '@/js/routes.js';
import App from '@/js/views/App';




/**
 * Next, we will create a fresh Vue application instance and attach it to
 * the page. Then, you may begin adding components to this application
 * or customize the JavaScript scaffolding to fit your unique needs.
 */

const app = new Vue({
    el: '#app',
    router: Routes,
    render: h => h(App),
});
export default app;

app.js

In the resources/views/welcome.blade.php file, we use the Auth::check method of Laravel to get user properties for the Authenticated user and also toggle the isLoggedin state. With this, we can make some changes to our layout if the user is authenticated or not. We pass this data to our application by creating the window.Laravel object like so:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{csrf_token()}}">
    <title>Laravel Airlock</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <link href=" {{ mix('css/app.css') }}" rel="stylesheet">
</head>
<body>
    @if (Auth::check())
        <script>
           window.Laravel = {!!json_encode([
               'isLoggedin' => true,
               'user' => Auth::user()
           ])!!}
        </script>
    @else
        <script>
            window.Laravel = {!!json_encode([
                'isLoggedin' => false
            ])!!}
        </script>
    @endif
    <div id="app"></div>
    <script src="{{ mix('js/app.js') }}"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
</body>
</html>

welcome.blade.php

We have also imported bootstrap CDN in the welcome.blade.php file above.

Next, inside the resources/js folder, we create pages folder and also create the views folder

In the pages folder, we create the following vue files

  • Dashboard.vue
  • Home.vue
  • Login.vue
  • Register.vue

In the views folder, we create App.vue file.

Dashboard.vue

    <template>
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-md-6">
                    <div class="card card-default">
                        <div class="card-header">Add Task</div>
                        <div class="card-body">
                            <input v-model="task" class="form-control" v-on:keyup.enter="submit">
                        </div>
                    </div>
                </div>
                <div class="col-md-6">
                    <div class="card">
                        <div class="card-header">All Tasks</div>
                        <div class="card-body">
                            <li style="list-style: none" v-for="(task, id) in tasks" :key="id">{{task.task}}</li>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </template>
    <script>
        export default {
            data (){
                return {
                    task: null,
                    tasks: null
                }
            },
            methods:{
                //we submit the task to the add-task api then clear the input field
                submit(){
                    axios.post('api/add-task', {
                        task: this.task,
                    })
                    .then(response => {
                        this.task = null
                    })
                    .catch(function (error) {
                    });
                },
                getTask(){
                    axios.get('api/get-task').then(response=>{
                        this.tasks = response.data.tasks
                    })
                }
            },
            watch: {
            // whenever task changes, this function will run
                task: function (newTask, oldTask) {
                this.getTask()
                }
            },
            created() {
                    this.getTask()
            },
            //before the route is mounted we check if the user is logged in
             beforeRouteEnter (to, from, next) {
               if (!window.Laravel.isLoggedin) {
                     return next('/');
                 }
                next();
            }
        }
    </script>

Dashboard.vue

Home.vue

 <template>
        <div class="flex-center position-ref full-height">
            <div class="content">
                <div  class="m-b-md">
                    <h2 class="title m-b-md">
                        Laravel Airlock
                    </h2>
                    <h3>
                        Your safety is our joy
                    </h3>
                </div>
            </div>
        </div>
    </template>
 <style scoped>
    .full-height {
        height: 100vh;
    }
    .flex-center {
        align-items: center;
        display: flex;
        justify-content: center;
    }
    .position-ref {
        position: relative;
    }
    .top-right {
        position: absolute;
        right: 10px;
        top: 18px;
    }
    .content {
        text-align: center;
    }
    .title {
        font-size: 60px;
    }
    .links > a {
        color: #636b6f;
        padding: 0 25px;
        font-size: 12px;
        font-weight: 600;
        letter-spacing: .1rem;
        text-decoration: none;
        text-transform: uppercase;
    }
    .m-b-md {
        margin-bottom: 30px;
        color: #000000;
    }
    </style>
     <script>
        export default {}
    </script>

Home.vue

Login.vue

<template>
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-md-8">
                    <div class="card card-default">
                        <div class="card-header">Login</div>

                        <div class="card-body">
                            <form method="POST" action="/login">
                                <div class="form-group row">
                                    <label for="email" class="col-sm-4 col-form-label text-md-right">E-Mail Address</label>

                                    <div class="col-md-6">
                                        <input id="email" type="email" class="form-control" v-model="email" required autofocus>
                                    </div>
                                </div>

                                <div class="form-group row">
                                    <label for="password" class="col-md-4 col-form-label text-md-right">Password</label>

                                    <div class="col-md-6">
                                        <input id="password" type="password" class="form-control" v-model="password" required>
                                    </div>
                                </div>

                                <div class="form-group row mb-0">
                                    <div class="col-md-8 offset-md-4">
                                        <button type="submit" class="btn btn-primary" @click="handleSubmit">
                                            Login
                                        </button>
                                    </div>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </template>

    <script>
        export default {
            data(){
                return {
                    email : "",
                    password : ""
                }
            },
            methods : {
                handleSubmit(e){
                    e.preventDefault()
                    if (this.password.length > 0) {
                        axios.get('/airlock/csrf-cookie').then(response => {
                           axios.post('api/login', {
                            email: this.email,
                            password: this.password
                          })
                          .then(response => {
                            this.$router.go('/dashboard')
                          })
                          .catch(function (error) {
                            console.error(error);
                          });
                        })
                    }
                }
            },
            beforeRouteEnter (to, from, next) {
               if (window.Laravel.isLoggedin) {
                     return next('dashboard');
                 }
                next();
            }
        }
    </script>

Login.vue

In the script section, we make an initial request to /airlock/csrf-cookie route to initialize CSRF protection for the application before login, this request to airlock/csrf-cookie return no data at all:

axios.get(‘/airlock/csrf-cookie’).then(response => {
   axios.post(‘api/login’, {
     email: this.email,
     password: this.password
})
   .then(response => {
     
     this.$router.go(‘/dashboard’)
})
    .catch(function (error) {
console.error(error);
   });
})

All other requests to our APIs are now authenticated.

Register.vue

 <template>
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-md-8">
                    <div class="card card-default">
                        <div class="card-header">Register</div>

                        <div class="card-body">
                            <form method="POST">
                                <div class="form-group row">
                                    <label for="name" class="col-md-4 col-form-label text-md-right">Name</label>

                                    <div class="col-md-6">
                                        <input id="name" type="text" class="form-control" v-model="name" required autofocus>
                                    </div>
                                </div>

                                <div class="form-group row">
                                    <label for="email" class="col-md-4 col-form-label text-md-right">E-Mail Address</label>

                                    <div class="col-md-6">
                                        <input id="email" type="email" class="form-control" v-model="email" required>
                                    </div>
                                </div>

                                <div class="form-group row">
                                    <label for="password" class="col-md-4 col-form-label text-md-right">Password</label>

                                    <div class="col-md-6">
                                        <input id="password" type="password" class="form-control" v-model="password" required>
                                    </div>
                                </div>

                                <div class="form-group row">
                                    <label for="password-confirm" class="col-md-4 col-form-label text-md-right">Confirm Password</label>

                                    <div class="col-md-6">
                                        <input id="password-confirm" type="password" class="form-control" v-model="password_confirmation" required>
                                    </div>
                                </div>

                                <div class="form-group row mb-0">
                                    <div class="col-md-6 offset-md-4">
                                        <button type="submit" class="btn btn-primary" @click="handleSubmit">
                                            Register
                                        </button>
                                    </div>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </template>

    <script>
        export default {
            data(){
                return {
                    name : "",
                    email : "",
                    password : "",
                    password_confirmation : ""
                }
            },
            methods : {
                handleSubmit(e) {
                    e.preventDefault()
                    if (this.password === this.password_confirmation && this.password.length > 0)
                    {
                        axios.post('api/register', {
                            name: this.name,
                            email: this.email,
                            password: this.password,
                            password_confirmation : this.password_confirmation
                          })
                          .then(response => {
                              //Initialize CSRF protection for the application
                              axios.get('/airlock/csrf-cookie').then(response => {
                                   this.$router.go('/dashboard')
                                });
                          })
                          .catch(error => {
                            console.error(error);
                          });
                    } else {
                        this.password = ""
                        this.passwordConfirm = ""
                        return alert('Passwords do not match')
                    }
                }
            },
             beforeRouteEnter (to, from, next) {
                 if (window.Laravel.isLoggedin) {
                     return next('dashboard');
                 }
                 next();
             }
        }
    </script>

Register.vue

In the script section we authenticate our API after successful registration like so:

axios.post(‘api/register’, {
   name: this.name,
   email: this.email,
   password: this.password,
   password_confirmation : this.password_confirmation
})
.then(response => {
//Initialize CSRF protection for the application
   axios.get(‘/airlock/csrf-cookie’).then(response => {
   this.$router.go(‘/dashboard’)
});
})
.catch(error => {
   console.error(error);
});

App.vue

<template>
 <div>
    <nav class="navbar navbar-expand-md navbar-light navbar-laravel">
        <div class="container">
            <router-link :to="{name: 'home'}" class="navbar-brand">AirLock</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">
                <!-- Left Side Of Navbar -->
                <ul class="navbar-nav mr-auto"></ul>
                <!-- Right Side Of Navbar -->
                <ul class="navbar-nav ml-auto">
                <!-- Authentication Links -->
                    <router-link :to="{ name: 'login' }" class="nav-link" v-if="!isLoggedIn">Login</router-link>
                    <router-link :to="{ name: 'register' }" class="nav-link" v-if="!isLoggedIn">Register</router-link>
                    <li class="nav-link" v-if="isLoggedIn"> Hi, {{name}}</li>
                    <router-link :to="{ name: 'dashboard' }" class="nav-link" v-if="isLoggedIn">Board</router-link>
                    <a class="nav-link" v-if="isLoggedIn" @click="logout"> Logout</a>
                </ul>

            </div>
        </div>
    </nav>
    <main class="py-4">
        <router-view></router-view>
    </main>
</div>
</template>
<script>
    export default {
          data(){
            return {
                isLoggedIn : true,
                name : null
            }
        },
        methods: {
            logout(){
                axios.get('api/logout')
            }
        },
        mounted(){
            //Check if the user is Authenticated
            this.isLoggedIn = window.Laravel.isLoggedin
            this.isLoggedIn ? this.name = window.Laravel.user['name'] : this.name = null
        }
    }
</script>

Here, we have our navbar components. Most importantly, we render all our vue components here through Vue Router <router-view> </ router-view>

Step 9

Start Application

At this point, one thing is left, run our application! If you are on localhost or VM, First ensure that your database machine is started.

Next, we build vue run npm run prod and start the Laravel server: run php artisan serve

#vuejs #javascript #laravel #vue-js

VueJS Auth Using Laravel Airlock
82.35 GEEK