How to Set Up Email Sign-In Using NextAuth and Mailjet

NextAuth (or Auth.js) provides email as an authentication method. You can use email to send magic links to the user's email address for them to sign in.

This article is a step-by-step guide to setup email authentication in NextJs using NextAuth.

Pre-requisites

Installing Nodemailer

We will be using SMTP configuration for our setup. To send email via SMPT we will be using Nodemailer. NextJS does not have nodemailer as a default dependency so we need to install it manually.

npm install nodemailer

Configuring EmailProvider in NextAuth

Add the EmailProvider option settings in your options.ts file.

import type { NextAuthOptions } from "next-auth";

import GoogleProvider from "next-auth/providers/google";
import EmailProvider from "next-auth/providers/email";

export const options: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID as string,
      clientSecret: process.env.GOOGLE_SECRET as string,
    }),
    EmailProvider({
      server: {
        host: process.env.EMAIL_SERVER_HOST,
        port: process.env.EMAIL_SERVER_PORT,
        auth: {
          user: process.env.EMAIL_SERVER_USER,
          pass: process.env.EMAIL_SERVER_PASSWORD,
        },
      },
      from: process.env.EMAIL_FROM,
    }),
  ],
};

We need to update the .env file for the definitions of:

  • EMAIL_SERVER_HOST

  • EMAIL_SERVER_PORT

  • EMAIL_SERVER_USER

  • EMAIL_SERVER_PASSWORD

  • EMAIL_FROM

But first we need to setup the SMTP server and get the exact settings from the server to update our .env file.

Creating a Mailjet Account

We will be using a third-party service for our SMTP server. Nodemailer works with many email providers that provide SMTP service. Here is a list of well known services provided in the nextAuth documentation site: https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/

For this tutorial we will be using Mailjet's free tier service. Go to https://www.mailjet.com/ to create an account.

Mailjet SMTP Settings

Once you have created a Mailjet account we will retrieve the SMTP settings that we need for the .env file.

Mailjet has a post on how to get their SMTP settings: https://documentation.mailjet.com/hc/en-us/articles/360043229473-How-can-I-configure-my-SMTP-parameters

In Mailjet, go to Account settings → SMTP and SEND API settings.

  • EMAIL_FROM - this is the email address you used to create the account.

  • EMAIL_SERVER_HOST - this is the SMTP server field in the screenshot. It is "in-v3.mailjet.com" in this case.

  • EMAIL_SERVER_PORT is the number in the port field. We can use the 587 port number here.

  • EMAIL_SERVER_USER - this is the Primary API key.

  • EMAIL_SERVER_PASSWORD - this is the Secret Key. You need to generate the secret key in Mailjet from this link: https://app.mailjet.com/account/apikeys

  • Screenshot showing the Primary API key and the Secret Key

Database Adapters

From NextAuth's documentation, we need to set up a database adapter. For this tutorial we will be using Prisma ORM with SQLite database.

Installing Sqlite

We will be using SQLite in this tutorial for the sake of simplicity. The database is just a file on your project! On the terminal, go to the projects directory and type the command below:

npm install sqlite3

Installing Prisma

Install Prisma with the Prisma Client:

npm install @prisma/client @auth/prisma-adapter
npm install prisma --save-dev

Initialize Prisma with an SQLite provider:

npx prisma init --datasource-provider sqlite

A "prisma" folder will be created at the root of your project directory.

It should have a "schema.prisma" file. The contents on this file would be below:

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

The .env file should have the DATABASE_URL which contains the file path to your database file.

DATABASE_URL="file:./dev.db"
💡
The database file is not automatically created. Create a new file "dev.db" in the "prisma" folder.

Creating the Schema

In the "schema.prisma" file, add the models needed for NextAuth.

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id            String          @id @default(cuid())
  name          String?
  email         String?         @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  // Optional for WebAuthn support
  Authenticator Authenticator[]

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model VerificationToken {
  identifier String
  token      String
  expires    DateTime

  @@unique([identifier, token])
}

