Decouple your Authentication and Authorization from your components!
npm install --save redux-auth-wrapper
- Motivation
- Tutorial
- API
- Authorization & Advanced Usage
- Where to define & apply the wrappers
- Protecting Multiple Routes
- Dispatching an Additional Redux Action on Redirect
- Server Side Rendering
- React Native
- Examples
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 (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 (React Router 2.0 now supports this with
onChange
)
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 (Version 4.0)
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route, browserHistory } from 'react-router'
import { routerReducer, syncHistoryWithStore, routerActions, routerMiddleware } from 'react-router-redux'
import { UserAuthWrapper } from 'redux-auth-wrapper'
import userReducer from '<project-path>/reducers/userReducer'
const reducer = combineReducers({
routing: routerReducer,
user: userReducer
})
const routingMiddleware = routerMiddleware(browserHistory)
// Note: passing middleware as the last argument requires redux@>=3.1.0
const store = createStore(
reducer,
applyMiddleware(routingMiddleware)
)
const history = syncHistoryWithStore(browserHistory, store)
// Redirects to /login by default
const UserIsAuthenticated = UserAuthWrapper({
authSelector: state => state.user, // how to get the user state
redirectAction: routerActions.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=%2foo
Notice the url contains the query parameter
redirect
for 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], [isOnEnter]): authData
(Function): A state selector for the auth data. Just likemapToStateProps
. ownProps will be null if isOnEnter is true because onEnter hooks cannot receive the component properties. Can be ignored when not using onEnter.authenticatingSelector(state, [ownProps]): Bool
(Function): A state selector indicating if the user is currently authenticating. Just likemapToStateProps
. Useful for async session loading.LoadingComponent
(Component): A React component to render whileauthenticatingSelector
istrue
. If not present, will be a<span/>
. Will be passed all properties passed into the wrapped component, includingchildren
.[failureRedirectPath]
(String | (state, [ownProps]): String): Optional path to redirect the browser to on a failed check. Defaults to/login
. Can also be a function of state and ownProps that returns a string.[redirectQueryParamName]
(String): Optional name of the query parameter added whenallowRedirectBack
is true. Defaults toredirect
.[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 theauthSelector
param. If it evaluates to false the browser will be redirected tofailureRedirectPath
, otherwiseDecoratedComponent
will be rendered. By default, it returns false ifauthData
is {} or null.[allowRedirectBack]
(Bool): Optional bool on whether to pass aredirect
query parameter to thefailureRedirectPath
. Defaults totrue
.
After applying the configObject, UserAuthWrapper
returns a function which can applied to a Component to wrap in authentication and
authorization checks. The function also has the following extra properties:
onEnter(store, nextState, replace)
(Function): Function to be optionally used in the onEnter property of a route.
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 propauthData
which is the result of theauthSelector
. The component is not modified and all static properties are hoisted to the returned component
/* Allow only users with first name Bob */
const OnlyBob = UserAuthWrapper({
authSelector: state => state.user,
redirectAction: routerActions.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: routerActions.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: routerActions.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.
If you are using getComponent
in React Router you should not apply the auth-wrapper inside getComponent
. This will cause
React Router to create a new component each time the route changes.
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 MyComponent extends Component {
}
Or with standard ES5/ES6 apply it inside the component file:
export default UserIsAuthenticated(MyComponent)
Because routes in React Router are not required to have paths, you can use nesting to protect mutliple routes without applying the wrapper mutliple times.
const Authenticated = UserIsAuthenticated((props) => props.children);
<Route path='/' component={App}>
<IndexRedirect to="/login" />
<Route path='login' component={Login} />
<Route component={Authenticated}>
<Route path="foo" component={Foo} />
<Route path="bar" component={Bar} />
</Route>
</Route>
You may want to dispatch an additonal redux action when a redirect occurs. One example of this is to display a notification message
that the user is being redirected or don't have access to that protected resource. To do this, you can chain the redirectAction
parameter using redux-thunk
middleware. It depends slightly on if you are using a redux + routing solution or just React Router.
import { replace } from 'react-router-redux'; // Or your redux-router equivalent
import addNotification from './notificationActions';
// Admin Authorization, redirects non-admins to /app
const UserIsAdmin = UserAuthWrapper({
failureRedirectPath: '/app',
predicate: user => user.isAdmin,
redirectAction: (newLoc) => (dispatch) => {
dispatch(replace(newLoc));
dispatch(addNotification({ message: 'Sorry, you are not an administrator' }));
},
...
})
import { browserHistory } from 'react-router';
import addNotification from './notificationActions';
// Admin Authorization, redirects non-admins to /app
const UserIsAdmin = UserAuthWrapper({
failureRedirectPath: '/app',
predicate: user => user.isAdmin,
redirectAction: (newLoc) => (dispatch) => {
browserHistory.replace(newLoc);
dispatch(addNotification({ message: 'Sorry, you are not an administrator' }));
},
...
})
In order to perform authentication and authorization checks for Server Side Rendering, you may need to use the onEnter
property
of a <Route>
. You can access the onEnter
method of the UserAuthWrapper
after applying the config parameters:
import { UserAuthWrapper } from 'redux-auth-wrapper';
const UserIsAuthenticated = UserAuthWrapper({
authSelector: state => state.user,
redirectAction: routerActions.replace,
wrapperDisplayName: 'UserIsAuthenticated'
})
const getRoutes = (store) => {
const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState);
return (
<Route>
<Route path="/" component={App}>
<Route path="login" component={Login}/>
<Route path="foo" component={UserIsAuthenticated(Foo)} onEnter={connect(UserIsAuthenticated.onEnter)} />
</Route>
</Route>
);
};
Usage as above except include the react-native implementation
import { UserAuthWrapper } from 'redux-auth-wrapper/lib/native';
const UserIsAuthenticated = UserAuthWrapper({
authSelector: state => state.user,
redirectAction: routerActions.replace,
wrapperDisplayName: 'UserIsAuthenticated'
})