DEV Community

huangyongshan46-a11y
huangyongshan46-a11y

Posted on

Auth.js v5 with Next.js 16: The Complete Authentication Guide (2026)

Auth.js v5 (formerly NextAuth) is a complete rewrite. Most tutorials online are still for v4. Here's the definitive guide for v5 with Next.js 16 App Router.

Why v5?

Auth.js v5 brings:

  • Native App Router support
  • Better TypeScript types
  • Edge runtime compatibility
  • Simpler configuration
  • Built-in CSRF protection

Installation

npm install next-auth@beta @auth/prisma-adapter
Enter fullscreen mode Exit fullscreen mode

The auth configuration

Create a single auth config file that exports everything:

// src/lib/auth.ts
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { db } from "@/lib/db";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(db),
  session: { strategy: "jwt" },
  pages: {
    signIn: "/login",
    error: "/login",
  },
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) return null;
        const user = await db.user.findUnique({
          where: { email: credentials.email as string },
        });
        if (!user || !user.password) return null;
        // Use bcrypt.compare() in production
        const isValid = credentials.password === user.password;
        if (!isValid) return null;
        return { id: user.id, email: user.email, name: user.name };
      },
    }),
  ],
  callbacks: {
    async session({ token, session }) {
      if (token.sub && session.user) {
        session.user.id = token.sub;
        session.user.role = token.role as string;
      }
      return session;
    },
    async jwt({ token }) {
      if (!token.sub) return token;
      const user = await db.user.findUnique({
        where: { id: token.sub },
        select: { role: true },
      });
      if (user) token.role = user.role;
      return token;
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

The key difference from v4: everything is exported from a single NextAuth() call. No separate authOptions.

Route handler

// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
Enter fullscreen mode Exit fullscreen mode

That's it. Two lines.

Protecting pages (Server Components)

// Any server component or page
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth();
  if (!session?.user) redirect("/login");

  return <h1>Welcome, {session.user.name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Protecting API routes

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

export async function GET() {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  // ... your logic
}
Enter fullscreen mode Exit fullscreen mode

Sign in and sign out

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

// In a form
<form action={async () => {
  "use server";
  await signIn("google");
}}>
  <button>Sign in with Google</button>
</form>

<form action={async () => {
  "use server";
  await signOut();
}}>
  <button>Sign out</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Extending the session type

Add custom fields (like role) to the session:

// src/types/next-auth.d.ts
import { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      role: string;
    } & DefaultSession["user"];
  }
}
Enter fullscreen mode Exit fullscreen mode

The Prisma schema

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  emailVerified DateTime?
  image         String?
  password      String?
  role          Role      @default(USER)
  accounts      Account[]
  sessions      Session[]
}

model Account {
  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)
  @@id([provider, providerAccountId])
}

enum Role {
  USER
  ADMIN
}
Enter fullscreen mode Exit fullscreen mode

Common mistakes

  1. Using getServerSession from v4 — In v5, just use auth()
  2. Forgetting the route handler — The [...nextauth]/route.ts file is required
  3. JWT vs database sessions — Use JWT for serverless/edge, database for traditional servers
  4. Not extending types — Add the .d.ts file or TypeScript will complain about custom fields

Want this pre-built?

All of the above (plus Stripe billing, AI chat, email, and a full dashboard) comes pre-wired in LaunchKit — a production-ready Next.js SaaS starter kit.

GitHub | Get LaunchKit ($49)

Top comments (0)