loading...
Cover image for Creating an Authentication Navigation Guard in Vue

Creating an Authentication Navigation Guard in Vue

laurieontech profile image Laurie Originally published at tenmilesquare.com Updated on ・4 min read

So, you've built a login page and authentication! You route everyone there before they can go anywhere else on your site. But what happens if they just type another path in the url? If they're unauthenticated, can they still get in?

😳 Oops. That's not very secure at all.

What we really want, is to make sure they're always sent to the login page, no matter where they try to go, as long as their unauthenticated. A great way to do this in Vue, is to use a navigation guard.

Whenever a user on your site attempts to route to a page, you know about it. A navigation guard enables you to introduce a logic check at that point. And then you decide whether the user is allowed to go to their destination, or if they have to go somewhere else.

The set up

Let's assume we have a router all set up called router. If you haven't done that before the docs are wonderful.

We've wired it up and defined some routes. Now what?

The Skeleton

To start, know that there are multiple navigation guard functions available to us. In this case, we will use beforeEach which fires every time a user navigates from one page to another and resolves before the page is rendered.

We link the function up to our router. We pass three arguments to the function. The route they're attempting to go to, the route they came from and next.

router.beforeEach((to, from, next) => {
})

Next

next is actually a function and it's very interesting. next has to be called in order to resolve our guard. So every logic path needs to hit next in some way.

There are multiple ways to call next, but I want to point out three.

  • next() sends you to the next set of logic. If there isn't any, the navigation is confirmed and the user gets sent to to.

  • next(false) this sends the user back to from and aborts their attempted navigation.

  • next(<route>) this sends the user elsewhere, wherever you determine that is.

We're going to make use of the first and last options in our navigation guard.

Our logic

Ok, so now we need to determine in what circumstances we're sending the user one place or the next. In our case, we want to check for authenticated users. However, not all of our pages require you to be authenticated. We can define that in our route metadata so we know if we care about checking or not.

const routes = [
  {
    path: '/',
    component: Home,
    meta: {
      requiresAuth: false,
    },
  }
]

That means that the first thing we want to look at is whether our to route requiresAuth.

If it does, we have more to write. If it doesn't, we've decided the user can navigate there, so we'll call next(). In this case, nothing follows that call, so next() will confirm the navigation.

router.beforeEach((to, from, next) => {
  if (to.matched.some((record) => record.meta.requiresAuth)) {
  } else {
     next()
  }
})

As it turns out, this works just fine without the else, and just letting next() be the catch-all. However, it causes problems later.

Our check

Now we're adding the last piece of the puzzle. If requiresAuth is true, then we want to check if our user is authenticated.

Note that we're not showing the implementation of isAuthenticated. This can be any number of things. We're just making the assumption we have a way to check.

If our user is authenticated, we want to confirm the navigation. Otherwise, we'll send them to the login page.

router.beforeEach((to, from, next) => {
  if (to.matched.some((record) => record.meta.requiresAuth)) {
    if (isAuthenticated()) {
      next()
    } else {
      next('/login')
    }
  } else {
    next()
  }
})

Minor refactor

To be honest, the implementation below is a bit cleaner. No need to call next() twice, less if/else logic. But for some reason I've never liked checking on a false case, it just seems a bit confusing. However, others may feel differently, so know that this is also an option.

router.beforeEach((to, from, next) => {
  if (to.matched.some((record) => record.meta.requiresAuth)) {
    if (!isAuthenticated()) {
       next('/login')
    }
  } else {
     next()
  }
})

My Rabbit Hole

Initially, I had code that looked like this. And it works just the same! But I couldn't figure out the return piece of the puzzle and why I needed it. So I wanted to explain.

router.beforeEach((to, from, next) => {
  if (to.matched.some((record) => record.meta.requiresAuth)) {
    if (isAuthenticated()) {
      next()
      return
    }
    next('/login')
  }
  next()
})

You could also write return next(), whichever you prefer.

next() only confirms the navigation if there are no hooks left in the pipeline. Since there were no else statements, only fall through behavior, next() didn't confirm anything, it just sent you to the "next" thing.

That didn't matter for records that didn't require auth, because you were sent to the final next() which was the end of the road. But for authenticated users, they'd always end up on the login page. So in order to make it work, the return is needed. It prevents the code that follows from being executed and confirms the navigation.

Thanks to a conversation with Eduardo (posva on twitter), a Vue maintainer, I wanted to add this clarification. There should only be one instance of next, in any of its forms, for each logical path through your navigation guard code block. Being able to hit it more than once will cause errors and bugs.

Conclusion

And that's it! We've built a navigation guard to check authentication for all our pages. Navigation guards, and vue-router in general, are incredibly powerful. There are tons of other things you can do and other options for how you accomplish it. Check out the docs and play around!

If you're interested in learning even more about vue-router I'm doing a live workshop on Oct 22nd! Use this link for early bird pricing that expires Monday (Oct 14th).

Discussion

pic
Editor guide
Collapse
krusenas profile image
Karolis

Hi! :) I pretty much always use this library for Vue to enable JWT auth github.com/websanova/vue-auth. Requires very little boilerplate on the UI side and just several APIs on the backend side. It manages token refresh, gets user info and performs login/logout. So far haven't noticed any problems (~2-3 years in prod).

Collapse
wparad profile image
Warren Parad

While this definitely works, it creates a bit of a monolith piece of code where your router.beforeEach hook now needs to know about the guards for each route. Rather than doing that I would suggest instead do use the functionality of the router.routes property which contains a beforeEnter property for each route. Instead of populating a metadata.requiresAuth, have this instead:

{
  path: '/team-health',
  name: 'TeamHealth',
  beforeEnter: multiguard([authManager.requireAuth(), teamManager.requireTeamAccess()]),
  component: TeamHealth
}

We're doing a bunch of things here:

  • Using vue-router-multiguard, which expects a list of async callback functions using next
  • custom guards for each route

The Team Health route (for Teaminator) both requires the user to be logged in, but also that they have a valid team. It's possible that they got removed or haven't selected a team yet, in which case we'll redirect them somewhere else.

But this also allows other routes to look like this:

{
  path: '/support',
  name: 'Support',
  component: Support,
  beforeEnter: authManager.attemptAuth()
}

On our support route, we'll attempt the login, but not require it. Since it is a great UX to let the user report problems for their account, we attempt to logi the user in, but it is also important to let them report problems without needing to login in case our authorization service is down for some reason.

Just in case you were curious, our requireAuth function looks like this:

requireAuth() {
  return async (to, from, next) => {
    const options = { from: from.fullPath, to: to.fullPath, route: to.fullPath };
    try {
      const result = await this.authProvider.getClient().ensureLoggedIn(options);
      if (!result || !result.success) {
        next(false);
        return;
      }

      // Change the global location
      if (result.path) {
        next(false);
        window.location.replace(result.path);
        return;
      }

      this.store.commit('setinvalidTokenCalls', 0);

      // Otherwise only change the local location
      if (result.route) {
        next({ path: result.route });
        return;
      }

      // If there is no redirect set, just continue
      next();
    } catch (error) {
      this.logger.error({ title: 'User login error', exception: error });
      this.alertHandler.createAlert(
        'Login issue',
        `We are having trouble logging you in.
         If this continues please contact support@teaminator.io`,
         null, 'danger', 3000);
      next(false);
    }
  };
}
Collapse
laurieontech profile image
Laurie Author

Definitely another way to do it.

So I had a chance to review this more closely, and it's a wonderful solution! But the constraints are slightly different than the solution I presented.

I point this out because I think that's the most important piece. Understand what your use case is, and determine the solution that makes sense. Yours is going to help a lot of people! As, I hope, will mine. And it's necessary that they look closely at their requirements when selecting their solution of choice.

Collapse
iwpgeek profile image
Mohammed Imtiyaz

Hey Laurie, I use the same mechanism to authenticate in my projects. Thanks for sharing.

Collapse
angelguerrero profile image
Ángel Guerrero

Awesome post thank you!!