Last updated on April 10, 2023

Next.js Authentication with next-auth, Prisma, and MongoDB

Prisma is an open-source ORM (Object Relational Mapper) which's main focus is to help developers avoid data inconsistency issues by writing readable and type-safe code with their custom schema definition language.

Until very recently, Prisma had focused on their wide adoption over relational databases like MySQL, PostgreSQL, and SQLite but this has all changed with their brand new release which brings one of the most requested features of Prisma which is support for the non-relational database MongoDB.

MongoDB is a document-oriented database designed to store and manage large scales of data and allows users to work with that data in a very efficient way. Its main difference from relational databases is its flexibility for storing information where instead of using tables and rows, MongoDB stores data in a JSON-like document whose content can follow any kind of structure (and can change from document to document).

Some of MongoDB's key features are its support for data searches by field, range queries, or regular expressions; It has an indexing system that significantly improves the performance of queries, a replication system that provides high availability of the database, and a native load balancing system to manage the connections to the data.

MongoDB & Next.js Configuration

Let's start by setting up our MongoDB database. For this, we will go through the Get Started Guide of MongoDB Atlas and we will create our free database cluster. Once our setup is complete, we will retrieve our database connection string which should have the following format:

mongodb+srv://<username>:<password>@<cluster>.mongodb.net/<database_name>?retryWrites=true&w=majority

Now, we will create our Next.js project with the command npx create-next-app@latest <project_folder_name> and then we will create a new .env file inside of our project folder where we will add our database connection string in the following way:

DATABASE_URL="mongodb+srv://<username>:<password>@<cluster>.mongodb.net/<database_name>?retryWrites=true&w=majority"

Install & Configure Prisma

In this step, we will learn how to install and set up Prisma in our project. For this, we will first install the Prisma CLI with the command npm install -D prisma and then, we will initialize Prisma by running the command npx prisma init --datasource-provider mongodb.

As you may have seen, the initialize command created inside our project a new folder called prisma, and inside of it, we have a new file called schema.prisma. This file is our main Prisma configuration file and it contains three basic elements which are:

  • Data sources (e.g. MongoDB database).
  • Generators that specify where our data models should be generated (e.g. Prisma Client).
  • Data models which share the structure of our data and how they relate to each other.

Until this point, our schema.prisma file should look like this:

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "mongodb"
  url = env("DATABASE_URL")
}

Now, let's build our first Prisma data model which will represent our MongoDB users collection (in the following step, we will use it with NextAuth). For this, we will update our schema.prisma file by adding the following code:

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "mongodb"
  url = env("DATABASE_URL")
}
 
model User {
  id              String      @id @default(auto()) @map("_id") @db.ObjectId
  email           String      @unique
  createdAt       DateTime    @default(now())
  emailVerified   DateTime?
}

Here, we defined a new model called User which will receive the same name in our MongoDB collection. Inside of this model, we defined different fields which are:

  • id: Unique identifier of the user. Since MongoDB uses _id as its default name, we will map the Prisma id field to _id with the map attribute (@map("_id")).
  • email: Email address of the user. Since we want to make sure the email address is unique, we used the unique attribute (@unique).
  • createdAt: Date when the user was created. We used the default attribute (@default(now())) to get the date when the document was created.
  • emailVerified: Date when the user's email address was verified. We used the ? modifier to indicate the field is optional.

Often, we might find that we don't want our MongoDB collection name to be the same as our Prisma data model name. For this, we can rename our collection by adding to our data model the code @@map("<collection_name>").

As we already used with the id field of our User model, we can change the name of any field by using the map attribute and adding the code @map("<field_name>").

Here is an example of what our schema.prisma file should look like after we change our collection name from User to users and the emailVerified field to verifiedAt:

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "mongodb"
  url = env("DATABASE_URL")
}
 
model User {
  id              String      @id @default(auto()) @map("_id") @db.ObjectId
  email           String      @unique
  createdAt       DateTime    @default(now())
  emailVerified   DateTime?   @map("verifiedAt")
  @@map("users")
}

The last step in our Prisma setup is to install the Prisma client with the command npm install --save @prisma/client. This package will let us send queries to our database that go from basic CRUD operations to select queries, relation queries, and filtering/sorting.

Now, every time that we make a change in our Prisma schema, we need to run the command npx prisma db push so our changes are deployed into our database.

Setup Authentication with NextAuth

Authentication is a system that verifies who a user is and controls what that user can access. As our next step, we will add an authentication system to our project. There are multiple tool alternatives that can be used for user authentication with Next.js but in this case, we will use one of the simplest and most used called NextAuth.

NextAuth is a complete open-source authentication system built for Next.js that provides out-of-the-box support for dozens of built-in providers like Google, Facebook, Github, email/password, magic links, and much more.

For the purpose of this tutorial, we will use the Email system which will send a magic link into the user's registered email and once that magic link is clicked, the user will be redirected to the platform and automatically authenticated.

To add NextAuth to our project, we will run the command npm install --save next-auth. Since we are using Prisma, we will need to use a special NextAuth feature called Adapter. The Adapter's role is to connect the application with the database or backend system that is being used to store the data (e.g. Prisma). To install our Adapter, we will run the command npm install --save @next-auth/prisma-adapter.

Our next step is to add the authentication API route. For this, we will create a new pages/api/auth folder path inside our project, and inside of it, we will create a new file called [...nextauth].js. This API route will contain the dynamic route handler for NextAuth which will be responsible for handling all the authentication requests of our application (e.g. signIn, signOut). Let's add the following code into our new [...nextauth].js file:

pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import EmailProvider from 'next-auth/providers/email';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { PrismaClient } from '@prisma/client';
 
const prisma = new PrismaClient();
 
export default NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    EmailProvider({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM
    })
  ],
});

Here, we defined our project NextAuth configuration. Inside of it, we established a providers array that contains all the authentication providers we want our application to use (in our case, we used only the email provider). If we wanted to add more providers in the future (e.g. Facebook, Twitter), we would only need to add them to this array for them to work in the application.

As you may have noticed, inside our email provider, we used two environmental variables which are process.env.EMAIL_SERVER and process.env.EMAIL_FROM. In order for our application to be able to send a magic link to the user's registered email, we need to configure our SMTP server connection.

For this, we will use the Amazon Simple Email Service (SES) but you can use any SMTP provider (e.g. SendGrid, Mailgun, Postmark). Once we have completed our provider setup, we will retrieve our SMTP server connection string which should have the following format:

smtps://<username>:<password>@email-smtp.<region_name>.amazonaws.com:<port>

Next, we will install the nodemailer package with the command npm install --save nodemailer whose main purpose is to add the capability to send emails from our next.js project. Once the package is installed, we will add to our .env file the following code:

EMAIL_SERVER="smtps://<username>:<password>@email-smtp.<region_name>.amazonaws.com:<port>"
EMAIL_FROM="email@example.com"

To complete our NextAuth setup, we will update our schema.prisma file with the following code (once your file is complete, don't forget to run the command npx prisma db push):

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "mongodb"
  url = env("DATABASE_URL")
}
 
model User {
  id              String      @id @default(auto()) @map("_id") @db.ObjectId
  email           String      @unique
  createdAt       DateTime    @default(now())
  emailVerified   DateTime?   @map("verifiedAt")
  sessions        Session[]
  @@map("users")
}
 
model Session {
  id              String      @id @default(auto()) @map("_id") @db.ObjectId
  sessionToken    String      @unique
  userId          String      @db.ObjectId
  expires         DateTime    @map("expiresAt")
  user            User        @relation(fields: [userId], references: [id])
  @@map("sessions")
}
 
model VerificationToken {
  id              String      @id @default(auto()) @map("_id") @db.ObjectId
  identifier      String
  token           String      @unique
  expires         DateTime    @map("expiresAt")
  @@unique([identifier, token])
  @@map("verification_tokens")
}

In this code, you will find some new features that we didn't use in the creation of our first User data model. These features are:

  • @relation: Connection between two models in the Prisma schema. For example, we used the code @relation(fields: [userId], references: [id]) to connect the user field from the Session data model to the id field of the User model.
  • @db.ObjectId: Special type used to build a unique MongoDB document identifier.
  • @@unique: Identify a compound of unique constraints of a model. If we want to define one field as unique, we would use the @unique attribute and if we want to define the composition of two fields as unique, we would use the @@unique attribute.

Now, this schema.prisma code is optimized to have only the necessary fields for the email authentication system to work. If we wanted to use other authentication providers (e.g. Facebook, Twitter), we would need to add a new data model called Account. For this, you can go through the following NextAuth Guide.

Add Client Functionality

The last step to complete our authentication system is to set up the client functionality to sign up, log in and log out of the user's account. For this, we will go into our pages/_app.js file and we will update its content with the following code:

pages/_app.js
import { SessionProvider } from "next-auth/react";
 
export default function MyApp({ Component, pageProps: { session, ...pageProps } }) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

Once our pages/_app.js file is complete, we will update our pages/index.js file by adding to it the authentication buttons to the user interface (UI) of the page. For this, we will update the file content with the following code:

pages/index.js
import { Fragment } from "react";
import { useSession, signIn, signOut } from "next-auth/react";
 
export default function Home() {
  const { data: session } = useSession();
 
  return (
    <Fragment>
      <h1> Welcome to Next.js!</h1>
 
      {session ? (
        <Fragment>
          <span>Signed in as {session.user.email}</span>
          <button onClick={() => signOut()}>Sign out</button>
        </Fragment>
      ) : (
        <Fragment>
          <span>You are not signed in </span>
          <button onClick={() => signIn()}>Sign in</button>
        </Fragment>
      )}
    </Fragment>
  )
}

If we run our project with the command npm run dev, we would get an error message from our console with the text [next-auth][warn][NO_SECRET]. For this, we will create a unique secret for our project with the command openssl rand -base64 32, and then we will add this secret into our .env file by adding the following code:

NEXTAUTH_SECRET="<secret>"
NEXTAUTH_URL="<url>"

If we are using our project through our development environment, we should replace <url> with http://localhost:3000. On the other hand, if our project is deployed in a production environment, we should replace <url> for our domain URL.

Conclusion

In this tutorial, we learned how to implement a simple email authentication system using Next.js, NextAuth, Prisma, and MongoDB. In the process, we learned the basics of Prisma, its data model system, and how to connect it with our client functionality. We also learned how to use NextAuth with Next.js and how to set up new authentication providers like Facebook, Twitter, and much more.