Safeguarding User Role-Based Private Routes in Next.js 13 or 14 app router: A Step-by-Step Guide

Safeguarding User Role-Based Private Routes in Next.js 13 or 14 app router: A Step-by-Step Guide

Introduction:

In the realm of web development, crafting a secure and user-centric application is non-negotiable. Next.js versions 13.4+ and 14 offer a fantastic environment to ensure that specific routes are accessible only to users with designated roles. In this comprehensive blog post, we’ll walk you through the process of safeguarding private routes based on user roles within the Next.js app router. We’ve simplified the code explanation to ensure that you can effortlessly integrate this essential security feature into your web application.

Prerequisites:

Before we embark on the journey to secure your routes, you must have a Next.js project in place with a robust authentication system. This system can be built using NextAuth, Firebase, or even a custom solution with JWT (JSON Web Tokens). Make sure to establish this foundation before proceeding with our guide.

Now, let’s delve into the main topic: dynamically securing your routes based on user roles using middleware.

Setting up the Middleware:

To begin, you’ll need to create an middleware.ts or middleware.js file in the root directory of your Next.js project. Typically, this file is located within the 'src' or 'app' directory.

In the middleware file, you’ll need to import NextRequest and NextResponse from "next/server" like this:

import { NextRequest, NextResponse } from "next/server";

After this, you should get the pathname used request.nextUrl and create an absolute URL to facilitate redirection or rewriting. Here's how:

const { pathname } = request.nextUrl;
const url = request.nextUrl.clone();

Now, let’s move on to obtaining the user’s token, assuming that you have already completed the steps to set up the token on a cookie or in local storage:

const cookie = request.cookies.get(process.env.TOKEN as string);
const token = cookie?.value;

With these crucial steps in place, we can proceed to create variables that contain arrays of the links you wish to protect for different user roles.

const userRoutes = [
    "/dashboard/user/profile",
    "/dashboard/user/change-password",
];

const adminRoutes = [
    "/dashboard/admin/profile",
    "/dashboard/admin/change-password",
];

const superAdminRoutes = [
    ...adminRoutes,
    "/dashboard/super-admin/admins",
    "/dashboard/super-admin/add-admin",
];

These arrays specify the routes that are restricted to different user roles. Feel free to customize these arrays to match the unique route structure of your application.

In the code above, we’ve imported the necessary dependencies, retrieved the user’s token, and defined arrays for the routes you wish to protect based on user roles. This setup is essential for dynamic route protection.

Now, let’s proceed to the conditional rendering and redirection logic.

  if (token) {
     url.pathname = "/not-found";
    // Handle the case where the token exists
    // If the token is expired, redirect to the login page
    if (user.exp < Date.now() / 1000) {
      url.pathname = "/login";
      return NextResponse.redirect(url);
    }

    // Check the user's role and restrict access to specific routes
    if (
      (user.role !== userRole.ADMIN || user.role !== userRole.SUPER_ADMIN) &&
      (adminRoutes.includes(pathname) || superAdminRoutes.includes(pathname))
    ) {
      return NextResponse.redirect(url);
    }

    if (user.role === userRole.ADMIN && userRoutes.includes(pathname)) {
      return NextResponse.redirect(url);
    }

    if (user.role === userRole.ADMIN && superAdminRoutes.includes(pathname)) {
      return NextResponse.redirect(url);
    }

    if (user.role === userRole.SUPER_ADMIN && userRoutes.includes(pathname)) {
      return NextResponse.redirect(url);
    }
  }
}

This part of the code checks if a token exists. If it does, it further verifies if the token is expired. If the token is expired, the user is redirected to the login page for reauthentication.

Based on the user’s role, the code restricts access to specific routes. If a user tries to access a route they are not authorized for, they will be redirected to a predefined location.

By following these steps and explanations, you can effectively implement user role-based route protection in your Next.js version 13.4+ and 14+ app router applications. This approach helps ensure data security and user privacy, making your web application safer and more user-friendly.

In conclusion, securing routes based on user roles is a crucial aspect of building a reliable and secure web application. With the code and guidance provided in this blog post, you’re well-equipped to implement this critical feature in your Next.js project.

Here is the complete code in one file:

import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const url = request.nextUrl.clone();

  const cookie = request.cookies.get(process.env.TOKEN as string);
  const token = cookie?.value;

  const userRoutes = [
    "/dashboard/user/profile",
    "/dashboard/user/change-password",
  ];

  const adminRoutes = [
    "/dashboard/admin/profile",
    "/dashboard/admin/change-password",
  ];

  const superAdminRoutes = [
    ...adminRoutes,
    "/dashboard/super-admin/admins",
    "/dashboard/super-admin/add-admin",
  ];

  if (token) {
    url.pathname = "/not-found";

    const user = decodedToken(token as string) as IUserInfo;

    // if token is expired then redirect to login page
    if (user.exp < Date.now() / 1000) {
      url.pathname = "/signin";
      return NextResponse.redirect(url);
    }
    // if user role is not admin and user is trying to access admin routes then redirect to login page
    if (
      (user.role !== userRole.ADMIN || user.role !== userRole.SUPER_ADMIN) &&
      (adminRoutes.includes(pathname) || superAdminRoutes.includes(pathname))
    ) {
      return NextResponse.redirect(url);
    }

    // if admin is trying to access user routes then redirect to login page
    if (user.role === userRole.ADMIN && userRoutes.includes(pathname)) {
      return NextResponse.redirect(url);
    }
    // if admin is trying to access super-admin routes then redirect to login page
    if (user.role === userRole.ADMIN && superAdminRoutes.includes(pathname)) {
      return NextResponse.redirect(url);
    }
    // if super-admin is trying to access user routes then redirect to login page
    if (user.role === userRole.SUPER_ADMIN && userRoutes.includes(pathname)) {
      return NextResponse.redirect(url);
    }
  }
}

Thanks for reading!