Blog

Creating an Authentication Navigation Guard in Vue

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!

Categories: Blog

Tags: , ,

Laurie Barth
08 Oct, 2019