React HOC to check for authorized user (protected routes)


#1

Wondering if anyone has an example or pointers on putting together a Higher Order Component that would be used to protect routes? It would check (via a network call, not from cache) for the current user, and it not found, would redirect (to /login or wherever)


#2

Hi, @mwickett, in this awesome post (https://crysislinux.com/limit-access-to-redux-apps-with-higher-order-components/) you can find what you are looking for, and I’m going to give an example of how I have been making my route protector HOC using React + Apollo.

First, you’ll need to have a query to get the current user, in my case I have saved that query in a file named CurrentUser.js

// CurrentUser.js
import gql from 'graphql-tag';

export default gql`
  query {
    user {
      id
      name
    }
  }
`;

Then now we’re going to code the Route Protector HOC, and RouteProtector.js is the file’s name that contains our HOC.

// RouteProtector.js
// See https://crysislinux.com/limit-access-to-redux-apps-with-higher-order-components/

// Dependencies
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { graphql } from 'react-apollo'
import { withRouter } from 'react-router'

// Components 
import Loading from './Loading'

// Queries
import CurrentUser from './CurrentUser';

export default (ProtectedRoute) => {
  class AuthHOC extends Component {

    constructor(props, context) {
      super(props, context);
    };

    // Check if there is validated user logged
    isLoggedin = () => {
      return this.props.Authorization.user
    };

    // Check if the Authorization query is loading
    isLoading = () => {
      return this.props.Authorization.loading
    };
    
    render () {
      // Return a Loading component while the isLoading function is 'true'
      if (this.isLoading()) {
          return <Loading/>
      }
      // Pass the received 'props' and created functions to the ProtectedRoute component
      return (
        <ProtectedRoute
          {...this.props}
          isLoggedin={this.isLoggedin}
          isLoading={this.isLoading}
        />
      )
    }
  }

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

  return graphql(CurrentUser, {
    name: 'Authorization',
    options: {fetchPolicy: 'network-only'}
  })(withRouter(AuthHOC));
};

In the current step you need to have a router file, let’s name it as router.js. Then, you will protect the Page route components using withAuthorization HOC, our route protector.

// router.js
// Dependencies
import React from 'react';
import { Route, Switch } from 'react-router-dom';

// Components
import withAuthorization from './RouteProtector.js'
const { Page1, Page2, SiginPage } = ./Components.js

export default () => (
  <Switch>
    <Route path='/route1' component={withAuthorization(Page1)}/>
    <Route path='/route2' component={withAuthorization(Page2)}/>
    <Route path='/signin' component={SiginPage}/>
  </Switch>
);

And finally in your Page route components that need authorization, the render function could be something like this:

  ...
  render () {
    // Redirect if no user is logged in
    if (!this.props.isLoggedin()) {
      console.log('Only logged in users can see this page');
      this.context.router.history.push({
        pathname: '/signin'
      });
    }
    ...
  }
  ...

In the case of there isn’t a logged user, redirect to /signin page using the isLoggedin function that was received from withAuthorization HOC.


#3

Instead of a HOC I’m using a custom Route component from React Router v4 inspired from the docs .

// PrivateRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';

const PrivateRoute = ({ component: Component, currentUser, ...rest }) =>
  <Route
    {...rest}
    render={props =>
      currentUser
        ? <Component currentUser={currentUser} {...props} />
        : <Redirect
            to={{
              pathname: '/login',
              state: { from: props.location }
            }}
          />}
  />;

export default PrivateRoute;

And in my app I’m using it like this instead of a normal Route component .

// App.js
...
import PrivateRoute from './components/PrivateRoute';
...

const App = ({ data: { loading, error, user } }) => {
  ...
  return (
    <div>
       <PrivateRoute path="/protected" currentUser={user} component={Protected} />
    </div>
  )
}

const currentUserQuery = gql`
  query currentUser {
    user {
      id
    }
  }
`;

export default graphql(currentUserQuery, {
  options: { fetchPolicy: 'network-only' }
})(App);
`;

#4

Good solution @bogdan, follow or be inspired by the docs always is nice. In fact, in my project, I have started something like your approach to solving authorization issues in my routes, but I also needed to protect some others kind of components, not just route components.

In the way that I have exposed, using the RouteProtector.js as withAuthorization HOC you can protect any kind of components, not only routes, is a generic solution to authorization issues.

However, your solution, using the <PrivateRoute/>, we always will get a <Route/> as a return, so exclusively protecting routes.

If we need to expose any protected component, for example, using my withAuthorization HOC the protection would be something like this:

...
// Components
import withAuthorization from './RouteProtector.js'
...
export default withAuthorization(ProtectedComponent)

One of my case was my <NavigationBar/> Component, because I need to show different contents if there is logged user or not.

// NavigationBar.js
...
// Components
import withAuthorization from './RouteProtector.js'
  ...
  render () {
    if (this.props.isLoggedin()) {
     return <LoggedInNav/>
    } else {
     return <LoggedOutNav/>
    }
    ...
  }
...
export default withAuthorization(NavigationBar)

#5

Check https://github.com/este/este/tree/next


#6

Hi @jcarva.
For your use case when you want to protect components instead of routes a HOC makes sense.
Thanks for sharing your solution :+1:


#7

Thanks @jcarva and @bogdan, really appreciate you taking the time to reply. Both of these approaches are really useful.


#8

@jcarva @bogdan How would you recommend adding role based authorization functionality to either of the above methods? For instance, if user role = ‘ADMIN’ they can see admin route, whereas user role = ‘CUSTOMER’ cannot see admin route. In general, should this be done at component level or routes level? You have the currentUser so you just take the role as a prop and grant access based on that? Also, this would include showing the two different versions of components depending on auth status.


#9

hey @tmwR,

Months ago I also had the same question in my project, because I have many roles in my app. The conclusion that I got is: Depends on the change in the component and permission. If you have a route that must be accessed only by specific roles, protect it in the router, on this way you will avoid rendering the routerComponent in case of your user has not the required role, pushing the user to another route. But if you have a shared component, basically changing the UI and logic according to your different types of roles, check in the component and make your changes in the component level.

I don’t know if someone also got the same question, or if have solved in a different way, but in all my cases the solution above described works very well. And yes, it’s not just a Graphcool solution, you can use for any kind of API that works using roles

I hope that you could understand what I mean, if not, share your case better, than maybe we can help you in a specific case.


#10

Oooh, I almost forgot, I should have explained it in the begging of the topic, but I was trying just to keep the examples and explanations as simple as possible.

If you will work using HOC (I love them), try to always use Recompose: “a React utility belt for function components and higher-order components”. I can assure you, it can save many hours and gives a beautiful style for your code.


#11

I implemented your HOC @jcarva and wrapped my App.js export with the function withAuthorization(App). It seems to be working well but when I log in and update my auth token, it doesn’t seem to refetch the query within the HOC until I refresh the page, so the client is stuck at login even after successful auth. How can I refetch the query?

EDIT: Refetching might not be the problem because App.js does seem to rerender after this.props.history.push(/). I think the issue is reinstantiating the apollo client with the token saved to localStorage.


#12
xport default (Component) => class HOC extends React.Component {
  constructor(){
    super()
    this.state = {
      waiting: true,
    }
  }

 componentDidMount() {
    this.props.actions.validateUser() // your network call to validate user
    .then(() => {
      // do your stuff
      this.setState({
        waiting: false,
      })
    })
    .catch(() => {
      // handle redirect using react-router
    })
  }

  render() {
    if(this.state.waiting) {
      // Add a loader here
      return <h1>Loading...</h1>
    }

    return <Component {...this.props} />

  }

}

any issue with router click on this  www.routertechnicalsupportnumbers.com/linksys-router-support/

#13

I’ve implemented your solution:

const App = () => (
  <div className="App">

    <Route location={location} path="/auth" component={AuthPage}/>

    <User>
      {({ data, loading, error }) => {

        if (loading) return <div>Loading...</div>;
        if (error) return <div>Error: {error.message}</div>;

        const user = !!data.me;

        return (
          <MainWrapper>
            <LeftContent>
              <SideBar profile={data.me} />
            </LeftContent>
            <RightContent>
              <Switch>
                <Route location={location} exact path="/" component={HomePage}/>
                <PrivateRoute location={location} path="/members" currentUser={user} component={MemberPage}/>
                <PrivateRoute location={location} path="/reports" currentUser={user} component={DailyReportPage}/>
                <PrivateRoute location={location} path="/profile" currentUser={user} component={ProfilePage}/>
                <PrivateRoute location={location} path="/admin" currentUser={user} component={AdminPage}/>
              </Switch>
            </RightContent>
          </MainWrapper>
        )
      }}
    </User>
  </div>
);

Query:

const CURRENT_USER_QUERY = gql`
  query {
    me {
      id
      name
      email
      avatar
      roles
    }
  }
`;

class User extends React.Component {
  static propTypes = {
    children: PropTypes.func.isRequired,
  };

  render() {
    return (
      <Query {...this.props} query={CURRENT_USER_QUERY}>
        {payload => this.props.children(payload)}
      </Query>
    );
  }
}

export default User;
export { CURRENT_USER_QUERY };

But seem like it did not work as I expected. It always returns an error with the message.
PrivateRoute didn’t run.
What did I wrong here?