Permission checks from components in a React/Redux application

16,334

Solution 1

onEnter is great, and useful in certain situations. However, here are some common authentication and authorization problems onEnter does not solve:

  • Decide authentication/authorization from redux store data (there are some workarounds)
  • Recheck authentication/authorization if the store updates (but not the current route)

  • Recheck authentication/authorization if a child route changes underneath the protected route

An alternative approach is to use Higher Order Components.

You can use Redux-auth-wrapper provides higher-order components for easy to read and apply authentication and authorization constraints for your components.


  • To get child methods you can use:refs, callback and callback from refs

  • To get child props you can use:this.refs.child.props.some or compInstance.props.some

Example for methods and props:

class Parent extends Component {
    constructor(props){
        super(props);
        this.checkChildMethod=this.checkChildMethod.bind(this);
        this.checkChildMethod2=this.checkChildMethod2.bind(this);
        this.checkChildMethod3=this.checkChildMethod3.bind(this);
    }
    checkChildMethod(){
        this.refs.child.someMethod();
        console.log(this.refs.child.props.test);
    }
    checkChildMethod2(){
        this._child2.someMethod();
        console.log(this._child2.props.test);
    }
    checkChildMethod3(){
        this._child3.someMethod();
        console.log(this._child3.props.test);
    }
    render(){
        return (
            <div>
                Parent
                <Child ref="child" test={"prop of child"}/>
                <ChildTwo ref={c=>this._child2=c} test={"prop of child2"}/>
                <ChildThree returnComp={c=>this._child3=c} test={"prop of child3"}/>
                <input type="button" value="Check method of child" onClick={this.checkChildMethod}/>
                <input type="button" value="Check method of childTwo" onClick={this.checkChildMethod2}/>
                <input type="button" value="Check method of childThree" onClick={this.checkChildMethod3}/>
            </div>
        );
    }
}

class Child extends Component {
    someMethod(){
        console.log('someMethod Child');
    }
    render(){
        return (<div>Child</div>);
    }
}
class ChildTwo extends Component {
    someMethod(){
        console.log('someMethod from ChildTwo');
    }
    render(){
        return (<div>Child</div>);
    }
}
class ChildThree extends Component {
    componentDidMount(){
        this.props.returnComp(this);
    }
    someMethod(){
        console.log('someMethod from ChildThree');
    }
    render(){
        return (<div>Child</div>);
    }
}

Solution 2

This seems a very interesting possibility. I hit this question Googling the same issue, and this is a new library so I figured doesn't hurt to link it in case anyone else can be helped by it. I have not decided if I am going to go this route myself yet, as I am 15 minutes into the Google-palooza. It's called CASL.

Link to the Article Explaining the Library

Link to the Library

Example Code from the Library per request:

