• Next.js
  • Security
  • Middleware
  • RBAC

Implementing Role-Based Access Control in Next.js Using Middleware

Amit Hariyale

Amit Hariyale

Full Stack Web Developer, Gigawave

6 min read · June 14, 2025

Implementing robust access control is crucial for any SaaS application. When using Next.js with a custom backend, developers often face the challenge of integrating JWT-based authentication with middleware for role-based access control (RBAC). This guide walks through an effective solution for managing page-level permissions using Next.js middleware.

The Challenge: Custom Backend Integration

  • Authentication handled by separate backend service
  • JWT tokens stored in HTTP-only cookies
  • Need to verify roles before page rendering
  • Centralized access control for all protected routes
Important NoteMiddleware runs on the edge network, so we need lightweight JWT verification methods

Step 1: Understanding Our JWT Structure

Our backend team implemented JWTs with a role claim in the payload:

jwt-payload.d.ts
1interface JwtPayload { 2 userId: string; 3 role: "USER" | "ADMIN" | "MODERATOR"; 4 // ...other claims 5}

Step 2: Creating the Middleware

The middleware handles authentication and authorization before requests reach your pages:

middleware.ts
1import { NextResponse } from 'next/server'; 2import type { NextRequest } from 'next/server'; 3import { verifyToken } from '@/lib/auth'; // Custom JWT verifier 4 5// Role-based route configuration 6const roleRoutes: Record<string, string[]> = { 7 ADMIN: ['/dashboard', '/admin', '/settings'], 8 MODERATOR: ['/dashboard', '/mod-tools'], 9 USER: ['/dashboard'], 10}; 11 12export async function middleware(req: NextRequest) { 13 const token = req.cookies.get('auth-token')?.value; 14 const { pathname } = req.nextUrl; 15 16 // 1. Redirect unauthenticated users 17 if (!token) { 18 return NextResponse.redirect(new URL('/login', req.url)); 19 } 20 21 // 2. Verify JWT token 22 const payload = await verifyToken(token); 23 if (!payload) { 24 return NextResponse.redirect(new URL('/login', req.url)); 25 } 26 27 // 3. Check if path requires specific role 28 const requiredRole = Object.entries(roleRoutes).find(([_, paths]) => 29 paths.some(path => pathname.startsWith(path)) 30 )?.[0]; 31 32 // 4. Grant access if no specific role required 33 if (!requiredRole) return NextResponse.next(); 34 35 // 5. Verify user has required role 36 if (payload.role !== requiredRole) { 37 return NextResponse.redirect(new URL('/unauthorized', req.url)); 38 } 39 40 return NextResponse.next(); 41} 42 43export const config = { 44 matcher: [ 45 '/dashboard/:path*', 46 '/admin/:path*', 47 '/mod-tools/:path*', 48 '/settings/:path*' 49 ], 50};

Step 3: Key Components Explained

JWT Verification

Our custom verifyToken function:

lib/auth.ts
1import jwt from '@tsndr/cloudflare-worker-jwt'; // Lightweight JWT verifier 2 3export async function verifyToken(token: string) { 4 try { 5 const isValid = await jwt.verify(token, process.env.JWT_SECRET!); 6 if (!isValid) return null; 7 8 const { payload } = jwt.decode(token); 9 return payload as JwtPayload; 10 } catch (error) { 11 console.error('Token verification failed:', error); 12 return null; 13 } 14}

Role-Based Path Matching

Defining role/route relationships:

middleware.ts
1const roleRoutes = { 2 ADMIN: ['/dashboard', '/admin', '/settings'], 3 MODERATOR: ['/dashboard', '/mod-tools'], 4 USER: ['/dashboard'], 5};

Step 4: Handling Edge Cases

  • Token expiration handling
  • Role inheritance (Admin inherits Moderator permissions)
  • Public/private hybrid routes
  • API route protection
Pro TipFor role inheritance, implement a role hierarchy check:
middleware.ts
1const roleHierarchy = { 2 ADMIN: ['ADMIN', 'MODERATOR', 'USER'], 3 MODERATOR: ['MODERATOR', 'USER'], 4 USER: ['USER'] 5}; 6 7// In middleware: 8if (!roleHierarchy[payload.role].includes(requiredRole)) { 9 return NextResponse.redirect('/unauthorized'); 10}

Step 5: Server-Side Validation

Always verify roles in API routes too:

app/api/admin/route.ts
1import { verifyToken } from '@/lib/auth'; 2 3export async function GET(req: Request) { 4 const token = req.headers.get('Authorization')?.split(' ')[1]; 5 6 if (!token) { 7 return new Response('Unauthorized', { status: 401 }); 8 } 9 10 const payload = await verifyToken(token); 11 12 if (payload?.role !== 'ADMIN') { 13 return new Response('Forbidden', { status: 403 }); 14 } 15 16 // Admin-only logic 17}

Best Practices

  • Always verify tokens on both client and server
  • Implement short-lived access tokens (15-30 mins)
  • Use HTTP-only cookies for token storage
  • Maintain a centralized role definition
  • Log all access control failures

Useful Resources

Final Thoughts

Implementing RBAC with a custom backend required careful coordination between frontend and backend teams, but using Next.js middleware provided a clean, centralized solution. The key was establishing clear contracts for JWT structure and implementing lightweight verification for edge runtime.

Remember: Middleware is your first security layer, but always validate permissions in your backend too!

Next Blog

Mastering Next.js Routing From Basics to Advanced Patterns

Read Next