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
To follow this post well, one must have basic knowledge of NextJs. Typescript is used for the code. There is TailwindCSS for the styling, but you should be able to follow along.
This article is a follow-up to my previous blog about setting up NextAuth with Google authentication: https://lemreyes.hashnode.dev/signing-in-with-google-using-nextauth.
You can follow through the installation and coding using this Github repository from: https://github.com/lemreyes/next-auth-demo/tree/1bc95c5b5664bc509a54c976ecaf3b7443ecf92c. Be sure to use the version from 1bc95c5.
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
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"
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