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).
Top comments (5)
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).
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 therouter.routes
property which contains abeforeEnter
property for each route. Instead of populating ametadata.requiresAuth
, have this instead:We're doing a bunch of things here:
vue-router-multiguard
, which expects a list of async callback functions usingnext
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:
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: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.
Hey Laurie, I use the same mechanism to authenticate in my projects. Thanks for sharing.
Awesome post thank you!!