Authentication and Protected Routes in VueJS

Thursday, January 3rd 2019

Authentication and Protected Routes in VueJS

I’ve been very much enjoying using VueJS. I started out working with SPAs back in 2012 with KnockoutJS, then moved onto AngularJS shortly after that when we started working with Mongo + Node.

Having spent a few of years working with AngularJS, I found it to be a very capable tool but often it seemed tasks that should be simple could end up needlessly convoluted. I ended up writing a lot of uncomfortably hacky code that never quite sat right. Angular 2 solved a lot of these problems, but brought with it what felt like an very heavy footed framework that seemed to take over projects when often all was wanted was some simple interaction.

Enter Vue

I love the simplicity and ease of development that Vue brings. I love that it can be dropped onto a page as a script dependency (no build step necessary) and can be used to do something as straightforward as showing and hiding a DOM element, or gathering data from a form. These are the sort of jobs that many of us used jQuery for, and one of the great things about Vue is that it is light enough to be used as a jQuery replacement for such simple tasks. Sarah Drasner wrote an excellent post on this topic that provides a really approachable introduction to using Vue if you come from a jQuery background.

For more complex projects, where needed VueJS can be a powerful tool that can be used to drive an entire SPA complete with advanced routing and state management.

A common task required for any non-trivial application such as this is to provide a way to authenticate users, store their profile information on the client and protect certain routes so that only logged in users may access them. Lets see how we can accomplish this in Vue using the official vue-router plugin.

Some Assumptions

For this example, I’ll to assume that you already have a back-end API that uses JWT (JSON Web Token) authentication and provides an endpoint for creating tokens that we can hook up to from our login form.

If you’re using a 3rd party auth provider like Auth0, they have an excellent post on integrating Vue authentication and routing with their API. I’ve adapted my JWT handling and storage code from theirs, and many of the steps are similar.

Jump Ahead

If you already have some of the functionality that we’ll cover nailed down, skip to the relevant section to you:

  1. Scaffolding a Project
  2. Creating a Login Route and Receiving a Token
  3. Storing and Using the Token
  4. Protected Routes
  5. Displaying Content based on Login State

You can find the example source code here.

Scaffolding a Project

For any non-trivial Vue application, the Vue CLI is incredibly helpful for scaffolding, building and running your code, so if you’ve not done so already, you can go get it and install globally with npm:

1
npm install -g @vue/cli

The Vue CLI will now be installed globally and can be accessed in your terminal/command window in any folder using the “vue” command.

You can of course build the following code example into an existing project, however for simplicity of example we’ll start a fresh project. To scaffold your code, head to your usual code folder and run:

1
vue create vue-auth-example

This will start the Vue CLI project wizard. You could run the “default” template however in this case jump down to “Manually Select Features”.

At the next prompt, you can choose the packages Vue CLI will install and configure in your project. We’re going for a lightweight example, but we’ll need the Router and Babel at least.

Select “Y” for Router history mode (this is a great feature and I’ll cover this in a future post).

Accept the defaults for config location, preset save and the Vue CLI will then scaffold your new project. Once this is done, to ensure everything is installed and running correctly, CD into your project and run:

1
npm run serve

You should see a page something like this:

Creating a Login Route and Receiving a Token

Notice that there are two pages, Home and About. These are both routes, defined in the vue-router, and it is these routes that we want to protect and show to authenticated users only. In order to get to this point, we’ll first need a method for users to log in and receive an authentication token.

So before we go any further, we’ll need to add another couple of packages to our project.

Axios is a great little library for performing HTTP requests, which we’ll need at least to make our call to an authentication endpoint. JWT-Decode does exactly what it says on the tin, providing a nice way of handling JSON Web Tokens without having to decode Base64 strings or deal with delimiters etc. To grab these packages, run:

1
npm install --save axios jwt-decode

When done, open the folder (in this case “vue-auth-example”) created by the Vue CLI in your preferred code editor. I’m using VS Code. A handy shortcut to open Code in the selected folder from the terminal is to run:

1
code .

Your package.json will now look something like this (if starting from scratch):

We’ll now put together a simple login page, using the default styling that comes with the Vue template.

If you’re using VS Code, I highly recommend the Vetur extension when working with Vue projects. Lots of nice little syntax helpers + code highlighting.

