In the rapidly evolving world of web development, ensuring that your application has robust and secure access control is paramount. One of the challenges we often face is implementing a flexible yet straightforward role-based access control (RBAC) system that scales with our application's needs. In this blog post, we'll dive into how you can leverage Next.js and NextAuth to create a dynamic RBAC system that can handle complex access patterns, including deeply nested and dynamic routes.
Define the Access Control List (ACL)
At the core of our RBAC system is the access control listACL. This object defines which roles have access to specific endpoints within our application. For example, a "admin" can access a wide range of endpoints, from the homepage to detailed project views and payment information. In contrast, a "visitor" has more limited access, focusing mainly on informational endpoints.
The beauty of this setup lies in its simplicity and flexibility. By defining roles and their associated endpoints in a central object, we can easily update access rights as our application grows and evolves.
const roleAccessMap = {
"admin": [
"/",
"/companies",
"/companies/[id]",
"/tickets",
"/tickets/[id]",
"/team",
"/payments",
"/payments/[id]",
"/analytics",
],
"cs": [
"/",
"/companies",
"/companies/[id]",
"/tickets",
"/tickets/[id]",
"/team",
],
"developer": [
"/",
"/projects",
"/projects/[id]",
"/account",
"/company",
"/tickets",
"/tickets/[id]",
"/team",
],
"visitor": [
"/",
"/projects",
"/account",
"/company",
"/support",
],
};
Deep Link Access Control Logic
/tickets/[id]
, requires a more nuanced approach. This is where our doesRoleHaveAccessToURL
function shines. By converting our defined routes into regex patterns, we can accurately determine whether a role has access to a requested URL, even if that URL includes dynamic segments.
This approach ensures that our access control logic remains concise and maintainable, regardless of how complex our application's routing becomes.
function doesRoleHaveAccessToURL(role, url) {
const accessibleRoutes = roleAccessMap[role] || [];
return accessibleRoutes.some(route => {
// Create a regex from the route by replacing dynamic segments
const regexPattern = route.replace(/[.*?]/g, "[^/]+").replace("/", "\/");
const regex = new RegExp('^regexPattern$');
return regex.test(url);
});
}
Middleware for Authentication and Role Control
Integrating this RBAC system with NextAuth is straightforward thanks to Next.js middleware. Our custom middleware function checks the user's role against the requested URL's access rights. If the user doesn't have the appropriate access, they're redirected to a 403 error, enhancing the application's security posture.
export default withAuth(
function middleware(req) {
// Redirect to login page if there is no accessible token
if (!req.nextauth.token) {
return NextResponse.redirect("/auth/login");
}
const role = req.nextauth.token.role;
let haveAccess = doesRoleHaveAccessToURL(role,req.nextUrl.pathname)
if(!haveAccess)
{
// Redirect to login page if user has no access to that particular page
return NextResponse.rewrite(new URL("/403", req.url));
}
// Allow
},
);
Fine-Grained In-Page Access Levels
Beyond controlling route access, our system provides a mechanism for managing in-page action permissions through a custom hook, useRoleAccessLevel
. This hook takes a page name and optionally a user role, then returns the permissions for editing or removing content on that page. This allows for a nuanced control of UI elements based on the user's capabilities, enhancing the application's security and user experience.
First we need to define the role access level object for each page that we wanna set accesses.
const roles = {
'admin': {
projects: { edit: 1, remove: 0 },
ticket: { edit: 1, remove: 1 },
company: { edit: 1, remove: 0 },
},
'developer': {
projects: { edit: 1, remove: 0 },
ticket: { edit: 1, remove: 1 },
company: { edit: 1, remove: 0 },
},
'visitor': {
projects: { edit: 1, remove: 0 },
ticket: { edit: 1, remove: 0 },
company: { edit: 0, remove: 0 },
},
};
Then let's define the hook to recognize the active user role and active page;
// It's optional to define page and role here
const useRoleAccess = ({page, role}: {page?: string; role?: string}) => {
const session = useSession();
const router = useRouter();
if (session.status === 'loading' || session.status === "unauthenticated") return {};
if (!role) {
role = session.data && session.data.user.role;
}
if (!page) page = router.pathname;
const accessLevels = roles[role]?.[page];
return { editLevel: accessLevels.edit, removeLevel: accessLevels.remove};
};
export default useRoleAccess;
Using this hook, developers can conditionally render elements or enable actions, effectively tailoring the UI to match the user's permissions in the page.
const { editLevel, removeLevel } = useRoleAccess();
Conclusion
Implementing a sophisticated RBAC system in NextJS with NextAuth not only enhances the security of your application but also provides a seamless and dynamic user experience. By carefully mapping roles to routes, employing regex for dynamic route matching, and leveraging hooks for in-page permissions, developers can create highly customizable and secure applications.