Implementing RBAC Authorization in Next.js - 2024 Updated Guide
- Share:
Ensuring that users have the appropriate level of access to your Next.js application features and data is crucial, especially as web applications become increasingly complex.
Without a well-defined access control system, unauthorized users can easily access sensitive information or perform harmful actions.
Next.js is a popular framework for building server-side rendered web applications quickly and efficiently. However, implementing an efficient permission management system can be a big hassle. Permit.io can help with that by providing an end-to-end solution for managing users' permissions and roles with a simple, intuitive UI.
This 2024 up-to-date tutorial will guide you through building a permission management system into a Next.js application using Permit.io. We'll do this by creating a simple to-do application that includes a permission model. By the end of this tutorial, you'll have a solid understanding of how to implement a well-defined permission management system in your Next.js application.
Before we get into it, it's important to note that this article assumes you have a basic knowledge of JavaScript, React, and Next.js. If you need to brush up on those skills, we recommend checking out some beginner-friendly resources:
Learn JavaScript – a curriculum and interactive course
Learn Next.js - a full handbook
With that said, let's get started!
Setting up the Next.js project
To get started, let's create a new Next.js project. To save you time and as you already understand the basics of Next.JS, we already have a starter project set up where you'll find the simple to-do app we’ll be using in this tutorial.
First, make sure you have Node.js and npm installed on your machine. You can download them from the official Node.js website: https://nodejs.org/en/.
Open a terminal window and create a new Next.js project using the following command:
npx create-next-app@latest permit-todo --use-npm --example https://github.com/permitio/permit-next-todo-starter next-tutorial && cd permit-todo
This will create a new Next.js project with the default settings.
3. Once the project is created, navigate to the project directory by running:
cd permit-todo
4. Next, let's add the necessary dependencies. We'll need the `permitio` package to enable permission management using Permit. Run the following command to install this package:
npm install permitio --save
This package provides the necessary tools to implement permissions easily into the API layer in your Next.js application.
With the project set up and the necessary dependencies installed, let’s go over the todo application’s code.
To-Do Application Code Overview
One of the advantages of Next.JS is the ability to write server-side code as part of a UI application. This way, we can easily enforce the permission model even if our backend does not support it yet.
Let's break it down into the pages/api/tasks.ts file, which contains an API route handler that enables the creation, retrieval, updating, and deletion of tasks in memory.
First, we define a Task interface that describes the shape of our task objects. Each task has a text property (a string) and an isCompleted property (a boolean).
export type Task = {
text: string,
isCompleted: boolean,
}
Then, we define a Response interface that describes the shape of our API response messages. Each response has a message property (a string).
type Response = {
message: string,
}
Next, we create an array of tasks representing our in-memory data store. We initialize it with three sample tasks.
const tasks: Task[] = [
{
text: 'Learn Next.js',
isCompleted: true,
}, {
text: 'Learn React.js',
isCompleted: false,
}, {
text: 'Learn ReactNative',
isCompleted: false,
},
];
After that, we define an asynchronous request handler function handler that takes in a Next.js NextApiRequest object and a NextApiResponse object.
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Task | Task[] | Response>
) {
We use a switch statement inside the handler function to handle different HTTP methods. We will have a short code for each one that will create, read, update, or remove the data sent from the frontend application in the array we initialized out of the function.
...
switch (req.method) {
case 'POST': {
tasks.push(req.body);
res.status(200).json(req.body);
break;
}
...
To complete the application, we also created a simple UI for the application that you can find in the index.tsx file. Since we are not focusing in this article on the authorization feature toggling in the frontend, we will not go in-depth through the code there.
Let's execute npm run dev
to view our to-do application in a browser running on our local machine.
As you can see in the application, we have a fully functional to-do application. We can list tasks, create them, and update them by clicking on the task text and marking them as completed.
Now that we have our application up and running, let’s add a permission management system to it!
Defining a Permission Model
Overall, the code snippet above provides a solid foundation for building a full-stack simple in-memory application. However, in a real-world scenario, a more sophisticated policy model is required to ensure the app's security. For instance, we may need to restrict certain user roles to specific actions, such as only allowing some users to view tasks while permitting others to mark them as complete or defining that only administrators should be authorized to delete tasks.
To address these requirements, we will implement a Role-Based Access Control (RBAC) model. In this model, each user is assigned a role that determines the actions they can perform within the app. When enforcing this policy, we consider the user's identity, role, and the action they are trying to perform (IRA) to make a decision about their authorization status.
To design our permission model, we need to identify the app's potential identities, roles, and resources. Using this information, we can create tables that outline the desired permissions for each role. This table will help us enforce the policy at various enforcement points in the application.
Roles | Create | Update | Mark | Delete | Read |
Admin | |||||
Task | ✔ | ✔ | ✔ | ✔ | ✔ |
Editor | |||||
Task | ✔ | ✔ | |||
Moderator | |||||
Task | ✔ | ✔ | |||
Understanding the Identity, Resource, and Action components of the enforcement point is crucial to ensure the authorization is done right. This will help us maintain the security of our app.
Use Permit to Configure the IRA Table
To simplify the RBAC implementation process and avoid creating complex, intertwined code within the application, Permit offers a decoupled policy that enables effortless checks to be added where needed. Permit also provides an SDK for easy integration into the application. To get started with configuring permissions, log in to app.permit.io
Once you have logged in, we can proceed to create appropriate resources and actions for our IRA design. At present, we have only one resource named "Task." To maintain simplicity, we will utilize the same HTTP methods that we intend to use in our application for these actions.
Go to the Policy page and click Create > Resource
Create the following resources and actions:
Next, we will discuss our identities and their roles. For that, we will first create different roles in our application.
Go to the Policy page and click Create > Role
Add the following roles
To complete our identities, we need to create users in the system. In the real world, we may use our identity management APIs to sync users with Permit, but for now, let's just add one user per role.
Go to the Users screen
Create one user per role
Now that we have configured our IRA table, Permit will do all the rest for us by running a policy decision point (PDP) in the cloud. This way, we will have a web address that we can call to get the policy decision on each point we would like to enforce the policy we just set. Later in production, we will want to use a local container. This is important in order to avoid latency every time someone calls an endpoint. Permit supports this option, and you can find more details on it on the Connect page.
Check Permissions with the Permit SDK
The Permit SDK empowers us to verify permission decisions in our IRA table through an asynchronous function invocation. In order to confirm whether our administrator possesses the
authorization to execute a GET operation, we shall consult the SDK as follows:
permit.check(‘admin@todo.app’, ‘get’, ‘Task’)
For our application, which showcases distinct roles within a brief list of users, this straightforward verification employs a hard-coded email. However, in a production environment, we would want to utilize JWTs (or any other auth token) belonging to our authenticated users instead of relying on hard coded emails. The implementation of tokens will also facilitate the proper transmission of roles, thereby decoupling the verification from the identity and roles configuration.
Since we already installed the `permitio` SDK at the beginning of the tutorial, we don’t need to install any other dependencies. We are ready to go and implement the permissions in the handler.
Add Permissions Checks to the Tasks Handler
1. Get an API Key
The `permitio` SDK provides us with a simple way to make API calls. It does so by initializing a Permit instance once with the API key of our Permit account and makes all the calls (and other API requests) by using this key. To grab your account key, go to the Project page, and, within your relevant environment (if it is a new account, it’ll be Default/Production), click the three dots and Copy the API Key.
2. Store the API Key in the Application
To prevent the unintentional pushing of this confidential key to a remote repository, it is important to employ it as an environment variable within a file that git ignores. Subsequently, generate a fresh file in the primary directory named .env.local and insert the KEY=value snippet there:
PERMIT_SDK_KEY=<your_copied_sdk_key>
Now it’s time to initialize our Permit instance. Since we are just giving an example we will do it in the tasks.ts file. In a real application we will want to make it available globally for all the handlers so they will consume it without reinitializing it.
import { Permit } from 'permitio';
const permit = new Permit({
// We’ll use a cloud hosted policy decision point
pdp: "https://cloudpdp.api.permit.io",
// The secret token we got from the UI
token: process.env.PERMIT_SDK_TOKEN,
});
3. Add a Permission Check
Since we have already invoked our resources in the method name, we can perform a check for all handler operations in one centralized location. Navigate to our tasks.ts file and insert the ensuing code snippet, which first verifies the user's existence in the headers (authentication) and then incorporates the "magic" permit check function for authorization:
const { user } = req.headers;
if (!user) {
res.status(401).json({ message: 'unauthorized' });
return;
}
const isAllowedForOperation = await permit.check(user as string, req.method?.toLowerCase() as string, 'Task');
if (!isAllowedForOperation) {
res.status(403).json({ message: 'forbidden' });
return;
}
4. Add a User to the API Calls
Now, when we return to the application view in the browser we will see there are no visible tasks in the UI. This is because we have to add the user in our API call. Let’s do it in the index.tsx file, add the following code in the fetch config at the api function.
const req: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
user: 'admin@permit-todo.app'
},
};
Go back to the UI - you can now see that tasks can be listed, yet any other operation will fail with a ‘forbidden’ response. If you go back to the code and change it to admin@permit-todo.app, try to remove a task and see the magic happen 🙂
As we said already, now it looks a bit frustrating to manually replace the user for each test, but the real app will just get the authenticated user instead. Happy permissioning!
What Next?
Congratulations! By using Permit, you just successfully implemented role-based access control (RBAC) into a Next.js application. This tutorial taught us how to set up and configure Permit to secure the application and control user access based on their roles.
By implementing RBAC, you can improve the security of your application and restrict user access to the features and resources they should be allowed to access. This is crucial in protecting your application from unauthorized access and data breaches.
Now that you have implemented RBAC into your application, you can improve your applications' security by leveraging it to real use cases in your application and even use some advanced features in Permit, such as ABAC (Attribute-based access control), Elements, GitOps features for complex policy definitions, and many more.
Want to learn more about implementing authorization? Got questions? Reach out to us in our Slack community.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker