React router v4 - Authorized routes with HOC

Andrej Naumovski picture Andrej Naumovski · Sep 23, 2017 · Viewed 10.5k times · Source

I have a problem to prevent unauthorized users from accessing authorized-only routes/components - such as logged in users dashboard

I have the following code:

import React from 'react'
//other imports
import {withRouter} from 'react-router'

class User extends React.Component {
  constructor(props) {
    super(props)
    console.log('props', props)
    let user = JSON.parse(localStorage.getItem('userDetails'))
    if(!user || !user.user || props.match.params.steamId !== user.user.steamId) {
      props.history.push('/')
    } else {
      this.props.updateUserState(user)
      this.props.getUser(user.user.steamId)
    }
  }

  //render function
}

//mapStateToProps and mapDispatchToProps

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(User))

The router:

render() {
    return (
      <Router>
        <div>
          <Route exact path="/" component={Main}/>
          <Route path="/user/:steamId" component={User}/>
          <Route path="/completelogin" component={CompleteLogin}/>
        </div>
      </Router>
    )
  }

I tried logging to check if the condition is entered and it is, however I get an error from the render function saying it cannot read properties of null.

Is there a way to fix my problem and also a better approach to cater for my requirement? where specific components will be strictly accessible only to authorized users

Answer

Matthew Barbara picture Matthew Barbara · Sep 23, 2017

Warning: The following answer uses React's old context API. If you are using V16.3+, the following answer does not apply to you

Ok, so, according to your logic, unauthorized users are prohibited to access the User component. Simple and fair. No problems with that.

But my concern is that are you checking if the user is logged in inside a component which unauthenticated users should not get into. This is incorrect in my opinion because:

  1. It's an extra journey for our program - adds an extra bit of unnecessary inefficiency. There is a possibility that from the router we go to User component and the latter sends us back to the former. Ping pong.

  2. The User component looks dirty. Having irrelevant logic. Yes irrelevant. Because authentication check should not be done in User component. User component should contain user related stuff.

What do you think if instead of getting inside the User component to check users authentication, we check this in the router? User component, as it name describe, is dedicated for users and the logic to check authentication should be taken out from there.

Ok, Thats cool. But how?

We can create a Higher-Order Component (HOC) which as an argument will take any component was pass to it. Then, we add authentication logic inside the HOC and finally, depending on the logic we use we can either redirect to the homepage or allow the request to the given component.

In order for the HOC to be able to do the above it needs access to:

  1. State. We need to know whether the user is logged in and the state is where we store such data.
  2. Router. We may need to redirect users.

Lets name the HOC required_auth. Here is the code of it:

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

export default function(ComposedComponent) {
    class Authentication extends Component {
        static contextTypes = {
            router: React.PropTypes.object
        }

        componentWillMount() {
            if (!this.props.authenticated) {
                this.context.router.history.push('/');
            }
        }

        componentWillUpdate(nextProps) {
            if (!nextProps.authenticated) {
                this.context.router.history.push('/');
            }
        }

        render() {
            return <ComposedComponent {...this.props} />
        }
    }

    function mapStateToProps(state) {
        return { authenticated: state.auth.authed };
    }

    return connect(mapStateToProps)(Authentication);
}

As you can see, there is no black magic happening here. What might be confusing is

static contextTypes = {
    router: React.PropTypes.object
}

context is similar to props but it allows us to skip levels in our component hierarchy

Because this.context is very easy to access and abuse, React forces us to define the context in this way.

Do not use context unless you really know what you are doing. The use case for context is not that common. Read more on what the consequences could be here

To conclude on our HOC, it simply takes a component as an argument and it either redirect to the homepage or returns the component that we will pass to it.

Now to use it, in route file we import the HOC

 import RequiredAuth from './components/auth/required_auth';

and any routes which we want to protect from non-authoirzed users we simply route it like this:

<Route path="/user" component={RequiredAuth(User)}/>

The line above will either direct to homepage or returns the component which we are passing, User

References: https://facebook.github.io/react/docs/higher-order-components.html https://facebook.github.io/react/docs/context.html