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:
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
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.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.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 4 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
}
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[]
}
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 } from 'next-auth/react'
Then you use that to gather information on the state. loading
is true when 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' && 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/react'
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.
→ I wrote 17 books to help you become a better developer, download them all at $0 cost by joining my newsletter
→ JOIN MY CODING BOOTCAMP, an amazing cohort course that will be a huge step up in your coding career - covering React, Next.js - next edition February 2025