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:
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=
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);
}
We can install Laravel Airlock via composer
, so on the terminal, we run
`composer require laravel/airlock`
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.
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.
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.
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.
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.
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
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
<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
<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.
<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);
});
<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>
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