DEV Community

Cover image for Next.js 15 Authentication with App Router and Middleware
Taufiqul Islam
Taufiqul Islam

Posted on

Next.js 15 Authentication with App Router and Middleware

Here's a comprehensive guide to setting up authentication in Next.js 15 using the App Router, middleware, and NextAuth.js.

Folder Structure
Here's a visual representation of the folder structure :

/src/
├── app/
│   ├── auth/
│   │   ├── login/
│   │   │   └── page.tsx
│   │   └── register/
│   │       └── page.tsx
│   ├── dashboard/
│   │   └── page.tsx
│   ├── api/
│   │   └── auth/
│   │       └── [...nextauth]/
│   │           └── route.ts
│   ├── provider.tsx
│   ├── layout.tsx
│   └── page.tsx
├── middleware/
│   └── middleware.ts
├── lib/
│   ├── auth.ts
│   └── next-auth.d.ts
Enter fullscreen mode Exit fullscreen mode

🔐 1. Authentication Setup : /app/api/auth/[...nextauth]/route.ts

Sets up the API route for NextAuth using handlers imported from the central auth config (/lib/auth.ts).

import { GET, POST } from "@/lib/auth";
export { GET, POST };
Enter fullscreen mode Exit fullscreen mode

⚙️ 2. Auth Configuration : /lib/auth.ts
Defines NextAuth options, including the credentials provider, JWT/session callbacks, token refresh logic, and error handling.

import NextAuth, { type NextAuthOptions } from "next-auth";
import Credentials from "next-auth/providers/credentials";

import { AxiosError } from "axios";
import { post } from "./api/handlers";

type LoginResponse = {
  accessToken: string;
  refreshToken: string;

  user: {
    _id: string;
    stdId: string;
    name: string;
    email: string;
    hallName: string;
    description: string;
    role: string;
  };
};

const authOptions: NextAuthOptions = {
  providers: [
    Credentials({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        if (credentials === null) throw new Error("Missing credentials");

        try {
          const response = await post<LoginResponse>(
            "/api/auth/login",
            {
              email: credentials?.email,
              password: credentials?.password,
            },
            {
              "Content-Type": "application/json",
            },
          );
          console.log("API Response:", response);

          if (response.accessToken) {
            // Return an object that matches your User interface
            return {
              id: response.user._id,
              email: response.user.email,
              name: response.user.name,
              accessToken: response.accessToken,
              refreshToken: response.refreshToken,
              user: response.user,
            };
          }
          return null;
        } catch (error) {
          if (error instanceof AxiosError) {
            throw new Error(error.response?.data?.message || "Login failed");
          }
          console.error("Authentication error:", error);
          throw new Error("Login failed");
        }
      },
    }),
  ],
  callbacks: {
    jwt: async ({ token, user }) => {
      if (user) {
        // Include both the required fields and your custom data
        token.id = user.id;
        token.email = user.email;
        token.name = user.name;
        token.accessToken = user.accessToken;
        token.refreshToken = user.refreshToken;
        token.user = user.user;
        token.accessTokenExpires = Math.floor(Date.now() / 1000) + 60;
      }
      // Check if the current time is past the access token's expiry time
      const now = Math.floor(Date.now() / 1000);
      if (token.accessTokenExpires && now > token.accessTokenExpires) {
        try {
          // Attempt to refresh the access token
          const response = await post<{ accessToken: string }>(
            "/api/auth/refresh-token",
            {
              refreshToken: token.refreshToken,
            },
            {
              "Content-Type": "application/json",
            },
          );

          // Update the token with the new access token
          token.accessToken = response.accessToken;
          token.accessTokenExpires = now + 60; // Set new expiry time (1 minute from now)
        } catch (error) {
          console.error("Error refreshing access token", error);
          // Handle refresh token error (e.g., redirect to login)
          return { ...token, error: "RefreshAccessTokenError" };
        }
      }

      return token;
    },
    session: ({ session, token }) => {
      if (token) {
        session.user = {
          ...session.user,
          // id: token.id,
          email: token.email,
          name: token.name,
        };
        (session as any).accessToken = token.accessToken;
        (session as any).user = token.user;
      }
      return session;
    },
    redirect: async ({ url, baseUrl }) => {
      // Redirect to login page if there's an error with the refresh token
      if (url === baseUrl) {
        return `${process.env.NEXT_PUBLIC_BASE_URL}/login`;
      }
      return url;
    },
    // authorized: async ({ auth }) => {
    //   return !!auth;
    // },
  },
  pages: {
    signIn: "/login",
    error: "/login",
  },
  session: {
    strategy: "jwt" as const,
  },
  debug: process.env.NODE_ENV === "development",
  secret:
    process.env.NEXTAUTH_SECRET,
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST, authOptions };
export const auth = handler.auth;
Enter fullscreen mode Exit fullscreen mode

