code, travel & business

User authorization with React/Redux

Here's the problem. You want to prevent access to parts of your app for users that are not logged in, and additionally you want to prevent logged in users without the right role or access level (non-admins typically) from accessing yet more parts of your app. You are using react, redux and maybe even react-router (and its trusty companion react-router-redux).

There will probably be many slightly different ways to solve this problem, but here's one I've had success with.

Overview of solution

The idea is to create a Component that will render its children if the current user has the correct role, and then write a helper function that wraps another Component (typically a page) for more convenient usage with react-router.

The HasRole Component

We will start out with a Component that wont render its children unless the currently logged in User has the correct role. I've chosen to call this piece of work the HasRole component and it will be connected to the Redux store where the user role is expected to be available somewhere.

As a quick aside, for my most recent project I'm storing some info about the current user in a property called ui which contains the state of the user interface. This property gets fetched every time the user reloads the page so that the basic view state always remains the same, and I might write an article about why I think this is a good idea at a later time.

Now back to the HasRole component, which I've put in a file called HasRole.js and made to look like this:

import React, { Component } from 'react';
import propTypes from 'prop-types';
import { connect } from 'react-redux';

class HasRole extends Component {

  static propTypes = {
    currentUserRole: propTypes.string.isRequired,
    requiredRole: propTypes.string.isRequired
  }

  render() {
    const { children, currentUserRole, requiredRole } = this.props;
    if(currentUserRole !== requiredRole) return null;
    return (
      <div className={className}>
        {children}
      </div>
    );
  }

}

const getMapStateToProps = (extendWith = {}) => state => {
  const ui = state.ui || {};
    return {
    currentUserRole: ui.self && ui.self.role ? ui.self.role : 'nobody',
    ...extendWith
  };
};

export default connect(getMapStateToProps())(HasRole);
export const IsAdmin = connect(getMapStateToProps({requiredRole: 'admin'}))(HasRole);
export const IsUser = connect(getMapStateToProps({requiredRole: 'user'}))(HasRole);

I've added some helpers at the bottom that hardcodes the roles that I have in my application. This helps to avoid mistakes with filling in the correct properties and allows me to keep my code short and expressive.

HasRole (and companions IsAdmin and IsUser) can now be used like this:

import React, { Component } from 'react';
import HasRole, { IsAdmin, IsUser } from './HasRole';

class HasRoleDemonstration extends Component {
  render() {
        return (
            <div>
              Anyone can see this
                <IsAdmin>Only logged in admins can see this</IsAdmin>
                <IsUser>Only logged in users can see this</IsUser>
                <HasRole requiredUserRole="admin">Only can admins can see this too...</HasRole>
            </div>
        );
    }
}

This gets us most of the way and is perfect for quickly hiding/revealing stuff in your user interface. However, it does not solve the problem protecting entire pages, which we will now have a look at.

A helper for the router

Sometimes you will be looking for a way to restrict access single pages or subsets of pages based on how they are routed. We are now going to explore a way to make defining these restricted routes easy, preferably as part of the actual definition of the route instead of within the render function of the page.

When using react-dom along with react-router and react-router-redux, you might have a ReactDOM.render function that ends up looking something like this:

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route path="/" component={pages.Home} />
      <Route path="/login" component={pages.Login} />
      <Route path="*" component={pages.ErrorNotFound} />
    </Router>
  </Provider>,
  document.getElementById('root')
);

Now suppose you wanted to add a protected route only for logged in users at /protected here, with an additional page only for admins at /protected/admins. How can we use the concepts from our HasRole component from earlier to accomplish this?

The easy solution would be to just wrap the render function for these pages in IsAdmin or IsUser and be done with it. This will work, but would show a completely blank page to the non-authorized user trying to open it which is less than ideal.

A better solution would be to create a wrapping function that receives the page component and returns a wrapped component for use in the router, that itself does one of the following three things:

  1. Redirects non-logged in users to the login page
  2. Shows a "Forbidden" page (pages.Forbidden) for authenticated users that are not authorized to view the admin page.
  3. Allows the page to be rendered for users with the correct role (admins).

Here is my idea of a component like that, and I've chosen to call it RequireRole.

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { browserHistory } from 'react-router';
import propTypes from 'prop-types';

export class RequireRoleBase extends Component {

  static propTypes = {
    isLoggedIn: propTypes.bool.isRequired,
    currentUserRole: propTypes.string.isRequired,
    requiredRole: propTypes.string
  }

