In this tutorial, I’ll walk you through setting up authentication in your Next.js 13 app directory using NextAuth.js. It’s worth noting that while we use the NextAuth package in this tutorial, you may be using the Auth.js package by the time you read this, as the libraries are now interchangeable.

For authentication, we’ll use the credential provider, which requires a username or email and a password. First, we’ll validate and authenticate the credentials included in the request body against our database, which in this case is PostgreSQL. However, you can easily switch to another Prisma-supported database if needed.

Once we’ve authenticated the user, we’ll explore different methods for retrieving and modifying session information, as well as how to protect private routes in your application.

By following this tutorial, you’ll gain a solid understanding of how to implement NextAuth.js in your Next.js 13 app directory for authentication.

To set up NextAuth in Next.js 14, refer to the article titled ‘Setup and Use NextAuth.js in Next.js 14 App Directory‘.

More practice:

Setup and Use NextAuth.js in Next.js 13 App Directory

Setup the Next.js 13 Project

After completing this guide, your file and folder organization will resemble the one shown in the screenshot below.

Folder and File Structure of the NextAuth and Next.js 13 App Directory Project

Begin by choosing a suitable location on your machine and opening a terminal window in that directory. From there, you can start the process of setting up your Next.js 13 project by running one of the commands below, depending on your preferred package manager.


yarn create next-app nextauth-nextjs13-prisma
# or
npx create-next-app@latest nextauth-nextjs13-prisma
# or
pnpm create next-app nextauth-nextjs13-prisma

As you go through the setup process, you’ll encounter a few prompts that you need to respond to. First, you’ll be asked whether to enable TypeScript and ESLint; choose “Yes” for both. Then, select “Yes” for both the experimental app/ directory and src/ directory options. Finally, you’ll be prompted to choose an import alias. Press the Tab key to select the first option, and then hit Enter.

After you’ve provided your responses to the prompts, the project will be created and all required dependencies will be automatically installed. Once the installation process is complete, you’re ready to open the project in your favourite IDE or text editor and start working.

Setup Next Auth API Route

To begin implementing the authentication logic, we’ll need to install the NextAuth package. For now, we’ll install a specific build from a pull request that includes the added functionality we need to work in the app directory.

However, by the time you’re reading this article, this functionality should have been added to a beta or stable release. To install the NextAuth package, choose the appropriate command based on your package manager and run it.


yarn add next-auth@0.0.0-pr.6777.c5550344
# or 
npm i next-auth@0.0.0-pr.6777.c5550344
# or
pnpm add next-auth@0.0.0-pr.6777.c5550344

Let’s move on to defining the NextAuth options. Initially, we defined and exported the NextAuth options in the src/app/api/auth/[...nextauth]/route.ts file. However, some users encountered export errors when running the code. To address this issue, we can define and export the NextAuth options in a separate file, such as the lib/auth.ts file. I wasn’t initially aware of this solution, but a developer named Kevin mentioned it in the comment section. You can read more about the export errors in the comments.

To implement this solution, navigate to the src directory and create a new folder called lib. Inside the lib folder, create a file named auth.ts and copy the following NextAuth configuration code into it.

src/lib/auth.ts


import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export const authOptions: NextAuthOptions = {
  session: {
    strategy: "jwt",
  },
  providers: [
    CredentialsProvider({
      name: "Sign in",
      credentials: {
        email: {
          label: "Email",
          type: "email",
          placeholder: "example@example.com",
        },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const user = { id: "1", name: "Admin", email: "admin@admin.com" };
        return user;
      },
    }),
  ],
};

The above code demonstrates the process of setting up authentication in a Next.js 13 app using the NextAuth library. We first imported the CredentialsProvider module, which we’ll use for validation. Then, we defined an object called ‘authOptions‘ that contains the configuration for our authentication process.

In the credentials key of the CredentialsProvider() method, we listed the email and password fields, which will be available on the sign-in form. For the authorization step, we’re currently using a simple mock implementation that returns a fixed user object.

The next step is to create an API route that can handle authentication requests from NextAuth. We’ll use the NextAuth() method to create an API handler and then export it as GET and POST functions for use in our application.

To get started, navigate to the api directory within the src/app folder. Within the api directory, create a new folder called auth. Inside the ‘auth‘ folder, create a folder named [...nextauth]. Finally, create a new file named route.ts within the ‘[…nextauth]’ folder and add the code provided below.

When copying and pasting the URL below, please ensure that you manually retype the three dots (...) in [...nextauth]. This is important because WordPress may automatically convert the three dots into ellipses, even though I used real three dots in the path when creating the article. This automatic conversion can cause errors. To avoid any issues, kindly ensure that the three dots are accurately represented in the URL path.

src/app/api/auth/[…nextauth]/route.ts


import { authOptions } from "@/lib/auth";
import NextAuth from "next-auth";

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

Create Reusable Buttons

To make navigating between pages in the application easier, we’ll create buttons instead of manually typing URLs in the browser. Start by creating a “components” folder inside the “src” directory. Inside “components“, create a buttons.component.tsx file and add the following code:

src/components/buttons.component.tsx


"use client";

import { signIn, signOut } from "next-auth/react";
import Link from "next/link";

export const LoginButton = () => {
  return (
    <button style={{ marginRight: 10 }} onClick={() => signIn()}>
      Sign in
    </button>
  );
};

export const RegisterButton = () => {
  return (
    <Link href="/register" style={{ marginRight: 10 }}>
      Register
    </Link>
  );
};

export const LogoutButton = () => {
  return (
    <button style={{ marginRight: 10 }} onClick={() => signOut()}>
      Sign Out
    </button>
  );
};

export const ProfileButton = () => {
  return <Link href="/profile">Profile</Link>;
};

After that, you can import the buttons into the src/app/page.tsx file and use them in the JSX code to display them in the user interface.

src/app/page.tsx


import {
  LoginButton,
  LogoutButton,
  ProfileButton,
  RegisterButton,
} from "@/components/buttons.component";

export default function Home() {
  return (
    <main
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "70vh",
      }}
    >
      <div>
        <LoginButton />
        <RegisterButton />
        <LogoutButton />
        <ProfileButton />
      </div>
    </main>
  );
}

Before testing the authentication flow, we need to configure the required environment variables for NextAuth to function properly. These variables include a secret for JWT encryption and the root URL of your application.

Although it’s possible to avoid setting these variables if you’re only working with client-side logic, we must set them since we’ll be working with server-side rendering. To set these variables, create a .env file in the root directory and add the following environment variables to it.

.env


NEXTAUTH_SECRET=my_ultra_secure_nextauth_secret
NEXTAUTH_URL=http://localhost:3000

After completing the basic setup, it’s important to make a small adjustment to the src/app/layout.tsx file. Removing the import "./globals.css"; line is necessary to prevent the CSS code that comes with Next.js 13 from being applied.

Once this adjustment is made, you can start the development server to build the project and visit http://localhost:3000/ to access the application. From the home page, you can click on the “sign in” button. If you are correctly redirected to the default NextAuth sign-in page, you are ready to proceed.

On the sign-in page, enter your email and password and click the button to submit the form data to the API endpoint. Since we are using mock authorization, you can enter any random email and password. Once the authentication is successful, you will be redirected back to the homepage.

default nextauth login page

To view the cookies sent by NextAuth, you can open the Application tab of your browser’s developer tool and click on http://localhost:3000/ in the Cookies section.

Here, you will see various cookies including the session token that NextAuth uses for authentication. Another cookie you’ll see is the CSRF token, which is a security feature used by NextAuth to prevent cross-site request forgery attacks. Finally, you’ll also find a callback URL cookie, which is crucial for NextAuth to redirect users to the correct page after authentication.

Cookies Sent By NextAuth

Three Ways of Getting the NextAuth Session Data

Now that authentication is complete, we need a way to access the session data to make use of it. There are three locations where we can obtain the session data. The first is server-side in a React server component, the second is also server-side in any API route, and the last is on the client-side. This implies that two of the places are server-side, while one is client-side.

In the latest version of NextAuth, obtaining the session information on the server-side has become significantly easier, but acquiring it on the client-side takes a bit of preparation.

Get the Session in a Server Component

Now let’s demonstrate how to retrieve session information on the server-side using a React server component. This can be done by calling the getServerSession function and providing the ‘authOptions‘ object that was exported from the lib/auth.ts file during the NextAuth setup.

To implement this, simply replace the content of src/app/page.tsx with the code snippet below. Once you’ve done this, start the Next.js development server and navigate to http://localhost:3000/ to view the session data output on the screen.

src/app/page.tsx


import {
  LoginButton,
  LogoutButton,
  ProfileButton,
  RegisterButton,
} from "@/components/buttons.component";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

export default async function Home() {
  const session = await getServerSession(authOptions);
  console.log(session);

  return (
    <main
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "70vh",
      }}
    >
      <div>
        <LoginButton />
        <RegisterButton />
        <LogoutButton />
        <ProfileButton />

        <h1>Server Session</h1>
        <pre>{JSON.stringify(session)}</pre>
      </div>
    </main>
  );
}

Get the Session in an API Route

Let’s move on to retrieving the session data in an API route, which also operates on the server. To accomplish this, we’ll use the getServerSession function and provide the authOptions to acquire the session data.

Create a route.ts file in a new “session” directory within the src/app/api folder. Here’s the code to include:

src/app/api/session/route.ts


import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const session = await getServerSession(authOptions);

  return NextResponse.json({
    authenticated: !!session,
    session,
  });
}

Once you are done, navigate to http://localhost:3000/api/session and you’ll find the session data printed on the screen in a JSON object.

NextAuth session data for the mocked authorization

Get the Session in a Client Component

Lastly, let’s dive into retrieving the session data in a client-side component. For this, NextAuth requires a session provider to be set up on the client-side. Once the provider is in place and wraps around your application, you can use a client-side hook called useSession to obtain the session information.

Since the client can’t decode the JSON Web Token (JWT) on its own, the useSession hook makes an HTTP request to the server to retrieve the session information. The server decodes the JWT and sends it back, and NextAuth stores the session data in the provider, which the useSession hook can then access.

It’s worth noting that there may be some latency added when making the session request for the first time, as the server needs to decode the JWT. But once the data is stored in the provider, subsequent requests will be fast and seamless.

To create the session provider, simply create a providers.tsx file in the “src/app” directory and add the following code.

src/app/providers.tsx


"use client";

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

type Props = {
  children?: React.ReactNode;
};

export const NextAuthProvider = ({ children }: Props) => {
  return <SessionProvider>{children}</SessionProvider>;
};

After creating the session provider, wrap it around {children} in the src/app/layout.tsx file so that all client-side components can access the session data. Here’s the code you can use:

src/app/layout.tsx


import { NextAuthProvider } from "./providers";

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <NextAuthProvider>{children}</NextAuthProvider>
      </body>
    </html>
  );
}

Now, we’ll create a new component that will allow us to use the useSession hook to retrieve the session data and display it on the client-side. First, navigate to the ‘components‘ directory and create a new file named user.component.tsx. Then, paste the following code into the new file.

src/components/user.component.tsx


"use client";

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

export const User = () => {
  const { data: session } = useSession();

  return (
    <>
      <h1>Client Session</h1>
      <pre>{JSON.stringify(session)}</pre>
    </>
  );
};

To display the user’s session information on the page, we need to import the User component into src/app/page.tsx and include it in the JSX code.

src/app/page.tsx


import {
  LoginButton,
  LogoutButton,
  ProfileButton,
  RegisterButton,
} from "@/components/buttons.component";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { User } from "@/components/user.component";

export default async function Home() {
  const session = await getServerSession(authOptions);
  console.log(session);

  return (
    <main
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "70vh",
      }}
    >
      <div>
        <LoginButton />
        <RegisterButton />
        <LogoutButton />
        <ProfileButton />

        <h1>Server Session</h1>
        <pre>{JSON.stringify(session)}</pre>

        <User />
      </div>
    </main>
  );
}

When you visit the homepage and refresh the browser, you’ll notice that the session data displayed in the React server component appears instantly. However, the session data displayed in the client-side component takes a bit longer to appear since the client needs to make an HTTP request to the server to decode the JWT.

Integrate a Database

With a high-level understanding of the authentication process, it’s time to integrate a database to verify users’ identities instead of using a hard-coded object. This involves looking up the user’s information and checking their hashed password against the one stored in the database.

Setup PostgreSQL

Here, we will set up a PostgreSQL server using Docker. To do this, you can create a docker-compose.yml file and add the following Docker Compose configurations.

docker-compose.yml


version: '3'
services:
  postgres:
    image: postgres:latest
    container_name: postgres
    ports:
      - '6500:5432'
    volumes:
      - progresDB:/var/lib/postgresql/data
    env_file:
      - ./.env
volumes:
  progresDB:

Now it’s time to configure the .env file with the necessary environment variables for the Postgres Docker image. Once you’ve added them, run docker-compose up -d to start the Postgres server in the Docker container.

.env


NEXTAUTH_SECRET=my_ultra_secure_nextauth_secret
NEXTAUTH_URL=http://localhost:3000


POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=nextauth_prisma

DATABASE_URL=postgresql://admin:password123@localhost:6500/nextauth_prisma?schema=public

Setup Prisma ORM

Let’s proceed with setting up Prisma to enable us to communicate with the Postgres database. First, open your terminal and install the necessary dependencies by running the following commands based on your package manager.


yarn add @prisma/client bcryptjs && yarn add -D ts-node prisma @types/bcryptjs
# or
npm i @prisma/client bcryptjs && npm i -D ts-node prisma @types/bcryptjs
# or 
pnpm add @prisma/client bcryptjs && pnpm add -D ts-node prisma @types/bcryptjs

To set up Prisma and connect to your Postgres database, run the following command to initialize Prisma in your project and create a datasource for Postgres in the prisma/schema.prisma file:


yarn prisma init --datasource-provider postgresql
# or
npx prisma init --datasource-provider postgresql
# or
pnpm prisma init --datasource-provider postgresql

Now you’ll need to create a User model in your prisma/schema.prisma file. You can use the code below as a reference, or create your own.

prisma/schema.prisma


// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id       String @id @default(uuid())
  name     String
  email    String @unique
  password String
}

NextAuth doesn’t provide a built-in way to handle user registration, as this is unnecessary for certain authentication methods such as magic links, email login, or OAuth. However, when using credential authentication, it’s necessary to create a user account first, usually through a registration page.

In this tutorial, to save time, we’ll seed the database with a test user account rather than implementing a registration flow. Note that we’ll return to user registration at the end of this tutorial.

To get started, create a seed.ts file in the prisma directory and copy the following code into it.

prisma/seed.ts


import { PrismaClient } from "@prisma/client";
import { hash } from "bcryptjs";

const prisma = new PrismaClient();

async function main() {
  const password = await hash("password123", 12);
  const user = await prisma.user.upsert({
    where: { email: "admin@admin.com" },
    update: {},
    create: {
      email: "admin@admin.com",
      name: "Admin",
      password,
    },
  });
  console.log({ user });
}
main()
  .then(() => prisma.$disconnect())
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

To enable us to easily seed the database with a test user, we’ll add a script to the package.json file. Open the package.json file and add the following script:

package.json


{
"prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
  },
}

With Prisma set up, it’s time to generate the migration scripts and push the schema to the PostgreSQL database. To create the migration files, run the command npx prisma migrate dev --name init. This command will not only generate the Prisma migration files but also create the Prisma client in the node_modules folder.

After generating the migration files, use the command npx prisma db seed to add the test user to the database.

Next, we’ll create a global PrismaClient instance using the @prisma/client package, which will enable us to communicate with the PostgreSQL database. To do this, we need to create a file named prisma.ts within the “lib” directory and add the following code to it.

src/lib/prisma.ts


import { PrismaClient } from "@prisma/client";

const globalForPrisma = global as unknown as { prisma: PrismaClient };

export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log: ["query"],
  });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

Implement the NextAuth Authentication Code

With Prisma and Postgres set up, let’s implement the authentication logic. In the API route, replace the existing code with the updated code below. We made changes to the async authorize(credentials) {} function to first verify if the email and password information were included in the request body.

Then, we retrieve the user with the provided email address and use Bcrypt to verify their password against the hashed one. The user object is returned upon successful verification.

src/lib/auth.ts


import { prisma } from "@/lib/prisma";
import { compare } from "bcryptjs";
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export const authOptions: NextAuthOptions = {
  session: {
    strategy: "jwt",
  },
  providers: [
    CredentialsProvider({
      name: "Sign in",
      credentials: {
        email: {
          label: "Email",
          type: "email",
          placeholder: "example@example.com",
        },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials.password) {
          return null;
        }

        const user = await prisma.user.findUnique({
          where: {
            email: credentials.email,
          },
        });

        if (!user || !(await compare(credentials.password, user.password))) {
          return null;
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          randomKey: "Hey cool",
        };
      },
    }),
  ],
};

After modifying the NextAuth configuration options, if you encounter an error in the browser, it may indicate that Next.js is trying to include the Bcrypt module in the client bundle, which is not necessary since we only use it on the server.

To prevent certain packages from being included in the client bundle, we can add their names to the serverComponentsExternalPackages array in the ‘experimental‘ key of the next.config.js file. In this case, we need to add @prisma/client and bcryptjs to the array. Here’s an example code for the next.config.js file:

next.config.js


/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
    serverComponentsExternalPackages: ["@prisma/client", "bcryptjs"]
  },
}

module.exports = nextConfig

Now that we’ve incorporated the database into the authentication process, attempting to log in with incorrect email or password credentials will result in an error. To successfully sign in, you’ll need to use the login credentials of the test user that we previously seeded into the database.

Get an Error When You Try a Email or Password

Store Custom Keys in the JWT

You may have noticed that when we printed out the session object in both the React server and client components, the user’s ID was missing. This could be frustrating if you want to use it for other tasks later on. Fortunately, NextAuth provides two handy callbacks – jwt and session – that allows us to add our own custom information to the session object.

To add your custom keys, you can modify these two callbacks in the callbacks property of the NextAuth configuration. This way, you can include the information you need in the session object and JWT, and access it anytime and anywhere in your application.

src/lib/auth.ts


import { prisma } from "@/lib/prisma";
import { compare } from "bcryptjs";
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export const authOptions: NextAuthOptions = {
  session: {
    strategy: "jwt",
  },
  providers: [
    CredentialsProvider({
      name: "Sign in",
      credentials: {
        email: {
          label: "Email",
          type: "email",
          placeholder: "example@example.com",
        },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials.password) {
          return null;
        }

        const user = await prisma.user.findUnique({
          where: {
            email: credentials.email,
          },
        });

        if (!user || !(await compare(credentials.password, user.password))) {
          return null;
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          randomKey: "Hey cool",
        };
      },
    }),
  ],
  callbacks: {
    session: ({ session, token }) => {
      console.log("Session Callback", { session, token });
      return {
        ...session,
        user: {
          ...session.user,
          id: token.id,
          randomKey: token.randomKey,
        },
      };
    },
    jwt: ({ token, user }) => {
      console.log("JWT Callback", { token, user });
      if (user) {
        const u = user as unknown as any;
        return {
          ...token,
          id: u.id,
          randomKey: u.randomKey,
        };
      }
      return token;
    },
  },
};

