Passwordless Authentication with Next.js, Prisma, and next-auth
In this post, you'll learn how to add passwordless authentication to your Next.js app using Prisma and next-auth. By the end of this tutorial, your users will be able to log in to your app with either their GitHub account or a Slack-styled magic link sent right to their Email inbox.
If you want to follow along, clone this repo and switch to the start-here
branch! 😃
If you want to see the live coding version of this tutorial, check out the recording below! 👇
Step 0: Dependencies and database setup
Before we start, let's install Prisma and next-auth
into the Next.js project.
npm i next-auth
npm i -D @prisma/cli @types/next-auth
I'm using TypeScript in this tutorial, so I'll also install the type definitions for next-auth
You will also need a PostgreSQL database to store all the user data and active tokens.
If you don't have access to a database yet, Heroku allows us to host PostgreSQL databases for free, super handy! You can check out this post by Nikolas Burk to see how to set it up.
If you are a Docker fan and would rather keep everything during development local, you can also check out this video I did on how to do this with Docker Compose.
Before moving on to the next step, make sure you have a PostgreSQL URI in this format:
postgresql://<USER>:<PASSWORD>@<HOST_NAME>:<PORT>/<DB_NAME>
Step 1: Initialize Prisma
Awesome! Let's generate a starter Prisma schema and a @prisma/client
module into the project.
npx prisma init
Notice that a new directory prisma
is created under your project. This is where all the database magic happens. 🧙♂️
Now, replace the dummy database URI in /prisma/.env
with your own.
Step 2: Define database schema for authentication
next-auth
requires us to have specific tables in our database for it to work seamlessly. In our project, the schema file is located at /prisma/schema.prisma
.
Let's use the default schema for now, but know that you can always extend or customize the data models yourself.
Note: If you have an existing database, after replacing the dummy database URI, you can run
npx prisma introspect
to generate theschema.prisma
for your database and work from there. Then, you should add the following data models to the generatedschema.prisma
file.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Account {
id Int @default(autoincrement()) @id
compoundId String @unique @map(name: "compound_id")
userId Int @map(name: "user_id")
providerType String @map(name: "provider_type")
providerId String @map(name: "provider_id")
providerAccountId String @map(name: "provider_account_id")
refreshToken String? @map(name: "refresh_token")
accessToken String? @map(name: "access_token")
accessTokenExpires DateTime? @map(name: "access_token_expires")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@index([providerAccountId], name: "providerAccountId")
@@index([providerId], name: "providerId")
@@index([userId], name: "userId")
@@map(name: "accounts")
}
model Session {
id Int @default(autoincrement()) @id
userId Int @map(name: "user_id")
expires DateTime
sessionToken String @unique @map(name: "session_token")
accessToken String @unique @map(name: "access_token")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "sessions")
}
model User {
id Int @default(autoincrement()) @id
name String?
email String? @unique
emailVerified DateTime? @map(name: "email_verified")
image String?
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "users")
}
model VerificationRequest {
id Int @default(autoincrement()) @id
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "verification_requests")
}
Let's break it down a bit:
In the schema file, we defined 4 data models - Account
, Session
, User
and VerificationRequest
. The User
and Account
models are for storing user information, the Session
model is for managing active sessions of the user, and VerificationRequest
is for storing valid tokens that are generated for magic link Email sign in.
The @map
attribute is for mapping the Prisma field name to a database column name, such as compoundId
to compound_id
, which is what next-auth
needs to have it working.
snake_case is often used as a naming convention in database environments, but camelCase is how we usually name things in JavaScript and TypeScript. It's perfectly fine to name Prisma fields in snake_case, but it wouldn't look so nice. :)
Next, let's run these commands to populate the database with the tables we need.
npx prisma migrate save --experimental
npx prisma migrate up --experimental
Then, run this command to generate a Prisma client tailored to the database schema.
npx prisma generate
Now, if you open up Prisma Studio with the following command, you will be able to inspect all the tables that we just created in the database.
npx prisma studio
Step 3: Configure next-auth
Before we start configuring next-auth
, let's create another .env
file in the project root to store secrets that will be used by next-auth
(or rename the .env.example
file from the template, if you cloned the tutorial repo).
SECRET=RAMDOM_STRING
SMTP_HOST=YOUR_SMTP_HOST
SMTP_PORT=YOUR_SMTP_PORT
SMTP_USER=YOUR_SMTP_USERNAME
SMTP_PASSWORD=YOUR_SMTP_PASSWORD
SMTP_FROM=YOUR_REPLY_TO_EMAIL_ADDRESS
GITHUB_SECRET=YOUR_GITHUB_API_CLIENT_SECRET
GITHUB_ID=YOUR_GITHUB_API_CLIENT_ID
DATABASE_URL=postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public
Now, let's create a new file at /pages/api/auth/[...nextauth].ts
as a "catch-all" Next.js API route for all the requests sent to your-app-url-root/api/auth
(like localhost:3000/api/auth
).
Inside the file, first import the essential modules from next-auth
, and define an API handler which passes the request to the NextAuth
function, which sends back a response that can either be an entirely generated login form page or a callback redirect. To connect next-auth
to the database with Prisma, you will also need to import PrismaClient
and initialize a Prisma Client instance.
import { NextApiHandler } from "next";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import Adapters from "next-auth/adapters";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// we will define `options` up next
const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default authHandler;
Now let's create the options
object. Here, you can choose from a wide variety of builtin authentication providers. In this tutorial, we will use GitHub OAuth and "magic links" Email to authenticate the visitors.
Step 3.1: Set up GitHub OAuth
For the builtin OAuth providers like GitHub, you will need a clientId
and a clientSecret
, both of which can be obtained by registering a new OAuth app at Github.
First, log into your GitHub account, go to Settings, then navigate to Developer Settings, then switch to OAuth Apps.
Clicking on the Register a new application button will redirect you to a registration form to fill out some information for your app. The Authorization callback URL should be the Next.js /api/auth
route that we defined earlier (http://localhost:3000/api/auth
).
An important thing to note here is that the Authorization callback URL field only supports 1 URL, unlike Auth0, which allows you to add additional callback URLs separated with a comma. This means if you want to deploy your app later with a production URL, you will need to set up a new GitHub OAuth app.
Click on the Register Application button, and then you will be able to find your newly generated Client ID and Client Secret. Copy this info into your .env
file in the root directory.
Now, let's go back to /api/auth/[...nextauth].ts
and create a new object called options
, and source the GitHub OAuth credentials like below.
const options = {
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
};
OAuth providers typically work the same way, so if your choice is supported by next-auth
, you can configure it the same way as we did with GitHub here. If there is no builtin support, you can still define a custom provider.
Step 3.2: Set up passwordless Email authentication
To allow users to authenticate with magic link Emails, you will need to have access to an SMTP server. These kinds of Emails are considered transactional Emails. If you don't have your own SMTP server or your mail provider has strict restrictions regarding outgoing Emails, I would recommend using SendGrid, or alternatively Amazon SES, Mailgun and others.
When you have your SMTP credentials ready, you can put that information into the .env
file, add a Providers.Email({})
to the list of providers, and source the environment variables like below.
const options = {
providers: [
// Providers.GitHub ...
Providers.Email({
server: {
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
},
from: process.env.SMTP_FROM, // The "from" address that you want to use
}),
],
};
Step 3.3: Link up Prisma
The final step for setting up next-auth
is to tell it to use Prisma to talk to the database. For this, we will use the Prisma adapter and add it to the options
object. We will also need a secret key to sign and encrypt tokens and cookies for next-auth
to work securely - this secret should also be sourced from environment variables.
const options = {
providers: [
// ...
],
adapter: Adapters.Prisma.Adapter({ prisma }),
secret: process.env.SECRET,
};
To summarize, your pages/api/auth/[...nextauth].ts
should look like the following:
import { NextApiHandler } from "next";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import Adapters from "next-auth/adapters";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default authHandler;
const options = {
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Providers.Email({
server: {
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
},
from: process.env.SMTP_FROM,
}),
],
adapter: Adapters.Prisma.Adapter({
prisma,
}),
secret: process.env.SECRET,
};
Step 4: Implement authentication on the frontend
In the application, you can use next-auth
to check if a visitor has cookies/tokens corresponding to a valid session. If no session can be found, then it means the user is not logged in.
With next-auth
, you have 2 options for checking the sessions - it can be done inside a React component using the useSession()
hook, or on the backend (getServerSideProps
or in API routes) with the helper function getSession()
.
Let's have a look at how it works.
Step 4.1: Checking user sessions with the useSession()
hook
In order to use the hook, you'll need to wrap the component inside a next-auth
provider. For the authentication flow to work anywhere in your entire Next.js app, create a new file called /pages/_app.tsx
.
import { Provider } from "next-auth/client";
import { AppProps } from "next/app";
const App = ({ Component, pageProps }: AppProps) => {
return (
<Provider session={pageProps.session}>
<Component {...pageProps} />
</Provider>
);
};
export default App;
Now, you can go to /pages/index.tsx
, and import the useSession
hook from the next-auth/client
module. You will also need the signIn
and signOut
functions to implement the authentication interaction. ThesignIn
function will redirect users to a login form, which is automatically generated by next-auth
.
import { signIn, signOut, useSession } from "next-auth/client";
The useSession()
hook returns an array with the first element being the user session, and the second one a boolean indicating the loading status.
// ...
const IndexPage = () => {
const [session, loading] = useSession();
if (loading) {
return <div>Loading...</div>;
}
};
If the session
object is null
, it means the user is not logged in. Additionally, we can obtain the user information from session.user
.
// ...
if (session) {
return (
<div>
Hello, {session.user.email ?? session.user.name} <br />
<button onClick={() => signOut()}>Sign out</button>
</div>
);
} else {
return (
<div>
You are not logged in! <br />
<button onClick={() => signIn()}>Sign in</button>
</div>
);
}
The finished /pages/index.tsx
file should look like the following.
import { signIn, signOut, useSession } from "next-auth/client";
const IndexPage = () => {
const [session, loading] = useSession();
if (loading) {
return <div>Loading...</div>;
}
if (session) {
return (
<div>
Hello, {session.user.email ?? session.user.name} <br />
<button onClick={() => signOut()}>Sign out</button>
</div>
);
} else {
return (
<div>
You are not logged in! <br />
<button onClick={() => signIn()}>Sign in</button>
</div>
);
}
};
export default IndexPage;
Now, you can spin up the Next.js dev server with npm run dev
, and play with the authentication flow!
Step 4.2: Checking user sessions with getSession()
on the backend
To get user sessions from the backend code, inside either getServerSideProps()
or an API request handler, you will need to use the getSession()
async function.
Let's create a new /pages/api/secret.ts
file for now like below. The same principles from the frontend apply here - if the user doesn't have a valid session, then it means they are not logged in, in which case we will return a message with a 403 status code.
import { NextApiHandler } from "next";
import { getSession } from "next-auth/client";
const secretHandler: NextApiHandler = async (req, res) => {
const session = await getSession({ req });
if (session) {
res.end(
`Welcome to the VIP club, ${session.user.name || session.user.email}!`
);
} else {
res.statusCode = 403;
res.end("Hold on, you're not allowed in here!");
}
};
export default secretHandler;
Go visit localhost:3000/api/secret
without logging in, and you will see something like in the following image.
Conclusion
And that's it, authentication is so much easier with next-auth
!
I hope you have enjoyed this tutorial and have learned something useful! You can always find the starter code and the completed project in this GitHub repo.
Also, check out the Awesome Prisma list for more tutorials and starter projects in the Prisma ecosystem!
Top comments (11)
I started to use prisma and seems good, thanks for this post! 💪🏻
You are welcome, hope you found this tutorial useful! 👍
Have you joined the Prisma Slack channel? If you encountered any problem with Prisma, feel free to post it there!
I didn’t know this channel, thank you very much 👍🏻
Hey, I know this tutorial already has 3 years, probalby requiere an update since next-auth, change table names of how they store the session.
In another topic, I don't understand why is requiere to add an OAuth provider on Step 3.1. Can you explain why is requiere if is this solution is "passwordless" ?
Hi, I wanna curios something. Can we use Prisma and UpstashRedis adapter at the same time in NextJs? I'm using Prisma Adapter and also user sessions in redis but I cannot do that in NextJs. Do you have any idea or comment for that ?
Thanks :)
how would I add new columns to the user model? the offical documentation is not very helpfule.
i want to extend the users schema with user roles for authz
Hi Gary,
While it's still experimental, and will be updated in the coming weeks, you can use Prisma migrate to do that.
Update your schema with the columns you need, run migrate (or db push if you don't need to keep and share the history of your schema migrations)
The docs about that are here: prisma.io/docs/reference/tools-and...
Hope it helps!
Great post thank you very much.
I am struggling with integrating this into my cypress tests. Do you have a post for this? If not can I suggest one please.
Awesome tutorial. Any way you can expand and do an example with email/password combination? Thanks
Thank you for very nice tutorial!
I believe the DATABASE_URL no longer needs to be recorded into root .env file since Prisma takes it from its own .env
So, uh.. doesn't this allow anyone with an email address to log into the app? Where is the user/email whitelist?