Niels Stubbe

Niels Stubbe

Developer

© 2020

Route guards with roles in Vue.js

Role-dependant route guards in Vue router

When creating a web application, you’re often confronted with the need to have users in multiple roles, with access to different pages. In case of a blog, we might have the following roles:

  • User: can read and comment on posts.
  • Editor: is able to create and edit posts.
  • Admin: can view reports and manage users.

In this article I’m going to show you how to block users without the required roles from accessing pages using a very simple and flexible route guard setup.

A word of warning: I’m assuming you already have a basic Vue application setup with Vue router and Vuex. I’m going to retrieve user info from our Vuex store as if a user is already logged in. How to set up and use Vuex, and how to login users are outside the scope of this article.

Getting started

First of all, we’re going to create a few routes in our router.js file. In the example below I started from the default router template and added several pages. I’ve added comments to the router on who I want to be able to access each of these routes.

import Vue from "vue";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const routes = [
  // Anyone should be able to visit these paths
  {
    path: "/",
    name: "Home"
  },
  {
    path: "/blog",
    name: "Blog"
  },
  {
    path: "/login",
    name: "Login"
  },
  // All logged in users are allowed to comment
  {
    path: "/blog/comment",
    name: "Comment"
  },
  // Only users with the Editor role are allowed here
  {
    path: "/blog/create",
    name: "Create"
  },
  // Pages under the /admin/ path are only accessible to those with the Admin role
  {
    path: "/admin/reports",
    name: "Reports"
  },
  {
    path: "/admin/users",
    name: "Users"
  }
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

export default router;

Currently all these routes are wide open. To shield them from unwanted visitors, we need to create guards.

Adding guards

To add router guards, we need to use router.beforeEach. This code-block will execute every time a user tries to navigate to any of your routes.

import Vue from "vue";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home"
  },
  {
    path: "/blog",
    name: "Blog"
  },
  {
    path: "/login",
    name: "Login"
  },
  {
    path: "/blog/comment",
    name: "Comment"
  },
  {
    path: "/blog/create",
    name: "CreateBlog"
  },
  {
    path: "/admin/reports",
    name: "Reports"
  },
  {
    path: "/admin/users",
    name: "Users"
  }
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

router.beforeEach((to, from, next) => {
  // Guard logic goes here
});

export default router;

Now how do we guard a route? Below is an example. We simply write some logic to determine if a user can continue. If they can, we call next(). If they can’t, we’ll redirect them to the login page.

router.beforeEach((to, from, next) => {
  if (userMeetsOurRequirements) {
    next();
  } else {
    next({ name: "Login" });
  }
});

IMPORTANT: when next() is called the rest of the code will still be executed! Below is an example of what NOT to do:

// DO NOT DO THIS !
// Every time you call your router, you will be directed to the login route!
router.beforeEach((to, from, next) => {
  if (userMeetsOurRequirements) {
    next();
  }
  next({ name: "Login" });
});

Require a user to be authenticated before accessing a route

Now that we understand how we can set up our guards, let’s get started on properly configuring them. First I’m going to check if the user is logged in.

If we look at our list of routes, we see we have three routes that anyone can visit, but the rest should be guarded. To differentiate between the guarded and un-guarded routes, I’m going to use the meta property of a route.

The meta property allows you to add variables and objects to a route. If we add a requiresAuth boolean to each route, we can easily use that to know if our user needs to be logged in.

Here’s how my routes look after adding the requiresAuth property:

const routes = [
  {
    path: "/",
    name: "Home",
  },
  {
    path: "/blog",
    name: "Blog"
  },
	{
    path: "/login",
    name: "Login"
  },
	{
    path: "/blog/comment",
    name: "Comment"
		meta: { requiresAuth: true}
  },
  {
    path: "/blog/create",
    name: "CreateBlog"
		meta: { requiresAuth: true}
  },
  {
    path: "/admin/reports",
    name: "Reports"
		meta: { requiresAuth: true}
  },
  {
    path: "/admin/users",
    name: "Users"
		meta: { requiresAuth: true}
  }
];

Alright, we now know which routes require you to be authenticated. Next, we’re going to write our guard logic. Again remember I’m retrieving user data from Vuex.

