Next.js Email Authentication using NextAuth

By

Learn how to add email-based authentication to Next.js with NextAuth.js, from configuring an SMTP email provider and JWT sessions to the useSession hook.

~~~

Managing authentication in Next.js can be done in many different ways.

In my site I chose to implement email-based authentication with JWT tokens via NextAuth.js and here’s how I did it.

The code in this post targets NextAuth.js v4 (next-auth@4, the current latest on npm) with the Pages Router (pages/api/auth/[...nextauth].js). The next major, v5, was renamed Auth.js and is still in beta (next-auth@beta). It has different imports and a different setup, documented at authjs.dev, so don’t mix the two.

An external database is needed. You can use a local database, or a cloud one. I chose PostgreSQL but you can use anything you want.

I suppose you already have a Next.js website up.

Run npm install next-auth@4 nodemailer pg to install NextAuth v4, Nodemailer (the Email provider uses it to send the login link, and NextAuth does not bundle it) and the PostgreSQL library.

Then add to your .env file:

DATABASE_URL=<enter URL of the postgresql:// database>
EMAIL_SERVER=smtp://user:[email protected]:465
EMAIL_FROM=Your name <[email protected]>
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=

Make sure you add a NEXTAUTH_SECRET value. You can use https://generate-secret.vercel.app/32 to generate it. NextAuth uses it to sign the JWT (older versions of this post called the variable SECRET, v4 reads NEXTAUTH_SECRET).

I use https://mailtrap.io to test the emails, it’s quite handy while you are setting things up.

Create a pages/api/auth/[...nextauth].js file with this content:

import NextAuth from 'next-auth'
import EmailProvider from 'next-auth/providers/email'

export const authOptions = {
  providers: [
    EmailProvider({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM,
    }),
  ],

  secret: process.env.NEXTAUTH_SECRET,

  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },

  debug: true,
}

export default NextAuth(authOptions)

This is not complete yet. The Email provider saves the login tokens it emails out in the database, and in v4 you connect the database through an adapter. The database: process.env.DATABASE_URL option that older versions accepted is gone.

Which adapter depends a lot on your data access layer. If you use the Prisma ORM, also install @next-auth/prisma-adapter with

npm install @next-auth/prisma-adapter @prisma/client@7
npm install -D prisma@7

I pin Prisma to the 7 line because the latest tag of the prisma package on npm currently points at an 8.0 release candidate.

and include it in [...nextauth].js:

import NextAuth from 'next-auth'
import EmailProvider from 'next-auth/providers/email'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import prisma from 'lib/prisma'

export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    EmailProvider({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM,
    }),
  ],

  secret: process.env.NEXTAUTH_SECRET,

  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },

  debug: true,
}

export default NextAuth(authOptions)

You need to add these models to your schema.prisma (v4 naming: note VerificationToken, not the old VerificationRequest):

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

  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)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

Remember to run npx prisma migrate dev any time you modify the schema to apply the changes to the database.

Now open pages/_app.js and add

import { SessionProvider } from 'next-auth/react'

And wrap your <Component /> call:

return <Component {...pageProps} />

with it:

return (
<SessionProvider session={pageProps.session}>
  <Component {...pageProps} />
</SessionProvider>
)

Now add into your app a link that points to /api/auth/signin. This will be the login form.

Finally, in pages you want to require a logged in session active, you first import the useSession hook:

import { useSession, signOut } from 'next-auth/react'

Then you use that to gather information on the state. status === 'loading' means the session info is still loading.

const { data: session, status } = useSession()

We can use this session object to print information on screen when the user is logged in:

{session && (
  <p>
    {session.user.email}{' '}
    <button
      className="underline"
      onClick={() => {
        signOut()
        router.push('/')
      }}
    >
      logout
    </button>
  </p>
)}

We can also use that information to not return anything unless we finished loading, and unless the session is established:

if (typeof window !== 'undefined' && status === 'loading') return null

if (typeof window !== 'undefined' && !session) {
  router.push('/api/auth/signin')
}

if (!session) { //for server-side rendering
  return null
}

I send the browser to /api/auth/signin if the user is not logged in. You can also create a custom form if you want, but those are the basics.

Server-side, you use

import { getServerSession } from 'next-auth/next'
import { authOptions } from './api/auth/[...nextauth]'

authOptions is the object we exported from [...nextauth].js, that’s why it was exported separately. Then:

const session = await getServerSession(req, res, authOptions)

to get the session data, either in an API route or inside getServerSideProps({ req, res }).

That’s how I use NextAuth for a very basic authentication setup.

The NextAuth package is very complete and provides tons of options and customizations, check them out on https://next-auth.js.org.

Tagged: Next.js · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about next: