Verify and refresh JWT access token in using Vue Router and Vuex

Viewed 1292

I've created simple VueCLI auth module using axios and Vuex.

In store.js I've got all logic for tokens using api from session.js:

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
import sessionSerivce from '@/services/session.js'

Vue.use(Vuex)
Vue.use(require('vue-cookies'))

export const store = new Vuex.Store({
  state: {
    status: '',
    accessToken: $cookies.get('accessToken') || '',
    refreshToken: $cookies.get('refreshToken') || '',
    user: $cookies.get('user') || '',
  },
  actions: {
    login({ commit }, data) {
      return new Promise((resolve, reject) => {
        commit('auth_request')
        sessionSerivce
          .logIn(data)
          .then((resp) => {
            const commitData = {
              accessToken: resp.data.access_token,
              refreshToken: resp.data.refresh_token,
              user: resp.data.user,
            }

            $cookies.set('accessToken', commitData.accessToken)
            $cookies.set('refreshToken', commitData.refreshToken)
            $cookies.set('user', JSON.stringify(commitData.user))

            axios.defaults.headers.common['Authorization'] =
              commitData.accessToken

            commit('auth_success', commitData)
            resolve(resp)
          })
          .catch((err) => {
            commit('auth_error')
            $cookies.remove('accessToken')
            $cookies.remove('refreshToken')
            $cookies.remove('user')
            reject(err)
          })
      })
    },

    verifyToken({ commit, state }) {},

    register({ commit }, data) {
      return new Promise((resolve, reject) => {
        commit('auth_request')
        sessionSerivce
          .register(data)
          .then((resp) => {
            const commitData = {
              accessToken: resp.data.access_token,
              refreshToken: resp.data.refresh_token,
              user: resp.data.user,
            }

            $cookies.set('accessToken', commitData.accessToken)
            $cookies.set('refreshToken', commitData.refreshToken)
            $cookies.set('user', JSON.stringify(commitData.user))

            axios.defaults.headers.common['Authorization'] =
              commitData.accessToken

            commit('auth_success', commitData)
            resolve(resp)
          })
          .catch((err) => {
            commit('auth_error')
            $cookies.remove('accessToken')
            $cookies.remove('refreshToken')
            $cookies.remove('user')

            reject(err)
          })
      })
    },

    logout({ commit }) {
      return new Promise((resolve, reject) => {
        commit('logout')
        $cookies.remove('accessToken')
        $cookies.remove('refreshToken')
        $cookies.remove('user')
        delete axios.defaults.headers.common['Authorization']
        resolve()
      })
    },
  },
  mutations: {
    auth_request(state) {
      state.status = 'loading'
    },
    auth_success(state, commitData) {
      state.status = 'success'
      state.accessToken = commitData.accessToken
      state.refreshToken = commitData.refreshToken
      state.user = commitData.user
    },
    auth_error(state) {
      state.status = 'error'
    },
    refresh_token(state, accessToken) {
      state.accessToken = accessToken
    },
    logout(state) {
      state.status = ''
      state.accessToken = ''
      state.refreshToken = ''
      state.user = ''
    },
  },
  getters: {
    isLoggedIn: (state) => {
      return !!state.accessToken
    },
    authStatus: (state) => state.status,
  },
})

In main.js I use this function to check:

router.beforeEach(async (to, from, next) => {

  if (to.matched.some((record) => record.meta.requiresAuth)) {
    if (store.getters.isLoggedIn) {
      next()
      return
    }

    next('/login')
  } else next()
})

The problem is that code above checks only if access token exists in Vuex. I want to verify using api before any route, that requires auth and if it's not successfully I want to refresh It with api using refresh token. If both are unsuccessful(access and refresh tokens are both invalid) user gonna log out.

Example route which requires auth:

path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
meta: {
  requiresAuth: true,
},

I've tried code like this:

router.beforeEach(async (to, from, next) => {
  if (to.matched.some((record) => record.meta.requiresAuth)) {
    if (store.state.accessToken) {
      await store.dispatch('verifyToken')

      if (store.getters.isLoggedIn) {
        next()
        return
      }
    }
    next('/login')
  } else next()
})

Action in Vuex:

verifyToken({ commit, state }) {
      const accessToken = state.accessToken
      const refreshToken = state.accessToken
      sessionSerivce
        .verifyToken(accessToken)
        .then((resp) => {})
        .catch((err) => {
          sessionSerivce
            .refreshToken(refreshToken)
            .then((resp) => {
              console.log('Refreshuje token')
              const accessToken = resp.data.access_token
              localStorage.setItem('accessToken', accessToken)
              axios.defaults.headers.common['Authorization'] = accessToken
              commit('refresh_token', accessToken)
            })
            .catch((err) => {
              commit('logout')
              localStorage.removeItem('accessToken')
              localStorage.removeItem('refreshToken')
              delete axios.defaults.headers.common['Authorization']
            })
        })
    },

Note that in code above i used localstorage but i've changed my mind and I'm using cookie, as You can see in previous code.

Unfortunately this code didn't work as expected - if (store.getters.isLoggedIn) { next(); return; } is starting to execute before await store.dispatch('verifyToken') ends, which is bad.

Any ideas?

0 Answers
Related