NOTE: For the sake of simplicity this article is going to be based on React JS along with React Router and Node JS on Express JS for backend, all of which are awesome and do check them out.
Have you ever wondered how in the world certain large scale applications like airbnb, facebook, youtube etc., manages to block a certain part of their application for certain people or release features based on certain conditions ? They all use a tried and tested methodology called RBAC this solution gained traction during the 90โs and still is widely used in a lot of forms. RBAC stands for Role Based Access Control as the title says and this article helps you fathom the RBAC game.
...
Parameters to considers for RBAC :-
1. Route Guarding ๐ฃ๏ธ:
This is a process of authenticating ๐ and authorising routes or part of a url in your web app. Typically you have a router switch with route paths couple. Here you can see that the routes stay in a different file to act as s single source of truth. In the switch instead of routes we wrap it with a wrapper and pass roles required path and component to be rendered as props.
module.exports = {
homePage: {
route: '/',
roles: ['ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_EXTERNAL_SOURCER']
},
createQuestion: {
route: '/question-bank/create/:type/',
roles: ['ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_EXTERNAL_SOURCER']
},
testAttempt: {
route: '/student/test/:gid',
roles: ['ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_STUDENT']
}
};
Role.js
<Switch>
<RouteWrapper
exact
path={routePaths.homePage.route}
roles={routePaths.homePage.roles}
Component={QuestionListPage}
/>
<RouteWrapper
exact
path={routePaths.createQuestion.route}
roles={routePaths.createQuestion.roles}
Component={CreateQuestions}
/>
<RouteWrapper
exact
path={routePaths.testListPage.route}
roles={routePaths.testListPage.roles}
Component={TestListPage}
/>
<Route path="/error/:code">
<ErrorComponent />
</Route>
<Route path="">
<Redirect to="/error/404" />
</Route>
</Switch>
Router.js
const RouteWrapper = ({Component, ...props }) => {
const auth = checkAuth();
let role = getUserRole(),
access = checkAccess(role, props.roles);
if (!auth) {
logout();
} else if (auth && !access) {
return <Redirect to="/error/401" />;
}
return (
<Route
{...props}
render={routeProps => <Component {...routeProps} />}
/>
);
};
Route.js
route wrapper here will check for authentication data and access based on the role needed by route and user role matrix. If you are un-authenticated the app will log you out to prevent conflicts, If you are authenticated but not authorised to access the route we will redirect to an error page. If your authenticated and authorised we will finally render the component. This check happens on all route changes for our protected routes.
2. Restricting access to a part of the page ๐:
There might be certain situations where you might want to block features or a section of a page (route) while granting access to others. Hereโs how we do it.
import { useSelector } from 'react-redux';
import { allowedUserChecker } from 'utils';
import PropTypes from 'prop-types';
const RBAC = ({ allowedRoles, children }) => {
let userRoles = useSelector(state => state.userInfo.roles);
let access = allowedUserChecker(userRoles, allowedRoles);
return access && children;
};
RBAC.propTypes = {
allowedRoles: PropTypes.arrayOf(PropTypes.string),
children: PropTypes.element
};
export default RBAC;
Rbac.js
This is a RBAC HOC ( higher order components ) we get the user roles from a global store (can be through other means as well) and we try to derive the access based on user role matrix check. We render the wrapped component only if access is permitted.
import RBAC from './dir/RBAC';
...
<RBAC allowedRoles={[ 'ROLE_AUTHOR', 'ROLE_ADMIN']}>
<Button type="submit" label="VERIFY AND PUBLISH" />
</RBAC>
...
3. Securing Backend Endpoints ๐:
Last but not the least we need to make sure the backend secures their APIโs from their end as well.
Well there are a ton of ways to do this using some API gateway, Proxy Servers etc., For the sake of simplicity Iโm assuming certain parameters. These parameters are not necessary and you can implement the logic in way that you need as the crux is core logic only.
First thing first youโll need a role to routes file similar to what we saw in frontend.
module.exports = {
homePage: {
route: '/',
roles: ['ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_EXTERNAL_SOURCER']
},
createQuestion: {
route: '/question-bank/create/:type/',
roles: ['ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_EXTERNAL_SOURCER']
},
testAttempt: {
route: '/student/test/:gid',
roles: ['ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_STUDENT']
}
};
role.js
โน๏ธ This might be a duplicate but try to host this somewhere common to solve your DRY OCD ๐ issues if any ๐ .
then this RBAC middleware below will help manage things for you automatically. Here itโs pretty simple check access and authorise / respond with error.
module.exports = (rolesWithAccess) => {
let {roles} = req.user;
// eg: roles = ['author'] comming from JWT token
return (req, res, next) => {
if (rolesWithAccess.length && !rolesWithAccess.includes(roles)) {
return res.status(401).json({ message: 'Unauthorized' });
// send however you want this one way of sending it.
} else {
next();
}
}
};
rbac middleware
Now we need to wire it up with our route like this. Thats it we got them unauthorised users ๐
const routes from './dir/routes';
const rbac from '/dir/to/rbac';
const publishController from '/dir/to/controllers';
...
app.use(routes.publish.route, rbac(routes.publish.roles), publishController);
...
La End
Stay tuned ๐ for part 2 of this article I might write about implementation other platform such as Next JS and GraphQL.
Iโm sure there are modifications / implementations to better the above article and also fun fact route paths can be a regex(/^\/(api|rest)\/.+$/) this is something that now many of you may know. Likewise, my work at upGrad is now only about learning new things to better my career but also to build a platform that empowers multiple people in transitioning their careers as well. Do visit upgrad.comto check out our programs that are completely online! If you wish to work with our ever-enthusiastic team, check out our careers page . We are always on the lookout for the ambitious, talented folks!
Top comments (2)
Hi @thearvindnarayan , great article indeed! One thing that i think might have been a mistake is in
Router.js
line 4: we need to passrole
instead ofuserRole
to thecheckAccess()
method. Other than that, i found this article to be really simple and well explained. Keep up the amazing work! Kudos ๐๐Thanks Krishna for pointing it out. It flew under the radar! and thanks for the appreciation