I added the randomKey to the configuration simply to demonstrate that any additional information can be included in the session. It doesn’t have a specific purpose or functionality within the code. Its purpose is solely to illustrate the flexibility of including custom data or variables in the session.

Want to see the custom keys you added to the session object? Just follow these steps: go to the homepage and sign out by clicking on the “Sign Out” button. Then sign in again, and you’ll notice that the custom keys are now included in the session object printed in both the server and client components. Additionally, you can view the arbitrary keys by visiting the http://localhost:3000/api/session API route that we created earlier.

See the Custom Keys in the Session Object

Different Ways to Protect Routes

We’re almost done with the tutorial now, and we’ve seen how to add custom information to the NextAuth session object. One of the most crucial parts of any authentication system is protecting certain routes, whether it’s a whole section of your app, a single page, or an API endpoint. In Next.js, there are four main ways to implement route protection: in a server component, in a client component, in an API route, or using middleware.

While all four methods are possible, it’s generally recommended to use server-side protection or middleware. We’ll delve into these approaches shortly.

Client-Side Route Protection

The first way to implement protected routes is to use the useSession hook in a client-side component to load the session. This approach is similar to what we’ve seen before, but this time we’ll add an onUnauthenticated() method to the object passed to the hook, which will be called when the user is not logged in.

Keep in mind that the first time this hook is called, there might be some latency as it needs to decode the JWT server-side and retrieve the session information. In the onUnauthenticated() method, we can add the logic to redirect the user to the sign-in page if they’re not logged in.

To see this in action, let’s create a new folder called “profile” inside the “src/app” directory. Inside the “profile” folder, create a page.tsx file and add the following code. This page will serve as our private page that only authenticated users can access. Once the user is authenticated, a list of users will be displayed.

src/app/profile/page.tsx


"use client";

import { redirect } from "next/navigation";
import { useSession } from "next-auth/react";
import { cache, use } from "react";

type User = {
  id: number;
  name: string;
  email: string;
};

const getUsers = cache(() =>
  fetch("https://jsonplaceholder.typicode.com/users").then((res) => res.json())
);

export default function Profile() {
  const { status } = useSession({
    required: true,
    onUnauthenticated() {
      redirect("/api/auth/signin");
    },
  });

  if (status === "loading") {
    return <p>Loading....</p>;
  }

  let users = use<User[]>(getUsers());

  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "1fr 1fr 1fr 1fr",
          gap: 20,
        }}
      >
        {users.map((user) => (
          <div
            key={user.id}
            style={{ border: "1px solid #ccc", textAlign: "center" }}
          >
            <img
              src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
              alt={user.name}
              style={{ height: 180, width: 180 }}
            />
            <h3>{user.name}</h3>
          </div>
        ))}
      </div>
    </main>
  );
}

To test this feature, go back to the homepage and sign out. Then, try accessing the profile page at http://localhost:3000/profile. You will immediately be redirected to the sign-in page. However, there will be a brief loading period because the useSession hook needs to make an HTTP request to the server to decode the JWT.

After it receives the result from the server, it will check if you have a valid session. If you do not, it will trigger the onUnauthenticated() method, which in turn will call the redirect() function to redirect you to the sign-in page.

Server-Side Route Protection

The next method for implementing protected routes is by using a React server component. This approach is relatively straightforward, as we’ll utilize the getServerSession function to retrieve the session information, and then use an if statement to check if the session was successfully retrieved. If the user isn’t logged in, null will be returned, and we can redirect them to the sign-in page.

To see this in action, navigate to the src/app/profile/page.tsx file and replace its contents with the code provided below.

src/app/profile/page.tsx


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

type User = {
  id: number;
  name: string;
  email: string;
};

export default async function Profile() {
  const session = await getServerSession(authOptions);

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

  const users: User[] = await fetch(
    "https://jsonplaceholder.typicode.com/users"
  ).then((res) => res.json());

  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "1fr 1fr 1fr 1fr",
          gap: 20,
        }}
      >
        {users.map((user) => (
          <div
            key={user.id}
            style={{ border: "1px solid #ccc", textAlign: "center" }}
          >
            <img
              src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
              alt={user.name}
              style={{ height: 180, width: 180 }}
            />
            <h3>{user.name}</h3>
          </div>
        ))}
      </div>
    </main>
  );
}

