Skip to main content

Authentication

Auth.js (formerly NextAuth.js) is the standard authentication library for Next.js. Version 5 integrates deeply with the App Router, supporting OAuth providers, credentials, sessions, and middleware-based route protection.

Installation

pnpm add next-auth@beta

Generate an auth secret:

npx auth secret

This adds AUTH_SECRET to your .env.local file.

Basic Setup

Create auth.ts at the project root:

import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
  ],
});

Create the API route at app/api/auth/[...nextauth]/route.ts:

import { handlers } from "@/auth";

export const { GET, POST } = handlers;

Environment Variables

Add these to .env.local:

AUTH_SECRET="your-generated-secret"

# GitHub OAuth
AUTH_GITHUB_ID="your-github-client-id"
AUTH_GITHUB_SECRET="your-github-client-secret"

# Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"

Sign In and Sign Out

Server Component Buttons

import { signIn, signOut, auth } from "@/auth";

export default async function AuthButtons() {
  const session = await auth();

  if (session?.user) {
    return (
      <div>
        <p>Signed in as {session.user.name}</p>
        <form
          action={async ()=> {
            "use server";
            await signOut();
          }}
        >
          <button type="submit">Sign Out</button>
        </form>
      </div>
    );
  }

  return (
    <div>
      <form
        action={async ()=> {
          "use server";
          await signIn("github");
        }}
      >
        <button type="submit">Sign in with GitHub</button>
      </form>
      <form
        action={async ()=> {
          "use server";
          await signIn("google");
        }}
      >
        <button type="submit">Sign in with Google</button>
      </form>
    </div>
  );
}

Client Component Buttons

"use client";

import { signIn, signOut } from "next-auth/react";

export function SignInButton() {
  return <button onClick={()=> signIn("github")}>Sign in with GitHub</button>;
}

export function SignOutButton() {
  return <button onClick={()=> signOut()}>Sign Out</button>;
}

Reading the Session

In Server Components

import { auth } from "@/auth";

export default async function DashboardPage() {
  const session = await auth();

  if (!session) {
    return <p>Please sign in to view the dashboard.</p>;
  }

  return (
    <div>
      <h1>Welcome, {session.user?.name}</h1>
      <p>Email: {session.user?.email}</p>
      <img src={session.user?.image ?? ""} alt="Avatar" width={64} height={64} />
    </div>
  );
}

In Server Actions

"use server";

import { auth } from "@/auth";

export async function createPost(formData: FormData) {
  const session = await auth();
  if (!session?.user) {
    throw new Error("Unauthorized");
  }

  // ... create post with session.user info
}

In Route Handlers

import { auth } from "@/auth";
import { NextResponse } from "next/server";

export async function GET() {
  const session = await auth();
  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json({ user: session.user });
}

Protecting Routes with Middleware

The most efficient way to protect routes is with middleware. Create or update middleware.ts:

import { auth } from "@/auth";

export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard");
  const isOnAdmin = req.nextUrl.pathname.startsWith("/admin");

  if ((isOnDashboard || isOnAdmin) && !isLoggedIn) {
    return Response.redirect(new URL("/api/auth/signin", req.nextUrl));
  }
});

export const config = {
  matcher: ["/dashboard/:path*", "/admin/:path*"],
};

Database Sessions with Prisma

For persistent sessions stored in your database, add the Prisma adapter:

pnpm add @auth/prisma-adapter

Update auth.ts:

import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/app/lib/db";
import GitHub from "next-auth/providers/github";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [GitHub],
  session: { strategy: "database" },
});

Add the required models to schema.prisma:

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  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])
}

Run npx prisma migrate dev --name add-auth to apply the schema changes.

Extending the Session

Add custom fields to the session by using callbacks:

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
  callbacks: {
    async session({ session, user }) {
      // Add the user ID to the session
      session.user.id = user.id;
      return session;
    },
    async authorized({ auth, request }) {
      // Return true if the user is authenticated
      return !!auth?.user;
    },
  },
});

Summary

  • Auth.js v5 integrates natively with the Next.js App Router.
  • Configure providers (GitHub, Google, etc.) in a root auth.ts file.
  • Use auth() to read the session in Server Components, Actions, and Route Handlers.
  • Use middleware to protect entire route groups efficiently.
  • Add the Prisma adapter for database-backed sessions.
  • Extend sessions with custom data using callbacks.