Md Nazmus Shakib
Back to articles
Technology

Implementing Role-Based Access Control (RBAC) and JWT in Next.js App Router

Md Nazmus Shakib

Md Nazmus Shakib

MERN Stack Developer

27m Total Read
Implementing Role-Based Access Control (RBAC) and JWT in Next.js App Router

Building a modern SaaS application, especially in critical sectors like healthcare or enterprise management, requires a bulletproof authorization layer. A system where a 'Patient' can accidentally or maliciously access a 'Doctor's dashboard or 'Admin' settings is a catastrophic security failure.

With the introduction of the Next.js App Router, the paradigm of route protection has shifted entirely to the server side. In this deep-dive technical guide, we will engineer a secure Role-Based Access Control (RBAC) system utilizing JSON Web Tokens (JWT) and Next.js Edge Middleware to isolate privileges across Administrators, Doctors, and Patients.

1. Architectural Foundation: JWT Payload Engineering

The core philosophy of a stateless JWT architecture is that the token itself must carry the user's identity and their authorization claims (Roles). If your middleware has to query the database on every single page load to check if a user is an Admin, your application will suffer from severe latency bottlenecks.

When authenticating a user (via NextAuth.js or a custom backend), you must explicitly embed their designated role into the token signature.

auth/options.ts
import { NextAuthOptions } from "next-auth";

export const authOptions: NextAuthOptions = {
  session: { strategy: "jwt" },
  callbacks: {
    async jwt({ token, user }) {
      // On initial login, 'user' object is available
      if (user) {
        token.id = user.id;
        token.role = user.role; // e.g., 'ADMIN', 'DOCTOR', 'PATIENT'
        token.clinicId = user.clinicId;
      }
      return token;
    },
    async session({ session, token }) {
      // Expose the role to the client-side session
      if (token) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
        session.user.clinicId = token.clinicId as string;
      }
      return session;
    }
  }
};

2. Edge Middleware: The Gatekeeper

Client-side route protection (using React useEffect to redirect users) is purely cosmetic. A malicious actor can easily disable JavaScript or inspect network payloads to bypass it. True security occurs at the Edge, before the server even renders the HTML.

Next.js Middleware intercepts incoming requests. We can decode the JWT at this layer and cross-reference the user's role against the requested URL path.

middleware.ts
import { NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import type { NextRequest } from "next/server";

export async function middleware(req: NextRequest) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  const url = req.nextUrl.pathname;

  // 1. Unauthenticated users
  if (!token) {
    if (url.startsWith("/dashboard")) {
      return NextResponse.redirect(new URL("/login", req.url));
    }
    return NextResponse.next();
  }

  // 2. Role-Based Access Control Matrices
  const userRole = token.role;

  // Protect Admin Routes
  if (url.startsWith("/dashboard/admin") && userRole !== "ADMIN") {
    return NextResponse.redirect(new URL("/unauthorized", req.url));
  }

  // Protect Doctor Routes (Admins can also access)
  if (url.startsWith("/dashboard/doctor") && !["ADMIN", "DOCTOR"].includes(userRole)) {
    return NextResponse.redirect(new URL("/unauthorized", req.url));
  }

  // Patients can only access their specific portal
  if (url.startsWith("/dashboard/patient") && userRole !== "PATIENT") {
    return NextResponse.redirect(new URL("/unauthorized", req.url));
  }

  return NextResponse.next();
}

// Optimize middleware execution
export const config = {
  matcher: ["/dashboard/:path*"],
};
Security Warning: API Protection

Middleware protects the rendering of pages, but it is not enough. You must also enforce RBAC directly inside your API Route Handlers and Server Actions. A clever attacker can bypass page UI and send POST requests directly to your API endpoints via Postman.

3. Securing Server Actions & Data Mutations

When executing database writes—such as a doctor updating a patient's medical record—you must independently verify the session token and role within the Server Action context.

app/actions/medical-records.ts
"use server";

import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import db from "@/lib/db";

export async function updatePatientPrescription(patientId: string, data: any) {
  // 1. Verify Authentication
  const session = await getServerSession(authOptions);
  
  if (!session) {
    throw new Error("Unauthorized: Please log in.");
  }

  // 2. Enforce Strict Role Verification
  if (session.user.role !== "DOCTOR" && session.user.role !== "ADMIN") {
    throw new Error("Forbidden: Insufficient privileges to alter medical records.");
  }

  // 3. Safe Database Execution
  await db.prescriptions.updateOne(
    { patientId, clinicId: session.user.clinicId },
    { $set: { ...data, updatedBy: session.user.id } }
  );

  return { success: true };
}

4. Server-Side UI Condensation

Finally, your UI should adapt dynamically so users don't even see buttons or links they aren't allowed to click. Because Next.js App Router heavily utilizes React Server Components (RSC), you can read the session directly on the server without sending unnecessary JavaScript to the browser.

import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import Link from "next/link";

export default async function Sidebar() {
  const session = await getServerSession(authOptions);
  const role = session?.user?.role;

  return (
    <nav>
      <Link href="/dashboard">Overview</Link>
      
      {/* Conditionally Render Admin Links securely on the server */}
      {role === "ADMIN" && (
        <Link href="/dashboard/admin/billing">Platform Billing</Link>
      )}
      
      {/* Doctor Links */}
      {role === "DOCTOR" && (
        <Link href="/dashboard/doctor/patients">My Patients</Link>
      )}
    </nav>
  );
}

Conclusion

Implementing Role-Based Access Control in Next.js is a multi-layered security engineering task. By embedding roles efficiently within JWT payloads, enforcing strict routing boundaries at the Edge Middleware, and validating privileges at the Server Action layer, you construct a highly scalable and impenetrable architecture suited for enterprise SaaS and healthcare applications.

#Next.js#Security#JWT#RBAC#Web Architecture#Authentication#SaaS
Md Nazmus Shakib
Written By

Md Nazmus Shakib

MERN Stack Developer

Specializing in the MERN stack and modern frameworks to engineer secure, SEO-optimized, and scalable digital solutions for visionary brands.

More about the author →