Reputation: 46477
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.
Upvotes: 3
Views: 4194
Reputation: 896
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.
Upvotes: 1
Reputation: 20614
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)
}
Upvotes: 2
Reputation: 5352
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>
);
}
}
Upvotes: 1