Security with Next.js: Utilizing Server Components, NextAuth v5, and Multi-Middleware

Apr 20, 2024

next-jsauthsecurity

In today's digital age, selecting the right tools and frameworks is crucial not only for the efficiency of our development processes but also for the security of our applications. Next.js is a leading framework in the React ecosystem which has recently introduced a game-changing feature called React Server Components (RSC). This feature promises to improve performance and efficiency by intelligently dividing rendering responsibilities between the server and the client. However, with this advanced feature comes a greater responsibility, particularly in terms of ensuring the security of the application.

In this blog post, we'll take a deep dive into the security aspects of using Next.js with React Server Components. We'll explore different data handling models that are perfect for different project needs - from large-scale enterprise apps to quick prototypes. We'll also talk about how NextAuth.js can be used for strong authentication mechanisms and how using multiple middleware in Next.js can make your app more secure. We'll examine specific strategies and best practices so that you're well-equipped to implement RSC effectively and with top-notch security in mind.

How about we make the most of Next.js while keeping our apps safe from the ever-changing web dangers? ✨

Understanding Middleware in Next.js for Enhanced Security

Next.js has a feature called middleware that helps developers to do stuff on the server before responding to a request. That's great for things like making sure users have the right permissions to access certain parts of an app. You know, to keep things secure and only let the right people in 🔐

Here's an example of how to implement middleware using Next.js:


import { chain } from '@/lib/middlewares/chain';
import { authMiddleware } from '@/lib/middlewares/auth-middleware';
import { permissionMiddleware } from '@/lib/middlewares/permissions-middleware';

export default chain([authMiddleware, permissionMiddleware]);

export const config = {
    matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

Code Explanation:

This setup thing is super helpful in a Next.js app where we gotta have control over security and access rights in different parts of our app

To keep things secure, we double-check that you are who you say you are and that you have the right permissions before we let you access anything important. By doing this, we make sure that everything is safe and sound. Take a look below, I'll break down each part of the strategy for you:

Importing Middleware Functions:
import { NextMiddlewareResult } from 'next/dist/server/web/types';
import { NextResponse } from 'next/server';
import type { NextFetchEvent, NextRequest } from 'next/server';

/* It's super important to have these imports in Next.js 
if you want to handle middleware function types and HTTP request-response handling. */
CustomMiddleware Type:
export type CustomMiddleware = (
    request: NextRequest, 
    event: NextFetchEvent, 
    response: NextResponse
) => NextMiddlewareResult | Promise<NextMiddlewareResult>;
type MiddlewareFactory = (middleware: CustomMiddleware) => CustomMiddleware;

/*
Defines a middleware function type that takes a request, 
fetch event, and response, and returns a result or a promise that
resolves to a result.. */
Chain Function:
export function chain(functions: MiddlewareFactory[], index = 0): CustomMiddleware {
    const current = functions[index];
    ...
}

/* TThe chain function is a loop that keeps adding middleware 
functions to a stack. Each middleware in the list is wrapped 
around the next middleware, and this keeps happening until all 
the middlewares are stacked up properly. This creates a middleware 
sequence that can be used to handle requests. */

Code Explanation of chain in Next.js

import { NextMiddlewareResult } from 'next/dist/server/web/types';
import { NextResponse } from 'next/server';
import type { NextFetchEvent, NextRequest } from 'next/server';

export type CustomMiddleware = (
    request: NextRequest,
    event: NextFetchEvent,
    response: NextResponse
) => NextMiddlewareResult | Promise<NextMiddlewareResult>;

type MiddlewareFactory = (middleware: CustomMiddleware) => CustomMiddleware;

export function chain(functions: MiddlewareFactory[], index = 0): CustomMiddleware {
    const current = functions[index];

    if (current) {
        const next = chain(functions, index + 1);
        return current(next);
    }

    return (request: NextRequest, event: NextFetchEvent, response: NextResponse) => {
        return response;
    };
}

Imports and Type Definitions:
// Essential imports for Next.js middleware handling
import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server';
// These imports are necessary for working with HTTP request-response cycles within Next.js.

import { CustomMiddleware } from '@middlewares/chain';
// CustomMiddleware is a type that represents the structure of middleware functions used for chaining.

import { auth } from '@auth/providers';
// 'auth' is an authentication function, typically used to check if the user has an active session.   
Middleware Function Definition
// Defines the middleware function which accepts another middleware to continue the chain
export function authMiddleware(middleware: CustomMiddleware) {
    // Comment: This function will wrap around the provided 'middleware', allowing for sequential execution.
    ...
Session Authentication and Route Handling
// Async function to check user session
const session = await auth();
// This awaits the 'auth' function that returns the user's session details, if logged in.

const isLoggedIn = !!session?.user;
// 'isLoggedIn' is a boolean that is true if 'session' contains a 'user' object.

const isPrivateRoutes = request.nextUrl.pathname.startsWith('/private');
// 'isPrivateRoutes' is true if the request URL starts with '/private', marking it as a route requiring authentication.

const isAuthRoutes = request.nextUrl.pathname.startsWith('/auth');
// 'isAuthRoutes' is true if the request URL starts with '/auth', marking it as a route related to authentication processes.
Redirect Logic
/ Redirect unauthenticated users attempting to access private routes to the login page
if (!isLoggedIn && isPrivateRoutes) {
    return NextResponse.redirect(new URL('/auth/login', nextUrl));
}
// If the user is not logged in and the route is private, redirect to the login page.

// Redirect authenticated users trying to access auth routes to a default route
if (isLoggedIn && isAuthRoutes) {
    return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT ?? '/private/my-private-page', nextUrl));
}
// If the user is logged in and trying to hit an auth-specific route, redirect to a default private page or user list.