router.beforeEach((to, from, next) => {
  // Get isAuthenticated from Vuex store
  const userIsAuthenticated = store.state.auth.isAuthenticated;

  // Check if the requested page has requiresAuth === true.
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);

  // If the route requires authentication, check if the user is authenticated. If they are authenticated or the page requires no authentication, continue to the requested page. Else go to the login page.
  if ((requiresAuth && userIsAuthenticated) || !requiredAuth) {
    next();
  } else {
    next({ name: "Login" });
  }
});

Good, now anonymous visitors can’t acces our guarded pages anymore. If they do, they’ll be redirected to our login page.

Make sure users have the right role

Finally we want to make sure everyone can only access the pages they should be accessing. We’re going to add an array with required roles to the meta property.

const routes = [
  {
    path: "/",
    name: "Home",
  },
  {
    path: "/blog",
    name: "Blog"
  },
	{
    path: "/login",
    name: "Login"
  },
	{
    path: "/blog/comment",
    name: "Comment"
		meta: { requiresAuth: true}
  },
  {
    path: "/blog/create",
    name: "CreateBlog"
    meta: { requiresAuth: true, requiredRoles: ["editor, admin"] }
  },
  {
    path: "/admin/reports",
    name: "Reports"
    meta: { requiresAuth: true, requiredRoles: ["admin"] }
  },
  {
    path: "/admin/users",
    name: "Users"
    meta: { requiresAuth: true, requiredRoles: ["admin"] }
  }
];

Notice I didn’t add any required roles to the Comment page. Anyone who is authenticated should be able to access that page. The CreateBlog page is also accessible by both Admin and Editor roles.

Finally we add the required logic to our guard. I put the code that checks if the user has the required roles, in a seperate function called hasRequiredRoles.

const hasRequiredRoles = to => {
  // Get the required roles array from the route
  const requiredRoles = to.meta.requiredRoles;
  // Get the roles the current user has
  const userRoles = store.state.auth.roles;

  // Are there any required roles?
  if (requiredRoles.length > 0) {
    for (let role of requiredRoles) {
      // If the user has any of the required roles, return true.
      if (userRoles.includes(role)) {
        return true;
      }
    }
    // If no roles are required, return true
  } else return true;
};

router.beforeEach((to, from, next) => {
  const userIsAuthenticated = store.state.auth.isAuthenticated;
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);

  // If the user has all requirements we configured in our meta property, let them through
  // Else, send them to the login page
  if (
    (requiresAuth && userIsAuthenticated && hasRequiredRoles(to)) ||
    !requiresAuth
  ) {
    next();
  } else next({ name: "Login" });
});

Conclusion

Our complete router.js file now looks like this:

import Vue from "vue";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
  },
  {
    path: "/blog",
    name: "Blog"
  },
	{
    path: "/login",
    name: "Login"
  },
	{
    path: "/blog/comment",
    name: "Comment"
		meta: { requiresAuth: true}
  },
  {
    path: "/blog/create",
    name: "CreateBlog"
    meta: { requiresAuth: true, requiredRoles: ["editor"] }
  },
  {
    path: "/admin/reports",
    name: "Reports"
    meta: { requiresAuth: true, requiredRoles: ["admin"] }
  },
  {
    path: "/admin/users",
    name: "Users"
    meta: { requiresAuth: true, requiredRoles: ["admin"] }
  }
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

const hasRequiredRoles = to => {
  const requiredRoles = to.meta.requiredRoles;
  const userRoles = store.state.auth.roles;

  if (requiredRoles.length > 0) {
    for (let role of requiredRoles) {
      if (userRoles.includes(role)) {
        return true;
      }
    }
  } else return true;
};

router.beforeEach((to, from, next) => {
  const userIsAuthenticated = store.state.auth.isAuthenticated;
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);

  if ((requiresAuth && userIsAuthenticated && hasRequiredRoles(to)) ||
    !requiresAuth
  ) {
    next();
  } else next({ name: "Login" });;

export default router;
});

As you can see, it’s quite simple to set up guards for your Vue application based on roles. Obviously this isn’t the only way to implement this. E.g.: you could simply check if the path matches /admin/ to require the Admin role. However, the current setup allows both flexibility and control on a route-per-route level which I personally prefer.

Hopefully this article has been of use to you. If you notice any mistakes or have any questions, feel free to reach out to me.