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:
- Admin: Full access to all features
- Manager: Access to most features except critical system settings
- Editor: Can create and modify content but not delete
- 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:
// 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:
// 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:
// 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
// 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
// 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
// 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:
// 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:
// 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:
// 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:
- UI elements: Do role-specific UI elements appear/hide correctly?
- Route access: Are protected routes accessible only to authorized roles?
- API endpoints: Do API routes properly validate user roles?
Here's a simple test user setup for manual testing:
// 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:
- Caching user roles: Store roles in JWT claims or in-memory cache
- Minimize database lookups: Avoid fetching role information on every request
- Use client-side validation for UI elements to reduce server load
- 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:
-
Problem: Flickering UI when role-based components load Solution: Use skeleton loaders or suspense boundaries
-
Problem: Client-side RBAC being bypassed Solution: Always implement server-side validation as well
-
Problem: Role checking logic duplicated across components Solution: Use custom hooks or higher-order components
-
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
- Next.js Authentication Documentation
- Role-Based Access Control (NIST)
- JSON Web Tokens (JWT)
- NextAuth.js - A complete authentication solution for Next.js
Exercise
-
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
-
Create a user settings page that shows different options based on the user's role.
-
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! :)