In your ./src/views folder, create a new file called Login.vue and add the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<div class="input-group">
<label>Username</label>
<input type="text" v-model="username">
</div>

<div class="input-group">
<label>Password</label>
<input type="password" v-model="password">
</div>

<button type="button" v-on:click="login">Login</button>
</div>
</template>

<script>
export default {
name: 'login'
}
</script>

<style>
.input-group {
margin: 1rem;
}

.input-group label {
margin-right: 0.5rem;
}
</style>

Vue components and views, such as this, are split into 3 parts: template, script and style. The template contains the HTML layout of your component, the script any code the component requires to be run, along with component lifecycle hooks and events, and the style contains CSS (or SCSS, if specified) for the component.

The template contains some custom attributes, v-model on the inputs and v-on:click on the button. This allows us to bind the value of the inputs to properties on the Vue component, and the click event handler of the button to the login() method. We’ll add these properties and method shortly.

At build time, the Vue CLI uses Webpack to bundle these parts into minified Javascript and CSS files.

In more recent CLI versions (~v3 and above) you won’t see any webpack.config files in your folder structure by default, as these are abstracted away by the vue-cli-service. This makes for a lot cleaner project out-of-the-box. If you want to customise the Webpack process (translation: if you enjoy pain and frustration) then you can still of course provide custom config to the CLI.

This is really all that is needed for a basic component (you don’t even really need the style section, only added here so it doesn’t look too basic).

We’ll add interactivity in a moment, but before we do that we need to tell the Vue router where to find our new view by adding a /login route.

To do this, open up the ./src/router.js file created by the CLI scaffolder, and add the following to the import list near the top of the file:

1
import Login from './views/Login.vue'

Then to the Router constructor, under the routes array, add:

1
2
3
4
5
{
path: '/login',
name: 'login',
component: Login
}

This simple route entry consists of 3 sections:

  • path - The path, relative to the hostname, that we’ll use to navigate to the route. On our local dev server, this will look something like localhost:8080/login
  • name - This is a unique name by which we can identify the route if navigating programatically or using router-link to build hyperlinks
  • component - This is a reference to the component we have just created and imported.

We’ll come back to the router file later and add some extra metadata that will allow us to specify protected and anonymous routes.

Once the router.js file has been updated, if you haven’t already, run npm run serve in the terminal to build and your project.

npm run serve starts up a Webpack server that watches your project and fast-builds when any changes are detected. To save time you can keep it running in the background while making code changes.

If you manually navigate to /login, you’ll see something like this:

Great. But it doesn’t do much. We need to hook the “login” button up to the token endpoint on our API. For our example, we have a login/create token endpoint running on localhost:3000/api/v1/auth/token. We’ll use Axios to make a request to this endpoint and get back a token.

Head back to your Login.vue file and import Axios into your component. Add the following just under the first <script> tag:

1
import axios from 'axios'

Then update the <script></script> section to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import axios from 'axios'

export default {
name: 'login',
data() {
return {
username: '',
password: ''
}
},
methods: {
async login() {
try {
let res = await axios({
url: 'http://localhost:3000/api/v1/auth/token',
method: 'POST',
data: {
username: this.username,
password: this.password,
grant_type: 'password'
}
})

alert(`Token received: ${res.data.token}`)
}
catch (err) {
alert(`Error: ${err}`)
}
}
}
}

Here, we’ve added a couple of properties, username and password to our component. These are available within the component code, but also within the template. We’ve already bound their values to our form inputs using v-model.

We’ve also added a methods: { } section to the component along with a login() method. Within the login() method, we’re using Axios to invoke the Create Token endpoint on our API, specifying request method as ‘POST’ and passing up the data we’ve captured from our form using this.username and this.password to reference our component properties.

If the user enters the correct values, we’ll alert out the JWT received in the response payload, if anything went wrong (such as incorrect credentials), we’ll let the user know too.

If npm run serve is still running, your code will be built and ready to try in the browser.

All good. But again, it doesn’t really help us as we don’t store the token or do anything else with it. We’ll fix that in the next section.

Storing and Using the Token

In order to check the user login state and make a decision on whether to allow access when changing routes, we need to store the JWT we received in our login process, and provide methods for decoding the token and checking it’s validity.

We could do all this inside the components and router, however to neaten things up and make our code a little bit more reusable, we’ll create an authentication helper.

