User authorization with React/Redux
Here’s the problem. You want to prevent access to parts of your app for users that are not logged in, and additionally you want to prevent logged in users without the right role or access level (non-admins typically) from accessing yet more parts of your app.
There will probably be many slightly different ways to solve this problem, but here’s one I’ve had success with.
Which stack are you using in this tutorial?
You are using react
, redux
and maybe even react-router
(and its trusty companion react-router-redux
).
Overview of solution
The idea is to create a Component that will render its children if the current user has the correct role, and then write a helper function that wraps another Component (typically a page) for more convenient usage with react-router
.
The HasRole Component
We will start out with a Component that wont render its children unless the currently logged in User has the correct role. I’ve chosen to call this piece of work the HasRole component and it will be connected to the Redux store where the user role is expected to be available somewhere.
As a quick aside, for my most recent project I’m storing some info about the current user in a property called ui
which contains the state of the user interface. This property gets fetched every time the user reloads the page so that the basic view state always remains the same, and I might write an article about why I think this is a good idea at a later time.
Now back to the HasRole component, which I’ve put in a file called HasRole.js
and made to look like this:
|
|
I’ve added some helpers at the bottom that hardcodes the roles that I have in my application. This helps to avoid mistakes with filling in the correct properties and allows me to keep my code short and expressive.
HasRole
(and companions IsAdmin
and IsUser
) can now be used like this:
|
|
This gets us most of the way and is perfect for quickly hiding/revealing stuff in your user interface. However, it does not solve the problem protecting entire pages, which we will now have a look at.
A helper for the router
Sometimes you will be looking for a way to restrict access single pages or subsets of pages based on how they are routed. We are now going to explore a way to make defining these restricted routes easy, preferably as part of the actual definition of the route instead of within the render function of the page.
When using react-dom
along with react-router
and react-router-redux
, you might have a ReactDOM.render
function that ends up looking something like this:
|
|
Now suppose you wanted to add a protected route only for logged in users at /protected
here, with an additional page only for admins at /protected/admins
. How can we use the concepts from our HasRole
component from earlier to accomplish this?
The easy solution would be to just wrap the render function for these pages in IsAdmin
or IsUser
and be done with it. This will work, but would show a completely blank page to the non-authorized user trying to open it which is less than ideal.
A better solution would be to create a wrapping function that receives the page component and returns a wrapped component for use in the router, that itself does one of the following three things:
- Redirects non-logged in users to the login page
- Shows a “Forbidden” page (
pages.Forbidden
) for authenticated users that are not authorized to view the admin page. - Allows the page to be rendered for users with the correct role (admins).
Here is my idea of a component like that, and I’ve chosen to call it RequireRole
.
|
|
You might notice that the base component RequireRoleBase
looks and functions similar to HasRole
from earlier with some important distinctions. Firstly, instead of not rendering when the user does not have the correct role, it uses browserHistory
from react-router
to redirect to either /login
when the user is not logged in, or /forbidden
when the logged in user does not have the correct role.
Secondly, if the prop requiredRole
has a falsy value, such as an empty string ''
, it will render the child components. The reason for this is to enable the component to be used just as a check if the user is logged in our not, regardless of which role the user has.
Thirdly, a new variable isRehydrated
has been introduced that will be explained shortly. This boolean variable acts as a guard to prevent the user from being redirected before the redux state has settled.
It also takes extra care not to render any children when the above conditions have not been met, since doing so may trigger additional errors in the application (such as requests failing on the server side due to insufficient privileges) which we would like to avoid.
However, this is still not enough to use as a drop in in the router definition. To accomplish that, we need to export a function that wraps around the component of the page you are actually trying to protect. Adding to the end of RequireRole.js
:
|
|
We first connect to the store, expecting the role of the current user to be available, but also two boolean values isLoggedIn
and isRehydrated
. The first one has an obvious meaning, the second one relates to redux-persist which is the library I’m using to save the a part of the state of the redux store to localStorage
in the browser.
How to setup redux-persist
will be explained in a future post, but isRehydrated
is in any case set to true
when the saved state in localStorage
has been loaded back into the application state. This is important because the state is empty on page reload, and rendering this component just after that event will cause a redirect to /login
even if the user is actually logged in. Thus we need to wait for the state to be loaded back before we take any action with the router.
Now we are ready to use this component in our routing definition file, resulting in some quite clean code in my own opinion.
Don’t forget to setup a route for the 403 Forbidden page also!
|
|
Using the above code, any attempt to open /protected
will redirect the unauthenticated user to the /login
page. Users that are authenticated can open /protected
, but they must be authenticated as admins if they wish to open /protected/admin
, or else they will be redirected to /forbidden
- just like we wanted.
That is all for today. I hope you enjoyed this article, and some follow ups explaining some of the concepts above will be explored on in later articles!