Decouple your Authentication and Authorization from your components!
npm install --save redux-auth-wrapper
At first, handling authentication and authorization seems easy in React-Router and Redux. After all, we have a handy onEnter method, shouldn't we use it?
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
- 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.
A higher-order component is just a function that takes an existing component and returns another component that wraps it
Redux-auth-wrapper provides higher-order components for easy to read and apply authentication and authorization constraints for your components.
Usage with React-Router-Redux
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route } from 'react-router'
import { createHistory } from 'history'
import { syncReduxAndRouter, routeReducer, routeActions } from 'react-router-redux'
import { UserAuthWrapper } from 'redux-auth-wrapper'
import userReducer from '<project-path>/reducers/userReducer'
const reducer = combineReducers({
routing: routeReducer,
user: userReducer
})
const history = createHistory()
const routingMiddleware = syncHistory(history)
const finalCreateStore = compose(
applyMiddleware(routingMiddleware)
)(createStore);
const store = finalCreateStore(reducer)
routingMiddleware.listenForReplays(store)
// Redirects to /login by default
const UserIsAuthenticated = UserAuthWrapper({
authSelector: state => state.user, // how to get the user state
redirectAction: routeActions.replace, // the redux action to dispatch for redirect
wrapperDisplayName: 'UserIsAuthenticated' // a nice name for this auth check
})
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<Route path="login" component={Login}/>
<Route path="foo" component={UserIsAuthenticated(Foo)}/>
<Route path="bar" component={Bar}/>
</Route>
</Router>
</Provider>,
document.getElementById('mount')
)And your userReducer looks something like:
const userReducer = (state = {}, { type, payload }) => {
if (type === USER_LOGGED_IN) {
return payload
}
if (type === USER_LOGGED_OUT) {
return {}
}
return state
}When the user navigates to /foo, one of the following occurs:
-
If The user data is null or an empty object:
The user is redirected to
/login?redirect=%2fooNotice the url contains the query parameter
redirectfor sending the user back to after you log them into your app -
Otherwise:
The
<Foo>component is rendered and passed the user data as a property
Any time the user data changes, the UserAuthWrapper will re-check for authentication.
UserAuthWrapper(configObject)(DecoratedComponent)
authSelector(state, [ownProps]): authData(Function): A state selector for the auth data. Just likemapToStateProps[failureRedirectPath](String): Optional path to redirect the browser to on a failed check. Defaults to/login[redirectAction](Function): Optional redux action creator for redirecting the user. If not present, will use React-Router's router context to perform the transition.[wrapperDisplayName](String): Optional name describing this authentication or authorization check. It will display in React-devtools. Defaults toUserAuthWrapper[predicate(authData): Bool](Function): Optional function to be passed the result of theuserAuthSelectorparam. If it evaluates to false the browser will be redirected tofailureRedirectPath, otherwiseDecoratedComponentwill be rendered.[allowRedirect](Bool): Optional bool on whether to pass aredirectquery parameter to thefailureRedirectPath
DecoratedComponent(React Component): The component to be wrapped in the auth check. It will pass down all props given to the returned component as well as the propauthDatawhich is the result of theauthSelector
/* Allow only users with first name Bob */
const OnlyBob = UserAuthWrapper({
authSelector: state => state.user,
redirectAction: routeActions.replace,
failureRedirectPath: '/app',
wrapperDisplayName: 'UserIsOnlyBob',
predicate: user => user.firstName === 'Bob'
})
/* Admins only */
// Take the regular authentication & redirect to login from before
const UserIsAuthenticated = UserAuthWrapper({
authSelector: state => state.user,
redirectAction: routeActions.replace,
wrapperDisplayName: 'UserIsAuthenticated'
})
// Admin Authorization, redirects non-admins to /app and don't send a redirect param
const UserIsAdmin = UserAuthWrapper({
authSelector: state => state.user,
redirectAction: routeActions.replace,
failureRedirectPath: '/app',
wrapperDisplayName: 'UserIsAdmin',
predicate: user => user.isAdmin,
allowRedirectBack: false
})
// Now to secure the component:
<Route path="foo" component={UserIsAuthenticated(UserIsAdmin(Admin))}/>The ordering of the nested higher order components is important because UserIsAuthenticated(UserIsAdmin(Admin))
means that logged out admins will be redirected to /login before checking if they are an admin.
Otherwise admins would be sent to /app if they weren't logged in and then redirected to /login, only to find themselves at /app
after entering their credentials.
One benefit of the beginning example is that it is clear from looking at the Routes where the authentication & authorization logic is applied.
An alternative choice might be to use es7 decorators (after turning on the proper presets) in your component:
import { UserIsAuthenticated } from '<projectpath>/auth/authWrappers';
@UserIsAuthenticated
class MyComponents extends Component {
}