// Optional for WebAuthn support
model Authenticator {
  credentialID         String  @unique
  userId               String
  providerAccountId    String
  credentialPublicKey  String
  counter              Int
  credentialDeviceType String
  credentialBackedUp   Boolean
  transports           String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@id([userId, credentialID])
}

Applying The Schema

On the terminal type:

npm exec prisma migrate dev

The schema should be reflected on to your dev.db file.

TIP: You can view the contents of your dev.db file by using DB Browser for SQLite. https://sqlitebrowser.org/

Adding The Prisma Adapter to The Options File

Before we add the Prisma adapter to the Options.ts file, lets create a utility function first to initialize the Prisma client with a singleton pattern. The singleton pattern is needed so that only one Prisma client instance will be generated over the lifetime of your application. Otherwise your program could crash as it runs out of memory.

Create a "Utilities" folder in \src\app. Create a "prismaUtil.ts" file with the contents below:

import { PrismaClient } from "@prisma/client";

const prismaClientSingleton = () => {
  return new PrismaClient();
};

type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClientSingleton | undefined;
};

const prisma = globalForPrisma.prisma ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

In the "options.ts" file, add the prisma adapter configuration:

import { PrismaAdapter } from "@auth/prisma-adapter";
import prisma from "@/app/Utilities/prismaUtil";

export const options: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
      ...
  ],
};

Don't forget to import the PrismaAdapter and also the instance of prisma client from the utility function.

Adding The "Signin With Email" Button

Let's add the "Signin With Email" button in the LoginButton component. This will add an text field for the email address and a button to sign in.

This is the contents of your LoginButton.tsx:

"use client";
import { useSession, signIn, signOut } from "next-auth/react";
import { ChangeEvent, useState } from "react";

export default function LoginButton() {
  const { data: session } = useSession();
  const [email, setEmail] = useState("");

  const hdlEmailInputOnChange = (event: ChangeEvent<HTMLInputElement>) => {
    setEmail(event.target.value);
  };

  if (session) {
    return (
      <>
        Signed in as {session.user?.email} <br />
        <button
          onClick={() => signOut()}
          className="bg-gray-400 px-4 py-2 rounded-lg"
        >
          Sign out
        </button>
      </>
    );
  } else {
    return (
      <div>
        <p>Not signed in.</p>
        <button
          onClick={() => signIn("google", { callbackUrl: "/" })}
          className="bg-gray-400 px-4 py-2 rounded-lg"
        >
          Sign in to Google
        </button>
        <p>or</p>
        <label htmlFor="email">Email: </label>
        <input type="text" id="email" onChange={hdlEmailInputOnChange} className="border border-black mr-2"></input>
        <button
          className="bg-gray-400 px-4 py-2 rounded-lg"
          onClick={() => signIn("email", { email, callbackUrl: "/" })}
        >
          Login with Email
        </button>
      </div>
    );
  }
}

Testing the Signin with Email

We are done with all the coding. It is now time to test the application.

In your terminal type:

npm run dev

In the browser go to localhost:3000. It should display the login button component with the text field for the email and the signin button.

Type in a valid email address and click on the "Login with Email" button.

A confirmation screen should appear if email was sent successfully.

Check your email inbox (check also the spam/junk folder), you should receive an email with a magic link.

Clicking on the "Sign in" button would sign you in to the web app successfully!

Caveat

The current setup will always successfully send an email even if an email address is not from a registered user. You will have to configure the Signin more as described in the NextAuth documentation: https://next-auth.js.org/providers/email#sending-magic-links-to-existing-users

Wrap-up

We have just learned to signin using email using the NextAuth library. We've also learned how to integrate Mailjet as our SMTP server, and using Prisma with SQLite as our data adapter. The knowledge learned here should be enough to implement email authentication in your projects.

To view the source code please go to: https://github.com/lemreyes/next-auth-demo