Code Explanation of authMiddleware in Next.js

import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server';
import { CustomMiddleware } from '@middlewares/chain';
import { auth } from '@auth/providers';

export function authMiddleware(middleware: CustomMiddleware) {
    return async (request: NextRequest, event: NextFetchEvent) => {
        const session = await auth();
        const { nextUrl } = request;
        const { DEFAULT_LOGIN_REDIRECT } = process.env;

        const isLoggedIn = !!session?.user;
        const isPrivateRoutes = request.nextUrl.pathname.startsWith('/private');
        const isAuthRoutes = request.nextUrl.pathname.startsWith('/auth');

        if (!isLoggedIn && isPrivateRoutes) {
            return Response.redirect(new URL('/auth/login', nextUrl));
        }
        if (isLoggedIn && isAuthRoutes) {
            return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT ?? '/private/user/list', nextUrl));
        }

        const response = NextResponse.next();

        return middleware(request, event, response);
    };
}

Imports and Initial Setup:
// Essential imports for Next.js middleware handling
import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server';
// These imports are necessary for working with HTTP request-response cycles within Next.js.

import { CustomMiddleware } from '@middlewares/chain';
// CustomMiddleware is a type that represents the structure of middleware functions used for chaining.

import { auth } from '@auth/providers';
// 'auth' is an authentication function, typically used to check if the user has an active session.   
Middleware Function Definition
// Defines the middleware function which accepts another middleware to continue the chain
export function authMiddleware(middleware: CustomMiddleware) {
    // Comment: This function will wrap around the provided 'middleware', allowing for sequential execution.
    ...
Session Authentication and Route Handling
// Async function to check user session
const session = await auth();
// This awaits the 'auth' function that returns the user's session details, if logged in.

const isLoggedIn = !!session?.user;
// 'isLoggedIn' is a boolean that is true if 'session' contains a 'user' object.

const isPrivateRoutes = request.nextUrl.pathname.startsWith('/private');
// 'isPrivateRoutes' is true if the request URL starts with '/private', marking it as a route requiring authentication.

const isAuthRoutes = request.nextUrl.pathname.startsWith('/auth');
// 'isAuthRoutes' is true if the request URL starts with '/auth', marking it as a route related to authentication processes.
Redirect Logic
/ Redirect unauthenticated users attempting to access private routes to the login page
if (!isLoggedIn && isPrivateRoutes) {
    return NextResponse.redirect(new URL('/auth/login', nextUrl));
}
// If the user is not logged in and the route is private, redirect to the login page.

// Redirect authenticated users trying to access auth routes to a default route
if (isLoggedIn && isAuthRoutes) {
    return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT ?? '/private/my-private-page', nextUrl));
}
// If the user is logged in and trying to hit an auth-specific route, redirect to a default private page or user list.