  ensureAuth(props) {
    const { isLoggedIn, isRehydrated } = props;
    if(!isRehydrated) {
      return false;
    }
    if(!isLoggedIn) {
      // send to login
      browserHistory.push('/login');
    } else if(!this.hasRequiredRole(props)) {
      // send to forbidden page
      browserHistory.push('/forbidden');
    }
    return true;
  }

  hasRequiredRole({ requiredRole, currentUserRole }) {
    return !requiredRole || requiredRole === currentUserRole;
  }

  componentWillReceiveProps(props) {
    this.ensureAuth(props);
  }

  componentDidMount() {
    this.ensureAuth(this.props);
  }

  render() {
    const { isLoggedIn, children, isRehydrated } = this.props;
    if(!isRehydrated || !isLoggedIn || !this.hasRequiredRole(this.props)) {
      // don't accidentally render anything
      return null;
    }
    return <div>{children}</div>;
  }

}

You might notice that the base component RequireRoleBase looks and functions similar to HasRole from earlier with some important distinctions. Firstly, instead of not rendering when the user does not have the correct role, it uses browserHistory from react-router to redirect to either /login when the user is not logged in, or /forbidden when the logged in user does not have the correct role.

Secondly, if the prop requiredRole has a falsy value, such as an empty string '', it will render the child components. The reason for this is to enable the component to be used just as a check if the user is logged in our not, regardless of which role the user has.

Thirdly, a new variable isRehydrated has been introduced that will be explained shortly. This boolean variable acts as a guard to prevent the user from being redirected before the redux state has settled.

It also takes extra care not to render any children when the above conditions have not been met, since doing so may trigger additional errors in the application (such as requests failing on the server side due to insufficient privileges) which we would like to avoid.

However, this is still not enough to use as a drop in in the router definition. To accomplish that, we need to export a function that wraps around the component of the page you are actually trying to protect. Adding to the end of RequireRole.js:

const mapStateToProps = state => {
  const auth = state.auth || {};
  return {
    isRehydrated: auth.isRehydrated,
    isLoggedIn: auth.isLoggedIn,
    currentUserRole: auth.self && auth.self.role ? auth.self.role : 'nobody'
  };
};

const RequireRoleConnected = connect(mapStateToProps)(RequireRoleBase);

export const RequireRole = (WrappedComponent, requireRoleProps = {}) => {
  return function(props) {
    return (
      <RequireRoleConnected {...requireRoleProps}>
        <WrappedComponent {...props} />
      </RequireRoleConnected>
    );
  };
};

We first connect to the store, expecting the role of the current user to be available, but also two boolean values isLoggedIn and isRehydrated. The first one has an obvious meaning, the second one relates to redux-persist which is the library I'm using to save the a part of the state of the redux store to localStorage in the browser.

How to setup redux-persist will be explained in a future post, but isRehydrated is in any case set to true when the saved state in localStorage has been loaded back into the application state. This is important because the state is empty on page reload, and rendering this component just after that event will cause a redirect to /login even if the user is actually logged in. Thus we need to wait for the state to be loaded back before we take any action with the router.

Now we are ready to use this component in our routing definition file, resulting in some quite clean code in my own opinion.

Don't forget to setup a route for the 403 Forbidden page also!

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route path="/protected" component={RequireRole(pages.ProtectedArea)}>
        <Route path="/admin" component={RequireRole(pages.ProtectedAdminArea, {requiredUserRole: 'admins'})}
      </Route>
      <Route path="/" component={pages.Home} />
      <Route path="/login" component={pages.Login} />
      <Route path="/forbidden" component={pages.ErrorForbidden} />
      <Route path="*" component={pages.ErrorNotFound} />
    </Router>
  </Provider>,
  document.getElementById('root')
);

Using the above code, any attempt to open /protected will redirect the unauthenticated user to the /login page. Users that are authenticated can open /protected, but they must be authenticated as admins if they wish to open /protected/admin, or else they will be redirected to /forbidden - just like we wanted.

That is all for today. I hope you enjoyed this article, and some follow ups explaining some of the concepts above will be explored on in later articles!

Leave a Reply

Your email address will not be published. Required fields are marked *

Creating swap space on Ubuntu

Here's a quick intro to swap space and how you can create new swap space on Ubuntu 16.04. It will most likely work verbatim on other Ubuntu versions, as well as other distributions of GNU/Linux too.

The Philippines ’13

I visited the Philippines in the end of 2013. The weather was mostly overcast the five weeks I spent there, and it was quite difficult to get good photos. Here's what came out okay.