Implementing Role Based Access Control (RABC) in React
- Share:
Role-Based Access Control (RBAC) provides a powerful way to manage user permissions in React applications. When properly implemented, RBAC allows your application to show different features to different users based on their roles, creating a personalized and secure user experience.
In this tutorial, we'll build a simple project management application with proper RBAC implementation using Permit.io and React. We'll cover:
- Setting up a React application with Permit.io integration
- Defining roles and permissions with Permit's dashboard
- Implementing feature toggling based on user permissions
- Monitoring access decisions with Permit's audit logs
By the end of this article, you'll have a solid understanding of how to implement fully functional RBAC authorization and feature flags in your React applications.
Before we dive into the code, let's ensure we have a solid grasp of the RBAC concepts and architecture decisions that will influence our implementation.
What is RBAC?
Role-Based Access Control (RBAC) is an approach to restricting system access to authorized users based on three key elements:
Roles: These are job functions or titles within your organization that define the authority and responsibility of users in that role, such as "Admin,” "Project Manager,” or "Team Member.”
Resources and Actions: Resources are the objects users interact with (like "projects," "tasks," or "teams"), while actions represent what users can do with those resources (such as "create," "read," "update," or "delete").
Policy Rules: These map roles to specific resource-action combinations, defining what each role can do. For example, "Admins can perform any action on any resource" or "Team Members can only read and update tasks assigned to them." These rules form the foundation of your permission model.
Instead of assigning permissions directly to users, RBAC assigns users to roles and then grants permissions to those roles. This creates a layer of abstraction that makes permission management more scalable and easier to audit.
What are Feature Flags?
Feature flags (also known as feature toggling or feature switches) allow you to turn features on or off at runtime. This is useful for gradually rolling out new features, A/B testing, or controlling access to features based on user roles.
In the context of RBAC, for example, an admin might see a "Delete Project" button while a regular user might not - thus showing or hiding UI elements based on a user's permissions.
Why Isn't a Basic RBAC Implementation Enough?
For React developers, implementing RBAC often feels like reinventing the wheel. You might start with a simple solution — maybe some conditional rendering based on a user's role:
{
user.role === "admin" && <AdminPanel />;
}
This quickly becomes too much to handle as your application grows. You end up with scattered permission checks throughout your codebase, making it difficult to maintain a consistent security model. And what happens when you need to enforce these same permissions on the backend? You're essentially maintaining two separate permission systems.
Where Should Permissions Be Enforced?
This is a crucial architectural decision that affects both security and user experience. There are two main approaches:
1. Backend Enforcement (API Access Control)
The most secure approach is to enforce permissions at the API level. Regardless of what happens on the front end, the backend serves as the ultimate gatekeeper, rejecting unauthorized requests.
// Example Express middleware for backend enforcement
const checkPermission = async (req, res, next) => {
const { user, resource, action } = req.query;
const permitted = await permit.check(user, action, resource);
if (!permitted) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
2. Frontend Toggling (UI Element Visibility)
As mentioned earlier, frontend toggling improves user experience by showing only UI elements that the user has permission to use. This prevents frustration from users clicking buttons only to receive an error message. However, it's important to remember that frontend controls are not a substitute for backend enforcement.
// Example React component with permission-based rendering
function ProjectActions({ project }) {
const { can } = usePermissions();
return (
<div className="actions">
{can("edit", "project", project.id) && (
<button className="edit-btn">Edit Project</button>
)}
{can("delete", "project", project.id) && (
<button className="delete-btn">Delete Project</button>
)}
</div>
);
}
The Ideal Approach: Both
To provide your application users with a good experience, you should implement both:
Backend enforcement as your security foundation
Frontend toggling for improved user experience
This way, even if someone tries to bypass your front-end controls or makes API requests directly, the back end will still prevent unauthorized actions. Meanwhile, regular users enjoy an interface that only shows what they're allowed to use.
Our Approach in This Tutorial
In our task management app, we'll implement frontend toggling with Permit.io's SDK for declarative permission checks with examples of how these same permissions would be enforced on the backend.
By focusing on the front-end implementation, we'll see how to create a clean, maintainable permission system that scales with your application. We'll also touch on how Permit.io allows you to define these permissions once and enforce them consistently across your stack.
Permit.io allows you to implement permission logic in a way that is centralized and consistent. Whether you're checking permissions on the frontend or backend, you're using the same permission model, which reduces duplication and potential security gaps.
Now that we understand the architectural considerations, let's set up our project and start implementing RBAC in our React application.
Setting Up the Project
I have created a starter project for this tutorial, which you can clone from the GitHub repository:
https://github.com/permitio/permit-react-demo
This project includes a simple React application with a few components and styles to get us started. We'll build on this foundation to implement our RBAC system. So, let's start by setting up our project and installing the necessary dependencies. We will use Permit.io's Node.js SDK for our permission checks.
Installing Dependencies
Navigate to the project directory and install its dependencies:
cd react-rbac-permitio-task-management-app
yarn install
Mock Data for User Roles
For the demonstration of this project, we will be using mock data to simulate user roles and permissions. In a real-world scenario, you would fetch this data from your backend API. You can find the mock data in the app/data directory.
Setting up your Authorization
Planning our RBAC Implementation
Before diving into the Permit.io setup, let's map out our RBAC structure clearly. This planning step is crucial regardless of which RBAC solution you use.
Resources and Actions
Our TaskFlow app will manage the following resources:
- Projects: Actions include
create
,read
,update
, anddelete
- Tasks: Actions include
create
,read
,update
, anddelete
- Team: Actions include
create
,read
,update
, anddelete
- Reports: Actions include
create
,read
,update
, anddelete
- Settings: Actions include
read
andupdate
Roles and Permissions
Let's define who can do what in our system:
Role | Projects | Tasks | Team | Reports | Settings |
---|---|---|---|---|---|
Admin | Full access | Full access | Full access | Full access | Read, Update |
Project Manager | Create, Read, Update | Create, Read, Update | Read only | Read only | No access |
Team Member | Read only | Create, Read, Update | No access | Read only | No access |
Viewer | Read only | Read only | No access | Read only | No access |
This permission structure makes sense for a task management application where:
Admins need full control over the entire system
Project Managers can manage projects but shouldn't delete them or modify system settings
Team Members work primarily with tasks but need to see a project’s context
Viewers have read-only access for monitoring purposes
With this structure clearly defined, we can move on to implementing it in Permit.io, where we'll create these roles and assign the appropriate permissions through policy rules.
Creating a Permit.io Organization
To use Permit.io, you'll need to sign up for an account. Once you have an account, visit your “Project” dashboard to create a new project. Then copy your project API key and paste it in your .env.local file.
Creating a new project in Permit.io
Copying a project’s API key in Permit.io
Permit.io Policy Configuration
A policy is a set of rules that define the permissions for your application. You can create policies for different roles, such as "Admin", "Project Manager", "Team Member", and "Viewer". Each policy will define the permissions that users in that role have.
To create a policy, we first need to create the following resources:`
projects
tasks
teams
reports
settings
Each of them represents a different part of our application that users can interact with.
In Permit.io, go to the "Policy" dashboard and click on the "Resources" tab to create these resources.
Creating a new resource in Permit.io
Next, go to the "Roles" tab to create the roles for your application. For this tutorial, we'll have the following roles:
admin
project_manager
team_member
viewer
Once you have done that, go back to the "Policy" tab to update the policies for each resource based on the roles you created.
Creating policies in Permit.io
Creating policies in Permit.io
Adding New Users
Since we are using mock data, we need to add our mock users to our project in Permit.io. In real-world scenarios, you would add users to Permit.io via the API or integrate with your existing user management system.
In the side menu, click on "Directory", then "Users", and add the following users:
export const users = [
{
id: "alex.smith@taskflow.com",
name: "Alex Smith",
role: "admin",
email: "alex.smith@taskflow.com",
photo: "<https://randomuser.me/api/portraits/men/6.jpg>",
},
{
id: "jane.doe@taskflow.com",
name: "Jane Doe",
role: "project_manager",
email: "jane.doe@taskflow.com",
photo: "<https://randomuser.me/api/portraits/women/7.jpg>",
},
{
id: "jamie.chen@taskflow.com",
name: "Jamie Chen",
role: "team_member",
email: "jamie.chen@taskflow.com",
photo: "<https://randomuser.me/api/portraits/men/7.jpg>",
},
{
id: "taylor.wong@taskflow.com",
name: "Taylor Wong",
role: "viewer",
email: "taylor.wong@taskflow.com",
photo: "<https://randomuser.me/api/portraits/women/7.jpg>",
},
];
Adding users in Permit.io
Integrating the Permit.io SDK
Now that we have set up our project and defined our policies, we can integrate Permit.io's SDK into our React application. We'll use the SDK to check permissions and render UI elements based on the user's role.
Creating the Backend Service
To check permissions on the frontend, we need to create a server that interacts with Permit.io's API. This server will expose an endpoint that our frontend can call to check permissions. I have created one in the server
directory. You can start it by running:
cd server
yarn install
node server.js
The server will run on http://localhost:4000
. You can test it by sending a GET
request to http://localhost:4000/?user=jane.doe@taskflow.com&action=update&resource=projects
or visiting the URL in your browser.
// server.js
const { Permit } = require("permitio");
const express = require("express");
const app = express();
const port = 4000;
app.use(express.json());
// allow cors
app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
next();
});
// initialize permit
const permit = new Permit({
pdp: "<https://cloudpdp.api.permit.io>",
token: "your-permit-key",
});
// add route that get user resource and action as get parameters and check if user is permitted
app.get("/", async (req, res) => {
const { user, resource, action } = req.query;
const permitted = await permit.check(user, action, resource);
res.send({ permitted });
});
// create async function to check if user is permitted to access resource
const checkIfPermitted = async (user, resourcesAndActions) => {
let isPermittedList = [];
const promises = resourcesAndActions.map(async (resourceAndAction, index) => {
const { resource, action, resourceAttributes } = resourceAndAction;
const resourceObj = { type: resource, attributes: resourceAttributes };
const permitted = await permit.check(user, action, resourceObj);
console.log(permitted, "bulk");
isPermittedList = [...isPermittedList, { index, permitted }];
});
await Promise.all(promises);
return isPermittedList
.sort((a, b) => a.index - b.index)
.map(item => item.permitted);
};
// add route that get user and list of resources and actions object as post parameters and check if user is permitted
app.post("/bulk", async (req, res) => {
const { user, resourcesAndActions } = req.body;
res.send(await checkIfPermitted(user, resourcesAndActions));
});
// start server
app.listen(port, () => {
console.log(`Example app listening at <http://localhost>:${port}`);
});
The server.js
file is the backend service that interfaces with Permit.io's API to handle permission checks. Let's break down what each part does:
const { Permit } = require("permitio");
const express = require("express");
const app = express();
const port = 4000;
app.use(express.json());
This section imports the necessary dependencies: the Permit.io SDK and Express for creating our API endpoints. We initialize an Express app and set it to use JSON parsing middleware for handling request bodies.
app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
next();
});
These CORS (Cross-Origin Resource Sharing) headers allow our frontend application to make requests to this server from a different origin. This is necessary for local development where your React app and Express server run on different ports.
const permit = new Permit({
pdp: "<https://cloudpdp.api.permit.io>",
token: "your-permit-key",
});
Here, we initialize the Permit.io SDK with the Policy Decision Point (PDP) URL and your API key. The PDP is where permission decisions are made based on your policy.
app.get("/", async (req, res) => {
const { user, resource, action } = req.query;
const permitted = await permit.check(user, action, resource);
res.send({ permitted });
});
This creates a simple GET endpoint that accepts three query parameters:
user
: The user's identifier (email in our case)resource
: The resource they're trying to access (projects, tasks, etc.)action
: The action they're trying to perform (create, read, update, delete)
The endpoint calls Permit.io's permit.check()
method, which returns a Boolean indicating whether the user is permitted to perform the requested action on the resource. This response is then sent back to the frontend.
const checkIfPermitted = async (user, resourcesAndActions) => {
let isPermittedList = [];
const promises = resourcesAndActions.map(async (resourceAndAction, index) => {
const { resource, action, resourceAttributes } = resourceAndAction;
const resourceObj = { type: resource, attributes: resourceAttributes };
const permitted = await permit.check(user, action, resourceObj);
console.log(permitted, "bulk");
isPermittedList = [...isPermittedList, { index, permitted }];
});
await Promise.all(promises);
return isPermittedList
.sort((a, b) => a.index - b.index)
.map(item => item.permitted);
};
This function handles checking permissions in bulk, which is more efficient than making separate API calls for each permission check. It takes a user identifier and an array of resources and actions to check. For each check:
- It extracts the resource, action, and any resource attributes
- It constructs a resource object with the type and attributes
- It checks if the user has permission to use Permit.io's SDK
- It stores the result along with the original index to maintain order
The function uses Promise.all()
to run all permission checks concurrently for better performance. Finally, it sorts the results back to their original order and returns just the array of boolean values indicating whether each action is permitted.
app.post("/bulk", async (req, res) => {
const { user, resourcesAndActions } = req.body;
res.send(await checkIfPermitted(user, resourcesAndActions));
});
This POST endpoint implements the bulk permission checking. It expects a JSON body with:
user
: The user identifierresourcesAndActions
: An array of objects, each containingresource
,action
, and optionalresourceAttributes
This endpoint is particularly useful for UI rendering, where you need to check multiple permissions at once, such as determining which buttons to show in a dashboard.
app.listen(port, () => {
console.log(`TaskFlow app listening at <http://localhost>:${port}`);
});
Finally, this starts the Express server on the specified port and logs a message when it's running.
Custom Hook for Permission Checks
Create a custom hook called usePermissions.js
in the app
directory. This hook will be used to check permissions in our React components.
// src/hooks/usePermissions.js
import { useEffect, useState } from "react";
export const usePermissions = (userId: string | undefined) => {
const [permissions, setPermissions] = useState<Record<string, boolean>>({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!userId) {
setError("User ID not provided");
return;
}
const actionsResources = [
{ action: "create", resource: "projects" },
{ action: "read", resource: "projects" },
{ action: "update", resource: "projects" },
{ action: "delete", resource: "projects" },
{ action: "create", resource: "tasks" },
{ action: "read", resource: "tasks" },
{ action: "update", resource: "tasks" },
{ action: "delete", resource: "tasks" },
{ action: "read", resource: "teams" },
{ action: "create", resource: "teams" },
{ action: "update", resource: "teams" },
{ action: "delete", resource: "teams" },
{ action: "create", resource: "reports" },
{ action: "read", resource: "reports" },
{ action: "update", resource: "reports" },
{ action: "delete", resource: "reports" },
{ action: "read", resource: "settings" },
{ action: "update", resource: "settings" },
];
// Fetch bulk permissions from the backend
fetch("<http://localhost:4000/bulk>", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user: userId,
resourcesAndActions: actionsResources,
}),
})
.then(res => res.json())
.then(data => {
const perms = actionsResources.reduce(
(acc, { action, resource }, index) => {
acc[`${action}:${resource}`] = data[index];
return acc;
},
{} as Record<string, boolean>
);
setPermissions(perms);
setError(null);
})
.catch(err => {
console.error("Error fetching permissions:", err);
setError("Error fetching permissions");
})
.finally(() => {
setIsLoading(false);
});
}, [userId]);
return { permissions, isLoading, error };
};
This hook centralizes permission-checking logic for our React components. It takes a user ID and makes a bulk permission request to our backend server, fetching all possible action/resource combinations for our application in a single network call. The hook maintains these permissions in the state as key-value pairs (where keys follow the format action:resource
and values are boolean permission results). When a component needs to check if a user can perform an action on a resource, it simply looks up the corresponding key in this permissions object. The hook runs whenever the user ID changes, ensuring permissions are always current with the logged-in user.
Implementing RBAC in React Components
Now that we have our backend service and custom hook set up, we can start implementing RBAC in our React components. We'll use the usePermissions
hook to check permissions and conditionally render UI elements based on the user's role.
In the overview.tsx
file, let's use the hook to conditionally render different parts of the dashboard.
Import the usePermissions
hook and the mock user's data:
// app/dashboard/overview.tsx
export default function Overview() {
const { permissions, isLoading, error } = usePermissions(user?.id);
if (!user || isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
...
)
}
Then replace all the conditional rendering logic with permission checks. For example, instead of:
<div className="bg-white p-6 rounded shadow">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Projects</h2>
{(user?.role === "admin" || user?.role === "project_manager") && (
<button className="px-3 py-1 bg-blue-600 text-white text-sm rounded hover:bg-blue-700">
New Project
</button>
)}
</div>
<p className="text-gray-600 mb-4">Manage your project portfolio</p>
<button className="w-full py-2 text-center border border-blue-600 text-blue-600 rounded hover:bg-blue-50">
View All Projects
</button>
</div>
You can now do:
{permissions["read:projects"] && (
<div className="bg-white p-6 rounded shadow">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Projects</h2>
{permissions["create:projects"] && (
<button className="px-3 py-1 bg-blue-600 text-white text-sm rounded hover:bg-blue-700">
New Project
</button>
)}
</div>
<p className="text-gray-600 mb-4">Manage your project portfolio</p>
<button className="w-full py-2 text-center border border-blue-600 text-blue-600 rounded hover:bg-blue-50">
View All Projects
</button>
</div>
)}
This way, the "New Project" button will only be shown if the user has permission to create projects. Similarly, the "Projects" card will only be shown if the user has permission to read projects.
Dashboard view for users with the admin role
Dashboard view for users with the project manager role
Dashboard view for users with the team member role
Dashboard view for users with the viewer role
Monitoring Access Decisions with Permit.io’s Audit Logs
Audit logs are a big deal for any app with different user roles. Think about it - if someone does something they shouldn't, how will you figure out what happened? Without proper logs, you're basically flying blind. That's why keeping track of who did what, when they did it, and whether they were allowed to do it is super important for security. When stuff goes wrong (and it will), you need these logs to figure out exactly what happened and fix it fast.
Permit.io makes this easy by automatically recording every permission check in your app. Every time someone tries to access something, Permit.io logs the user, what they tried to do, what they tried to access, and whether they were allowed or blocked. This is very efficient for debugging and auditing your permissions setup - you can see exactly which checks are happening and their results.
To view the audit log in Permit.io, visit the "Audit Log" tab in your project dashboard.
Audit logs in Permit.io
Permit.io also offers embeddable elements that you can use to visualize your access decisions directly in your application. They are easily customizable and can be embedded in any web page to provide real-time insights into your RBAC system.
Currently, they offer three types of embeddable elements:
Audit logs: A timeline of all access decisions made by your application
User Management: A user interface for managing users and their roles and permissions
Approval Flows: A workflow for requesting and approving access to resources
For example, to create an audit log element, visit the "Elements" section in your Permit.io dashboard, scroll down to the "Audit Log" section, and click on "Create Element.”
Creating Permit.io embeddable elements
Fill in the requested details and customize the element's appearance and behavior to match your application's design. Then click on the "Create" button to save your changes. Once you are done and satisfied with the settings, you can copy the embed code and paste it into your application.
Creating Permit.io Audit Log widget
You can learn more about Permit.io's embeddable elements in their documentation.
Conclusion
Role-based Access Control (RBAC) is a powerful security model for managing permissions in your application. By assigning roles to users and permissions to roles, you create a scalable and maintainable system for controlling access to your resources.
In this article, we've seen how to implement RBAC in a React application using Permit.io. We set up a project, defined roles and permissions, and integrated Permit.io's SDK to check permissions in our frontend components. We also created a backend service to handle permission checks and implemented a custom hook to centralize permission logic.
By using Permit.io, we were able to define our permission model once and enforce it consistently across our stack. This reduces duplication and ensures that our permissions are always up-to-date. We also saw how Permit.io's audit logs provide valuable insights into our access decisions, helping us debug and monitor our RBAC system.
I hope this tutorial has given you a solid foundation for implementing RBAC in your React applications. With Permit.io, you can build secure, scalable applications with confidence, knowing that your permissions are well-managed and audited.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker