7

Note: I have seperated my client(Vue.js) and server(DjangoRest). I'm using JWT to validate every request made from the client to the server. Flow- Client sends user credentials to server. Server sends back a refresh and access token if credentials are valid. Client stores the access and refresh token. I have set the refresh token expiry to 1 week,access to 30 mins. Next, I want to make sure that the access token is auto refreshed 15 mins prior to its expiry. To do this, the stored refresh token in client side is send to the server, the server then issues a new access token and refresh token, sends it back to the client. How do i implement this in the Vuex store?. I'm a complete newbie to web development and vue.js. It would be great if someone could provide some code or explain in details.

I have already implemented loginUser,logout user,registerUser in store and they are working fine. But I'm stuck with the auto refresh logic. My guess is that the client has to repeatedly check the access token expiry time left. When about 15 mins is left, we have to initialize the autorefresh function. Please help me with this logic.

Here's my Vueex store:

import Vue from 'vue'
import Vuex from 'vuex'
import axiosBase from './api/axios-base'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
     accessToken: '' || null,
     refreshToken: '' || null
  },
  getters: {
    loggedIn (state) {
      return state.accessToken != null
    }
  },
  mutations: {
    loginUser (state) {
      state.accessToken = localStorage.getItem('access_token')
      state.refreshToken = localStorage.getItem('refresh_token')
    },
    destroyToken (state) {
      state.accessToken = null
      state.refreshToken = null
    }
  },
  actions: {
    registerUser (context, data) {
      return new Promise((resolve, reject) => {
        this.axios.post('/register', {
          name: data.name,
          email: data.email,
          username: data.username,
          password: data.password,
          confirm: data.confirm
        })
          .then(response => {
            resolve(response)
          })
          .catch(error => {
            reject(error)
          })
      })
    },
    // fetch data from api whenever required.
    backendAPI (context, data) {

    },
    logoutUser (context) {
      if (context.getters.loggedIn) {
        return new Promise((resolve, reject) => {
          axiosBase.post('/api/token/logout/')
            .then(response => {
              localStorage.removeItem('access_token')
              localStorage.removeItem('refresh_token')
              context.commit('destroyToken')
            })
            .catch(error => {
              context.commit('destroyToken')
              resolve()
            })
        })
      }
    },
    autoRefresh (context, credentials) {

    },
    loginUser (context, credentials) {
      return new Promise((resolve, reject) => {
        axiosBase.post('/api/token/', {
          username: credentials.username,
          password: credentials.password
        })
          .then(response => {
            localStorage.setItem('access_token', response.data.access)
            localStorage.setItem('refresh_token', response.data.refresh)
            context.commit('loginUser')
            resolve(response)
          })
          .catch(error => {
            console.log(error)
            reject(error)
          })
      })
    }
  }
})

Thank you in advance.

nishant_boro
  • 374
  • 1
  • 2
  • 8

2 Answers2

19

This is very much an idea question as you've pointed out and as such, there are many ways of solving it.

One thing I try to keep in mind when dealing with such mechanisms is to always avoid polling when possible. Here's a solution inspired by that design principle.

JWT tokens are valid for a very specific amount of time. The time left for expiration is readily available as part of the access token. You can use a library such as jwt-decode to decode the access token and extract the expiration time. Once you have the expiration time, you have a several options available:

  • Check token every time before making a request to know if it needs to be refreshed
  • Use setTimeout to refresh it periodically X seconds before it expires

Your code could be implemented as follows:
Note: Please treat the following as pseudo-code. I have not tested it for errors---syntax or otherwise.

export default new Vuex.Store({
  ...
  actions: {
    refreshTokens (context, credentials) {
      // Do whatever you need to do to exchange refresh token for access token
      ...
      // Finally, call autoRefresh to set up the new timeout
      dispatch('autoRefresh', credentials)
    },
    autoRefresh (context, credentials) {
      const { state, commit, dispatch } = context
      const { accessToken } = state
      const { exp } = jwt_decode(accessToken)
      const now = Date.now() / 1000 // exp is represented in seconds since epoch
      let timeUntilRefresh = exp - now
      timeUntilRefresh -= (15 * 60) // Refresh 15 minutes before it expires
      const refreshTask = setTimeout(() => dispatch('refreshTokens', credentials), timeUntilRefresh * 1000)
      commit('refreshTask', refreshTask) // In case you want to cancel this task on logout
    }
  }
})
Guru Prasad
  • 4,053
  • 2
  • 25
  • 43
  • 1
    while setTimeout make sure to convert the `timeUntilRefresh` into milliseconds by multiplying with 1000. `const refreshTask = setTimeout(() => dispatch('refreshTokens', credentials), timeUntilRefresh)` – Rishabh Batra Dec 06 '20 at 10:46
  • What do you do with ```commit('refreshTask', refreshTask)``` – Pixsa Apr 24 '21 at 20:02
  • It's used to cancel the timeout later, if necessary. One such situation could be if the user logged out. – Guru Prasad Apr 24 '21 at 22:40
  • The way I have it is as follows: I refresh the token and retry all the requests that failed due to 401 however I am not sure how to update components with the new data. Any ideas? – Robert Jun 20 '22 at 16:56
10

It's better to rely on your server response code than expiration time. Try to access the protected route, if it returns 401, ask for a new access token and then try again. If your refresh route also returns 401, make your user log in again.

What you need is an axios interceptor.

I implemented this one for a project

import axios from 'axios'
import router from './router'
import store from './store'
import Promise from 'es6-promise'

const ax = axios.create({
  baseURL: 'http://localhost:3000',
  headers: {
    'Content-type': 'application/json'
  }
})

ax.interceptors.response.use(
  (response) => {
    return response
  },
  (err) => {
    console.log(err.response.config.url)
    // return other errors
    if (err.response.status !== 401) {
      return new Promise((resolve, reject) => {
        reject(err)
      })
    }
    // error on login
    if (err.response.config.url === '/auth/login') {
      return new Promise((resolve, reject) => {
        reject(err)
      })
    }
    // error on refresh
    if (err.response.config.url === '/auth/refresh') {
      console.log('ERRO NO REFRESH')
      router.push('/logout')
      return new Promise((resolve, reject) => {
        reject(err)
      })
    }
    // refresh
    return ax.get('/auth/refresh', { withCredentials: true }).then(
      success => {
        const config = err.response.config
        config.headers.Authorization = 'Bearer ' + success.data.access_token
        store.commit('setToken', success.data.access_token)
        return ax(config)
      }
    )
  }
)

export default ax
Bruno Canongia
  • 514
  • 6
  • 12
  • 3
    I used to think like this but, I think there's no point of refresh_token to be there at the first place then? – Richie Permana Aug 12 '20 at 02:54
  • 1
    Yes, it's better to relay in the 401 error. Because sometimes the secret of the backend changes and if the frontend only checks de exp time, it's going to get an error even though the token is invalid. – bryan-gc May 23 '22 at 17:09
  • How do you then update the component with new data, how does the interceptor tells components that we did get the new data? – Robert Jun 20 '22 at 16:45