Skip to main content

Next.js Role-Based Access

Introduction

Role-Based Access Control (RBAC) is a security mechanism that restricts system access to authorized users based on their roles within an organization. In web applications, implementing RBAC ensures that users can only access the features and resources appropriate for their assigned role.

In this tutorial, we'll learn how to implement role-based access control in a Next.js application. We'll cover:

  • Understanding the basics of RBAC
  • Setting up a role system in Next.js
  • Protecting routes based on user roles
  • Creating role-based UI components
  • Testing and troubleshooting your RBAC implementation

Understanding Role-Based Access Control

Before diving into code, let's understand the core concepts of RBAC:

  • Roles: Categories assigned to users (e.g., Admin, Editor, User)
  • Permissions: Specific actions that can be performed (e.g., create, read, update, delete)
  • Access Control: Mechanisms that enforce role-based restrictions

A typical RBAC system might have roles like:

  1. Admin: Full access to all features
  2. Manager: Access to most features except critical system settings
  3. Editor: Can create and modify content but not delete
  4. User: Limited access to view content only

Setting Up User Roles in Next.js

Let's start by setting up a basic authentication system with user roles.

1. Define Role Types

First, create a type definition for your roles:

typescript
// types/auth.ts
export type UserRole = 'admin' | 'manager' | 'editor' | 'user';

export interface User {
id: string;
name: string;
email: string;
role: UserRole;
permissions?: string[];
}

2. Create an Authentication Context

Next, set up an authentication context to manage the user state and roles:

tsx
// contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { User, UserRole } from '../types/auth';

interface AuthContextType {
user: User | null;
login: (user: User) => void;
logout: () => void;
hasRole: (roles: UserRole | UserRole[]) => boolean;
hasPermission: (permission: string) => boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
// Check for saved user in localStorage or session
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
}, []);

const login = (userData: User) => {
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
};

const logout = () => {
setUser(null);
localStorage.removeItem('user');
};

const hasRole = (roles: UserRole | UserRole[]) => {
if (!user) return false;

if (Array.isArray(roles)) {
return roles.includes(user.role);
}

return user.role === roles;
};

const hasPermission = (permission: string) => {
if (!user || !user.permissions) return false;
return user.permissions.includes(permission);
};

return (
<AuthContext.Provider value={{ user, login, logout, hasRole, hasPermission }}>
{children}
</AuthContext.Provider>
);
}

export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

3. Wrap Your Application with the AuthProvider

Update your _app.tsx file to include the AuthProvider:

tsx
// pages/_app.tsx
import { AppProps } from 'next/app';
import { AuthProvider } from '../contexts/AuthContext';

function MyApp({ Component, pageProps }: AppProps) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
}

export default MyApp;

Protecting Routes with Middleware

Next.js 12+ provides middleware support, which is perfect for implementing route protection based on user roles.

Create a Role-Based Middleware

typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
const path = req.nextUrl.pathname;

// Get the user data from a secure HTTP-only cookie
const user = req.cookies.get('user')?.value
? JSON.parse(req.cookies.get('user')?.value || '{}')
: null;

// Define protected routes and required roles
const protectedRoutes = {
'/admin': ['admin'],
'/dashboard': ['admin', 'manager', 'editor'],
'/editor': ['admin', 'editor']
};

// Check if the path is protected
for (const [route, allowedRoles] of Object.entries(protectedRoutes)) {
if (path.startsWith(route)) {
// If user is not logged in, redirect to login
if (!user) {
return NextResponse.redirect(new URL('/login', req.url));
}

// If user doesn't have the required role, redirect to unauthorized page
if (!allowedRoles.includes(user.role)) {
return NextResponse.redirect(new URL('/unauthorized', req.url));
}
}
}

return NextResponse.next();
}

// Specify which routes this middleware should run on
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*', '/editor/:path*']
};

Creating Role-Based UI Components

Let's create components that only render when the user has a specific role:

Role-Based Component

tsx
// components/RoleBasedComponent.tsx
import { ReactNode } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { UserRole } from '../types/auth';

interface RoleBasedComponentProps {
children: ReactNode;
allowedRoles: UserRole | UserRole[];
fallback?: ReactNode;
}

export default function RoleBasedComponent({
children,
allowedRoles,
fallback = null
}: RoleBasedComponentProps) {
const { hasRole } = useAuth();

if (hasRole(allowedRoles)) {
return <>{children}</>;
}

return <>{fallback}</>;
}

Usage Example

tsx
// pages/dashboard.tsx
import { useAuth } from '../contexts/AuthContext';
import RoleBasedComponent from '../components/RoleBasedComponent';

export default function Dashboard() {
const { user } = useAuth();

return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.name}!</p>

{/* Admin-only section */}
<RoleBasedComponent allowedRoles="admin">
<div className="admin-panel">
<h2>Admin Panel</h2>
<button>Manage Users</button>
<button>System Settings</button>
</div>
</RoleBasedComponent>

{/* Editors and admins */}
<RoleBasedComponent
allowedRoles={['admin', 'editor']}
fallback={<p>You don't have permission to edit content.</p>}
>
<div className="editor-tools">
<h2>Content Management</h2>
<button>Create Post</button>
<button>Manage Media</button>
</div>
</RoleBasedComponent>

{/* All authenticated users */}
<div className="user-content">
<h2>Your Activity</h2>
<p>Recent notifications and updates will appear here.</p>
</div>
</div>
);
}

API Route Protection

You should also protect your API routes based on user roles:

typescript
// pages/api/admin/users.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { verifyUserRole } from '../../../utils/auth';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
// Verify that user has admin role
const user = await verifyUserRole(req, ['admin']);

