Adding Authentication to a Next.js 13 App Router with next-auth Package

Authentication is a crucial aspect of web applications as it enables secure access to specific features or content. In this blog post, we will explore how to add authentication to a Next.js 13 app router using the next-auth package. We will cover the necessary code snippets and steps to implement this authentication functionality.

Step 1: Setting up the API Route

The first step involves creating an API route file called route.js under the api/auth/[…nextauth] directory. In this file, we import NextAuth from next-auth and the necessary authentication provider, such as GithubProvider. We also import the User model and the connectToDB function from the respective files.

import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";

import User from '@models/user';
import { connectToDB } from '@utils/database';

const handler = NextAuth({
  // Configure one or more authentication providers
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
    // ...add more providers here
  ],
  callbacks: {
    async session({ session }) {
      // store the user id from MongoDB to session
      const sessionUser = await User.findOne({ email: session.user.email });
      session.user.id = sessionUser._id.toString();

      return session;
    },
    async signIn({ account, profile, user, credentials }) {
      try {
        await connectToDB();

        // check if user already exists
        const userExists = await User.findOne({ email: profile.email });

        // if not, create a new document and save user in MongoDB
        if (!userExists) {
          await User.create({
            email: profile.email,
            username: profile.name.replace(" ", "").toLowerCase(),
            image: profile.picture,
          });
        }

        return true
      } catch (error) {
        console.log("Error checking if user exists: ", error.message);
        return false
      }
    },
  }
});

export { handler as GET, handler as POST };

Step 2: Creating the Provider Component

To wrap the entire application with the authentication provider, we need to create a Provider component. This component is responsible for initializing the session provider and passing the session data to the children components.

// components/Provider.jsx
"use client";

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

const Provider = ({ children, session}) => {
    return(
        < SessionProvider session={session} >
            {children}
        < /SessionProvider >
    )
}

export default Provider;

Step 3: Adding the Provider Component to the Root Layout

To incorporate the authentication provider into the app router, we modify the RootLayout component. In the RootLayout component, we import the Provider component we created earlier.

import Provider from '@components/Provider';

export default function RootLayout({ children }) {
  return (
    < html lang="en" >
      < body className={inter.className} >
        < Provider >
          {children}
        < /Provider >      
      < /body >
    < /html >
  )
}

Step 4: Using Authentication Hooks and Methods

To enable user authentication and session management, we can utilize the useSession, signIn, and signOut hooks and methods provided by next-auth/react.

We create a component called LoginBtn.jsx that imports the necessary hooks and methods. Inside the component, we use the useSession hook to access the session data. If a session exists, we display the user’s email and a “Sign out” button. Otherwise, we show a “Sign in” button.

// components/LoginBtn.jsx
'use client';
import { useSession, signIn, signOut } from "next-auth/react";

export default function LoginBtn() {
  const { data: session } = useSession();
  if (session) {
    return (
      < >
        Signed in as {session.user.email} <br />
        < button onClick={() => signOut()} >Sign out< /button >
      < / >
    )
  }
  return (
    < >
      Not signed in <br />
      < button onClick={() => signIn()} >Sign in< /button >
    < / >
  )
}

User Model:

To complete the authentication setup, we need to define the User model. Create a file called user.js in the models directory and add the following code:

// models/user.js
import { Schema, model, models } from 'mongoose';

const UserSchema = new Schema({
  email: {
    type: String,
    unique: [true, 'Email already exists!'],
    required: [true, 'Email is required!'],
  },
  username: {
    type: String,
    required: [true, 'Username is required!']
  },
  image: {
    type: String,
  }
});

const User = models.User || model("User", UserSchema);

export default User;

Database Utility:

Next, we need a utility file to establish a connection with the MongoDB database. Create a file called database.js in the utils directory and add the following code:

// utils/database.js
import mongoose from 'mongoose';

let isConnected = false; // track the connection

export const connectToDB = async () => {
  mongoose.set('strictQuery', true);

  if(isConnected) {
    console.log('MongoDB is already connected');
    return;
  }

  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      dbName: "share_prompt",
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });

    isConnected = true;

    console.log('MongoDB connected')
  } catch (error) {
    console.log(error);
  }
}