Build Role-Based Access Control (RBAC) in Next.js with Oso
Originally written for Oso.
Introduction
With complex applications becoming a norm, the right level of access to your application is essential for security and proper overall functionality. A rigid access system is a must to prevent unauthorized access to sensitive information.
Next.js is an open-source framework for building server-side rendered React applications. It is built on top of Node.js and provides several features that make creating fast, SEO-friendly web applications easy.
This article aims to implement a Role-based Access Control (RBAC) system in Next.js using Oso - an authorization provider - in TypeScript.
Note: This article will mention both authentication and authorization.
”Authentication is the mechanism for verifying who a user is. A username and password, for instance, verify that you can log in to an account.
Authorization is the mechanism for controlling what a user can do. For instance: after you’ve logged in, do you have access to a particular private git repository on GitHub?”
Prerequisites
To get started, make sure you have the following installed:
- Node.js
- TypeScript
- npm (comes bundled with Node.js)
Setting up the Next.js project
To set up this project, you will need to install Next.js and all dependencies required.
Open your terminal window and start by creating a new Next.js project using the following command:
$ npx create-next-app@latest --ts
This will create a new Next.js project with TypeScript. Now, change your directory inside that project.
$ cd <name of your project>
Next, you will need a few dependencies to get running. You’ll need oso-cloud
- an Oso API wrapper for Node.js and dotenv
for managing the environment variables. You might also need next-auth
to handle authentication providers depending on your authentication preferences.
$ npm i --save oso-cloud dotenv
This will set up the project and install all necessary dependencies.
Type Definitions
Now, in the src
folder, create a new folder types
. In this folder, we will store types and interfaces required to ensure type safety within the application.
Create a new file inside the src/types
folder interfaces.ts
. Inside the file, define the User interface:
export interface User {
name?: string | null | undefined;
role?: string;
}
If you’re using next-auth
for authentication, you will also need to create next-auth.d.ts
in the same folder and declare the module with the Session interface.
import NextAuth from "next-auth";
import { User } from "./interfaces";
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user: User;
}
}
Now, you’re ready to set up Rules in Oso and write some actual code.
Setting up Rules in Oso
In this step, you will set up a simple policy (Rule) allowing a single user to perform a specific action on a repository. In Oso, policies are written in Polar.
You can use Oso’s Rule Editor choose pre-built policies, or write a standalone policy.
Let’s create a simple policy that will allow John (a user) to read (an action) posts (a repository/resource).
In the Rules Editor, write the following Polar code.
allow("John", "read", "posts");
In the top-right corner, press Save to save the policy. You can also define tests for the policy like this:
test "john can read posts" {
assert allow("John", "read", "posts");
assert_not allow("John", "write", "posts");
}
Now, press Save and Run Tests in the top-right corner to save the policy and run the defined test. You should see “All tests passed!” if everything is good. Read more about testing policies in Oso.
Implementing the Oso Instance
In your Next.js project, you will create an authorization route in the Next.js API. Since Oso doesn’t support client-side execution, you will need to put it inside the API, which will make sure that sensitive information isn’t exposed to the end user.
In the src/pages/api folder
, create a file authorize.ts
. The authorize.ts
will serve as a route handler to perform an authorization call to Oso, checking if the policy requirements are met.
Start by importing all necessary dependencies:
import type { NextApiRequest, NextApiResponse } from 'next';
import { Oso } from 'oso-cloud';
const API_KEY: string = process.env.OSO_AUTH || '';
const API_URL: string = 'https://cloud.osohq.com';
Next, you will create a handler function:
export default function handler(req: NextApiRequest, res: NextApiResponse)
Create an Oso instance inside the handler
function and parse the query parameters sent from the client.
const oso = new Oso(API_URL, API_KEY);
const { userId, resource, action } = req.query;
To prevent stalled requests, you must wrap all response logic in a Promise. Afterward, you can perform an authorization call to Oso to check the query data against the defined policy.
return new Promise((resolve, reject) => {
oso
.authorize(String(userId), String(action), String(resource))
.then((result) => {
console.log(result);
res.status(200).json({ actionAllowed: result });
})
.catch((error) => {
console.log(error);
res.status(500).json({ actionAllowed: false });
resolve(error);
});
resolve(true);
});
Implementing NextAuth
If you opt-in for using NextAuth, follow the next steps to implement a simple Credentials Provider.
Create a [..nextauth].ts
file inside src/pages/api/auth
folder. Start by importing necessary dependencies.
import CredentialsProvider from 'next-auth/providers/credentials';
import NextAuth from 'next-auth';
import type { NextAuthOptions } from 'next-auth';
Next, add authentication options which will include your preferred providers, callbacks and pages.
export const authOptions: NextAuthOptions = {
// Configure one or more authentication providers
providers: [
// ...add more providers here
CredentialsProvider({
// The name to display on the sign in form (e.g. "Sign in with...")
name: 'Credentials',
// `credentials` is used to generate a form on the sign in page.
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
username: {
label: 'Username',
type: 'text',
placeholder: 'jsmith',
},
password: {
label: 'Password',
type: 'password',
},
},
Continue with implementing the authorize
function which takes the credentials and the request as its parameters.
async authorize(credentials, req) {
const { username, password } = credentials as any;
const res = await fetch('http://localhost:8000/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
});
const user = await res.json();
console.log({ user });
if (res.ok && user) {
return user;
} else return null;
},
}),
],
Now, you will implement callbacks. Callbacks are asynchronous functions that control what happens when a specific action is performed.
callbacks: {
async jwt({ token, user }) {
return { ...token, ...user };
},
async session({ session, token, user }) {
// Send properties to the client, like an access_token from a provider.
session.user = token;
return session;
},
},
Finally, add the pages object, containing the routes to authentication-specific pages.
NextAuth automatically creates simple, unbranded authentication pages for handling Sign in, Sign out, Email Verification and displaying error messages.
pages: {
signIn: '/auth/login',
},
At the end, you just have to export the NextAuth with the following:
export default NextAuth(authOptions);
That’s it. You have now successfully implemented NextAuth with a very simple credentials-based provider.
Implementing Authorization in Next.js with Oso
In this step, you will implement this authorization logic inside a Next.js page facing the client.
From the pages
folder, open the index.tsx
file (or create a new page that will be protected with authorization).
Start by importing the dependencies:
import { useState, useEffect } from 'react';
import { User } from '../types/interfaces';
You will now create a helper function canRead
which will return a boolean indicating whether the user can perform a specific action (in this case, read).
async function canRead(user: User): Promise<boolean> {
First, the function call the API route you created previously.
const response = await fetch(
`/api/authorize?userId=${user.name}&action=read&resource=posts`
);
We will take the actionAllowed
object from the response and return the boolean accordingly.
const { actionAllowed } = await response.json();
return actionAllowed;
}
Next, for the sake of simplicity, you will create a temporary user containing the name and role.
In a real example, you’d preferably take the user from the session.
const tempUser: User = {
name: 'John',
role: 'admin',
};
Inside the Home
function (or the functional component of your page) add the data state hook.
const [data, setData] = useState<User | null>(null);
To update the page accordingly, use the useEffect
hook.
useEffect(() => {
canRead(tempUser).then((readable) => readable && setData(tempUser));
}, []);
return <>{data?.name || 'denied'}</>;
That’s it. A simple logic that will display the user’s name if the user is authorized. Note that this shouldn’t be used in production - it’s just a simple explanation of how you could use Oso with Next.js.
You can find the full code example on GitHub.
Testing the Application
To test the application, make sure that the user defined in the Oso policy matches the one in the development environment.
In the terminal window, start the Next.js project by running npm run dev
. This will start the server by default on http://localhost:3000
.
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info - Loaded env from /Users/oso/Projects/oso/oso-rbac-nextjs/.env
(node:5474) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
event - compiled client and server successfully in 318 ms (170 modules)
Head to your browser and open the designated server URL (http://localhost:3000).
In case the user is authorized, you should see the name of the user shown on the screen. If the user is unauthorized, you will see denied
.
Authorized:
Unauthorized:
Further Resources and Steps
As the complexity of the application increases, the need for a robust authorization system increases too. Oso is an Authorization-as-a-Service provider dedicated to simplifying authorization infrastructure and drastically reducing time-to-market.
To learn more about Oso and how to use it, refer to the official Oso Cloud Documentation.
Oso’s Authorization Academy is an excellent place to learn about authorization, Polar policies, RBAC, ReBAC (Relationship-Based Access Control), and more.
RBAC, as a policy access-control mechanism, is designed around roles and privileges. Implementing RBAC for granular control over resource access, simplified management of access control, and scalability in managing roles and permissions are some of the reasons to utilize RBAC.
To use this application in production, it is advised to use NextAuth, or a similar authentication provider.
Conclusion
In this article, you have learned how to implement Role-Based Access Control (RBAC) in a Next.js application using Oso - an authorization provider. Covering the project setup, type definitions, policy setup and logic handling inside the Next.js API.
Oso is used and trusted by major players like Visa, Verizon and Intercom that opt for AaaS providers in their complex infrastructures.