Front-End Insights

Role-based authorization using React-Router

ProgrammingReactTutorial

Based on the content of this article I made a library which contains the React components described below. The library is called react-router-role-authorization and is now available on GitHub and NPM (click the links to drill down to the library). 

Besides user authentication, role-based authorization is one of the most common tasks in every application – in client side apps too. I was faced with this issue recently in the project I’m involved in. I did a search using google and found some articles which described some approaches but most of them were out of date. Some of them were nice but useless with the current version of React and react-router – that is why I decided to utilise them and create my own approach which is applicable to the most recent versions of these libraries.

What I wanted to achieve

Ok, before I start describing my solution, I will show you what I wanted to achieve. First of all I wanted to limit the access to some routes to specific roles. For example, if we have a route /admin/profile we may want to prevent users with only an employee role to open this route. In such situations, the user should be redirected to some error page (e.g. 404).

The second thing I had to deal with was the home page which has the same address (route) for all types of user roles. The thing was that different roles have access to different functionality so I wanted to show different components on the home page.

I think these two requirements might be common in many applications so it’s good to find an efficient way to resolve these issues. Let’s see how I did that.

Limiting access to the route for specific roles

Ok, let’s imagine we have created routing (using react-router) like below:

<Router history={history}>
 <Route component={MainContainer} path="/">
   <IndexRoute component={Home} />
   <Route component={Profile}>
     <Route component={PhotosList} path="/profile/photos" />
   </Route>
   <Route component={EmployeesManagement}>
     <Route component={EmployeesList} path="/employees/list" />
     <Route component={EmployeeAdd} path="/employees/add" />
   </Route>
 </Route>
</Router>

Now we want to make the Profile route (and all its child routes) available only for users with the employee role, and the EmployeesManagement route available for users with the admin role.

To do so, we can use utilise the fact that when we configure routes we can pass, besides standard ones, additional attributes and they will be accessible later in the component. Please see below how I added an additional parameter authorize to the root routes of the application:

<Router history={history}>
  <Route component={MainContainer} path="/">
    <IndexRoute authorize={['employee', 'admin']} component={Home} />
    <Route authorize={['employee']} component={Profile}>
      <Route component={PhotosList} path="/profile/photos" />
    </Route>
    <Route authorize={['admin']} component={EmployeesManagement}>
      <Route component={EmployeesList} path="/employees/list" />
      <Route component={EmployeeAdd} path="/employees/add" />
    </Route>
  </Route>
</Router>

Ok, as you may have noticed I’ve added my custom authorize attribute to all three main routes: the “home” route is available for both employee and admin roles, the “profile” route will be available only for employees, and the “employees management” route will be available for admins.

But this is not all – we still have to check these passed roles with roles given by the server during the login process. To do that, let’s create a new React class – we can call it AuthorizedComponent.js:

import React, { PropTypes } from 'react';
import _ from 'lodash';

class AuthorizedComponent extends React.Component {
  static propTypes = {
    routes: PropTypes.array.isRequired
  };

  static contextTypes = {
    router: PropTypes.object.isRequired
  };

  componentWillMount() {
    const { routes } = this.props; // array of routes
    const { router } = this.context;

    // check if user data available
    const user = JSON.parse(localStorage.getItem('user'));
    if (!user) {
      // redirect to login if not
      router.push('/login');
    }

    // get all roles available for this route
    const routeRoles = _.chain(routes)
      .filter(item => item.authorize) // access to custom attribute
      .map(item => item.authorize)
      .flattenDeep()
      .value();

    // compare routes with user data
    if (_.intersection(routeRoles, user.roles).length === 0) {
      router.push('/not-authorized');
    }
  }
}

export default AuthorizedComponent;

First of all, please note the two static objects declared at the beginning – these are propTypes and contextTypes which tell React that the component expects routes and router objects in the this.props and this.context objects.

The core functionality of this component is placed inside the componentWillMount method. As you can see, we get a routes array from the this.props object. This array contains information about all routes that match the current address: e.g. if we are opening the /profile/photos route, this array contains information about routes: “main container”, “profile” and “photo list”.

What is most important here is that objects of the routes array may contain our custom authorize property – we will use it later in this method to compare the value of this property with the roles stored in the user object retrieved during authentication (please see line number 25 – getting all available roles for the current route; and later, line number 32 – comparing it with user roles).

