Authentication (logging in!) is a crucial part of many websites. Let’s look at how to go about it on a site using Vue, in the same way it can be done with any custom back end. Vue can’t actually do authentication all by itself, —we’ll need another service for that, so we’ll be using another service (Firebase) for that, but then integrating the whole experience in Vue.
Authentication works quite differently on Single Page Applications (SPAs) than it works on sites that reload every page. You don’t have to make an SPA with Vue, but we will in this tutorial.
Here’s the plan. We’ll build a UI for users to log in and the submitted data will be sent to a server to check if the user exists. If yes, we’ll be sent a token. That’s very useful, because it’s going to be used throughout our site tocheck if the user is still signed in. If no, the user can always sign up. In other words, it can be used in lots of conditional contexts. Beyond that, if we need any information from the server that requires been logged in, the token is sent to the server through the URL so that information can be only sent to logged in users.
The complete demo of this tutorial is posted on GitHub for those that who are comfortable reading through the code. The rest of us can follow through with the article. The starter file is also on GitHub so you can follow through as we code together.
After downloading the repo, you’ll run npm install
in your terminal. If you’re going to build this application completely on your own, you’ll have to install Vuex, Vue Router, and axios. We’ll also use Firebase for this project, so take a moment to set up a free account and create a new project in there.
After adding the project to Firebase, go to the authentication section, and set up a sign in method where we would be using the traditional email/password provider, that’ll be stored on our Firebase servers.
After that we’ll then go to the Firebase Auth REST API documentation to get our sign up and sign in API endpoints. We’ll need an API key to use those endpoints in our app and it can be found in the Firebase project settings.
Firebase offers authentication over the SDK, but we’re using the Auth API to demonstrate authentication over any custom back end server.
In our stater file, we have the sign up form below. We’re keeping things pretty simple here since we’re focusing on learning the concepts.
<template>
<div id="signup">
<div class="signup-form">
<form @submit.prevent="onSubmit">
<div class="input">
<label for="email">Mail</label>
<input
type="email"
id="email"
v-model="email">
</div>
<div class="input">
<label for="name">Your Name</label>
<input
type="text"
id="name"
v-model.number="name">
</div>
<div class="input">
<label for="password">Password</label>
<input
type="password"
id="password"
v-model="password">
</div>
<div class="submit">
<button type="submit">Submit</button>
</div>
</form>
</div>
</div>
</template>
If we weren’t working with an SPA, we would naturally use axios to send our data inside the script tag like this:
axios.post('https://identitytoolkit.googleapis.com/v1/account
s:signUp?key=[API_KEY]', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
})
.catch(error => console.log(error))
}
}
Sign up and log in
Working with an SPA (using Vue in this case) is very different from the above approach. Instead, we’ll be sending our authorization requests using Vuex in our actions in the store.js
file. We’re doing it this way because we want the entire app to be aware of any change to the user’s authentication status.
actions: {
signup ({commit}, authData) {
axios.post('https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
router.push("/dashboard")
})
.catch(error => console.log(error))
},
login ({commit}, authData) {
axios.post(https://identitytoolkit.googleapis.com/v1/accounts:signIn?key=[API_KEY]', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
router.push("/dashboard")
})
.catch(error => console.log(error))
}
}
We can use pretty much the same thing for the sign in method, but using the sign in API endpoint instead. We then dispatch both the sign up and log in from the components, to their respective actions in the store.
methods : {
onSubmit () {
const formData = {
email : this.email,
name : this.name,
password : this.password
}
this.$store.dispatch('signup', formData)
}
}
}
formData
contains the user’s data.
methods : {
onSubmit () {
const formData = {
email : this.email,
password : this.password
}
this.$store.dispatch('login', {email: formData.email, password: formData.password})
}
}
We’re taking the authentication data (i.e. the token and the user’s ID) that was received from the sign up/log in form, and using them as state with Vuex. It’ll initially result as null
.
state: {
idToken: null,
userId: null,
user: null
}
We now create a new method called authUser
in the mutations that’ll store the data that’s collected from the response. We need to import the router into the store as we’ll need that later.
import router from '/router'
mutations : {
authUser (state, userData) {
state.idToken = userData.token
state.userId = userData.userId
}
}
Inside the .then
block in the signup/login methods in our actions, we’ll commit our response to the authUser
mutation just created and save to local storage.
actions: {
signup ({commit}, authData) {
axios.post('https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]'), {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
})
localStorage.setItem('token', res.data.idToken)
localStorage.setItem('userId', res.data.localId)
router.push("/dashboard")
})
.catch(error => console.log(error))
},
login ({commit}, authData) {
axios.post('https://identitytoolkit.googleapis.com/v1/accounts:signIn?key=[API_KEY]'), {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
})
localStorage.setItem('token', res.data.idToken)
localStorage.setItem('userId', res.data.localId)
router.push("/dashboard")
})
.catch(error => console.log(error))
}
}
Setting up an Auth guard
Now that we have our token stored within the application, we’re going touse this token while setting up our Auth guard. What’s an Auth guard? It protects the dashboard from unauthenticated users access it without tokens.
First, we’ll go into our route file and import the store. The store is imported because of the token that’ll determine the logged in state of the user.
import store from './store.js'
Then within our routes array, go to the dashboard path and add the method beforeEnter
which takes three parameters: to
, from
and next
. Within this method, we’re simply saying that if the tokens are stored (which is automatically done if authenticated), then next
, meaning it continues with the designated route. Otherwise, we’re leading the unauthenticated user back to the sign up page.
{
path: '/dashboard',
component: DashboardPage,
beforeEnter (to, from, next) {
if (store.state.idToken) {
next()
}
else {
next('/signin')
}
}
}
Creating the UI state
At this point, we can still see the dashboard in the navigation whether we’re logged in or not, and that’s not what we want. We have to add another method under the getters called ifAuthenticated
which checks if the token within our state is null
, then update the navigation items accordingly.
getters: {
user (state) {
return state.user
},
ifAuthenticated (state) {
return state.idToken !== null
}
}
Next, let’s open up the header component and create a method called auth
inside the computed
property. That will dispatch to the ifAuthenticated
getters we just created in the store. ifAuthenticated
will return false
if there’s no token, which automatically means auth
would also be null
, and vice versa. After that, we add a v-if
to check if auth
is null
or not, determining whether the dashboard option would show in the navigation.
<template>
<header id="header">
<div class="logo">
<router-link to="/">Vue Authenticate</router-link>
</div>
<nav>
<ul>
<li v-if='auth'>
<router-link to="/dashboard">Dashboard</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signup">Register</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signin">Log In</router-link>
</li>
</ul>
</nav>
</header>
</template>
<script>
export default {
computed: {
auth () {
return this.$store.getters.ifAuthenticated
}
},
}
</script>
Logging out
What’s an application without a logout button? Let’s create a new mutation called clearAuth
, which sets both the token and userId
to null
.
mutations: {
authUser (state, userData) {
state.idToken = userData.token
state.userId = userData.userId
},
clearAuth (state) {
state.idToken = null
state.userId = null
}
}
Then, in our logout
action , we commit to clearAuth
, delete local storage and add router.replace('/')
to properly redirect the user following logout.
Back to the header component. We have an onLogout
method that dispatches our logout
action in the store. We then add a @click
to the button which calls the to the onLogout
method as we can see below:
<template>
<header id="header">
<div class="logo">
<router-link to="/">Vue Authenticate</router-link>
</div>
<nav>
<ul>
<li v-if='auth'>
<router-link to="/dashboard">Dashboard</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signup">Register</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signin">Log In</router-link>
</li>
<li v-if='auth'>
<ul @click="onLogout">Log Out</ul>
</li>
</ul>
</nav>
</header>
</template>
<script>
export default {
computed: {
auth () {
return this.$store.getters.ifAuthenticated
}
},
methods: {
onLogout() {
this.$store.dispatch('logout')
}
}
}
</script>
Auto login? Sure!
We’re almost done with our app. We can sign up, log in, and log out with all the UI changes we just made. But, when we refresh our app, we lose the data and are signed out, having to start all over again because we stored our token and Id in Vuex, which is JavaScript. This means everything in the app gets reloaded in the browser when refreshed.
What we’ll do is to retrieve the token within our local storage. By doing that, we can have the user’s token in the browser regardless of when we refresh the window, and even auto-login the user as long as the token is still valid.
Create a new actions method called AutoLogin
, where we’ll get the token and userId
from the local storage, only if the user has one. Then we commit our data to the authUser
method in the mutations.
actions : {
AutoLogin ({commit}) {
const token = localStorage.getItem('token')
if (!token) {
return
}
const userId = localStorage.getItem('userId')
const token = localStorage.getItem('token')
commit('authUser', {
idToken: token,
userId: userId
})
}
}
We then go to our App.vue
and make a created
method where we’ll dispatch the autoLogin
from our store when the app is loaded.
created () {
this.$store.dispatch('AutoLogin')
}
Yay! With that, we’ve successfully implemented authentication within our app and can now deploy using npm run build
. Check out the live demo to see it in action.
The example site is purely for demonstration purposes. Please do not share real data, like your real email and password, while testing the demo app.