Next.js Email Authentication using NextAuth

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.

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 pg to install NextAuth and the PostgreSQL library.

Then add to your .env file:

USER_DATABASE_URL=<enter URL of the postgresql:// database>
EMAIL_SERVER=smtp://user:pass@smtp.mailtrap.io:465
EMAIL_FROM=Your name <you@email.com>
NEXTAUTH_URL=http://localhost:3000
SECRET=

Make sure you add a SECRET code. You can use https://generate-secret.vercel.app/32 to generate it.

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 Providers from 'next-auth/providers'

export default NextAuth({
  providers: [
    Providers.Email({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM,
    }),
  ],

  database: process.env.USER_DATABASE_URL,
  secret: process.env.SECRET,

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

  jwt: {
    secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnX', //use a random secret token here
    encryption: true,
  },

  debug: true,
})

Now it depends a 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

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

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

export default NextAuth({
  providers: [
    Providers.Email({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM
    })
  ],

  database: process.env.USER_DATABASE_URL,
  secret: process.env.SECRET,

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

  jwt: {
    secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnX', //use a random secret token here
    encryption: true
  },

  debug: true,
  adapter: PrismaAdapter(prisma)
})

You need to add 3 models to your schema.prisma:

model VerificationRequest {
  id         String   @id @default(cuid())
  identifier String
  token      String   @unique
  expires    DateTime
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt

  @@unique([identifier, token])
}

model Account {
  id                 String    @id @default(cuid())
  providerType       String
  providerId         String
  providerAccountId  String
  refreshToken       String?
  accessToken        String?
  accessTokenExpires DateTime?
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt
  user               User      @relation(fields: [userId], references: [id])
  userId             Int
  @@unique([providerId, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  expires      DateTime
  sessionToken String   @unique
  accessToken  String   @unique
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  user         User     @relation(fields: [userId], references: [id])
  userId       Int
}

and a User model:

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

In my app I already had a User model because I set that up before integrating authentication, so all I had to do was to make sure I had those fields in it:

email     String? @unique
emailVerified DateTime?
accounts      Account[]
sessions      Session[]

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 { Provider } from 'next-auth/client'

And wrap your <Component /> call:

return <Component {...pageProps} key={router.asPath} />

with it:

return (
<Provider session={pageProps.session}>
  <Component {...pageProps} key={router.asPath} />
</Provider>
)

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 } from 'next-auth/client'

Then you use that to gather information on the state. loading is true when the session info is still loading.

const [session, loading] = 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' && 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 { getSession } from 'next-auth/client'

then

const session = await getSession({ req })

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

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.

Download my free Next.js Handbook!

⭐️ Join the waiting list for the JavaScript Masterclass ⭐️