next.jsnext-intl

How to Implement Role-Based Access Control in Next.js with Cookie-Based Session Auth from NestJS Backend


I'm working on a project with NestJS as the backend and Next.js 14 (app router) for the frontend. My backend implements cookie-based session authentication, where the session ID is stored in a cookie. I need help setting up role-based access control for protected routes in my Next.js app, utilizing session validation from my NestJS backend.

Here's the flow I'm trying to achieve:

  1. A user makes a request to the Next.js server.
  2. The Next.js server extracts the session cookie from the request and forwards it to the NestJS backend for session validation.
  3. The NestJS backend validates the session and, if valid, responds with the user's session data, including roles such as adminId, studentId, and teacherId.
  4. Based on the backend's response: If 401, the Next.js server redirects the user to /login. If 200, the Next.js server checks the user's role against the requested route (/admin, /student, /teacher) and serves the route only if the corresponding role ID is present in the session data.

Additionally, I'm using next-intl for internationalization, with the following middleware setup for locale handling:

import createMiddleware from "next-intl/middleware";

export default createMiddleware({
  locales: ["en", "uk"],
  defaultLocale: "en",
});

export const config = {
  matcher: ["/", "/(uk|en)/:path*"],
};

And my i18n.ts for locale validation:

import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";

const locales = ["en", "uk"];

export default getRequestConfig(async ({ locale }) => {
  if (!locales.includes(locale as any)) notFound();

  return {
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});

Solution

  • In your Next.js app, create a middleware function to handle session validation. This middleware should extract the session cookie from the request and forward it to the NestJS backend for validation. If the validation is successful, store the user's session data, including roles, in the request object for later use.

    it can look something like this

    // sessionValidationMiddleware.js
    import axios from 'axios';
    
    const sessionValidationMiddleware = async (req, res, next) => {
      const sessionId = req.cookies.sessionId;
    
      try {
        // Forward session validation request to NestJS backend
        const response = await axios.post('http://nestjs-backend.com/validate-session', { sessionId });
    
        if (response.status === 200) {
          req.user = response.data; // Store user data in the request object
          next();
        } else {
          res.redirect('/login'); // Redirect to login for unauthorized users
        }
      } catch (error) {
        console.error('Error validating session:', error);
        res.redirect('/login'); // Redirect to login on error
      }
    };
    
    export default sessionValidationMiddleware;
    

    In your middleware.ts file, import and use the session validation middleware for the routes that require authentication:

    // middleware.ts
    import { createMiddleware } from 'next';
    
    import sessionValidationMiddleware from './path-to-your-session-validation-middleware';
    
    const middleware = createMiddleware();
    
    // Apply session validation middleware to protected routes
    middleware.get('/admin', sessionValidationMiddleware);
    middleware.get('/student', sessionValidationMiddleware);
    middleware.get('/teacher', sessionValidationMiddleware);
    
    export default middleware;
    

    For each of your route files (e.g., admin.tsx, student.tsx, teacher.tsx), check the user's roles and handle access control:

    // admin.tsx
    const AdminPage = ({ user }) => {
      // Check if the user has the required role (adminId) for this route
      if (user.roles && user.roles.adminId) {
        return {
          props: { user },
        };
      } else {
        return {
          redirect: {
            destination: '/login',
            permanent: false,
          },
        };
      }
    };
    
    export default AdminPage;
    

    Hope this helps