- Published on
Authentication in Next.js 14 Using NextAuth + MongoDB
- Authors
- Name
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'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.