Vue 3 Refresh Token with Axios and JWT example

With previous posts, we’ve known how to build Authentication and Authorization in a Vue.js 3 + Vuex Application. In this tutorial, I will continue to show you way to implement Vue 3 Refresh Token with Axios Interceptors and JWT.

Related Posts:
Axios Interceptors tutorial with Refresh Token example
In-depth Introduction to JWT-JSON Web Token
Vue 3 Authentication & Authorization with JWT, Vuex, Axios and Vue Router

Vue 2 version: Vue Refresh Token with Axios and JWT example


Vue 3 Refresh Token with Axios overview

The diagram shows flow of how we implement Vue 3 + Vuex JWT Refresh Token example.

vue-3-refresh-token-axios-jwt-example-flow

– A refreshToken will be provided at the time user signs in.
– A legal JWT must be added to HTTP Header if Client accesses protected resources.
– With the help of Axios Interceptors, Vue App can check if the accessToken (JWT) is expired (401), sends /refreshToken request to receive new accessToken and use it for new resource request.

Let’s see how the Vue Refresh Token example works with demo UI.

– User signs in with a legal account first.

vue-3-refresh-token-axios-jwt-example-login

– Now the user can access resources with provided Access Token.

vue-3-refresh-token-axios-jwt-example-access-resources-ui

vue-3-refresh-token-axios-jwt-example-access-resources

– When the Access Token is expired, Vue 3 automatically send Refresh Token request, receive new Access Token and use it for new request.

vue-3-refresh-token-axios-jwt-example-access-token-expired

vue-3-refresh-token-axios-jwt-example-refresh-token

vue-3-refresh-token-axios-jwt-example-new-access-token

So the server still accepts resource access from the user.

vue-3-refresh-token-axios-jwt-example-access-resources-ui

– After a period of time, the new Access Token is expired again, and the Refresh Token too.

vue-3-refresh-token-axios-jwt-example-new-access-token-expired

vue-3-refresh-token-axios-jwt-example-refresh-token-expired

The Back-end server for this Vue.js Client can be found at:

We’re gonna implement Token Refresh feature basing on the code from previous posts, so you need to read following tutorial first:
Vue 3 Authentication and Authorization with JWT, Vuex, Axios and Vue Router

Vuex 4 with Refresh Token

Now we need to add a Vuex action and a mutation – refreshToken.

store/auth.module.js

import AuthService from '../services/auth.service';

const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
  ? { status: { loggedIn: true }, user }
  : { status: { loggedIn: false }, user: null };

export const auth = {
  namespaced: true,
  state: initialState,
  actions: {
    ...
    refreshToken({ commit }, accessToken) {
      commit('refreshToken', accessToken);
    }
  },
  mutations: {
    ...
    refreshToken(state, accessToken) {
      state.status.loggedIn = true;
      state.user = { ...state.user, accessToken: accessToken };
    }
  }
};

Axios interceptors in Vue 3 Application

Let’s create a service that provides an Axios instance.

services/api.js

import axios from "axios";

const instance = axios.create({
  baseURL: "http://localhost:8080/api",
  headers: {
    "Content-Type": "application/json",
  },
});

export default instance;

Then we’re gonna create a setupInterceptors() function with interceptors request and dispatch the Vuex action for refresh token in the response.

services/setupInterceptors.js

import axiosInstance from "./api";
import TokenService from "./token.service";

const setup = (store) => {
  axiosInstance.interceptors.request.use(
    (config) => {
      const token = TokenService.getLocalAccessToken();
      if (token) {
        // config.headers["Authorization"] = 'Bearer ' + token;  // for Spring Boot back-end
        config.headers["x-access-token"] = token; // for Node.js Express back-end
      }
      return config;
    },
    (error) => {
      return Promise.reject(error);
    }
  );

  axiosInstance.interceptors.response.use(
    (res) => {
      return res;
    },
    async (err) => {
      const originalConfig = err.config;

      if (originalConfig.url !== "/auth/signin" && err.response) {
        // Access Token was expired
        if (err.response.status === 401 && !originalConfig._retry) {
          originalConfig._retry = true;

          try {
            const rs = await axiosInstance.post("/auth/refreshtoken", {
              refreshToken: TokenService.getLocalRefreshToken(),
            });

            const { accessToken } = rs.data;

            store.dispatch('auth/refreshToken', accessToken);
            TokenService.updateLocalAccessToken(accessToken);

            return axiosInstance(originalConfig);
          } catch (_error) {
            return Promise.reject(_error);
          }
        }
      }

      return Promise.reject(err);
    }
  );
};

export default setup;

In the code above, we:
– intercept requests or responses before they are handled by then or catch.
– handle 401 status on interceptor response (without response of login request).
– use a flag call _retry on original Request (config) to handle Infinite loop. It is the case that request is failed again, and the server continue to return 401 status code.

For more details, please visit:
Axios Interceptors tutorial with Refresh Token example

Let’s import the setupInterceptors() function in main.js where we can pass the Vuex store.

src/main.js

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
...
import setupInterceptors from './services/setupInterceptors';

setupInterceptors(store);

createApp(App)
  .use(router)
  .use(store)
  .component("font-awesome-icon", FontAwesomeIcon)
  .mount("#app");

Update Services with new Axios Interceptors

Axios Interceptors manipulate the header, body, parameters of the requests sent to the server so that we don’t need to add headers in Axios requests like this:

axios.get(API_URL, { headers: authHeader() })

