• RBAC
  • Next.JS

How to Add RBAC Into a Next.JS Application

Learn how to implement RBAC in Next.js applications with Permit.io, a permission management system. Follow a step-by-step guide in a to-do app.

Gabriel L. Manor

Mar 09 2023
How to Add RBAC Into a Next.JS Application
Share:

Introduction

As web applications become increasingly complex, ensuring that users have the appropriate level of access to the application's features and data is crucial. Without a well-defined access control system, it's easy for unauthorized users to 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 a robust permission management system can be a daunting task. Permit.io provides an end-to-end solution for managing permissions and roles for users with a simple, intuitive UI.

This 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 robust permission model derived from permissions you can easily set using Permit's UI. 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 dive in, 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 React - a full 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.

  1. 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/.

  2. 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 containing 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 the tasks, create, and update by clicking on the task text and marking them as completed.

todo_ap.png

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 take into account 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 identitiesroles, 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.

Create

Update

Mark

Delete

Read

Admin

Task

Editor

Task

Moderator

Task

Roles…


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.

  1. Go to the Policy page and click  Create > Resource

  2. Create the following resource and actions:

    Frame 68089.png

Next, we will discuss our identities and their roles. For that, we will first create different roles in our application.

  1. Go to the Policy page and click Create > Role

  2. Add the following roles

    Frame 68088 (1).png

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.

  1. Go to the Users screen

  2. Create one user per role

    users.png

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.

saveAPI.png
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: "http://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)Permit ElementsGitOps features for complex policy definitions, and many more.

Gabriel L. Manor

Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker

decorative background