Sreenath Kumar
Sreenath Kumar

Reputation: 13

How to implement protected route with next-auth v5 and Next js 14/15

I want to get the login status of any request in the middleware and I want to redirect that to the protected route or the login route.

But I am not getting the session inside the middleware.ts. I used this code but still showing the error.

//auth.ts

import NextAuth from "next-auth"
 
export const { auth, handlers } = NextAuth({
  callbacks: {
    authorized: async ({ auth }) => {
      // Logged in users are authenticated, otherwise redirect to login page
      return !!auth
    },
  },
})
// middleware.ts

import { auth } from "@/auth"
 
export default auth((req) => {
  if (!req.auth && req.nextUrl.pathname !== "/login") {
    const newUrl = new URL("/login", req.nextUrl.origin)
    return Response.redirect(newUrl)
  }
})

Showing error like this enter image description here

Upvotes: 0

Views: 401

Answers (1)

Anshuman Shandilya
Anshuman Shandilya

Reputation: 1

For starters, migrate to AuthJS v5. Second, the issue doesn't seem to be from the next-auth, instead you don't have aws-sdk installed and that's why the error.

Assuming you are implementing RBAC (Role Based Access control), this middleware can work for you:

//middleware.ts
// Importing necessary functions from the "next/server" module
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// Importing the auth function from our auth.ts file
import { auth } from "@/auth";

// Defining the protected routes with the roles allowed to access them, including dynamic segments
const protectedRoutes = [
  { path: "/admin/:path", roles: ["ADMIN"] }, // Only ADMIN can access this route
  { path: "/user/:path", roles: ["ADMIN", "USER"] }, // Both ADMIN  and USER can access this route
];

// Function to find the current route in the protectedRoutes array
const findCurrentRoute = (pathname: string) => {
  return protectedRoutes.find((route) =>
    new RegExp(`^${route.path.replace(/:\w+/g, "\\w+")}$`).test(pathname)
  );
};

// Middleware function to handle requests
export default async function middleware(request: NextRequest) {
  // Get the session using the auth function
  const session = await auth();

  // Find the current route in the protectedRoutes array
  const currentRoute = findCurrentRoute(request.nextUrl.pathname);

  // Check if the request path is protected
  if (currentRoute) {
    // If the route is protected and there's no session, redirect to the login page
    if (!session) {
      const absoluteURL = new URL("/auth/login", request.nextUrl.origin);
      return NextResponse.redirect(absoluteURL.toString());
    }

    // If the user role is not allowed for the current route, redirect to the unauthorized page
    if (!currentRoute.roles.includes(session.user.role)) {
      const absoluteURL = new URL("/unauthorized", request.nextUrl.origin);
      return NextResponse.redirect(absoluteURL.toString());
    }
  }

  // If the route is not protected or the user has access, continue to the next middleware or the requested page
  return NextResponse.next();
}

// Configuration to match all paths except for certain static files and API routes
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Also, here's a beginner level configured auth.config.ts and auth.ts with Github, Google, and Credentials based authentication. Edge case compatible, if you are going to deploy on vercel.

//auth.config.ts
import Github from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import { type NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcrypt-edge";
import { prisma } from "./prisma";

// Adding the callbacks for managing the session and token
export default {
  providers: [
    Github({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
      allowDangerousEmailAccountLinking: true
    }),
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      allowDangerousEmailAccountLinking: true
    }),
    Credentials({
      name: "Credentials",
      credentials: {
        email: {
          label: "Email",
          type: "email",
          placeholder: "[email protected]",
        },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        if (!credentials || !credentials.email || !credentials.password) {
          return null;
        }

        const email = credentials.email as string;

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const user: any = await prisma.user.findUnique({
          where: {
            email,
          },
        });

        if (!user) {
          throw new Error("No account found with this email.");
        } else {
          const isMatch = bcrypt.compareSync(
            credentials.password as string,
            user.hashedPassword
          );
          if (!isMatch) {
            throw new Error("Incorrect password.");
          }
        }

        // Return user object with role information
        return { ...user, role: user.role };
      },
    }),
  ],
  callbacks: {
    // Callback to manage the JWT token
    jwt: async ({ token, user }) => {
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }
      return token;
    },
    // Callback to manage the session object
    session: async ({ session, token }) => {
      if (token) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
      }
      return session;
    },
  },
  // Additional configuration can go here (e.g., pages)
  pages: {
    signIn: "/sign-in",
  },
} satisfies NextAuthConfig;
//auth.ts
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "./prisma"
import authConfig from "./auth.config";

export const {
  handlers: { GET, POST },
  signIn,
  signOut,
  auth,
} = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: "jwt" },
  ...authConfig,
});

And the code for salting and hashing using bcrypt (bcrypt-edge):

import bcrypt from "bcrypt-edge";
export function saltAndHashPassword(password: any) {
  const saltRounds = 10; // Adjust the cost factor according to your security requirements
  const salt = bcrypt.genSaltSync(saltRounds); // Synchronously generate a salt
  const hash = bcrypt.hashSync(password, salt); // Synchronously hash the password
  return hash; // Return the hash directly as a string
}

You will probably get TypeScript error RBAC in auth.config.ts, here's how you can fix it:

  • create a types folder
  • Add a file named "next-auth.d.ts"
  • And put this code inside:
import { DefaultSession, DefaultUser } from "@auth";

// Extend the User interface to include role
declare module "next-auth" {
  interface User extends DefaultUser {
    role?: string;
  }

  interface Session {
    user?: {
      id?: string;
      role?: string;
    } & DefaultSession["USER"];
  }
}

Make sure that your user does have the role field. Example schema.prisma:

model User {
  id             String          @id @default(auto()) @map("_id") @db.ObjectId
  name           String?
  email          String?         @unique
  hashedPassword String?
  emailVerified  DateTime?
  image          String?
  accounts       Account[]
  sessions       Session[]
  role           UserRole?       @default(USER)
  Authenticator  Authenticator[]

  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  // Relations
  blogPosts      BlogPost[]
}

Also, you will have to make sure that your database has the necessary schema for these to work, you can get these here (select your connection method to database): AuthJS v5 database adapters

Upvotes: 0

Related Questions