Ok, we have the component. It’s a good time to use it. To do that we have to take the container components which are attached to the routes with our additional authorize attribute and inherit them from AuthorizedComponent. As an example, let’s have a look at the implementation of the Profile component:

import React from 'react';
import RouteHandler from '../../shared/RouteHandler.react';
import AuthorizedComponent from '../../shared/authorization/AuthorizedComponent.react';

class Profile extends AuthorizedComponent {
  render() {
    return (
      <div className="pure-g profile-container">
        <RouteHandler {...this.props} />;
      </div>
    );
  }
}

export default Profile;

As you can see, this is a very simple container component which shows different children components depending on the current route (the RouteHandler component takes care of it – this is a subject for another blog post). By inheriting the Profile class from our new AuthorizedComponent we can easily prevent showing all its child routes if they are not available for the current user’s roles.

Showing components depending on user roles

Now it’s time to discuss the second task I wanted to deal with. As I mentioned before, I have a home page which has the same address for users with different roles. And users with different roles should see different components on the home page. Let’s see how I did this task!

This time we will use inheritance again. First, I’ve created the base class which I called RoleAwareComponent:

import React  from 'react';
import { names, getAuthData } from '../../../common/helpers/storage/authStorageHelper';
import _ from 'lodash';

class RoleAwareComponent extends React.Component {
  constructor(props) {
    super(props);
    this.authorize = [];
  }

  shouldBeVisible() {
    const user = JSON.parse(localStorage.getItem('user'));
    if (user) {
      return _.intersection(this.authorize, user.roles).length > 0;
    }

    return false;
  }
}

export default RoleAwareComponent;

As you may have noticed we have a constructor here where we initialize the this.authorize property – this property contains roles which allow the component to be shown and should be overridden in the component inheriting from this one.

Beside the constructor, the component has only one method shouldBeVisible. This method compares the roles from the this.authorize property with the roles of the user.

Now, let’s see a sample usage of this base component:

import React from 'react';
import RoleAwareComponent from '../../../shared/authorization/RoleAwareComponent.react';

class PhotoBox extends RoleAwareComponent {
  constructor(props) {
    super(props);

    // component will be visible for the roles below:
    this.authorize = ['employee'];
  }

  render() {
    const jsx = (
      <div className="pure-u-13-24 box photo-box">
        <div className="box-wrapper">
          <h1>Your photo</h1>
          <img src="http://some.url/img1.jpg" />
        </div>
      </div>
    );

    return this.shouldBeVisible() ? jsx : null;
  }
}

export default PhotoBox;

To make it work we have to do few things: first we have to inherit from our RoleAwareComponent class; next we have to create a constructor and set up the this.authorize property to tell the component for which roles it should be visible; the last thing is to just use the this.shouldBeVisible method to decide if the component will render the JSX or not.

Summary

Simple right? 😉 This is my own solution for the mentioned issues. I wonder what you think about my approach? Perhaps some of you can propose a more sophisticated solution to resolve these problems? If so, please let me know in the comments below!

Related Post

I recommend Nozbe

Simply Get Everything Done

Get your tasks and projects done thanks to Nozbe system and apps for the Mac, Windows, Linux, Android, iPad and iPhone.

If you want to know more about Nozbe and its capabilities, please take a look at my article about this great application.


  • Cyril Quandalle

    Hi,

    Great article, did you integrate it in a github project ?

  • Champion Roy

    Hi, great article really! But what is RouteHandler component? I have no idea about this stuff.

    • First of all – thanks 😉

      RouteHandler is the wrapper I took from este – https://github.com/este/este

      Here’s its implementation:

      export default class RouterHandler extends React.Component {
      static propTypes = {
      children: PropTypes.object
      };

      render() {
      const { children } = this.props;

      // No children means nothing to render.
      if (!children) {
      return null;
      }

      // That makes nested routes working.
      const propsWithoutChildren = { …this.props };
      delete propsWithoutChildren.children;

      return React.cloneElement(children, propsWithoutChildren);
      }
      }

      So, as you can see it helps cloning elements without children inside props object.

      • Champion Roy

        Thank you very much 🙂

  • Prakhar Singh

    React router Link component breaks AuthorizedComponent and the user can visit the link.

  • Aseem Gupta

    Can you confirm me that though a user cannot access but he can see those routes which are not accessible to him from frontend JS.

  • Erich K

    Couldn’t a malicious user modify their role in local storage and break your front-end authorization?

    • of course, that’s why you always have to double check roles: on the front-end side to display appropriate view and on backend to keep everything safe