📄 3. Type Definitions: /lib/next-auth.d.ts
Extends NextAuth types (Session, User, and JWT) to include custom user data like role, hallName, and refreshToken.

import NextAuth from "next-auth";

declare module "next-auth" {
  interface Session {
    accessToken: string;
    refreshToken: string;
    user: {
      _id: string;
      stdId: string;
      name: string | null | undefined;
      email: string | null | undefined;
      hallName: string;
      description: string;
      role: string;
    };
  }

  interface User {
    id: string;
    accessToken: string;
    refreshToken: string;
    user: {
      _id: string;
      stdId: string;
      name: string;
      email: string;
      hallName: string;
      description: string;
      role: string;
    };
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id?: string;
    email?: string | null;
    name?: string | null;
    accessToken?: string;
    refreshToken?: string;
    accessTokenExpires?: number;
    error?: string;
    user?: {
      _id: string;
      stdId: string;
      name: string;
      email: string;
      hallName: string;
      description: string;
      role: string;
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

🧱 4. Middleware : /middleware/middleware.ts
Protects private routes by checking if the user is authenticated; redirects to login if no valid token is found.

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

export async function middleware(request: NextRequest) {
  const token = await getToken({ req: request });

  // Check if the user is trying to access a protected route
  if (!token && !request.nextUrl.pathname.startsWith("/auth/login")) {
    // Redirect to login page if there is no token
    return NextResponse.redirect(new URL("/auth/login", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
    "/dashboard/:path*",
  ],
};
Enter fullscreen mode Exit fullscreen mode

🧪 5. Session Provider : /app/provider.tsx
Wraps the app with SessionProvider so session data is accessible in all client components.

"use client";

import { SessionProvider } from "next-auth/react";
import React from "react";

interface AuthProviderProps {
  children: React.ReactNode;
}

const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  return <SessionProvider>{children}</SessionProvider>;
};

export default AuthProvider;
Enter fullscreen mode Exit fullscreen mode

🌐 6. Root Layout : /app/layout.tsx
Includes global providers (Auth, React Query, UI layout) and sets up the app’s foundational structure with session support.

// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

import { SessionProvider } from "next-auth/react";
import { Toaster } from "react-hot-toast";
import ReactQueryProvider from "@/providers/QueryClientProvider";
import LayoutProvider from "@/providers/LayoutProvider";
import AuthProvider from "./provider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "title",
  description: "description",
};

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  // const session = await auth();

  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <title>E Delivery Client</title>
      </head>
      <body className={inter.className}>
        <AuthProvider>
          <ReactQueryProvider>
            <LayoutProvider>{children}</LayoutProvider>
          </ReactQueryProvider>
        </AuthProvider>
        <Toaster position="top-right" />
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

🧠 Accessing Session Data

🧾 Client Component Example:
Uses useSession hook from NextAuth to get and display user session data in client-side components.

"use client";
import { useSession } from "next-auth/react";

export default function DashboardPage() {
  const { data: session, status } = useSession();

  return (
    <div>
      {session ? (
        <p>
          Logged in as name: {session.user.name} Email: {session.user.email}
        </p>
      ) : (
        <p>Not logged in</p>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🧾 Server Component Example
Fetches the session using auth() in server components, redirecting unauthenticated users to the login page.

import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";

export default async function ProfilePage() {
  const session = await auth();

  if (!session) {
    redirect("/auth/login");
  }

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold">Profile</h1>
      <div className="mt-4 space-y-2">
        <p>
          <span className="font-semibold">Email:</span> {session.user.email}
        </p>       
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

📌 Key Features

✅ Credentials Auth: Uses email & password login via a custom backend.
🔁 Token Refresh: Automatically refreshes JWT access tokens using refresh tokens.
🔒 Protected Routes: Middleware blocks access to routes if not logged in.
🛡️ Type Safety: Custom types improve safety and developer experience.
🔍 Session Access: Available in both server and client components.
🧱 Provider Pattern: Clean separation of logic using dedicated providers.

⚠️ Implementation Notes

Make sure to configure your environment variables properly, especially NEXTAUTH_SECRET
.env example :

NEXT_PUBLIC_BASE_URL=your base url
NODE_ENVL=development
NEXTAUTH_SECRET=hbbinmkbnnkvdfjvskvnkvDDVVfvmndjbvshbvhbrvv=
Enter fullscreen mode Exit fullscreen mode

The middleware checks for authentication status on every route change

The session data includes both standard fields (email, name) and custom fields (role, hallName, etc.)

Error handling is implemented for both login and token refresh operations

This setup provides a solid foundation for authentication in Next.js 15 applications using the App Router, with proper type safety and route protection.

Top comments (0)