This authMiddleware serves as a robust solution for handling session-based authentication and redirection in a Next.js application. It ensures that users are only able to access routes appropriate to their authentication state, enhancing security by preventing unauthorized access to private pages and redirecting users away from authentication pages when already logged in. This middleware effectively integrates into a larger middleware chain, allowing for flexible and layered application security management.

Code Explanation of permissionMiddleware in Next.js

import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server';
import { CustomMiddleware } from '@middlewares/chain';
import { auth } from '@auth/providers';
import { PERMISSIONS } from '@/lib/model/permissions';

export function permissionMiddleware(middleware: CustomMiddleware) {
    return async (request: NextRequest, event: NextFetchEvent, response: NextResponse) => {
        const pathname = request.nextUrl.pathname;
        const session = await auth();
        const { nextUrl } = request;

        if (pathname === '/private/permission-denied') {
            return middleware(request, event, response);
        }

        const isLoggedIn = !!session?.user;
        const userPermissions = session?.user.permissions || [];

        if (isLoggedIn) {
            const allowedPaths = userPermissions.reduce((acc: string[], permissionName) => {
                const permissionPaths = PERMISSIONS[permissionName]?.path || [];
                return acc.concat(permissionPaths);
            }, []);

            const isAllowed = allowedPaths.includes(pathname);

            if (!isAllowed) {
                const deniedURL = new URL('/private/permission-denied', nextUrl);
                deniedURL.searchParams.set('route', pathname);
                return NextResponse.redirect(deniedURL);
            }
        }

        return middleware(request, event, response);
    };
}

Imports and Initial Setup:
// Essential imports for Next.js middleware handling
import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server';
// These imports are necessary for working with HTTP request-response cycles within Next.js.

import { CustomMiddleware } from '@middlewares/chain';
// CustomMiddleware is a type that represents the structure of middleware functions used for chaining.

import { auth } from '@auth/providers';
// 'auth' is an authentication function, typically used to check if the user has an active session.

import { PERMISSIONS } from '@/lib/model/permissions';
// PERMISSIONS maps permissions names to their corresponding route paths.  

// Defines the shape of permissions in the application
export type PermissionType = {
    name: string; // The name of the permission
    path: string[]; // An array of paths that the permission grants access to
};
his TypeScript type defines a structure for permission objects, where each permission has a name and an array of path strings. These paths are the URLs within the application that the permission provides access to.

Middleware Function Definition
// Defines the middleware function which accepts another middleware to continue the chain
export function permissionMiddleware(middleware: CustomMiddleware) {
    // This function will apply permission-based checks before proceeding with the next middleware.
    ...
}
Session Authentication and Route Handling
// Extracts the pathname of the request for route checking
const pathname = request.nextUrl.pathname;
// Async function to check user session and permissions
const session = await auth();
// This awaits the 'auth' function that returns the user's session details, if logged in.

const isLoggedIn = !!session?.user;
// 'isLoggedIn' is a boolean that is true if 'session' contains a 'user' object.