In the root ./src directory, create a new folder called “utils” and then a new file within that called “auth.js” and add the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import decode from 'jwt-decode'
import axios from 'axios'

const REST_ENDPOINT = 'http://localhost:3000/'
const AUTH_TOKEN_KEY = 'authToken'

export function loginUser(username, password) {
return new Promise(async (resolve, reject) => {
try {
let res = await axios({
url: `${REST_ENDPOINT}api/v1/auth/token`,
method: 'POST',
data: {
username: username,
password: password,
grant_type: 'password'
}
})

setAuthToken(res.data.token)
resolve()
}
catch (err) {
console.error('Caught an error during login:', err)
reject(err)
}
})
}

export function logoutUser() {
clearAuthToken()
}

export function setAuthToken(token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
localStorage.setItem(AUTH_TOKEN_KEY, token)
}

export function getAuthToken() {
return localStorage.getItem(AUTH_TOKEN_KEY)
}

export function clearAuthToken() {
axios.defaults.headers.common['Authorization'] = ''
localStorage.removeItem(AUTH_TOKEN_KEY)
}

export function isLoggedIn() {
let authToken = getAuthToken()
return !!authToken && !isTokenExpired(authToken)
}

export function getUserInfo() {
if (isLoggedIn()) {
return decode(getAuthToken())
}
}

function getTokenExpirationDate(encodedToken) {
let token = decode(encodedToken)
if (!token.exp) {
return null
}

let date = new Date(0)
date.setUTCSeconds(token.exp)

return date
}

function isTokenExpired(token) {
let expirationDate = getTokenExpirationDate(token)
return expirationDate < new Date()
}

Breaking this down, we’ve created:

  • A login() function to abstract the logic we created earlier in our Login component. Once the token is received back from the API, we create a default Authorization header within Axios (so that future requests will be authorized) and save the token to local storage. Local storage is preserved between sessions, so even if the token is quite long lived, say 24 hours, the session will still be available to the user when they return. We’ve wrapped the return value from login() in a Promise so it can be easily awaited further up the stack.
  • An isLoggedIn() method to return the current logged in state of the user. This method retrieves the auth token from local storage and checks whether the expiry date on the token has passed. Notice that the getTokenExpirationDate() doesn’t verify the JWT, it just decodes the payload. Verification is performed by the server.
  • Methods to log the user out by clearing the token from storage and removing the default Authorization header from Axios

We now have a set of helper methods that can be used across the project to log the user in and out and verify the state of their token. Let’s update the Login component to use them, and redirect the user to the Home page when their login is successful.

Return to the Login component and import your required methods from the auth helper:

1
import { loginUser } from '../utils/auth'

Then update the body of your login() method to the following:

1
2
3
4
5
6
7
try {
await loginUser(this.username, this.password)
this.$router.push('/')
}
catch (err) {
alert(`Error: ${err}`)
}

Once the loginUser() promise resolves successfully, we simply set the route to that of the home page. We don’t need to do anything else as all of the token storage and validation is handled by our auth helper. Notice the parameter we pass to the $router.push() method, '/' matches the path of the Home route declared in the router.

On failure of the login, we’re alerting the user of this fact. In practice we could show a nice “login failed” message, however this will do for our simple example.

If you now head back to the browser and enter correct login details in the /login view, you’ll see you’re now redirected to the example Home page.

This is great, however there are a couple of issues. The first is that if you were to enter “/login” manually as the route in your address bar, you’d be taken back to the login page. Ideally we don’t want users who are already logged in to be shown the login page, rather we’d like to direct them back to the homepage if they hit the /login route.

The second issue is that if you were to clear the local storage of the token so the user is “logged out”, you could still access the Home and About routes, which should only be accessible for logged in users. What we’d really like is for users hitting these routes who are not logged in to be directed to the /login route.

Protected Routes

Helpfully, the Vue Router provides a way to hook into events that occur during the routing cycle. This is a very useful feature that can be used for logging and analytics, however we are primarily interested here in checking the login state of our user so that we can allow the route change, or if necessary cancel before it takes place.

The beauty of the Vue router is that this is a very simple, intuitive task and can be accomplished in just a few extra lines within our router.

Let’s first add some metadata to the routes we require to be accessed anonymously so this can be checked during a route change. Head back to your ./src/router.js file and update /login route we created earlier to this:

