Skip to content
Migrating from NextAuth.js v4? Read our migration guide.

Nodemailer Provider

Overview

The Nodemailer provider uses email to send “magic links” that contain URLs with verification tokens can be used to sign in.

Adding support for signing in via email in addition to one or more OAuth services provides a way for users to sign in if they lose access to their OAuth account (e.g. if it is locked or deleted).

The Nodemailer provider can be used in conjunction with (or instead of) one or more OAuth providers.

How it works

On initial sign in, a Verification Token is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used within that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.

If someone provides the email address of an existing account when signing in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.

⚠️

The Nodemailer provider can be used with both JSON Web Token and database managed sessions, however you must configure a database to use it. It is not possible to enable email sign in without using a database.

Configuration

  1. Auth.js does not include nodemailer as a dependency, so you’ll need to install it yourself if you want to use the Nodemailer provider.
npm install nodemailer
  1. You will need an SMTP account; ideally for one of the services known to work with nodemailer. Nodemailer also works with other transports, however if you want to use an HTTP based email service, we recommend using one of the other Auth.js providers designed for those, like Resend or Sendgrid.

  2. There are two ways to configure the SMTP server connection.

You can either use a connection string or a nodemailer configuration object.

.env
EMAIL_SERVER=smtp://username:password@smtp.example.com:587
EMAIL_FROM=noreply@example.com
./auth.ts
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: ...,
  providers: [
    Nodemailer({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM,
    }),
  ],
})
  1. Do not forget to setup one of the database adapters for storing the Email verification token.

  2. You can now start the sign process with an email address at /api/auth/signin.

A user account (i.e. an entry in the Users table) will not be created for the user until the first time they verify their email address. If an email address is already associated with an account, the user will be signed in to that account when they use the link in the email.

Customization

Email Body

You can fully customize the sign in email that is sent by passing a custom function as the sendVerificationRequest option to Nodemailer().

./auth.ts
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Nodemailer({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM,
      sendVerificationRequest({
        identifier: email,
        url,
        provider: { server, from },
      }) {
        // your function
      },
    }),
  ],
})

As an example, the following shows the source for our built-in sendVerificationRequest() method. Notice that we’re rendering the HTML (html()) and making the network call (transport.sendMail()) to the email provider to actually do the sending here in this method.

import { createTransport } from "nodemailer"
 
export async function sendVerificationRequest(params) {
  const { identifier, url, provider, theme } = params
  const { host } = new URL(url)
  // NOTE: You are not required to use `nodemailer`, use whatever you want.
  const transport = createTransport(provider.server)
  const result = await transport.sendMail({
    to: identifier,
    from: provider.from,
    subject: `Sign in to ${host}`,
    text: text({ url, host }),
    html: html({ url, host, theme }),
  })
  const failed = result.rejected.concat(result.pending).filter(Boolean)
  if (failed.length) {
    throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
  }
}
 
function html(params: { url: string; host: string; theme: Theme }) {
  const { url, host, theme } = params
 
  const escapedHost = host.replace(/\./g, "​.")
 
  const brandColor = theme.brandColor || "#346df1"
  const color = {
    background: "#f9f9f9",
    text: "#444",
    mainBackground: "#fff",
    buttonBackground: brandColor,
    buttonBorder: brandColor,
    buttonText: theme.buttonText || "#fff",
  }
 
  return `
<body style="background: ${color.background};">
  <table width="100%" border="0" cellspacing="20" cellpadding="0"
    style="background: ${color.mainBackground}; max-width: 600px; margin: auto; border-radius: 10px;">
    <tr>
      <td align="center"
        style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
        Sign in to <strong>${escapedHost}</strong>
      </td>
    </tr>
    <tr>
      <td align="center" style="padding: 20px 0;">
        <table border="0" cellspacing="0" cellpadding="0">
          <tr>
            <td align="center" style="border-radius: 5px;" bgcolor="${color.buttonBackground}"><a href="${url}"
                target="_blank"
                style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${color.buttonText}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${color.buttonBorder}; display: inline-block; font-weight: bold;">Sign
                in</a></td>
          </tr>
        </table>
      </td>
    </tr>
    <tr>
      <td align="center"
        style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
        If you did not request this email you can safely ignore it.
      </td>
    </tr>
  </table>
</body>
`
}
 
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
function text({ url, host }: { url: string; host: string }) {
  return `Sign in to ${host}\n${url}\n\n`
}

If you want to generate great looking emails with React that are compatible with many email clients, check out mjml or react-email

Verification Tokens

By default, we are generating a random verification token. You can define a generateVerificationToken method in your provider options if you want to override it:

./auth.ts
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Nodemailer({
      async generateVerificationToken() {
        return crypto.randomUUID()
      },
    }),
  ],
})

Normalizing Email Addresses

By default, Auth.js will normalize the email address. It treats the address as case-insensitive (which is technically not compliant to the RFC 2821 spec, but in practice this causes more problems than it solves, i.e. when looking up users by e-mail from databases.) and also removes any secondary email address that may have been passed in as a comma-separated list. You can apply your own normalization via the normalizeIdentifier method on the Nodemailer provider. The following example shows the default behavior:

./auth.ts
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Nodemailer({
      normalizeIdentifier(identifier: string): string {
        // Get the first two elements only,
        // separated by `@` from user input.
        let [local, domain] = identifier.toLowerCase().trim().split("@")
        // The part before "@" can contain a ","
        // but we remove it on the domain part
        domain = domain.split(",")[0]
        return `${local}@${domain}`
 
        // You can also throw an error, which will redirect the user
        // to the sign-in page with error=EmailSignin in the URL
        // if (identifier.split("@").length > 2) {
        //   throw new Error("Only one email allowed")
        // }
      },
    }),
  ],
})
⚠️

Always make sure this returns a single e-mail address, even if multiple ones were passed in.

Auth.js © Balázs Orbán and Team - 2024