This tutorial is out of date and no longer maintained.
Traditionally, many people use localStorage to manage tokens generated through client-side authentication. A big concern is always a better way to manage authorization tokens to allow us to store even more information on users.
This is where Vuex comes in. Vuex manages states 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.
Sounds like a better alternative to always checking localStorage? Let’s explore it.
This tutorial was originally written for vue
v2.15.16, vue-router
v3.0.1, vuex
v3.0.1, and axios
v0.18.0. The latest versions of these packages may not adhere to the methods and syntax in this tutorial.
For this project, we want to create a Vue application that has Vuex and vue-router. We will use the Vue CLI to create a new Vue project.
Run the following command to set it up:
- vue create vue-auth-vuex
Follow the dialogue that shows up and add the necessary information. Manually select features and select Router and Vuex from the options. And complete the installation.
Next, install axios:
- npm install axios --save
We will need Axios across many of our components. Let’s set it up at the entry level so we do not have to import it every time we need it.
Open the ./src/main.js
file and add the following:
// ...
import Axios from 'axios'
Vue.prototype.$http = Axios
const token = localStorage.getItem('token')
if (token) {
Vue.prototype.$http.defaults.headers.common['Authorization'] = token
}
// ...
Now, when we want to use Axios inside our component, we can do this.$http
and it will be like calling Axios directly. We also set the Authorization
on Axios header to our token, so our requests can be processed if a token is required. This way, we do not have to set token anytime we want to make a request.
When that is done, let’s set up the server to handle authentication.
I already wrote about this when explaining how to handle authentication with vue-router
. Check out the Setup Node.js Server section of this tutorial.
Create a file Login.vue
in the ./src/components
directory. Then, add the template for the login page:
<template>
<div>
<form class="login" @submit.prevent="login">
<h1>Sign in</h1>
<label>E-Mail Address</label>
<input required v-model="email" type="email" placeholder="E-Mail"/>
<label>Password</label>
<input required v-model="password" type="password" placeholder="Password"/>
<hr/>
<button type="submit">Login</button>
</form>
</div>
</template>
When you are done, add the data attributes that would bind to the HTML form:
<script>
export default {
data() {
return {
email: "",
password: ""
}
},
}
</script>
Now, let’s add the method for handling login:
<script>
export default {
// ...
methods: {
login: function() {
let email = this.email
let password = this.password
this.$store.dispatch('login', { email, password })
.then(() => this.$router.push('/'))
.catch(err => console.log(err))
}
}
}
</script>
We are using a vuex action — login
to handle this authentication. We can resolve actions into promises so we can do cool things with them inside our component.
Like the component for login, let’s make one for registering users. Start by creating a file Register.vue
in the components directory and add the following to it:
<template>
<div>
<h4>Register</h4>
<form @submit.prevent="register">
<label for="name">Name</label>
<div>
<input id="name" type="text" v-model="name" required autofocus>
</div>
<label for="email" >E-Mail Address</label>
<div>
<input id="email" type="email" v-model="email" required>
</div>
<label for="password">Password</label>
<div>
<input id="password" type="password" v-model="password" required>
</div>
<label for="password-confirm">Confirm Password</label>
<div>
<input id="password-confirm" type="password" v-model="password_confirmation" required>
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
</div>
</template>
Let define the data attributes we will bind to the form:
<script>
export default {
data() {
return {
name: "",
email: "",
password: "",
password_confirmation: "",
is_admin: null
}
},
}
</script>
Now, let’s add the method for handling login:
<script>
export default {
// ...
methods: {
register: function() {
let data = {
name: this.name,
email: this.email,
password: this.password,
is_admin: this.is_admin
}
this.$store.dispatch('register', data)
.then(() => this.$router.push('/'))
.catch(err => console.log(err))
}
}
}
</script>
Let’s make a simple component that would only display if our user is authenticated. Create the component file Secure.vue
and add the following to it:
<template>
<div>
<h1>This page is protected by auth</h1>
</div>
</template>
Open ./src/App.vue
file and add the following to it:
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
<span v-if="isLoggedIn"> | <a @click="logout">Logout</a></span>
</div>
<router-view/>
</div>
</template>
Can you see the Logout
link we set to only show up if a user is logged in? Great.
Now, let’s add the logic behind the logout:
<script>
export default {
computed: {
isLoggedIn: function() { return this.$store.getters.isLoggedIn }
},
methods: {
logout: function() {
this.$store.dispatch('logout')
.then(() => {
this.$router.push('/login')
})
}
},
}
</script>
We are doing two things — computing the authentication state of the user and dispatching a logout action to our Vuex store when a user clicks the logout button. After the log out, we send the user to login
page using this.$router.push('/login')
. You can change where the user gets sent to if you want.
That’s it. Let’s make the auth module using Vuex.
If you read past the Setup Node.js Server section, you would notice we had to store user auth token in localStorage and we had to retrieve both the token and user information anytime we wanted to check if the user is authenticated. This works, but it is not really elegant. We will rebuild the authentication to use Vuex.
First, let’s setup our store/index.js
file for Vuex:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
status: '',
token: localStorage.getItem('token') || '',
user: {}
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
If you noticed, we have imported vue
, vuex
, and axios
, then asked vue
to use vuex
. This is because we mean serious business here.
We have defined the attributes of the state. Now the Vuex state would hold our authentication status, jwt
token and user information.
login
ActionVuex actions are used to commit mutations to the vuex store. We will create a login
action that would authenticate a user with the server and commit user credentials to the Vuex store. Open the ./src/store/index.js
file and add the following to actions
object:
// ...
actions: {
// ...
login({commit}, user) {
return new Promise((resolve, reject) => {
commit('auth_request')
axios({ url: 'http://localhost:3000/login', data: user, method: 'POST' })
.then(resp => {
const token = resp.data.token
const user = resp.data.user
localStorage.setItem('token', token)
axios.defaults.headers.common['Authorization'] = token
commit('auth_success', { token: token, user: user })
resolve(resp)
})
.catch(err => {
commit('auth_error')
localStorage.removeItem('token')
reject(err)
})
})
},
}
The login action passes Vuex commit
helper that we will use to trigger mutations. Mutations make changes to vuex store.
We are making a call to the server’s login route and returning the necessary data. We store the token on localStorage, then pass the token and user information to auth_success
to update the store’s attributes. We also set the header for axios
at this point as well.
Note: We could store the token in vuex store, but if the user leaves our application, all of the data in the Vuex store disappears. To ensure we allow the user to return to the application within the validity time of the token and not have to log in again, we have to keep the token in localStorage.
It’s important you know how these work so you can decide what exactly it is you want to achieve.
We return a Promise so we can return a response to a user after login is complete.
register
ActionLike the login
action, the register
action will work almost the same way. In the same file, add the following in the actions
object:
// ...
actions: {
// ...
register({commit}, user) {
return new Promise((resolve, reject) => {
commit('auth_request')
axios({ url: 'http://localhost:3000/register', data: user, method: 'POST' })
.then(resp => {
const token = resp.data.token
const user = resp.data.user
localStorage.setItem('token', token)
axios.defaults.headers.common['Authorization'] = token
commit('auth_success', { token: token, user: user })
resolve(resp)
})
.catch(err => {
commit('auth_error', err)
localStorage.removeItem('token')
reject(err)
})
})
},
}
This works similarly to login
action, calling the same mutators as our login
and register
actions have the same simple goal — get a user into the system.
logout
ActionWe want the user to have the ability to log out of the system, and we want to destroy all data created during the last authenticated session. In the same actions
object, add the following:
// ...
actions: {
// ...
logout({commit}) {
return new Promise((resolve, reject) => {
commit('logout')
localStorage.removeItem('token')
delete axios.defaults.headers.common['Authorization']
resolve()
})
}
}
Now, when the user clicks to log out, we will remove the jwt
token we stored along with the axios
header we set. There is no way they can perform a transaction requiring a token now.
Like I mentioned earlier, mutators are used to change the state of a Vuex store. Let’s define the mutators we had used throughout our application. In the mutators object, add the following:
// ...
mutations: {
auth_request(state) {
state.status = 'loading'
},
auth_success(state, payload) {
state.status = 'success'
state.token = payload.token
state.user = payload.user
},
auth_error(state) {
state.status = 'error'
},
logout(state) {
state.status = ''
state.token = ''
},
},
We use getter to get the value of the attributes of Vuex state. The role of our getter in the situation is to separate application data from application logic and ensure we do not give away sensitive information.
Add the following to the getters
object:
// ...
getters: {
isLoggedIn: state => !!state.token,
authStatus: state => state.status,
}
You would agree with me that this is a neater way to access data in the store.
The whole purpose of this article is to implement authentication and keep certain pages away from a user who is not authentication. To achieve this, we need to know the page the user wants to visit and equally have a way to check if the user is authenticated. We also need a way to say if the page is reserved for only authenticated user or unauthenticated user alone or both. These things are important considerations which, luckily, we can achieve with vue-router
.
Open the ./src/router/index.js
file and import what we need for this setup:
import Vue from 'vue'
import Router from 'vue-router'
import store from '../store/index.js'
import Home from '../views/Home.vue'
import Login from '../components/Login.vue'
import Secure from '../components/Secure.vue'
import Register from '../components/Register.vue'
Vue.use(Router)
As you can see, we have imported vue
, vue-router
and our vuex
store setup. We also imported all the components we defined and set Vue to use our router.
Let’s define the routes:
// ...
let router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/login',
name: 'login',
component: Login
},
{
path: '/register',
name: 'register',
component: Register
},
{
path: '/secure',
name: 'secure',
component: Secure,
meta: {
requiresAuth: true
}
},
{
path: '/about',
name: 'about',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
})
export default router
Our route definition is simple. For routes requiring authentication, we add extra data to it to enable us identify it when the user tries to access it. This is the essence of the meta
attribute added to the route definition. If you are asking ”Can I add more data to this meta
and use it?” then I’m pleased to tell you that you are absolutely right.
We have our routes defined. Now, let’s check for unauthorized access and take action.
In the router/index.js
file, add the following before the export default router
:
// ...
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (store.getters.isLoggedIn) {
next()
return
}
next('/login')
} else {
next()
}
})
// ...
From the article on using vue-router
for authentication, you can recall we had a really complex mechanism here that grew very big and got very confusing. Vuex has helped us simplify that completely, and we can go on to add any condition to our route. In our Vuex store, we can then define actions to check these conditions and getters to return them.
Because we store our token in localStorage, it can remain there perpetually. This means that whenever we open our application, it would automatically authenticate a user even if the token has expired. What would happen at most is that our requests would keep failing because of an invalid token. This is bad for user experience.
Now, open ./src/App.vue
file and in the script, add the following to it:
export default {
// ...
created: function() {
this.$http.interceptors.response.use(undefined, function (err) {
return new Promise(function (resolve, reject) {
if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
this.$store.dispatch(logout)
}
throw err;
});
});
}
}
We are intercepting Axios call to determine if we get 401 Unauthorized
response. If we do, we dispatch the logout
action and the user gets logged out of the application. This takes them to the login
page like we designed earlier and they can log in again.
We can agree that this will greatly improve the user’s experience.
Using Vuex allows us to store and manage authentication state and proceed to check state in our application using only a few lines of code.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
With the Auth Flow proposed here one could bypass it by just creating a ‘token=dummytoken’ in local storage if the view that you are accessing does not have an http call to trigger the interceptor.
I thought about checking the key.
I would suggest that one solution is to check before going to the view
Where checkKey:
With this you check that the user is actually allowed to go to see the view and still have checks in place should the token expire while the user is browsing.
I received server error 500.
it return undefinde value of (user) , what the problem?
Final code block should perhaps check
error.response.status
instead oferror.status
Very helpful, thank you.