Authentication best practices for vuejs

vuejsauthentication

Overview of how to handle authentication in VueJS 2


Introduction

Whenever you start to get serious with a project, you will most likely face the issue of how to handle client-side token-based authentication.

You will have to answer these questions:

  • How do I store my user’s token?
  • How do I redirect the user after authentication actions (login/logout)?
  • How do I prevent route access to authenticated and unauthenticated users?

This article will walk you through these questions and try to illustrate with clear code and good practices in mind.

However, keep in mind that all projects have different authenticated behavior. Projects can be just a loading screen until you are logged in (Gmail), or a view of the application without access to every feature (Amazon), so you will probably have to adjust what I will describe here.

Before we start

I have made this repo if you want to run some code.

We use Vuex as the global state library. Vuex is especially suited for auth management since it’s application-scoped. If you don’t want to use Vuex, no worries — we also give some code example without Vuex 🙂

We also use the axios library for the ajax calls.

Finally, this article will not cover how to implement the backend side of the authentication process. We will only focus on the client side.

Login

Let’s start with a simple login form:

<template>
  <div>
    <form class="login" @submit.prevent="login">
      <h1>Sign in</h1>
      <label>User name</label>
      <input required v-model="username" type="text" placeholder="Snoopy" />
      <label>Password</label>
      <input
        required
        v-model="password"
        type="password"
        placeholder="Password"
      />
      <hr />
      <button type="submit">Login</button>
    </form>
  </div>
</template>

When the user has filled the inputs and clicked Login, we execute the login method.

...
methods: {
 login: function () {
   const { username, password } = this
   this.$store.dispatch(AUTH_REQUEST, { username, password }).then(() => {
     this.$router.push('/')
   })
 }
}
...

Here is the first important bit of this code snippet.

  • Vuex actions returning promises.
this.$store.dispatch(AUTH_REQUEST, { username, password }).then(...)

Did you know? You can also treat action dispatch as promises. Now we can react to successful logins from the component. Allowing us to redirect accordingly.

The same code without Vuex:

// Exported in a shared file
const myLoginRoutine = user => new Promise ((resolve, reject) => {
  axios({url: 'auth', data: user, method: 'POST' })
    .then(resp => {
      const token = resp.data.token
      localStorage.setItem('user-token', token) // store the token in localstorage
      resolve(resp)
    })
  .catch(err => {
    localStorage.removeItem('user-token') // if the request fails, remove any possible user token if possible
    reject(err)
  })
})

// in your vue component
import {myLoginRoutine} from 'the-created-shared-file-containing-auth-api-logic'
...
methods: {
 login: function () {
   const { username, password } = this
   myLoginRoutine({ username, password }).then(() => {
     this.$router.push('/')
   })
 }
}
...

Vuex auth module

Let’s now look at this auth module:

First, let’s initialize the state.

We will have the token field (using local storage stored token if present) and a status field, representing the status of the API call (loading, success or error).

const state = {
  token: localStorage.getItem('user-token') || ''
  status: '',
}

What about using localStorage and not cookies ?

Well they are actually both good solutions, with their own advantages and disadvantages. This post and this page answer this question pretty well.

Some useful getters:

const getters = {
  isAuthenticated: (state) => !!state.token,
  authStatus: (state) => state.status,
};

The ‘isAuthenticated’ getter can seem overkill, however it’s a great way to keep your authentication future proof. By having this getter, you separate data from app logic making it future proof 🙂

Now the action:

const actions = {
  [AUTH_REQUEST]: ({ commit, dispatch }, user) => {
    return new Promise((resolve, reject) => {
      // The Promise used for router redirect in login
      commit(AUTH_REQUEST);
      axios({ url: "auth", data: user, method: "POST" })
        .then((resp) => {
          const token = resp.data.token;
          localStorage.setItem("user-token", token); // store the token in localstorage
          commit(AUTH_SUCCESS, token);
          // you have your token, now log in your user :)
          dispatch(USER_REQUEST);
          resolve(resp);
        })
        .catch((err) => {
          commit(AUTH_ERROR, err);
          localStorage.removeItem("user-token"); // if the request fails, remove any possible user token if possible
          reject(err);
        });
    });
  },
};

And the mutation:

// basic mutations, showing loading, success, error to reflect the api call status and the token when loaded
const mutations = {
  [AUTH_REQUEST]: (state) => {
    state.status = "loading";
  },
  [AUTH_SUCCESS]: (state, token) => {
    state.status = "success";
    state.token = token;
  },
  [AUTH_ERROR]: (state) => {
    state.status = "error";
  },
};

A fairly simple API call from a module. The important bits are:

  • Token state being initialized by its local storage value, if possible.
  • The Authentication request action returns a Promise, useful for redirect when a successful login happens.
  • Good practice: pass the login credentials in the request body, not in the URL. The reason behind it is that servers might log URLs, so you don’t have to worry about credential leaks through logs.

