Logo
Published on

Authentication in Next.js 14 Using NextAuth + MongoDB

Authors
  • Name
    Twitter

Image from NextAuth

Hey, I’m Marcos, I’m going to show you how to implement NextAuth authentication in NextJS 14 with MongoDB. You can see the repository of this article clicking here.

I’m looking for part-time work! check out my portfolio and contact me.

What is NextAuth.js

NextAuth is a popular authentication library for Next.js. It is easy, flexible and secure. In this article I will show you how to setup username and password authentication using NextAuth with Credential provider. You can see more information about this library in NextAuth.

Setting Up Project

Let’s start with setting up NextJS project. Run the below command in the terminal to create NextJSfolder architecture.

npx create-next-app@latest
What is your project named? <your project name>  
Would you like to use TypeScript? **Yes**  
Would you like to use ESLint? **Yes**  
Would you like to use Tailwind CSS? **Yes**  
Would you like to use `src/` directory? **Yes**  
Would you like to use App Router? (recommended) **Yes**  
Would you like to customize the default import alias (@/\*)? **Yes**

Install dependencies

Before we started, let’s install some dependencies for this application.

npm install next-auth @types/bcryptjs mongoose react-icons

Setup Environment Variables

Create .env in the root directory where the package.json file is located.

In this file we will add the URI of our MongoDB database, our Google Auth keys, our NextAuth key and the URL of our application.

.env.local

MONGODB_URI="YOUR_DATABASE_KEY"
GOOGLE_CLIENT_ID="YOUR_CLIENT_ID"
GOOGLE_CLIENT_SECRET="YOUR_SECRET_ID"
NEXTAUTH_SECRET="NEXTAUTH_SECRET"
NEXT_PUBLIC_APP_URL="http://localhost:3000"

If you have doubts about how to create a database in MongoDB click here.

If you have doubts about how to obtain Google Auth keys click here.

To get the NextAuth secret key simply run this on your terminal and it will generate a random key for you.

npx auth secret

Libs folder

Create a libs folder inside the src directory, in the libs folder you will create a file called auth.ts, in this file you will find all the authentication options.

src/libs/auth.ts

import { connectDB } from  "@/libs/mongodb";
import  User  from  "@/models/User";
import  type { NextAuthOptions } from  "next-auth";
import  CredentialsProvider  from  "next-auth/providers/credentials";
import  GoogleProvider  from  "next-auth/providers/google";
import bcrypt from  "bcryptjs";

export  const  authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID  as  string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET  as  string
}),
CredentialsProvider({
name: "Credentials",
id: "credentials",
credentials: {
email: { label: "Email", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
},
async  authorize(credentials) {
await  connectDB();
const userFound = await  User.findOne({
email: credentials?.email,
}).select("+password");

if (!userFound) throw  new  Error("Invalid Email");

const passwordMatch = await bcrypt.compare(
credentials!.password,
userFound.password
);

if (!passwordMatch) throw  new  Error("Invalid Password");
return userFound;
},
}),
],
pages: {
signIn: "/login",
},
session: {
strategy: "jwt",
},
callbacks: {
async  jwt({ token, user, session, trigger }) {
if (trigger === "update" && session?.name) {
token.name = session.name;
}

if (trigger === "update" && session?.email) {
token.email = session.email;
}

if (user) {
const u = user as  unknown  as  any;
return {
...token,
id: u.id,
phone: u.phone,
};
}
return token;
},
async  session({ session, token }) {
return {
...session,
user: {
...session.user,
_id: token.id,
name: token.name,
phone: token.phone,
}
};
},
},
};

Inside the libs folder we will also create a file called mongodb.ts, this will be the file from which our application will connect to the MongoDB database.

src/libs/mongodb.ts

import mongoose from "mongoose";

const { MONGODB_URI } = process.env;

if (!MONGODB_URI) {
  throw new Error("MONGODB_URI must be defined");
}

export const connectDB = async () => {
  try {
    const { connection } = await mongoose.connect(MONGODB_URI);
    if (connection.readyState === 1) {
      console.log("MongoDB Connected");
      return Promise.resolve(true);
    }
  } catch (error) {
    console.error(error);
    return Promise.reject(error);
  }
};

Models folder

Inside the src folder also create a folder called models and a file called User.ts, here we will create our model to store the users in the database.

src/models/User.ts

import { Schema, model, models } from "mongoose";

