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

Forward Email Provider

Overview

The Forward Email 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 Forward Email 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 Forward Email provider can be used with both BasicAuth 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. First, you’ll need to add your domain to your Forward Email account. This is required by Forward Email and this domain of the address you use in the from provider option.

  2. Next, you will have to generate an API key in the My Account → Security. You can save this API key as the AUTH_FORWARDEMAIL_KEY environment variable.

AUTH_FORWARDEMAIL_KEY=abc

If you name your environment variable AUTH_FORWARDEMAIL_KEY, the provider will pick it up automatically and your Auth.js configuration object can be simpler. If you’d like to rename it to something else, however, you’ll have to manually pass it into the provider in your Auth.js configuration.

./auth.ts
import NextAuth from "next-auth"
import ForwardEmail from "next-auth/providers/forwardemail"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: ...,
  providers: [
    ForwardEmail({
      // If your environment variable is named differently than default
      apiKey: AUTH_FORWARDEMAIL_KEY,
      from: "no-reply@company.com"
    }),
  ],
})
  1. Do not forget to setup one of the database adapters for storing the Email verification token.

  2. You can now start the sign-in 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 click the link in magic link email and use up the verification token.

Customization

Email Body

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

./auth.ts
import NextAuth from "next-auth"
import ForwardEmail from "next-auth/providers/forwardemail"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    ForwardEmail({
      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 (fetch()) to Forward Email to actually do the sending here in this method.

./lib/authSendRequest.ts
export async function sendVerificationRequest(params) {
  const { identifier: to, provider, url, theme } = params
  const { host } = new URL(url)
  const res = await fetch("https://api.forwardemail.net/v1/emails", {
    method: "POST",
    headers: {
      Authorization: `Basic ${btoa(provider.apiKey + ":")}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: provider.from,
      to,
      subject: `Sign in to ${host}`,
      html: html({ url, host, theme }),
      text: text({ url, host }),
    }),
  })
 
  if (!res.ok)
    throw new Error("Forward Email error: " + JSON.stringify(await res.json()))
}
 
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 ForwardEmail from "next-auth/providers/forwardemail"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    ForwardEmail({
      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 ForwardEmail provider. The following example shows the default behavior:

./auth.ts
import NextAuth from "next-auth"
import ForwardEmail from "next-auth/providers/forwardemail"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    ForwardEmail({
      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