Next.js Email Authentication using NextAuth
By Flavio Copes
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.
This guide uses NextAuth.js v4 (next-auth@4.24.15) with the Pages Router (pages/api/auth/[...nextauth].js). Auth.js / NextAuth v5 is still a different API surface, so stay on v4 for this setup.
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 (used by the Email provider), and the PostgreSQL library.
Then add to your .env file:
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
NEXTAUTH_SECRET=
Make sure you add a NEXTAUTH_SECRET value. You can use https://generate-secret.vercel.app/32 to generate it. In v4 this replaces the old plain SECRET env name for signing/encryption.
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)
The Email provider needs a database adapter to store verification tokens. The old v3 database: process.env.DATABASE_URL shortcut is gone in v4.
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 @prisma/client
npm install -D prisma
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 devany 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]'
Export your config as authOptions from [...nextauth].js if you want to reuse it, 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.
Want me to talk about your product? You can sponsor this site.
Related posts about next: