Search code examples
firebasevue.jsaxiosvuexinterceptor

Vue Axios Interceptor Response Firebase 401 Token Expired/Refresh (undefined)


I'm using the following interceptors in a Vuejs v2 website to push a firebase token to my node backend. There in the backend, I detect/verify the token, pull some data using the uid from a database and then process any api calls.

Even though I am using the firebase onIdTokenChanged to automatically retrieve new ID tokens, sometimes, if the user is logged in, yet inactive for an hour, the token expires without refreshing. Now, this isn't a huge deal - I could check in the axios response interceptor and push them to a login page, but that seems annoying, if I can detect a 401 token expired, resend the axios call and have a refreshed token, the user won't even know it happened if they happen to interact with a component that requires data from an API call. So here is what I have:

main.js

Vue.prototype.$axios.interceptors.request.use(function (config) {
     const token = store.getters.getSessionToken;
     config.headers.Authorization = `Bearer ${token}`;
     return config; 
});

 

Vue.prototype.$axios.interceptors.response.use((response) => {
    return response   }, async function (error) {
    let originalRequest = error.config
  
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      let user = auth.currentUser;
      await store.dispatch("setUser", {user: user, refresh: true}).then(() => {
        const token = store.getters.getSessionToken;
        Vue.prototype.$axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
        return Vue.prototype.$axios.request(originalRequest);
      });
    }
    return Promise.reject(error);   });


let app;

auth.onAuthStateChanged(async user => {
  await store.dispatch("setUser", {user: user, refresh: false}).then(() => {
    if (!app) {
      app = new Vue({
        router,
        store,
        vuetify,
        render: h => h(App)
      }).$mount('#app')
    }
  })
  .catch(error => {
    console.log(error);
  }); 
});

vuex

setUser({dispatch, commit}, {user, refresh}) {
    return new Promise((resolve) => {
      if(user)
      {   
        user.getIdToken(refresh).then(token => {
          commit('SET_SESSION_TOKEN', token);
          this._vm.$axios.get('/api/user/session').then((response) => {
              if(response.status === 200) {
                commit('SET_SESSION_USER', response.data);
                resolve(response);
              }
           })
          .catch(error => {
              dispatch('logout');
              dispatch('setSnackbar', {
                color: "error",
                timeout: 4000,
                text: 'Server unavailable: '+error
              });
              resolve();
          });
            
        })
        .catch(error => {
          dispatch('logout');
          dispatch('setSnackbar', {
            color: "error",
            timeout: 4000,
            text: 'Unable to verify auth token.'+error
          });
          resolve();
      });
      }
      else
      {
        console.log('running logout');
          commit('SET_SESSION_USER', null);
          commit('SET_SESSION_TOKEN', null);
        resolve();
      }  
    })
  },

I am setting the token in vuex and then using it in the interceptors for all API calls. So the issue I am seeing with this code is, I'm making an API call with an expired token to the backend. This returns a 401 and the axios response interceptor picks it up and goes through the process of refreshing the firebase token. This then makes a new API call with the same config as the original to the backend with the updated token and returns it to the original API call (below).

This all seems to work, and I can see in dev tools/network, the response from the API call is sending back the correct data. However, it seems to be falling into the catch of the following api call/code. I get an "undefined" when trying to load the form field with response.data.server, for example. This page loads everything normally if I refresh the page (again, as it should with the normal token/loading process), so I know there aren't loading issues.

vue component (loads smtp settings into the page)

getSMTPSettings: async function() {
    await this.$axios.get('/api/smtp')
      .then((response) => {
        this.form.server = response.data.server;
        this.form.port = response.data.port;
        this.form.authemail = response.data.authemail;
        this.form.authpassword = response.data.authpassword;
        this.form.sendemail = response.data.sendemail;
        this.form.testemail = response.data.testemail;
        this.form.protocol = response.data.protocol;
      })
      .catch(error => {
        console.log(error);
      });

  },

I have been looking at this for a few days and I can't figure out why it won't load it. The data seems to be there. Is the timing of what I'm doing causing me issues? It doesn't appear to be a CORS problem, I am not getting any errors there.


Solution

  • Your main issue is mixing async / await with .then(). Your response interceptor isn't returning the next response because you've wrapped that part in then() without returning the outer promise.

    Keep things simple with async / await everywhere.

    Also, setting common headers defeats the point in using interceptors. You've already got a request interceptor, let it do its job

    // wait for this to complete
    await store.dispatch("setUser", { user, refresh: true })
    
    // your token is now in the store and can be used by the request interceptor
    // re-run the original request
    return Vue.prototype.$axios.request(originalRequest)
    

    Your store action also falls into the explicit promise construction antipattern and can be simplified

    async setUser({ dispatch, commit }, { user, refresh }) {
      if(user) {
        try {
          const token = await user.getIdToken(refresh);
          commit('SET_SESSION_TOKEN', token);
          try {
            const { data } = await this._vm.$axios.get('/api/user/session');
            commit('SET_SESSION_USER', data);
          } catch (err) {
            dispatch('logout');
            dispatch('setSnackbar', {
              color: "error",
              timeout: 4000,
              text: `Server unavailable: ${err.response?.data ?? err.message}`
            })
          }
        } catch (err) {
          dispatch('logout');
          dispatch('setSnackbar', {
            color: "error",
            timeout: 4000,
            text: `Unable to verify auth token. ${error}`
          })
        }
      } else {
        console.log('running logout');
        commit('SET_SESSION_USER', null);
        commit('SET_SESSION_TOKEN', null);
      }  
    }