Limit Access to Redux Apps with Higher Order Components

Most of the serious web apps need a login system. But implementing one can struggle for newcomers. They search and find that they can rely on JWT tokens. Yeah, that's a good choice, but how? Fortunately, there are articles out there which help with that. Secure Your React and Redux App with JWT Authentication or Securing React Redux Apps With JWT Tokens will help a lot if someone want to implement JWT Authentication in their Redux apps.

Now, a reader of the two articles above, Bob, have built a great login system, his users can log in, log out now. But how can he protect the content of his app?

The following code comes from Secure Your React and Redux App with JWT Authentication.

import React, { Component, PropTypes } from 'react'

export default class Quotes extends Component {

  render() {
    const { onQuoteClick, onSecretQuoteClick, isAuthenticated, quote, isSecretQuote } = this.props

    return (
      <div>
        <div className='col-sm-3'>
          <button onClick={onQuoteClick} className="btn btn-primary">
            Get Quote
          </button>
        </div>

        { isAuthenticated &&
          <div className='col-sm-3'>
            <button onClick={onSecretQuoteClick} className="btn btn-warning">
              Get Secret Quote
            </button>
          </div>
        }

        <div className='col-sm-6'>
          { quote && !isSecretQuote &&
            <div>
              <blockquote>{quote}</blockquote>
            </div>
          }

          { quote && isAuthenticated && isSecretQuote &&
            <div>
              <span className="label label-danger">Secret Quote</span>
              <hr/>
              <blockquote>
                {quote}
              </blockquote>
            </div>
          }
        </div>
      </div>
    )
  }
}

Quotes.propTypes = {  
  onQuoteClick: PropTypes.func.isRequired,
  onSecretQuoteClick: PropTypes.func.isRequired,
  isAuthenticated: PropTypes.bool.isRequired,
  quote: PropTypes.string,
  isSecretQuote: PropTypes.bool.isRequired
}

It's very clear that how the app limits access to protected resources. The "Get Secret Quote" button will not show if the value of "isAuthenticated" is false. That's good, isn't it?

That's great if Bob just wants to show some content according to the login state of his user. What if he wishes to redirect users to the login page in some particular parts of his app. It may be boring to check "isAuthenticated" again and again. Think about the following scene.

We have two component, one requires login, the other do not. Let' call them LoginReqiredBox and OpenBox. Redirect users to '/login' if they go to '/login-required' but have not logged in. How should we do that? "Hum, I have learned that." Bob said.

import React, { Component, PropTypes } from 'react'  
import LoginReqiredBox from '../components/LoginReqiredBox'  
import OpenBox from '../components/OpenBox'

export default class MainContent extends Component {  
  componentWillMount() {
    const { isAuthenticated, location, pushState } = this.props
    if (location === '/login-required' && !isAuthenticated) {
      pushState('/login')
    }
  }
  render() {
    const { isAuthenticated, location } = this.props
    return (
      <div>
        { location === '/login-required' && isAuthenticated &&
          <LoginReqiredBox />
        }
        { location === '/open' &&
          <OpenBox />
        }
      </div>
    )
  }
}

MainContent.propTypes = {  
  isAuthenticated: PropTypes.bool.isRequired,
  location: PropTypes.isRequired,
  pushState: PropTypes.func.isRequired,
}

Great. But why is there a "location" prop? It will be hard to manage when the app grows. Could we use react-router instead? "Of course." Bob said, "I also know how to use react-router with Redux".

import React from 'react'  
import { Route } from 'react-router'  
import App from './containers/App'  
import LoginReqiredPage from './containers/LoginReqiredPage'  
import OpenPage from './containers/OpenPage'  
import LoginPage from './containers/LoginPage'

function requireLogin(nextState, replace) {  
  if (/* have not login */) {
    console.log('redirect')
    replace('/login')
  }
}

export default (  
  <Route path="/" component={App}>
    <Route path="/login-require"
           component={LoginReqiredPage} onEnter={requireLogin} />
    <Route path="/open"
           component={OpenPage} />
    <Route path="/login"
           component={LoginPage} />
  </Route>
)

"Er, how can I know the user has logged in or not?"

Maybe

function requireLogin(nextState, replace) {  
  let token = sessionStorage.getItem(β€˜jwtToken’);
  if(!token || token === β€˜β€™) {
    replace('/login')
  }
}

"Hum, Seems work now. But it is a little strange, and I do not know whether it's right to put storage code in this place."

That's good, anyway, We have made it work. But What if we need something from the redux store to decide whether to redirect the user to '/login'? Is there a better way to force the user to login? Definitely yes.

The answer is Higher Order Components

A higher-order component is just a function that takes an existing component and returns another component that wraps it

import React from 'react';  
import {connect} from 'react-redux';  
import {pushState} from 'redux-router';

export function requireAuthentication(Component) {

  class AuthenticatedComponent extends React.Component {

    componentWillMount() {
      this.checkAuth()
    }

    componentWillReceiveProps(nextProps) {
      this.checkAuth()
    }

    checkAuth() {
      if (!this.props.isAuthenticated) {
        let redirectAfterLogin = this.props.location.pathname
        this.props.dispatch(pushState(null, `/login?next=${redirectAfterLogin}`))
      }
    }

    render() {
      return (
        <div>
          {this.props.isAuthenticated === true
            ? <Component {...this.props}/>
            : null
          }
        </div>
      )

    }
  }

  const mapStateToProps = (state) => ({
    token: state.auth.token,
    userName: state.auth.userName,
    isAuthenticated: state.auth.isAuthenticated
  })

  return connect(mapStateToProps)(AuthenticatedComponent)
}

Use requireAuthentication like this.

import React from 'react'  
import { Route } from 'react-router'  
import App from './containers/App'  
import requireAuthentication from '../enhance/requireAuthentication'  
import LoginReqiredPage from './containers/LoginReqiredPage'  
import OpenPage from './containers/OpenPage'  
import LoginPage from './containers/LoginPage'

export default (  
  <Route path="/" component={App}>
    <Route path="/login-require"
           component={requireAuthentication(LoginReqiredPage)} />
    <Route path="/open"
           component={OpenPage} />
    <Route path="/login"
           component={LoginPage} />
  </Route>
)

See that. We do not need to change even single line code of LoginReqiredPage. Just wrap it with requireAuthentication and everything works like a charm.

How does it work?

Just check Dan's Higher Order Components and the code of requireAuthentication. It's simple once you get the idea of Higher Order Components.

Further reading

Thanks for reading. Welcome comments if you have any questions or problems.

πŸŽ‰ If you like this post, please share it on twitter (https://twitter.com/crysislinux)πŸŽ‰

Luo Gang

Read more posts by this author.