const userPermissions = session?.user.permissions || [];
// 'userPermissions' holds an array of permissions associated with the user's session.
Permission Evaluation Logic
// Collects the paths the user has permission to access based on their permissions
const allowedPaths = userPermissions.reduce((acc: string[], permissionName) => {
    const permissionPaths = PERMISSIONS[permissionName]?.path || [];
    return acc.concat(permissionPaths);
}, []);

const isAllowed = allowedPaths.includes(pathname);
// 'isAllowed' is true if the user's permissions include the current route's pathname.

// Redirect to the 'permission-denied' page if the user is not permitted to access the route
if (!isAllowed) {
    const deniedURL = new URL('/private/permission-denied', nextUrl);
    deniedURL.searchParams.set('route', pathname);
    return NextResponse.redirect(deniedURL);
}

This authMiddleware serves as a robust solution for handling session-based authentication and redirection in a Next.js application. It ensures that users are only able to access routes appropriate to their authentication state, enhancing security by preventing unauthorized access to private pages and redirecting users away from authentication pages when already logged in. This middleware effectively integrates into a larger middleware chain, allowing for flexible and layered application security management.

By adding authMiddleware and permissionMiddleware to the main middleware chain, we can ensure that our Next.js app applies both authentication and authorization evenly to all its routes. Chaining these middleware functions is essential to keep our application secure.

  • Authentication First: The authMiddleware checks if a user is authenticated, which is the first line of defense against unauthorized access. It makes sure that a user is logged in before any further checks are made or access is granted to protected routes.
  • Authorization Next: Following successful authentication, the permissionMiddleware checks if the user has the right permissions for the specific route they are trying to access. It adds a second layer of security, confirming that the user not only is who they say they are but also has appropriate rights.

Basically, you can connect these middleware functions by using a main middleware, like this:

import { chain } from '@/lib/middlewares/chain';
import { authMiddleware } from '@/lib/middlewares/auth-middleware';
import { permissionMiddleware } from '@/lib/middlewares/permissions-middleware';

// Combines the middleware into a single chain that runs sequentially
export default chain([authMiddleware, permissionMiddleware]);

// Configuration to apply the middleware chain to all routes except for specified exceptions
export const config = {
    matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

... We make sure that our security is really strong by doing the following:

  • Ensures a Consistent Security Flow: By having a centralized place to call our security checks, we maintain a uniform security practice across the application, reducing the risk of security holes and making it easier to manage and update security logic.
  • Streamlines the Request Handling: The chain function provided by @/lib/middlewares/chain takes an array of middleware functions and runs them in sequence, allowing each middleware to perform its checks and potentially modify the request or decide to redirect or respond early.
  • Customizes Route Matching: The config object's matcher property ensures that our security middleware is only applied to the necessary routes, preventing unnecessary overhead on our API routes and static resources.

Next-Auth V5 Infos

NextAuth.js version 5 is a major rewrite of the next-auth package, that being said, it introduces as few breaking changes as possible. For all else, this document will guide you through the migration process.

Summary

We just went through some important security stuff for modern web apps that use Next.js. We talked about things like:

  • NextAuth.js v5: The latest version offers improved security features and easier integration with Next.js for handling authentication.
  • Next.js App Directory: The new file system-based routing mechanism in Next.js which can benefit from the middleware security layer.
  • Multiple Middleware Strategy: Utilizing a sequence of middleware functions to enforce both authentication and authorization for a robust security posture.
  • Permissioning: Managing user access across your application through permission-based controls.
  • And more: We discussed best practices and specific strategies to keep your Next.js application secure.

Resources

Here are the resources mentioned in this post blog:

  • NextAuth.js Documentation: The official NextAuth.js docs for comprehensive guidance on implementing authentication.
  • Next.js Middleware Documentation: Official documentation on middleware in Next.js, detailing its setup and use cases.
  • Next.js File System Routing: Learn about the new file system-based routing in Next.js, part of the App Directory structure.
  • React Server Components: An introduction to React Server Components and their impact on performance and data handling.
© 2025 Erick Eduardo. All rights reserved.