Authentication systems are a vital part of most modern applications, and should thus be appropriately implemented.
In this article, you will learn how to build an authentication system using Vue.js and Laravel Sanctum (former Airlock).
We are going to create separate projects for the front end, and for the back end, that will interact with one another through a REST API.
Let's dive in!
Back end (Laravel)
Step #1
For Laravel installation instructions, visit an Official Documentation page.
Create a new Laravel project by running in the terminal
laravel new my-app
or
composer create-project --prefer-dist laravel/laravel my-app
I'm using Laravel Valet, which automatically allows us to access our site at the http://my-app.test
domain.
On your machine, it will be accessible according to your local development environmental settings.
Step #2
Create a new database called my-app
and set DB_DATABASE=my-app
in .env
file in your app's directory.
Step #3
Install Laravel Sanctum.
composer require laravel/sanctum
Publish the Sanctum configuration and migration files using the vendor:publish
Artisan command. The sanctum
configuration file will be placed in your config
directory:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
Run your database migrations to create a database table in which to store API tokens:
php artisan migrate
Add the Sanctum's middleware to your api
middleware group within your app/Http/Kernel.php
../app/Http/Kernel.php
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
...
protected $middlewareGroups = [
...
'api' => [
EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
...
],
Step #4
To use tokens for users, we have to add HasApiTokens
to the User
model in app/User.php.
../app/User.php
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
}
Step #5
Let's create the seeder for the User
model. We'll need that a bit later to test the login proccess.
php artisan make:seeder UsersTableSeeder
Now let's insert
DB::table('users')->insert([
'name' => 'John Doe',
'email' => 'john@doe.com',
'password' => Hash::make('password')
]);
into the run()
function in database/seeds/UsersTableSeeder.php
To seed users
table with user, let's run:
php artisan db:seed --class=UsersTableSeeder
So now we have a new user in our database called John Doe
with email john@doe.com
and password password
.
Step #6
Let's create a /login
route in the routes/api.php file:
../routes/api.php
use App\User;
use Illuminate\Support\Facades\Hash;
Route::post('/login', function (Request $request) {
$data = $request->validate([
'email' => 'required|email',
'password' => 'required'
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response([
'message' => ['These credentials do not match our records.']
], 404);
}
$token = $user->createToken('my-app-token')->plainTextToken;
$response = [
'user' => $user,
'token' => $token
];
return response($response, 201);
});
Step #7
Let's send a POST request with email john@doe.com
and password password
as parameters to the http://my-app.test/api/login
route. You can use the Postman or Insomnia software packages to accomplish this.
If everything is working well, we will receive a JSON object as response to our request:
{
"user": {
"id": 1,
"name": "John Doe",
"email": "john@doe.com",
"email_verified_at": null,
"created_at": null,
"updated_at": null
},
"token": "AbQzDgXa..."
}
Step #8
Next, we need to change some middleware. We do this in the /routes/api.php file by replacing auth:api
with auth:sanctum
:
../routes/api.php
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Step #9
Before we continue to the front end, we have to setup cross-origin requests CORS handling.
../config/cors.php
'paths' => ['api/*', 'login', 'logout'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
../.env
SANCTUM_STATEFUL_DOMAINS=127.0.0.1
Front end (Vue.js)
We'll use Vuex for state management, Vue Router for routing and axios to make HTTP requests.
Step #1
We are going to use Vue CLI to create a new Vue project. If you are not familiar with this standard tooling for Vue.js development, please, read this guide.
In the directory we are using for our projects, let's run the following command:
vue create my-vue-app
Choose to Manually select features
and then opt for Router
and Vuex
After successfully creating the my-vue-app
project, run the following commands:
cd my-vue-app
npm run serve
Now our app should be available at the http://localhost:8080/
domain.
Step #2
Let's create a new file for a Login
view.
..src/views/Login.vue
<template>
<div>
<h1>Login</h1>
<form @submit.prevent="login">
<input type="email" name="email" v-model="email">
<input type="password" name="password" v-model="password">
<button type="submit">Login</button>
</form>
</div>
</template>
<script>
export default {
data () {
return {
email: '',
password: ''
}
},
methods: {
login () {
this.$store
.dispatch('login', {
email: this.email,
password: this.password
})
.then(() => {
this.$router.push({ name: 'About' })
})
.catch(err => {
console.log(err)
})
}
}
}
</script>
In the Vue Router
, we have to implement a route for the Login
view.
../src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
Now, if we navigate to http://localhost:8080/login
in a browser, we can see a login page.
Step #3
We have to install axios in our frontend directory to make HTTP requests:
npm install axios
Step #4
Let's implement some user authentication actions (login/logout)
in Vuex.
../src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
axios.defaults.baseURL = 'http://app-backend.test/api'
export default new Vuex.Store({
state: {
user: null
},
mutations: {
setUserData (state, userData) {
state.user = userData
localStorage.setItem('user', JSON.stringify(userData))
axios.defaults.headers.common.Authorization = `Bearer ${userData.token}`
},
clearUserData () {
localStorage.removeItem('user')
location.reload()
}
},
actions: {
login ({ commit }, credentials) {
return axios
.post('/login', credentials)
.then(({ data }) => {
commit('setUserData', data)
})
},
logout ({ commit }) {
commit('clearUserData')
}
},
getters : {
isLogged: state => !!state.user
}
})
After successfully logging in, we are going to store some user data in the user
variable and the localStorage
.
Step #5
Let's define routes for authenticated and unauthenticated pages.
We can make an About
page accessible for authenticated users only.
For this purpose let's add the meta
field to the About
route.
Let's use Vue Router's beforeEach
method to check if the user is logged in. If the user is not authenticated, we'll redirect them back to the login page.
../src/router.index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
meta: {
auth: true
},
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
const loggedIn = localStorage.getItem('user')
if (to.matched.some(record => record.meta.auth) && !loggedIn) {
next('/login')
return
}
next()
})
export default router
Step #6
What if the user refreshes a page? Should we ask him to log in again?
Of course not!
Let's add a created()
method to the Vue
instance to handle that scenario.
created () {
const userInfo = localStorage.getItem('user')
if (userInfo) {
const userData = JSON.parse(userInfo)
this.$store.commit('setUserData', userData)
}
}
Step #7
We also need to handle cases when a token is expired or if the user is unauthorized.
Let's do that in the created()
method by using interceptors.
So our updated main.js
file looks like this:
../src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
created () {
const userInfo = localStorage.getItem('user')
if (userInfo) {
const userData = JSON.parse(userInfo)
this.$store.commit('setUserData', userData)
}
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
this.$store.dispatch('logout')
}
return Promise.reject(error)
}
)
},
render: h => h(App)
}).$mount('#app')
Step #8
We haven't implemented a Logout
feature yet. Let's do that in the App.vue
file.
Also, let's show the About
and Logout
buttons only when a user is logged in.
../src/App.vue
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about" v-if="isLogged">About</router-link>
<router-link to="/login" v-else>Login</router-link>
<button type="button" @click="logout()" v-if="isLogged">
Logout
</button>
</div>
<router-view/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters([
'isLogged'
])
},
methods: {
logout () {
this.$store.dispatch('logout')
}
}
}
</script>
Ok, our tutorial is done.
I hope you found this info helpful!
Top comments (43)
Great Article! I am having an issue with CORs. Everything works great locally but when I upload my backend to api.example.com and my frontend to example.com I get this issue.
Access to XMLHttpRequest at 'api.example.com/api/banlist/fetch' from origin 'example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Have you tried to add SANCTUM_STATEFUL_DOMAINS=127.0.0.1 to .env file on the server?
Yes this is set in the .env file.
It is set but still same error. Could it be because the API and frontend are in different directories? I have tried just about everything and I am still getting the same error. I am almost to the point of moving the frontend into Laravel.
Have you set baseURL and headers in main.js?
Just tried this and same error.
Could you share your repositories? I could have a look tomorrow. My email address is roman@paprotsky.com
Cool thank you. I sent you an email.
Thank you!
Hi @derek how was this problem solved?
hi, did you manage to solve the problem?
Great post! I noticed you store the token in localstorage, is this safe? I always thought this was an insecure way of storing sensitive data seeing as the token is more or less the users username/password combo?
Hi @moris14 it's really wrong storing sensitive data in LocalStorage meanwhile i haven't seen any article on a better way to go about this, please share if you have any.
(Based on my app)
U can verify token owner by comparing ip address, browser: version, device, custom cookies, etc
With this feature, personally I can offer the user to see, what devices are they are logged in, logout from specific device / all devices, etc
If anyone got better idea, I would like to know and learn :D
Thanks for this tutorial. I had tried to do the same with React. And I had never managed to do it. VueJs is much simpler!
A little evolution please. Imagine an 'about2' route. If I try to access it without being logged in, the app sends me back to the login page. Perfect ! But then, how can I be redirected automatically to 'about2' and not to 'about'? I guess I must store the target route name in the store but after ?
thanks again for this tutorial.
Hi Dominique,
I guess it definitely make sense to store the target route name in the store and check it out in the
login()
method in the..src/views/Login.vue
view beforethis.$router.push({ name: 'About' })
This Vue Screencasts video can be useful.
Nice tutorial! The way it is written it is easy to understand. Thanks.
I also recommend to watch this ( youtube.com/watch?v=8Uwn5M6WTe0 ) video tutorial, here Laravel Sanctum is explained step by step in detail.
Hi! Nice tutorial! How to use axios in other compoents for further api requests?
That's a good question, Balázs!
You can add in
main.js
:After that in components you can write:
From child component I had to refer using this keyword
I really love this article! I love the style of writing, and the way to code is presented. Furthermore the way the code is presented is very clear and concise. This is a great template to get to know a number of modern day Laravel/Vue.js concepts - APIs, Sanctum, Vuex, Vue routing. It will be my go to reference going forward. Thank you!
Thank you for the kind words, Eugene!
Hey Roman, thanks for the amazing thread, can you please guide us about using the Sanctum with multiple guards e.g. Admin, User.
Did you find any solution related multiple guard authentication sanctum
In Laravel multiple guards only work in session based auth that's why I ended up using "Token Abilities" which works fine for me, you can read more about Token Abilities in Laravel Sanctum docs, in Passport this feature is called token scopes. I hope it will help you.
Hi, thanks for this tutorial, but you are using tokens here, what about cookies?
Maybe, this article could be useful for you.
Could you clarify your question, Antonio?
Great Article, Currently I'm doing some coding practice with the laravel socialite package using postman for api request. Do you have a written article for this kind of log in using 3rd party API like google. facebook and twitter?
Thanks for this! What should we use for SANCTUM_STATEFUL_DOMAINS=127.0.0.1 in a dockerised local dev environment?
I don't have work experience with Laravel + Docker, sorry.
That's ok - thanks for coming back to me. I'll reply to myself and answer as and when I find out.