1

I'm using firebase to do authentication in my vuejs app. In my root (main.js) vue component.

  created() {
    auth.onAuthStateChanged((user) => {
      this.$store.commit('user/SET_USER', user);
      if (user && user.emailVerified) {
        this.$store.dispatch('user/UPDATE_VERIFIED', {
          userId: user.uid,
        });
        // SUCCESSFUL LOGIN
        this.$router.replace('dashboard');
      } else if (user && !user.emailVerified) {
        this.$router.replace('verify');
      }
    });

So essentially, my router navigation guard is checking auth status and doing routing before each route as well.

router.beforeEach((to, from, next) => {
  const currentUser = firebaseApp.auth().currentUser;
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  const allowUnverified = to.matched.some(record => record.meta.allowUnverified);

  if (requiresAuth && !currentUser) {
    next('login');
  } else if (currentUser
              && !currentUser.emailVerified
              && !allowUnverified
              && to.path !== '/verify') {
    next('verify');
  } else if (!requiresAuth && currentUser) {
    next('dashboard');
  } else {
    next();
  }
});

What happens is, when you refresh the page (with a valid auth token), you hit branch 1 of the route guard, stateChanges and the handler is called to redirect to /dashboard. So refreshing while logged in, always brings you to the dashboard route instead of the current route youre on.

How can I handle this case? Adding a beforeEnter guard to each auth component smells bad to me Data Fetching Vue Docs.

Should this be in the store instead of the created hook on the root instance? Any help is greatly appreciated. This pattern is stumping me.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Brandon Deo
  • 4,195
  • 4
  • 25
  • 42
  • I would put the auth logic in a service like module, remove all of the routing from that logic and just handle routing in the router, where you're already doing most of it (in the navigation guard). – Emile Bergeron Apr 03 '18 at 03:53
  • @frank I feel like the firebase tags are irrelevant to this question. – Emile Bergeron Apr 03 '18 at 03:55
  • 1
    I merely replaced the generic [firebase] with the more specific [firebase-authentication], which is what OP is using here. Feel free to make any edits you think improve the chances of it getting answered. – Frank van Puffelen Apr 03 '18 at 04:01
  • @EmileBergeron but routing and auth need to be aware of each other. If auth state goes to "unauthorized" then route needs to go back to login. – Brandon Deo Apr 03 '18 at 04:07

1 Answers1

3

Quick fix

If the user exists and doesn't require authentication, why redirect him to another page (the dashboard)?

  } else if (!requiresAuth && currentUser) {
    next('dashboard');
  } else {
    next();
  }

You could just let the routing continue.

  if (requiresAuth && !currentUser) {
    next('login');
  } else if (requiresAuth && !currentUser.emailVerified && !allowUnverified) {
    next('verify');
  } else {
    next();
  }

You'd also need to remove this.$router.replace('dashboard'); from the onAuthStateChanged callback. See below for a more in depth answer.

In depth

I would avoid putting authentication and routing logic in Vue components. While it may someday makes sense, in this case, it can be totally avoided.

I like to put the Vuex store instance in an isolated module so I can use it elsewhere, not limited to Vue.

import Vue from 'vue';
import Vuex from 'vuex';
// Configuration is generated with a function, so testing the store is easier
import getConfig from './config';

Vue.use(Vuex);

export default new Vuex.Store(getConfig());

So auth logic could be moved to a auth.js service.

import store from './store';
import router from './router';

// other auth related code, you can even export a common API
// to use within your app.
// like this simple wrapper, which hides the fact that you're using firebase
export function getUser() {
  return firebaseApp.auth().currentUser;
}
export default { getUser }

auth.onAuthStateChanged((user) => {
  // store/business logic should be inside the store.
  store.dispatch('user/AUTH_STATE_CHANGED', user);
  // minimal handling of mandatory steps in the login or sign up process
  if (!user) {
     router.push('login');
  } else if (user && !user.emailVerified) {
    router.push('verify');
  }
});

If the user is correctly logged in, there is no reason to redirect here. Since the global navigation guard is going to do most of the redirection work, you could just redirect to the current route with router.go() or router.push(router.currentRoute).

In fact, this navigation guard could be registered inside the auth.js service mentioned above.

router.beforeEach((to, from, next) => {
  const currentUser = getUser();
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  const allowUnverified = to.matched.some(record => record.meta.allowUnverified);

  if (requiresAuth && !currentUser) {
    next('login');
  } else if (requiresAuth && !currentUser.emailVerified && !allowUnverified) {
    next('verify');
  } else {
    next();
  }
});

And then, add simple navigation guards to routes which shouldn't be reachable once logged in.

path: '/login',
beforeEnter: (to, from, next) => {
    if (auth.getUser()) {
        next('dashboard');
    } else {
        next();
    }
},

The point is: only force a redirect when navigating to a restricted page.

When a logged in user refreshes the page, there are no reason to redirect to /dashboard. If that same user would try to navigate to login, then there is a reason to redirect to /dashboard.

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
  • What to do if getUser returned null? This happens when the auth module didn't verify user yet, but `beforeEach` callbacks were called. How to wait for user authentication and showing the load page all this time? – Andru Dec 02 '20 at 07:05