To test this approach, ensure that you have signed out from your account on the homepage. Then, try to access the profile page again. You will be redirected to the signin page, but unlike the previous approach, there won’t be any loading period. This is because the server component immediately checks if the user is authenticated before serving the page. This creates a seamless and secure user experience.

Protect an API Route

The next approach involves protecting an API route, and it is also straightforward as we already have access to the session on the server-side. To achieve this, we can use the getServerSession function to obtain the session information and check if it exists. If it doesn’t, we can return an unauthorized error with the message “You are not logged in“.

src/app/api/session/route.ts


import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const session = await getServerSession(authOptions);

  if (!session) {
    return new NextResponse(
      JSON.stringify({ status: "fail", message: "You are not logged in" }),
      { status: 401 }
    );
  }

  return NextResponse.json({
    authenticated: !!session,
    session,
  });
}

To test this approach, first sign out from your account on the homepage. Then, visit the API route at http://localhost:3000/api/session. If you are not logged in, you should see an unauthorized error sent by the server, which includes a message stating “You are not logged in“. This confirms that the API route is properly protected and unauthorized users cannot access it.

Unauthorized Response From the Next.js API Route

Middleware Route Protection

The final and most preferable approach to protecting routes is by using middleware. This is the best way because it enables you to protect an entire subdirectory or all pages of your application, rather than adding route protection logic to each individual page.

To protect all pages of your Next.js application with NextAuth, you can simply create a middleware.ts file in your src directory and export the default middleware wrapper provided by NextAuth using the following line of code:

src/middleware.ts


export { default } from "next-auth/middleware";

If you need to protect single or multiple pages, or API routes, you can export a config object with a matcher key. The matcher is an array that can contain the routes you want to protect. In the code below, we added "/((?!register|api|login).*)" to the matcher array. This ensures that any route other than those for the register, login, and api directories will be protected.

src/middleware.ts


export { default } from "next-auth/middleware";

export const config = {
  // matcher: ["/profile"],
  matcher: ["/((?!register|api|login).*)"],
};

To finalize the setup, navigate to the src/app/profile/page.tsx file and remove the route protection logic, as we are now using the middleware approach for route protection. Once you have logged out, attempt to access the profile page. If successful, you should be redirected to the sign-in page, confirming that the middleware is effectively safeguarding the route.

src/app/profile/page.tsx


type User = {
  id: number;
  name: string;
  email: string;
};

export default async function Profile() {
  const users: User[] = await fetch(
    "https://jsonplaceholder.typicode.com/users"
  ).then((res) => res.json());

  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "1fr 1fr 1fr 1fr",
          gap: 20,
        }}
      >
        {users.map((user) => (
          <div
            key={user.id}
            style={{ border: "1px solid #ccc", textAlign: "center" }}
          >
            <img
              src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
              alt={user.name}
              style={{ height: 180, width: 180 }}
            />
            <h3>{user.name}</h3>
          </div>
        ))}
      </div>
    </main>
  );
}

Implement the Account Registration Logic

Having learned about NextAuth, the next step is to implement user registration logic. While NextAuth provides authentication functionality, it does not include a built-in solution for user registration.

Create the API Route to Register Users

Now, let’s create an API route to handle user registration. We’ll define a route handler that extracts the user’s credentials from the request body, hashes the password, and saves the user to the database using Prisma.

To do this, navigate to the src/app/api directory and create a new subdirectory called ‘register‘. Within this directory, create a route.ts file and add the following code:

src/app/api/register/route.ts


