ReactHustle

How to Implement MongoDB Authentication in NextJS using NextAuth.js

Jasser Mark Arioste

Jasser Mark Arioste

How to Implement MongoDB Authentication in NextJS using NextAuth.js

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:

  1. Create a connection to MongoDB within NextJS
  2. Setup NextAuth.js CredentialsProvider
  3. Create a restricted page where only authenticated users can access
  4. 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 mongodbnext-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:

MongoDB Authentication with NextJS Credentials

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.

Next JS MongoDB Authentication Demo

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

Share this post!

Related Posts