How to build Authentication with Next.js, NextAuth, and Prisma

How to build Authentication with Next.js, NextAuth, and Prisma

We're going to run through one of our preferred ways to handle auth in Next.js apps!

There are so many ways to handle auth these days. You have full backend as a service tools like Supabase and Firebase offering auth solutions, independent providers like Clerk, Stytch, and Auth0, and you have plenty of options to roll your own.

Regardless of what solution you may want to use, implementing both authentication and authorization can have it's own set of opinions. That's where NextAuth comes in.

NextAuth (soon to be Auth.js!) has rapidly become the best way to handle auth when it comes to building Next.js apps. You get support for providers of your choice, access to email + password or magic link, and adapters to work with existing auth solutions in the market.

Let's run through how to complement this with the use of Prisma, tRPC, Zod!

Set up Next.js

Let's set up our Next.js scaffolding with Typescript and open our app

npx create-next-app@latest --ts next-auth-app
cd next-auth-app

Set up Prisma

Next, let's set up Prisma and our schema.

npm install prisma
npx primsa init

Update schema.prisma with

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model User {
  id        Int      @id @default(autoincrement())
  username  String   @unique
  email     String   @unique
  password  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Run a migration with

npx prisma migrate dev --migration-name init

Let's add a /server folder to and add a server/db.ts file

import { PrismaClient } from "@prisma/client";

export const prisma = new PrismaClient();

Set up tRPC

Let's bring everything we need for tRPC, including React Query and Zod.

npm install @trpc/client @trpc/server @trpc/react @trpc/next zod react-query

We'll create our tRPC context with a src/server folder and

To our Next.js api folder, we'll add a api/trpc folder, where we'll add [trpc].ts

Set up NextAuth

Let's add NextAuth, AKA Auth.js, to our project:

npm install next-auth

Next, let's set up our API files:

We'll first set up pages/api/auth/[...nextauth].js

import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
    // ...add more providers here
  ],
}

export default NextAuth(authOptions)

While usually you'd start with Email and Password, you'll find that Auth.js doesn't push this for security consideration. Instead, we'll set up a GitHub provider

GitHub OAuth

For the OAuth providers like GitHub, you will need a clientId and a clientSecret. We can get these by building a new OAuth app at Github.

  1. Sign in to your GitHub account

  2. Go to "Settings"

  3. Go to "Developer Settings"

  4. Find it in "OAuth Apps"

GitHub OAuth apps

Session Control

In our _app.tsx file, which gives us control over our Next.js pages, we'll set up our Session Provider

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

export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

Then, in any one of our files, we can access the session

import { useSession, signIn, signOut } from "next-auth/react"
export default function Component() {
  const { data: session } = useSession()
  if (session) {
    return (
      <>
        Signed in as {session.user.email} <br />
        <button onClick={() => signOut()}>Sign out</button>
      </>
    )
  }
  return (
    <>
      Not signed in <br />
      <button onClick={() => signIn()}>Sign in</button>
    </>
  )

And if we need to protect an API route specifically, we can access the session as follows

import { getServerSession } from "next-auth/next"
import { authOptions } from "./auth/[...nextauth]"
export default async (req, res) => {
  const session = await getServerSession(req, res, authOptions)
  if (session) {
    res.send({
      content:
        "This is protected content. You can access this content because you are signed in.",
    })
  } else {
    res.send({
      error: "You must be signed in to view the protected content on this page.",
    })
  }
}

Deploy

We need to set the environment variable for our product domain NEXTAUTH_URL=https://example.com

In Vercel, you can add this to your environment variables and then using the CLI, you can access it with vercel env pull