export interface UserDocument {
  email: string;
  password: string;
  name: string;
  phone: string;
  image: string;
  _id: string;
  createdAt: Date;
  updatedAt: Date;
}

const UserSchema =
  new Schema() <
  UserDocument >
  ({
    email: {
      type: String,
      unique: true,
      required: [true, "Email is required"],
      match: [
        /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
        "Email is invalid",
      ],
    },
    password: {
      type: String,
      required: [true, "Password is required"],
      select: false,
    },
    name: {
      type: String,
      required: [true, "Fullname is required"],
      minLength: [3, "fullname must be at least 3 characters"],
      maxLength: [25, "fullname must be at most 25 characters"],
    },
    phone: {
      type: String,
      default: "",
    },
  },
  {
    timestamps: true,
  });

const User = models.User || model < UserDocument > ("User", UserSchema);
export default User;

API folder

Inside the app directory create these folders and route.ts inside them.

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

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

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

[…nextauth] contains a dynamic route handler for NextAuth.js. To know more read the documentation.

Inside our auth folder we will also create a folder called signup and inside it a route.ts file. With the code in this file we will make our application able to interact with the database in three ways by creating, updating and deleting users.

app/api/auth/signup/route.ts

import { connectDB } from "@/libs/mongodb";
import User from "@/models/User";
import { NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import mongoose from "mongoose";

export async function POST(request: Request) {
  try {
    await connectDB();

    const { name, email, password, phone } = await request.json();

    if (password.length < 6) {
      return NextResponse.json(
        { message: "Password must be at least 6 characters" },
        { status: 400 }
      );
    }

    const userFound = await User.findOne({ email });

    if (userFound) {
      return NextResponse.json(
        { message: "Email already exists" },
        { status: 409 }
      );
    }

    const hashedPassword = await bcrypt.hash(password, 12);

    const user = new User({
      name,
      email,
      phone,
      password: hashedPassword,
    });

    const savedUser = await user.save();

    return NextResponse.json(
      {
        name: savedUser.name,
        email: savedUser.email,
        createdAt: savedUser.createdAt,
        updatedAt: savedUser.updatedAt,
      },
      { status: 201 }
    );
  } catch (error) {
    if (error instanceof mongoose.Error.ValidationError) {
      return NextResponse.json({ message: error.message }, { status: 400 });
    } else {
      console.error("Error during signup:", error);
      return NextResponse.error();
    }
  }
}

export async function PUT(request: Request) {
  try {
    await connectDB();

    const { userId, name, email, password, phone, address } =
      await request.json();

    if (password && password.length < 6) {
      return NextResponse.json(
        { message: "Password must be at least 6 characters" },
        { status: 400 }
      );
    }

    const userToUpdate = await User.findById(userId);

    if (!userToUpdate) {
      return NextResponse.json({ message: "User not found" }, { status: 404 });
    }

    if (name) {
      userToUpdate.name = name;
    }

    if (email) {
      userToUpdate.email = email;
    }

    if (password) {
      const hashedPassword = await bcrypt.hash(password, 12);
      userToUpdate.password = hashedPassword;
    }

    if (phone) {
      userToUpdate.phone = phone;
    }

    if (address) {
      userToUpdate.address = address;
    }

    await userToUpdate.save();

    console.log(userToUpdate);

    return NextResponse.json(
      {
        message: "User updated successfully",
        updatedUser: {
          id: userToUpdate._id,
          name: userToUpdate.name,
          email: userToUpdate.email,
          createdAt: userToUpdate.createdAt,
          updatedAt: userToUpdate.updatedAt,
        },
      },
      { status: 200 }
    );
  } catch (error) {
    if (error instanceof mongoose.Error.ValidationError) {
      return NextResponse.json({ message: error.message }, { status: 400 });
    } else {
      console.error("Error during user update:", error);
      return NextResponse.error();
    }
  }
}

export async function DELETE(request: Request) {
  try {
    await connectDB();

    const { userId } = await request.json();

    const user = await User.findById(userId);

    if (!user) {
      return NextResponse.json({ message: "User not found" }, { status: 404 });
    }

    await user.remove();

    return NextResponse.json(
      { message: "User deleted successfully" },
      { status: 200 }
    );
  } catch (error) {
    console.error("Error during user/cart item deletion:", error);
    return NextResponse.error();
  }
}

Login and register routes

We are going to create the fontend paths through which the user will be able to navigate, inside the app folder we will create two folders, one called login and the other register and each of them will have a file called page.tsx.

app/login/page.tsx

"use client";

import { FormEvent, useEffect, useState } from  "react";
import { signIn } from  "next-auth/react";
import { useRouter } from  "next/navigation";
import  Link  from  "next/link";
import { useSession } from  'next-auth/react';
import { BiLogoGoogle } from  'react-icons/bi';
import { BiSolidShow } from  'react-icons/bi';
import { BiSolidHide } from  'react-icons/bi';

const  Signin = () => {
const [error, setError] = useState("");
const [showPassword, setShowPassword] = useState(false);
const router = useRouter();
const { data: session } = useSession();

const labelStyles = "w-full text-sm";

useEffect(() => {
if (session?.user) {
router.push("/");
}
}, [session, router]);

const  handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new  FormData(event.currentTarget);
const res = await  signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirect: false,
});

if (res?.error) {
setError(res.error  as  string)
};

if (!res?.error) {
return router.push("/")
};
};

return (
<section className="w-full h-screen flex items-center justify-center">
<form
className="p-6 xs:p-10 w-full max-w-[350px] flex flex-col justify-between items-center gap-2.5
border border-solid border-[#242424] bg-[#0a0a0a] rounded"
onSubmit={handleSubmit}
>
{error && <div className="">{error}</div>}
<h1 className="mb-5 w-full text-2xl font-bold">Signin</h1>

<label className={labelStyles}>Email:</label>
<input
type="email"
placeholder="Email"
className="w-full h-8 border border-solid border-[#242424] py-1 px-2.5 rounded bg-black text-[13px]"
name="email"
/>

<label className={labelStyles}>Password:</label>
<div className="flex w-full">
<input
type={showPassword ? "text" : "password"}
placeholder="Password"
className="w-full h-8 border border-solid border-[#242424] py-1 px-2.5 rounded-l bg-black text-[13px]"
name="password"
/>
<button
className="w-2/12 border-y border-r border-solid border-[#242424] bg-black rounded-r
flex items-center justify-center transition duration-150 ease hover:bg-[#1A1A1A]"
onClick={(e) => {
e.preventDefault();
setShowPassword(!showPassword)
}}
>
{showPassword ? <BiSolidHide /> : <BiSolidShow />}
</button>
</div>
<button className="w-full bg-black border border-solid border-[#242424] py-1.5 mt-2.5 rounded
transition duration-150 ease hover:bg-[#1A1A1A] text-[13px]"
>
Signup
</button>

<div className="w-full h-10 relative flex items-center justify-center">
<div className="absolute h-px w-full top-2/4 bg-[#242424]"></div>
<p className="w-8 h-6 bg-[#0a0a0a] z-10 flex items-center justify-center">or</p>
</div>

<button
className="flex py-2 px-4 text-sm align-middle items-center rounded text-999 bg-black
border border-solid border-[#242424] transition duration-150 ease hover:bg-[#1A1A1A] gap-3"
onClick={(e) => {
e.preventDefault();
signIn("google")
}}>
<BiLogoGoogle className="text-2xl" /> Sign in with Google
</button>
<Link href="/register" className="text-sm text-[#888] transition duration-150 ease hover:text-white">
Don&apos;t have an account?
</Link>
</form>
</section>
);
}

export  default  Signin;

app/register/page.tsx

"use client";

import { FormEvent, useEffect, useState } from "react";
import axios, { AxiosError } from "axios";
import { signIn, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { BiLogoGoogle } from "react-icons/bi";
import { BiSolidShow } from "react-icons/bi";
import { BiSolidHide } from "react-icons/bi";

const Signup = () => {
  const [error, setError] = useState();
  const [showPassword, setShowPassword] = useState(false);
  const router = useRouter();
  const { data: session } = useSession();

  const labelStyles = "w-full text-sm";

  useEffect(() => {
    if (session) {
      router.push("/");
    }
  }, [session, router]);

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    try {
      const formData = new FormData(event.currentTarget);
      const signupResponse = await axios.post(
        `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/signup`,
        {
          email: formData.get("email"),
          password: formData.get("password"),
          name: formData.get("name"),
          phone: formData.get("phone"),
        }
      );

      const res = await signIn("credentials", {
        email: signupResponse.data.email,
        password: formData.get("password"),
        redirect: false,
      });

      if (res?.ok) return router.push("/");
    } catch (error) {
      console.log(error);
      if (error instanceof AxiosError) {
        const errorMessage = error.response?.data.message;
        setError(errorMessage);
      }
    }
  };

  return (
    <section className="w-full h-screen flex items-center justify-center">
      <form
        onSubmit={handleSubmit}
        className="p-6 xs:p-10 w-full max-w-[350px] flex flex-col justify-between items-center gap-2.5  
border border-solid border-[#242424] bg-[#0a0a0a] rounded"
      >
        {error && <div className="">{error}</div>}
        <h1 className="mb-5 w-full text-2xl font-bold">Signup</h1>

        <label className={labelStyles}>Fullname:</label>
        <input
          type="text"
          placeholder="Fullname"
          className="w-full h-8 border border-solid border-[#242424] py-1 px-2.5 rounded bg-black text-[13px]"
          name="name"
        />

        <label className={labelStyles}>Email:</label>
        <input
          type="email"
          placeholder="Email"
          className="w-full h-8 border border-solid border-[#242424] py-1 px-2.5 rounded bg-black text-[13px]"
          name="email"
        />

        <label className={labelStyles}>Password:</label>
        <div className="flex w-full">
          <input
            type={showPassword ? "text" : "password"}
            placeholder="Password"
            className="w-full h-8 border border-solid border-[#242424] py-1 px-2.5 rounded-l bg-black text-[13px]"
            name="password"
          />
          <button
            className="w-2/12 border-y border-r border-solid border-[#242424] bg-black rounded-r  
flex items-center justify-center transition duration-150 ease hover:bg-[#1A1A1A]"
            onClick={(e) => {
              e.preventDefault();
              setShowPassword(!showPassword);
            }}
          >
            {showPassword ? <BiSolidHide /> : <BiSolidShow />}
          </button>
        </div>

        <label className={labelStyles}>Phone:</label>
        <input
          type="text"
          placeholder="Phone (not required)"
          className="w-full h-8 border border-solid border-[#242424] py-1 px-2.5 rounded bg-black text-[13px]"
          name="phone"
        />

        <button
          className="w-full bg-black border border-solid border-[#242424] py-1.5 mt-2.5 rounded  
transition duration-150 ease hover:bg-[#1A1A1A] text-[13px]"
        >
          Signup
        </button>

        <div className="w-full h-10 relative flex items-center justify-center">
          <div className="absolute h-px w-full top-2/4 bg-[#242424]"></div>
          <p className="w-8 h-6 bg-[#0a0a0a] z-10 flex items-center justify-center">
            or
          </p>
        </div>

        <button
          className="flex py-2 px-4 text-sm align-middle items-center rounded text-999 bg-black  
border border-solid border-[#242424] transition duration-150 ease hover:bg-[#1A1A1A] gap-3"
          onClick={() => signIn("google")}
        >
          <BiLogoGoogle className="text-2xl" /> Sign in with Google
        </button>
        <Link
          href="/login"
          className="text-sm text-[#888] transition duration-150 ease hover:text-white"
        >
          Already have an account?
        </Link>
      </form>
    </section>
  );
};

export default Signup;

Main page and layout

On the main page we check that the code works correctly and implement the signOut functionality.

app/page.tsx

"use client";

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

export default function Home() {
  const { status } = useSession();

  const showSession = () => {
    if (status === "authenticated") {
      return (
        <button
          className="text-[#888] text-sm text-999 mt-7 transition duration-150 ease hover:text-white"
          onClick={() => {
            signOut();
          }}
        >
          Logout here
        </button>
      );
    } else if (status === "loading") {
      return <span className="text-[#888] text-sm mt-7">Loading...</span>;
    } else {
      return (
        <Link
          href="/login"
          className="text-[#888] text-sm text-999 mt-7 transition duration-150 ease hover:text-white"
        >
          Login here
        </Link>
      );
    }
  };
  return (
    <main className="flex min-h-screen flex-col items-center justify-center">
      <h1 className="text-xl">NextAuth APP</h1>
      {showSession()}
    </main>
  );
}

We will also create a file called Providers.tsx inside the app folder where we will put the NextAuth session provider.

app/Provider.tsx

"use client";

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

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

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

In the layout file we simply add the Provider and our application is ready to run correctly.

app/layout.tsx

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { Provider } from "./Provider";

import "./globals.css";

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

export const metadata: Metadata = {
  title: "NextAuth + MongoDB Tutorial",
  description: "Generated by create next app",
};

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

Thank you very much for getting this far, if you found this article helpful please give a star to my GitHub repository so I can help more people.