if (ability.can('delete', post)) {
  <button onClick={this.deletePost.bind(this}>Delete</button>
}

replaces something like:

if (user.role === ADMIN || user.auth && post.author === user.id) {
  <button onClick={this.deletePost.bind(this}>Delete</button>
}

Which in the article the author furthered with a custom component to get:

<Can run="delete" on={this.props.todo}>
  <button onClick={this.deleteTodo.bind(this}>Delete</button>
</Can>

It basically allows the developer to be more declarative in their code for ease of use and maintainability.

Solution 3

If you use react-router, the recommended way to handle authorization is through the onEnter property in the Route component.

<Route path="/" component={Component} onEnter={Component.onEnter} />  

See the docs.

And it's also an answer to your question:

Is there a way to access the properties/methods of an exported React component such that they can be accessed from a containing component?

So just make them static properties/methods (like Component.onEnter).

Solution 4

I found an article here, the gist of which i am writing here. You can add a prop to you component like so

<Route path="/" component={App}>

//BOD routes
<Route authorisedUsers={['KR']} path="/home" component={HomeContainer} />

//HR routes
<Route authorisedUsers={['HR']} path="/hrhome" component={HRDashboardContainer} />

//common routes    
<Route authorisedUsers={['KR', 'HR']} path="/notes" component={NotesContainer} />

and then add the following code in your component, that renders on path='/'

Role based routing react redux
componentDidUpdate() {
  const { 
      children,  //Component to be rendered (HomeContainer if route = '/home')
      pathname: {location},  //location.pathname gives us the current url user is trying to hit. (with react router)
      profileId, //profileId required by profile page common to all user journeys.
      role } = this.props; 
  this.reRoute(role, this.props.children, location.pathname, ProfileId)
}

decideRoute(role, ProfileId) { //decide routes based on role
  if(role==='HR')
    return 'hrhome';
  else if(role==='KR')
    return 'home';
  else if(role==='USER'&&ProfileId)
    return 'profile/'+ProfileId;
  else
  return '/error';
}

isAuthorised(authorisedUsers, role) {
  return _.includes(authorisedUsers, role)
}

reRoute(role, children, path, ProfileId) {
  if(role===null && path!=='/') // user hit a different path without being authenticated first
  {
    hashHistory.replace('/');  //this is where we implemented login
    return;
  }
  let route = this.decideRoute(role, ProfileId)  //if role has already been fetched from the backend, use it to decide the landing page for each role.
  if(children)  // if we already are on one of the user journey screens ...
  {
    const authorisedUsers = children.props.route.authorisedUsers 
    if(!this.isAuthorised(authorisedUsers,role)) //... and the user is not allowed to view this page...
    hashHistory.replace(`/${route}/`);  //... redirect him to the home page corresponding to his role.
  }
  else
  hashHistory.replace(`/${route}/`);  // if the user has just logged in(still on / page or login page), and we know his role, redirect him to his home page.
}//if none of these holds true, user is allowed to go ahead and view the page

This essentially adds a gateway check that would work on all your containers and will direct you based on your role. Also, it wont allow you access if you somehow hit the wrong url.

Solution 5

For anybody looking for a quick fix for it with an open source project, you can try to use react-permissible.

  • manage the visibility of particular components depending on users permissions
  • replace particular component when the user isn't permitted to see it
  • manage accessibility to particular view depending on users
    permissions
  • fire a callback when the user isn't allowed to go to access the component/route
Share:
16,334

Related videos on Youtube

Himmel
Author by

Himmel

My name is Matt McDaniel and I'm a JavaScript developer currently building projects with React and D3.js. I'm eager to learn new things and expand my knowledge domains, both within computer science and outside of it. I want to apply unique perspectives to technical domains to broaden the field and promote creativity.

Updated on June 04, 2022

Comments

  • Himmel
    Himmel almost 2 years

    I'm trying to work with a team building a React application, and trying to figure out the best way to create a "higher-order" React component (one that wraps another) to perform Authentication in conjunction with the Redux data store.

    My approach thus far has been to create a module that consists of a function that returns a new React component depending on whether or not there is an authenticated user.

    export default function auth(Component) {
    
        class Authenticated extends React.Component {
    
            // conditional logic
    
            render(){
                const isAuth = this.props.isAuthenticated;
    
                return (
                    <div>
                        {isAuth ? <Component {...this.props} /> : null}
                    </div>
                )
            }
        }
    
        ...
    
        return connect(mapStateToProps)(Authenticated);
    
    }
    

    This makes it easy for other people on my team to specify whether or not a component requires certain permissions.

    render() {
        return auth(<MyComponent />);
    }
    

    If you are performing role-based checks, this approach makes sense, as you may only have a few roles. In such a case, you could just call auth(<MyComponent />, admin).

    Passing arguments becomes unwieldy for permissions-based checks. It may however be feasible to specify permissions at the component level as the components are being constructed (as well as manageable in a team environment). Setting static methods/properties seems like a decent solution, but, as far as I can tell, es6 classes export as functions, which don't reveal callable methods.

    Is there a way to access the properties/methods of an exported React component such that they can be accessed from a containing component?

  • Himmel
    Himmel almost 8 years
    I want to be able to wrap the component in a function so that I can use it outside of the route declaration, especially for those components that aren't tied to a route.
  • Diego Haz
    Diego Haz almost 8 years
    You could just write a wrapper function for your auth which has the signature needed by the onEnter
  • Himmel
    Himmel almost 8 years
    I want to be able to specify the signature at the component level for the sake of not creating two JS files per component that needs auth.
  • Diego Haz
    Diego Haz almost 8 years
    So what's the problem on adding a static onEnter (prevState, nextState, replace) to your component?
  • Himmel
    Himmel almost 8 years
    onEnter doesn't coordinate very well with Redux applications for the following reasons (github.com/mjrussell/redux-auth-wrapper) ... * Decide authentication/authorization from redux store data (there are some workarounds) * Recheck authentication/authorization if the store updates (but not the current route) * Recheck authentication/authorization if a child route changes underneath the protected route
  • Himmel
    Himmel almost 8 years
    So child methods are available through refs, great to know, thank you!
  • Grant Miller
    Grant Miller almost 6 years
    Add the relevant content from your link explaining the library to the body of your answer.
  • Daniel Brown
    Daniel Brown almost 6 years
    Better? Thank you for the feedback. Per my profile, I am new to contributing. (I am not new to consuming, and hence felt needed to give back). Hope I can help for real some time soon.