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 toto
.next(false)
this sends the user back tofrom
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).