import { prisma } from "@/lib/prisma";
import { hash } from "bcryptjs";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  try {
    const { name, email, password } = (await req.json()) as {
      name: string;
      email: string;
      password: string;
    };
    const hashed_password = await hash(password, 12);

    const user = await prisma.user.create({
      data: {
        name,
        email: email.toLowerCase(),
        password: hashed_password,
      },
    });

    return NextResponse.json({
      user: {
        name: user.name,
        email: user.email,
      },
    });
  } catch (error: any) {
    return new NextResponse(
      JSON.stringify({
        status: "error",
        message: error.message,
      }),
      { status: 500 }
    );
  }
}

Create the Form Component

Now that we have the API logic in place, let’s create a form component for user registration. Since we’ll be handling the form using hooks and DOM events, it’s important to make sure this component is only rendered in the browser. You can achieve this by adding the "use client"; flag at the top of the file.

The registration form will allow users to input their registration details and submit them to the API. To get started, create a ‘register‘ directory within the ‘src/app‘ directory. Then, inside the ‘register‘ directory, create a file called form.tsx. This file will contain the code for the registration form.

src/app/register/form.tsx


"use client";

import { signIn } from "next-auth/react";
import { ChangeEvent, useState } from "react";

export const RegisterForm = () => {
  let [loading, setLoading] = useState(false);
  let [formValues, setFormValues] = useState({
    name: "",
    email: "",
    password: "",
  });

  const onSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);

    try {
      const res = await fetch("/api/register", {
        method: "POST",
        body: JSON.stringify(formValues),
        headers: {
          "Content-Type": "application/json",
        },
      });

      setLoading(false);
      if (!res.ok) {
        alert((await res.json()).message);
        return;
      }

      signIn(undefined, { callbackUrl: "/" });
    } catch (error: any) {
      setLoading(false);
      console.error(error);
      alert(error.message);
    }
  };

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setFormValues({ ...formValues, [name]: value });
  };

  return (
    <form
      onSubmit={onSubmit}
      style={{
        display: "flex",
        flexDirection: "column",
        width: 500,
        rowGap: 10,
      }}
    >
      <label htmlFor="name">Name</label>
      <input
        required
        type="text"
        name="name"
        value={formValues.name}
        onChange={handleChange}
        style={{ padding: "1rem" }}
      />
      <label htmlFor="email">Email</label>
      <input
        required
        type="email"
        name="email"
        value={formValues.email}
        onChange={handleChange}
        style={{ padding: "1rem" }}
      />
      <label htmlFor="password">Password</label>
      <input
        required
        type="password"
        name="password"
        value={formValues.password}
        onChange={handleChange}
        style={{ padding: "1rem" }}
      />
      <button
        style={{
          backgroundColor: `${loading ? "#ccc" : "#3446eb"}`,
          color: "#fff",
          padding: "1rem",
          cursor: "pointer",
        }}
        disabled={loading}
      >
        {loading ? "loading..." : "Register"}
      </button>
    </form>
  );
};

Create the Account Registration Page

To complete the registration feature, we need to create a page that will render the registration form. Inside the src/app/register directory, create a file called page.tsx. In this file, import the Register component we created earlier, and use it in the JSX to render it on the page.

src/app/register/page.tsx


import { RegisterForm } from "./form";

export default function RegisterPage() {
  return (
    <div
      style={{
        display: "flex",
        height: "70vh",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <div>
        <h1>Register</h1>
        <RegisterForm />
      </div>
    </div>
  );
}

After completing the setup, navigate to the registration page at http://localhost:3000/register to create a new account.

Account Registration Form

After successfully completing the registration process, you will be redirected to the default NextAuth sign-in page. There, you need to sign in using the same credentials you used during the account creation. Once signed in, NextAuth will redirect you to the homepage as specified by the callback function signIn(undefined, { callbackUrl: "/" }) in the registration form.

Conclusion

That’s it! The source code for this Next.js 13 and NextAuth project is available on GitHub.

In this tutorial, you’ve learned how to integrate NextAuth into the new Next.js 13 app structure. I hope you found this article informative and enjoyable. If you have any feedback or questions, please feel free to leave a comment.