How to Implement MongoDB Authentication in NextJS using NextAuth.js
Jasser Mark Arioste
Hello! In this tutorial, you'll learn how to implement credentials authentication with MongoDB and NextJS.
Introduction #
Sometimes implementing authentication is tricky, but since the emergence of the very popular library NextAuth.js, it has been a breeze. However, NextAuth.js is designed for passwordless authentication. Fortunately, they have a way to support the good ol' credentials authentication with the very flexible CredentialsProvider.
The CredentialsProvider is intended to support use cases where you have an existing system you need to authenticate users against.- https://next-auth.js.org/configuration/providers/credentials
Tutorial Objectives #
Below are the tutorial objectives:
- Create a connection to MongoDB within NextJS
- Setup NextAuth.js CredentialsProvider
- Create a restricted page where only authenticated users can access
- Create a Test user and login
Step 1: Project Setup #
This step is optional if you already have an existing project. But you can do it if you want to follow along with the tutorial. First, create a new NextJS project by invoking the command below:
npx create-next-app --ts --app --tailwind --use-yarn next-js-mongodb-authentication
# explanation:
# --ts - initializes the project with typescript. Creates tsconfig.json, .ts, and .tsx files
# --app - initializes the project with the /app folder
# --use-yarn - (optional) explicitly specifies to use yarn for the project
# --tailwind - (optional) initializes the project with tailwindcss.
# next-js-mongodb-authentication - name of the project
# for more info use: npx create-next-app --help
Next, start the development server by invoking the command:
yarn dev
1
You can now go to http://localhost:3000 to access the app, but we'll go back to this later.
Step 2: Installing Dependencies #
Next, install the mongodb, next-auth, and bcrypt packages by running the command below. Both packages already support Typescript so we don't have to install the @types/*.
yarn add mongodb next-auth bcrypt
Next, let's install the @types/bcrypt package since it's not typescript supported out-of-the-box.
yarn add -D @types/bcrypt
Later, you'll use bcrypt for hashing the user's password since it's not safe to store the password directly in the database. So when comparing the passwords later, we'll compare the hashes instead of the actual password.
Step 3: Connecting to MongoDB #
Next, create a lib/mongodb.ts
file. This file will be used to connect to the MongoDB database. This is taken from the next js example: with-mongodb.
// lib/mongodb.ts import { MongoClient, MongoClientOptions } from "mongodb"; if (!process.env.MONGODB_URI) { throw new Error('Invalid/Missing environment variable: "MONGODB_URI"'); } const IS_DEVELOPMENT = process.env.NODE_ENV === "development"; const uri = process.env.MONGODB_URI; const options: MongoClientOptions = {}; let client; let clientPromise: Promise<MongoClient>; if (IS_DEVELOPMENT) { // In development mode, use a global variable so that the value // is preserved across module reloads caused by HMR (Hot Module Replacement). let globalWithMongo = global as typeof globalThis & { _mongoClientPromise?: Promise<MongoClient>; }; if (!globalWithMongo._mongoClientPromise) { client = new MongoClient(uri, options); globalWithMongo._mongoClientPromise = client.connect(); } clientPromise = globalWithMongo._mongoClientPromise; } else { // In production mode, it's best to not use a global variable. client = new MongoClient(uri, options); clientPromise = client.connect(); } // Export a module-scoped MongoClient promise. By doing this in a // separate module, the client can be shared across functions. export default clientPromise;
1234567891011121314151617181920212223242526272829303132333435
Next, create an .env.local
file at the root directory of your project.
# env.local
MONGODB_URI=mongodb://localhost:27017
DB_NAME=mydatabase
# NEXTAUTH_SECRET default environment variable that next-auth uses. This is important, otherwise, sign in work
NEXTAUTH_SECRET=yoursecretvariablehere
Step 4: Implementing the CredentialsProvider #
Next, we'll set up next-auth CredentialsProvider
to authenticate users from MongoDB. First, create the file lib/nextauthOptions.ts
:
// lib/nextauthOptions.ts import CredentialsProvider from "next-auth/providers/credentials"; import bcrypt from "bcrypt"; import clientPromise from "./mongodb"; import { AuthOptions } from "next-auth"; export const nextauthOptions: AuthOptions = { // Configure one or more authentication providers providers: [ CredentialsProvider({ id: "credentials", credentials: { email: { label: "E-mail", type: "text", }, password: { label: "Password", type: "password", }, }, async authorize(credentials) { const client = await clientPromise; const usersCollection = client .db(process.env.DB_NAME) .collection("users"); const email = credentials?.email.toLowerCase(); const user = await usersCollection.findOne({ email }); if (!user) { throw new Error("User does not exist."); } //validate password const passwordIsValid = await bcrypt.compare( credentials?.password!, user.password ); if (!passwordIsValid) { throw new Error("Invalid credentials"); } return { id: user._id.toString(), ...user, }; }, }), // ...add more providers here ], session: { strategy: "jwt", }, };
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
We create a separate options file since we'll use this later when retrieving sessions from server components.
In lines 12-15, You define the fields to be used in the sign-in page. Here it's the email and password but it can be any field. So when you go to /api/auth/signin
, it will be as below:
In lines 16-43, you define the authorize
function. Which accepts the credentials defined in lines 12-15 and returns a user. In this function, you connect to the database, search the users
collection, and compare the password hash using bcrypt. Once everything lines up, you return the user
.
Next, create the file /pages/api/auth/[...nextauth].ts
.
import { nextauthOptions } from "@/lib/nextauthOptions"; import NextAuth from "next-auth"; export default NextAuth(nextauthOptions);
1234
This will be the actual route handler. This will handle routes such as /api/auth/signin
, /api/auth/signout
, etc.
Step 5: Creating a Restricted Route #
Next, let's create a restricted route that only authenticated users can access. First, create the file app/restricted/page.tsx
.
import { nextauthOptions } from "@/lib/nextauthOptions"; import { getServerSession } from "next-auth/next"; import React from "react"; import { redirect } from "next/navigation"; export default async function RestrictedPage() { // get the session const session = await getServerSession(nextauthOptions); // redirect to signin if there is no session. if (!session?.user) { const url = new URL("/api/auth/signin", "http://localhost:3000"); url.searchParams.append("callbackUrl", "/restricted"); redirect(url.toString()); } // display the page return ( <div> <h1>Welcome to the Restricted Page, {session?.user?.name}</h1> </div> ); }
1234567891011121314151617181920212223
First, you get the session using getServerSession function. If there's no session, you redirect to the sign-in page, otherwise, display the page. There are many ways to get the session, but this method is best for SSR or Server components since it uses the information from the cookies
while avoiding an extra call to the API route /api/auth/session
.
Now, once you go to localhost:3000/restricted
, it redirects to http://localhost:3000/api/auth/signin?callbackUrl=%2Frestricted
Step 6: Creating a Test User #
Next, let's create an API route that creates an admin user so that you can sign in to the app. First, create the file /app/create-admin/route.ts
and copy the code below.
// app/create-admin/route.ts import clientPromise from "@/lib/mongodb"; import bcrypt from "bcrypt"; import { NextResponse } from "next/server"; export async function GET() { const client = await clientPromise; const users = client.db(process.env.DB_NAME).collection("users"); const password = bcrypt.hashSync("password", 10); await users.insertOne({ email: "admin@example.com", password: password, role: "admin", }); return NextResponse.json({ success: true }); }
123456789101112131415161718
Now, go to the route localhost:3000/create-admin
and it should return the following response:
{ "success": true }
123
Now, it's time to test our restricted page.
Step 7: Demo #
In the demo below, we go to the restricted page, and it redirects to the /api/auth/signin
route. After logging in, it redirects back to the /restricted
route.
Full Code #
The full code can be accessed at GitHub: jmarioste/next-js-mongodb-authentication-tutorial
Conclusion #
You learned how to implement MongoDB credentials authentication in NextJS 13.4 app folder.
If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on GitHub.
Resources #
Credits: Image by Tomasz Proszek from Pixabay