Search code examples
javascriptfirebasevue.jsvuexvue-router

Firebase Authentication Vue Router Guard Issue w/ Vuex


I am using firebase authentication with vue and in order to prevent initial flickering of "login" in the navigation bar on the dashboard, (instead of the logged in user's name on authentication), I initialize firebase in main.js and wrap the vue app in the onAuthStateChanged function.

main.js

initializeApp(firebaseConfig);

const auth = getAuth();
const app = new Vue({
  router,
  store,
  vuetify,
  render: h => h(App)
});
auth.onAuthStateChanged(user => {
  console.log('firedOnAuthStateChanged');
  store.dispatch("setUser", user).then(() => {
    app.$mount('#app');
  });
});

This works great. I sign in, the page doesn't load until the user is authenticated to firebase and I update the vuex store with updated user information (some from firebase, some from the backend).

The problem I'm having now, is I want to use the beforeEach route guard in the router but the route guards run before the authStateChange fires in main.js. Because of this, the user isn't set in vuex and when I use the stored user state to determine if the user is logged in, it believes they are not logged in because the firebase method hasn't updated it yet.

authentication.js (module from store.js)

setUser({dispatch, commit}, user) {
return new Promise((resolve, reject) => {

  if(user)
  {   
    user.getIdToken().then(token => {
      commit('SET_SESSION_TOKEN', token);
      console.log(token);
      this._vm.$axios.get('/api/user/login',{
          headers: {
              'Authorization': `Bearer ${token}`
          }
      })
      .then((response) => {
          commit('SET_SESSION_USER', response.data);
          resolve(response);
          
      })            
    });
  }
  else
  {
    this._vm.$axios.post('/api/user/logout').then((response) => {
      console.log(response);
      commit('SET_SESSION_USER', null);
      commit('SET_SESSION_TOKEN', null);
    });
    resolve();
  }  
})

},

router.js

router.beforeEach(async (to, from, next) => {
  
  console.log('Is Authenticated:' +store.getters.isAuthenticated);
  console.log('Session User:' +store.getters.getSessionUser);
  if (to.matched.some(record => record.meta.authRequired) && !store.getters.isAuthenticated) {//!userAuth) {
    next({ path: '/login'})
    } else {
      next();
    }
});

I've searched around and some solutions say to add your firebase onAuthStateChanged in the router. That does allow the route guards to work correctly but that recreated the problem I had before adding that method to main.js - the page flickers with pre-logged in information. I found other possibility that said you could add a promise around the onAuthStateChanged method and call that from the router. That may work but I'm not sure how to implement that with firebase in the main.js file wrapped around the vue app being initialized.

I would like to avoid setting cookies or using localstorage to make this work and I'm wondering if there is some way to get firebase, vuex state and vue route guards to function correctly together.


Solution

  • I figured out a way to do this. Login and logout works correctly, notifications work at login and logout (not at all on refresh), the user is perpetually logged in (onAuthState and token updates should handle the 1 hour timeout), and the page loads only after the firebase login finishes so there is no flash of "Login" instead of the username.

    main.js

    initializeApp(firebaseConfig);
    
    const auth = getAuth();
    
    let app;
    auth.onAuthStateChanged(user => {
      console.log('firedOnAuthStateChanged)')
      store.dispatch("setUser", user).then(() => {
        if (!app) {
          app = new Vue({
            router,
            store,
            vuetify,
            render: h => h(App)
          }).$mount('#app')
        }
      }); 
    });
    

    authentication.js (module from store.js)

    setUser({dispatch, commit}, user) {
        return new Promise((resolve, reject) => {
          if(user)
          {   
            user.getIdToken().then(token => {
              commit('SET_SESSION_TOKEN', token);
              this._vm.$axios.get('/api/user/login',{
                  headers: {
                      'Authorization': `Bearer ${token}`
                  }
              })
              .then((response) => {
                  commit('SET_SESSION_USER', response.data);
                  resolve(response);
                  
               })
              .catch(error => {
                  dispatch('setSnackbar', {
                    color: "error",
                    timeout: 4000,
                    text: 'Failed verifying token on login.'
                  });
                  reject();
              });
                
            });
          }
          else
          {
            this._vm.$axios.post('/api/user/logout').then((response) => {
              commit('SET_SESSION_USER', null);
              commit('SET_SESSION_TOKEN', null);
            });
            resolve();
          }  
        })
      }
    

    router.js

    router.beforeEach((to, from, next) => {
      const auth = getAuth();
      const user = auth.currentUser;
      if (to.matched.some(record => record.meta.authRequired) && !user) {
        next({ path: '/login'})
        } else {
          next();
        }
    });
    

    If you know of a more efficient way of doing this, please let me know.