Testing Next Auth email magic links with Playwright and MailSlurp

Next Auth email login tutorial and end-to-end testing guide.

  • Table of contents

NextAuth testing

Next Auth is a popular package for adding authentication and login to NextJS apps. It supports a range of authentication providers including email magic links. This post shows you how to test Next Auth email magic links using Playwright and MailSlurp.

All code for this tutorial can be found on Github

Setup Next

To send email links we'll need to setup a Next project and add Next Auth using the email provider. First let's set up a Next project

npx create-next-app@latest

Next add the dependencies we need for Next Auth and Playwright:

npm install --save next next-auth nodemailer mailslurp-client

Email provider requires a database, let's use sequelize:

npm install --save sequelize sqlite3 @auth/sequelize-adapter

Configure the email provider

Inside pages/api/auth/[...nextauth].ts add the following:

import NextAuth, { NextAuthOptions } from "next-auth"
import EmailProvider from "next-auth/providers/email"
import SequelizeAdapter from "@auth/sequelize-adapter";
import {Sequelize} from "sequelize";
const sequelize = new Sequelize("sqlite::memory:");
const adapter= SequelizeAdapter(sequelize);
// create tables
sequelize.sync();

export const authOptions: NextAuthOptions = {
  providers: [
    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,
    }),
  ],
  adapter: adapter as any,
  callbacks: {
    async jwt({ token }) {
      token.userRole = "admin"
      return token
    },
  },
}

export default NextAuth(authOptions)

Configure the email server

To allow Next to send emails on our behalf we need an SMTP server such as MailSlurp.

  1. Create a free account and obtain an API key.
  2. Get your SMTP access details from the homepage.
  3. Create an inbox with SMTP enabled.

Next create a .env.local file in the root of your project and add the following:

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=add-random-secret-here
EMAIL_SERVER_HOST=mailslurp.mx
EMAIL_SERVER_PORT=2587
EMAIL_SERVER_USER=add-smtp-username-here
EMAIL_SERVER_PASSWORD=add-smtp-password-here
EMAIL_FROM=add-email-sending-address-here

Running the app

When we run npm run dev we are greeted by the standard next auth app:

Next Auth login

If we click on a restricted page we can see the access denied screen:

access denied

Let's write a test now to test the login functionality using MailSlurp to capture the magic link and Playwright to submit it to the app.

Setup testing

First install Playwright:

npm install --save-dev playwright

Then configure the playwright config to start our server before tests:

import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
  testDir: './tests',
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  /* Run your local dev server before starting the tests */
  webServer: {
    command: 'npm run start',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Now we can write a test that performs the following actions:

  1. Create a new MailSlurp inbox
  2. Load the app and try login
  3. Wait for the email to arrive
  4. Extract the magic link from the email
  5. Visit the magic link in the browser and see the restricted page

Here is the test:

import { test, expect } from '@playwright/test';
import { MailSlurp } from 'mailslurp-client';
import { writeFile } from 'fs/promises'
const apiKey = process.env.API_KEY ?? '';

test('can login using magic link', async ({ page }) => {
  expect(apiKey, "MailSlurp API_KEY env should be set").toBeTruthy()
  // create an inbox for sign up
  const mailslurp = new MailSlurp({ apiKey })
  const userInbox = await mailslurp.createInbox()

  await page.goto('http://localhost:3000/');

  // load the app and try access
  await page.goto('http://localhost:3000/protected/');
  await page.waitForSelector('[data-id="access-denied"]')

  // now login
  await page.click('[data-id="access-link"]')
  await page.waitForURL(/signin/);

  // try sign in with email
  await page.fill('#input-email-for-email-provider', userInbox.emailAddress)
  await page.click('#submitButton')
  await page.waitForURL(/verify-request/)

  // wait for the login link email to arrive
  const email = await mailslurp.waitForLatestEmail(userInbox.id)
  
  // use mailslurp to extract the link
  const { links: [loginLink] } = await mailslurp.emailController.getEmailLinks({
    emailId: email.id,
  })
  expect(loginLink).toContain('localhost');

  // now go to link
  await page.goto(loginLink)
  await page.waitForSelector('[data-id="access-permitted"]')
});

Running the test

When we run npx playwright test we see the browser load the sign in page and fill it with a MailSlurp email address:

load sign in

Once submitted the page shows the verify request page:

verify

Next Playwright waits for the email and clicks the extracted magic link. This then loads the restricted page:

access page

How does it work?

MailSlurp creates a real email address during the test. The test invokes the waitForLatestEmail method that waits until an email arrives. We then use the getEmailLinks method to return a list of every link found in the email. If we debug the test we can see the email received by MailSlurp:

verify

Conclusion

Next Auth email links are a great way to authenticate users. MailSlurp adds the ability to test your login end to end. It's free for personal use so check it out at mailslurp.com.