Logout

Since we are at it, let’s implement our logout logic in modules/auth.js:

const actions = {
...

  [AUTH_LOGOUT]: ({commit, dispatch}) => {
    return new Promise((resolve, reject) => {
      commit(AUTH_LOGOUT)
      localStorage.removeItem('user-token') // clear your user's token from localstorage
      resolve()
    })
  }
}

When clicking on the logout button in one of your components responsible for logout:

methods: {
  logout: function () {
    this.$store.dispatch(AUTH_LOGOUT)
    .then(() => {
      this.$router.push('/login')
    })
  }
},

In this case, logging out for us means clearing out the user’s token and redirecting them. If you need to perform other Vuex state changes, listen to this AUTH_LOGOUT action and commit.

You can also add a token DELETE request in your action to delete your user token session when logging out.

Try to keep it simple, as few actions as possible to logout/login.

If you start to create one login/logout action per authentication type that you have, you will have a headache maintaining them.

Use the token

Now that we have managed to retrieve the token and store it, let’s use it!

The following example uses Axios and its default headers.

In modules/auth.js

const actions = {
  [AUTH_REQUEST]: ({ commit, dispatch }, user) => {
    return new Promise((resolve, reject) => {
      commit(AUTH_REQUEST);
      axios({ url: "auth", data: user, method: "POST" })
        .then((resp) => {
          const token = resp.data.token;
          localStorage.setItem("user-token", token);
          // Add the following line:
          axios.defaults.headers.common["Authorization"] = token;
          commit(AUTH_SUCCESS, resp);
          dispatch(USER_REQUEST);
          resolve(resp);
        })
        .catch((err) => {
          commit(AUTH_ERROR, err);
          localStorage.removeItem("user-token");
          reject(err);
        });
    });
  },
  [AUTH_LOGOUT]: ({ commit, dispatch }) => {
    return new Promise((resolve, reject) => {
      commit(AUTH_LOGOUT);
      localStorage.removeItem("user-token");
      // remove the axios default header
      delete axios.defaults.headers.common["Authorization"];
      resolve();
    });
  },
};

Now after login, all the Axios calls have the authorization header set to your token. All your API calls are authenticated! And when logging out, we delete the authorization header.

Auto-authentication

Right now if we refresh the app, we do have the state correctly set to the previous token. However, the authorization Axios header isn’t set. Let’s fix it!

In your main.js:

const token = localStorage.getItem("user-token");
if (token) {
  axios.defaults.headers.common["Authorization"] = token;
}

Now your API calls are authenticated when refreshing your app after login!

Authenticated routes

Now you probably want to restrict access to your routes depending on whether they are authenticated or not.

In this case, we want only authenticated users to reach /account.

And unauthenticated users should only be able to reach /login and /.

import store from "../store"; // your vuex store

const ifNotAuthenticated = (to, from, next) => {
  if (!store.getters.isAuthenticated) {
    next();
    return;
  }
  next("/");
};

const ifAuthenticated = (to, from, next) => {
  if (store.getters.isAuthenticated) {
    next();
    return;
  }
  next("/login");
};

export default new Router({
  mode: "history",
  routes: [
    {
      path: "/",
      name: "Home",
      component: Home,
    },
    {
      path: "/account",
      name: "Account",
      component: Account,
      beforeEnter: ifAuthenticated,
    },
    {
      path: "/login",
      name: "Login",
      component: Login,
      beforeEnter: ifNotAuthenticated,
    },
  ],
});

Here we use navigation guards. They allow us to put conditions on routes access that we use to our advantage in conjunction with the Vuex store.

Note 1: If you do not wish to use Vuex, you can still check for token presence in the local storage rather than looking at the store getters 🙂

Note 2: Ed @posva, maintainer of vue-router, also advises the usage of meta attributes, check it out 🙂

Handling the unauthorized case scenario

…but wait, what if the token is expired? What if the user is unauthorized?

No worries here.

Using Axios, you can intercept all responses, and especially the error response. Just check for all unauthorized responses (HTTP 401) and if so, dispatch a logout action.

In your App.vue

created: function () {
  axios.interceptors.response.use(undefined, function (err) {
    return new Promise(function (resolve, reject) {
      if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
      // if you ever get an unauthorized, logout the user
        this.$store.dispatch(AUTH_LOGOUT)
      // you can also redirect to /login if needed !
      }
      throw err;
    });
  });
}

And we are good to go!

Conclusion

What have we achieved here?

  • Isolated authentication logic from the app and other libs.
  • We won’t need to explicitly pass tokens to every API call.
  • Handle all unauthenticated API calls
  • We have auto-authentication
  • We have restricted routes access

What have we learned?

  • Separation of concerns
  • Avoid side effects
  • Action dispatch can return promises

This should give you a pretty solid starting point for handling all of your API calls in your app.

Hopefully, this will be helpful for your future projects!