1
2
3
4
5
6
7
8
{
path: '/login',
name: 'login',
component: Login,
meta: {
allowAnonymous: true
}
}

We want to perform checks on all route changes before they occur, and the Vue Router provides just such a capability - beforeEach(). The beforeEach() adds a callback that is performed before every route change, whether made programatically or manually by the user, so it is here we should add our login check logic.

First of all, import the isLoggedIn() method from our auth helper like so:

1
import { isLoggedIn } from './utils/auth'

We need to call the beforeEach() method on our Router, so we need to assign it to a variable first. Replace the Router definition export default new Router({ ...... }) with:

1
const router =  new Router({ ...... })

Then, after your Router() constructor closes, add the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
router.beforeEach((to, from, next) => {
if (!to.meta.allowAnonymous && !isLoggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
}
else {
next()
}
})

export default router

beforeEach() gives us 3 parameters to work with, to, from and next. We can inspect the to and from parameters to determine the route we are coming from and trying to go to, respectively. They contain the same data as specified in the routes: [] array so we can check for the presence of the allowAnonymous flag we specified earlier.

Notice the logic; if the route we’re about to go to is not an anonymous route (therefore, a protected route) and the user is not logged in then we call the next() callback supplied to us by beforeEach() with the path of the login route. This protects non-anonymous routes from being opened by users that are not logged in.

If the route is anonymous or the user is logged in, we allow the route change through by calling next() with no parameters.

We could of course do this the other way round, where protected routes were flagged as such - however we’d need to add this flag to all protected routes, which is a bit more error prone and verbose. This way, we assume a route is protected unless otherwise stated.

Head back to the browser, open the console and run the command localStorage.clear(). This will empty the localStorage for the localhost domain, and remove any saved tokens so that our isLoggedIn() method will return false and we can test our login process.

Notice if you now attempt to access the ‘/‘ or ‘/About’ routes, you will be redirected to the login page, as these are protected routes. Great!

Now, if you log in, you will be successfully redirected to the Home page and you can access the About route too.

Notice, however, that if you try to go back to the login route, you will be able to do so. This isn’t particularly desirable as we’d like to direct users straight to the Home page if they are logged in. To achieve this, we’ll just check if our destination is the login route and redirect to ‘/‘ if the user is logged in. Update the body of the beforeEach() method to:

1
2
3
4
5
6
7
8
9
10
11
12
if (to.name == 'login' && isLoggedIn()) {
next({ path: '/' })
}
else if (!to.meta.allowAnonymous && !isLoggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
}
else {
next()
}

Now, if you head back to the browser and enter the /login route while logged in, you’ll be directed to the Home page. Perfect!

Displaying Content based on Login State

All good. There’s just one final change to our example we can make. Notice if you clear the JWT from storage (localStorage.clear() in the console) and open the /login route, you can still see the links for “Home” and “About”. This is because they are part of the App template and are rendered regardless of the selected route.

In a live application, you would most likely have a navigation bar of some sort, and user control, logout buttons etc, that should only be displayed when the user is logged in.

We’ve already created helper methods to determine whether the user is logged in and their token is valid, so we can re-use these to control display of certain elements.

Open up the ./src/App.vue file created by the CLI, and if not already present, add a <script></script> tag with the following body:

1
2
3
4
5
6
7
8
9
import { isLoggedIn } from './utils/auth'

export default {
methods: {
isLoggedIn() {
return isLoggedIn()
}
}
}

Here we are importing the isLoggedIn() method from our auth helper, and exposing it for use within the component. To control the visibility of the navigation element, we use the v-if attribute. Find the element with id “nav” and change it’s definition to:

1
2
3
<div id="nav" v-if="isLoggedIn()">
......
</div>

Go back to the browser, and you’ll notice that the navigation element - the Home and About links - are no longer displayed on the login page. However if you log in, you’ll see they are displayed as expected.

Hopefully this has been helpful to you. You’ve likely already got an application you’d like to add this functionality to, so I’ve tried to keep the example as simple as possible without too many extra bells and whistles. There are some extras in the auth helper, so you can also extend the UI quite easily to add a logout() button, or perhaps display the user’s claims (such as firstname, lastname or roles).

You can find the full source code on Github. Let me know if there is anything I’ve missed. Look forward to hearing your thoughts in the comments.

Update 8th December 2019: Replaced promise resolution using .then() and .catch() with async/await


Comments: