a better pattern for authenticated React components and routes

3.8k views Asked by At

I'm working on adding Auth0 authentication to my React app, and even though I have it working, I feel like there's a better way to approach this. I'm struggling to figure out a better pattern for the authentication logic.

The app setup is react + redux + react-router-redux + redux-saga + immutable + auth0-lock.

Beginning at the top, the App component defines the basic page layout, both Builder and Editor components require the user to be logged in, and authenticated() wraps each in a Higher Order Component responsible for handling authentication.

// index.js

import App from './containers/App';
import Builder from './containers/Builder';
import Editor from './containers/Editor';
import Home from './containers/Home';
import Login from './containers/Login';

import AuthContainer from './containers/Auth0/AuthContainer';

...

ReactDOM.render(
  <Provider store={reduxStore}>
    <Router history={syncedHistory}>
      <Route path={'/'} component={App}>
        <IndexRoute component={Home} />
        <Route path={'login'} component={Login} />
        <Route component={AuthContainer}>
          <Route path={'builder'} component={Builder} />
          <Route path={'editor'} component={Editor} />
        </Route>
      </Route>
      <Redirect from={'*'} to={'/'} />
    </Router>
  </Provider>,
  document.getElementById('app')
);

At the moment, AuthContainer doesn't do much except check the redux store for isLoggedIn. If isLoggedIn is false, the user is not allowed to view the component, and is redirected to /login.

// containers/Auth0/AuthContainer.js

import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { redirectToLogin } from './Auth0Actions';

class AuthContainer extends React.Component {
  componentWillMount() {
    if (!this.props.isLoggedIn) {
      this.props.actions.redirectToLogin();
    }
  }
  render() {
    if (!this.props.isLoggedIn) {
      return null;
    }
    return this.props.children;
  }
}

// mapStateToProps(), mapDispatchToProps()

export default connect(mapStateToProps, mapDispatchToProps)(AuthContainer);

The next piece is Auth0. The Auth0 Lock works in "redirect" mode, which means the user will leave the app to log in, and then be redirected back to the app at /login. As part of the redirect, Auth0 attaches a token as part of the URL, which needs to be parsed when the app loads.

const lock = new Auth0Lock(__AUTH0_CLIENT_ID__, __AUTH0_DOMAIN__, {
  auth: {
    redirect: true,
    redirectUrl: `${window.location.origin}/login`,
    responseType: 'token'
  }
});

Since Auth0 will redirect to /login, the Login component also needs authentication logic. Similar to AuthContainer, it checks the redux store for isLoggedIn. If isLoggedIn is true, it redirects to the root /. If isLoggedIn is false, it'll attempt to authenticate.

// containers/Login/index.js

import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { authenticate, redirectToRoot } from '../Auth0/Auth0Actions';

class Login extends React.Component {
  componentDidMount() {
    if (!this.props.isLoggedIn) {
      this.props.actions.authenticate();
    }
    else {
      this.props.actions.redirectToRoot();
    }
  }
  render() {
    return (
      <div>Login Page</div>
    );
  }
}

// mapStateToProps(), mapDispatchToProps()

export default connect(mapStateToProps, mapDispatchToProps)(Login);

With these pieces in place, my integration with Auth0 seems to be working. However, I now have AuthContainer and Login component, and they are very similar. I can't place the Login component as a child to AuthContainer since the login page does not actually require the user to be logged in.

Ideally, all authentication logic lives in one place, but I'm struggling to figure out another way to get it working, especially with the special case of the Auth0 redirect. I can't help but think that there must be a different approach, a better pattern for authentication flow in a react + redux app.

One thing that would be helpful is to better understand how to dispatch an async action on page load, before the app starts initializing. Since Auth0 works with callbacks, I'm forced to delay setting the redux initial state until after Auth0 invokes the registered callback. What is the recommended way to handle async actions on page load?


I've left out some pieces for brevity, like the actions and sagas, but I'll be more than happy to provide those if it'll be helpful.

3

There are 3 answers

1
Thanh Nguyen On

I'm doing the same thing in my project and working fine with redux, react-router, just have a look at my code below:

routes:

export default (
  <div>
  <Route path="/" component={AuthenticatedComponent}>
    <Route path="user" component={User} />
    <Route path="user/:id" component={UserDetail} />
  </Route>    
  <Route path="/" component={notAuthenticatedComponent}>
    <Route path="register" component={RegisterView} />
    <Route path="login" component={LoginView} />
  </Route>    
  </div>
);

AuthenticatedComponent:

    export class AuthenticatedComponent extends React.Component {
      constructor( props ) {
        super( props );   
      }

      componentWillMount() {
        this.props.checkAuth().then( data => {
          if ( data ) {
            this.props.loginUserSuccess( data );
          } else {
            browserHistory.push( '/login' );
          }
        } );
      }

      render() {
        return (
        <div>
          { this.props.isAuthenticated && <div> { this.props.children } </div> }
        </div>
        );
      }
    }

notAuthenticatedComponent:

export class notAuthenticatedComponent extends React.Component {
  constructor(props){
    super(props);    
  }

  componentWillMount(){
    this.props.checkAuth().then((data) => {
      if(data && (this.props.location.pathname == 'login')){
        browserHistory.push('/home');
      }
    });
  }

  render(){
    return (
      <div>
      { this.props.children }
      </div>
    );
  }
}
10
azium On

May not be a complete answer, so sorry for that. Few things to address here:

Ideally, all authentication logic lives in one place

I'm not so sure this is ideal, depending on what you mean by "one place". There's noting wrong with having two functions that are similar but are different enough in some aspect that warrants a little repetition. From what I can see your code the logic is indeed slightly different so two components seems perfectly fine.

Instead of componentDidMount, use Route's onEnter prop

Putting your auth logic after component mounting will likely cause a flicker of your authenticated html showing before the auth logic can run. Conceptually, you would like to prevent rendering this component at all until the auth logic has run. Route's onEnter is perfect for this. https://github.com/ReactTraining/react-router/blob/master/docs/API.md#onenternextstate-replace-callback

let authenticate = (nextState, replace) => {
   // check store details here, if not logged in, redirect
}

<Route path={'builder'} onEnter={authenticate} component={Builder} />

how to dispatch an async action on page load, before the app starts initializing

This is quite a common question for React Apps / SPAs. I think the best possible user experience is to display something right away, perhaps a loading spinner or something that says "Fetching user details" or whatnot. You can do this in your top level App container or even before your first call to ReactDOM.render

ReactDOM.render(<SplashLoader />, element)

authCall().then(data =>
  ReactDOM.render(<App data={data} />, element)
).catch(err =>
  ReactDOM.render(<Login />, element)
}
1
Hussain Nawaz Lalee On

If you are following the Thanh Nguyen's answer use React's "Constructor" instead of "componentWillMount". As its the recommended way according to the docs.