if (!user) {
return res.status(403).json({
error: 'Unauthorized: Insufficient permissions'
});
}

// Handle the actual API logic for admin users
const users = await fetchAllUsers(); // Your database function

return res.status(200).json({ users });
} catch (error) {
return res.status(500).json({
error: 'Error processing request'
});
}
}

Here's the implementation of the verifyUserRole utility:

typescript
// utils/auth.ts
import { NextApiRequest } from 'next';
import { UserRole } from '../types/auth';
import { verifyToken } from './jwt'; // Your JWT verification utility

export async function verifyUserRole(
req: NextApiRequest,
allowedRoles: UserRole[]
) {
// Get the token from the authorization header
const token = req.headers.authorization?.split(' ')[1];

if (!token) {
return null;
}

try {
// Verify the JWT token and extract user data
const user = await verifyToken(token);

// Check if user has one of the allowed roles
if (user && allowedRoles.includes(user.role)) {
return user;
}

return null;
} catch (error) {
return null;
}
}

A Complete Example: Dashboard with Role-Based Navigation

Let's put everything together in a real-world example of a dashboard with role-based navigation:

tsx
// components/DashboardLayout.tsx
import { useState } from 'react';
import Link from 'next/link';
import { useAuth } from '../contexts/AuthContext';
import RoleBasedComponent from './RoleBasedComponent';

interface DashboardLayoutProps {
children: React.ReactNode;
}

export default function DashboardLayout({ children }: DashboardLayoutProps) {
const { user, logout } = useAuth();
const [isSidebarOpen, setIsSidebarOpen] = useState(true);

return (
<div className="dashboard-container">
<nav className="top-nav">
<button onClick={() => setIsSidebarOpen(!isSidebarOpen)}>
{isSidebarOpen ? '✖' : '☰'}
</button>
<div className="logo">CompanyApp</div>
<div className="user-menu">
<span>{user?.name} ({user?.role})</span>
<button onClick={logout}>Logout</button>
</div>
</nav>

<div className="dashboard-content">
<aside className={`sidebar ${isSidebarOpen ? 'open' : 'closed'}`}>
<ul>
<li><Link href="/dashboard">Dashboard Home</Link></li>
<li><Link href="/dashboard/profile">My Profile</Link></li>

<RoleBasedComponent allowedRoles={['admin', 'manager', 'editor']}>
<li><Link href="/dashboard/content">Content Management</Link></li>
</RoleBasedComponent>

<RoleBasedComponent allowedRoles={['admin', 'manager']}>
<li><Link href="/dashboard/analytics">Analytics</Link></li>
</RoleBasedComponent>

<RoleBasedComponent allowedRoles="admin">
<li className="admin-section">
<h3>Administration</h3>
<ul>
<li><Link href="/admin/users">User Management</Link></li>
<li><Link href="/admin/roles">Role Management</Link></li>
<li><Link href="/admin/settings">System Settings</Link></li>
</ul>
</li>
</RoleBasedComponent>
</ul>
</aside>

<main className="main-content">
{children}
</main>
</div>
</div>
);
}

Testing Your RBAC Implementation

To test your RBAC system, create test accounts with different roles and verify:

  1. UI elements: Do role-specific UI elements appear/hide correctly?
  2. Route access: Are protected routes accessible only to authorized roles?
  3. API endpoints: Do API routes properly validate user roles?

Here's a simple test user setup for manual testing:

typescript
// Mock users for testing
const testUsers = [
{
id: '1',
name: 'Admin User',
email: '[email protected]',
role: 'admin',
permissions: ['create', 'read', 'update', 'delete', 'manage_users']
},
{
id: '2',
name: 'Manager User',
email: '[email protected]',
role: 'manager',
permissions: ['create', 'read', 'update', 'delete']
},
{
id: '3',
name: 'Editor User',
email: '[email protected]',
role: 'editor',
permissions: ['create', 'read', 'update']
},
{
id: '4',
name: 'Basic User',
email: '[email protected]',
role: 'user',
permissions: ['read']
}
];

Performance Considerations

Role-based access control can impact performance if not implemented carefully:

  1. Caching user roles: Store roles in JWT claims or in-memory cache
  2. Minimize database lookups: Avoid fetching role information on every request
  3. Use client-side validation for UI elements to reduce server load
  4. Pre-compute permissions for complex RBAC systems with many roles

Common Pitfalls and Solutions

Here are some common issues when implementing RBAC in Next.js:

  1. Problem: Flickering UI when role-based components load Solution: Use skeleton loaders or suspense boundaries

  2. Problem: Client-side RBAC being bypassed Solution: Always implement server-side validation as well

  3. Problem: Role checking logic duplicated across components Solution: Use custom hooks or higher-order components

  4. Problem: Session timeout handling Solution: Implement token refresh logic and handle expired sessions gracefully

Summary

In this guide, we've learned how to implement role-based access control in Next.js applications:

  • Setting up an authentication context with role management
  • Protecting routes using Next.js middleware
  • Creating role-based UI components
  • Securing API endpoints based on user roles
  • Building a complete dashboard with role-based navigation

By implementing RBAC, you can create secure, personalized experiences for users with different roles and permissions within your Next.js application.

Further Resources

Exercise

  1. Implement a role-based access control system for a simple blog where:

    • Admins can create, edit, and delete any posts
    • Editors can create and edit their own posts
    • Users can only view posts
  2. Create a user settings page that shows different options based on the user's role.

  3. Implement a "permission" system on top of roles that allows for more granular access control.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)