So we remove auth-header.js file, then update services that use it with new api service.

services/user.service.js

import api from './api';

class UserService {
  getPublicContent() {
    return api.get('/test/all');
  }

  getUserBoard() {
    return api.get('/test/user');
  }

  getModeratorBoard() {
    return api.get('/test/mod');
  }

  getAdminBoard() {
    return api.get('/test/admin');
  }
}

export default new UserService();

services/auth.service.js

import api from "./api";
import TokenService from "./token.service";

class AuthService {
  login({ username, password }) {
    return api
      .post("/auth/signin", {
        username,
        password
      })
      .then((response) => {
        if (response.data.accessToken) {
          TokenService.setUser(response.data);
        }

        return response.data;
      });
  }

  logout() {
    TokenService.removeUser();
  }

  register({ username, email, password }) {
    return api.post("/auth/signup", {
      username,
      email,
      password
    });
  }
}

export default new AuthService();

We also need to create TokenService which Axios instance and other services use above.
TokenService provides get, set, remove methods to work with Token and User Data stored on Browser.

services/token.service.js

class TokenService {
  getLocalRefreshToken() {
    const user = JSON.parse(localStorage.getItem("user"));
    return user?.refreshToken;
  }

  getLocalAccessToken() {
    const user = JSON.parse(localStorage.getItem("user"));
    return user?.accessToken;
  }

  updateLocalAccessToken(token) {
    let user = JSON.parse(localStorage.getItem("user"));
    user.accessToken = token;
    localStorage.setItem("user", JSON.stringify(user));
  }

  getUser() {
    return JSON.parse(localStorage.getItem("user"));
  }

  setUser(user) {
    console.log(JSON.stringify(user));
    localStorage.setItem("user", JSON.stringify(user));
  }

  removeUser() {
    localStorage.removeItem("user");
  }
}

export default new TokenService();

Handle expired Token in Vue 3

When both Access Token and Refresh Token are expired, we need to logout the user. How to do it? We read response status from the server, then dispatch logout event to App component when the response status tells us the JWT token is expired.

First we need to set up a global event-driven system, or a PubSub system, which allows us to listen and dispatch events from independent components.

An Event Bus implements the PubSub pattern, events will be fired from other components so that they don’t have direct dependencies between each other.

We’re gonna create Event Bus with three methods: on, dispatch, and remove.

common/EventBus.js

const eventBus = {
  on(event, callback) {
    document.addEventListener(event, (e) => callback(e.detail));
  },
  dispatch(event, data) {
    document.dispatchEvent(new CustomEvent(event, { detail: data }));
  },
  remove(event, callback) {
    document.removeEventListener(event, callback);
  },
};

export default eventBus;

on() method attachs an EventListener to the document object. The callback will be called when the event gets fired.
dispatch() method fires an event using the CustomEvent API.
remove() method removes the attached event from the document object.

Next we import EventBus in App component and listen to "logout" event.

src/App.vue

<template>
  ..
</template>

<script>
import EventBus from "./common/EventBus";

export default {
  computed: {
    ..
  },
  methods: {
    logOut() {
      this.$store.dispatch('auth/logout');
      this.$router.push('/login');
    }
  },
  mounted() {
    EventBus.on("logout", () => {
      this.logOut();
    });
  },
  beforeDestroy() {
    EventBus.remove("logout");
  }
};
</script>

Finally we only need to dispatch "logout" event in the components when getting Unauthorized response status.

components/BoardUser.js

<template>
  ..
</template>

<script>
import UserService from '../services/user.service';
import EventBus from "../common/EventBus";

export default {
  name: 'User',
  ...
  mounted() {
    UserService.getUserBoard().then(
      response => { ... },
      error => {
        this.content =
          (error.response && error.response.data && error.response.data.message) ||
          error.message ||
          error.toString();

        if (error.response && error.response.status === 403) {
          EventBus.dispatch("logout");
        }
      }
    );
  }
};
</script>

Conclusion

Today we know how to implement JWT Refresh Token into a Vue 3 + Vuex Application using Axios Interceptors. For your understanding the logic flow, you should read one of following tutorial first:
Vue 3 Authentication & Authorization with JWT, Vuex, Axios and Vue Router

The Back-end server for this Vue Client can be found at:

Further Reading

Related Posts:
In-depth Introduction to JWT-JSON Web Token
Axios Interceptors tutorial with Refresh Token example

Fullstack Authentication & Authorization:
Vue + Spring Boot
Vue + Node.js Express

Source Code

The source code for this React Application can be found at Github.

Vue 2 version: Vue Refresh Token with Axios and JWT example

6 thoughts to “Vue 3 Refresh Token with Axios and JWT example”

  1. Hi Your tutorials are great, I have a question about the Eventbus.
    When the 403 is returned and the eventbus gets a “logout” dispatched to it
    from this line in the BoardUser.vue
    EventBus.dispatch(“logout”);
    Should the user be sent to the login page?
    I was expecting the logout() function to be called in App.vue from the EventBus.on(“logout” …
    which I thought would receive the event and log the user out and return them to the login page.
    This does not work I just get a message saying “Refresh token was expired. Please make a new signin request”
    Should the user be redirected to the login page?

  2. Hi, I’ve got a question: when does the refresh token expire? Can I set the expire time on backend?

    1. Hi, the expiration time is set on backend side. You can read the backend tutorials that I mentioned above for details. 🙂

Comments are closed to reduce spam. If you have any question, please send me an email.