NextAuth.js / Auth.js v5 - Authentication for Next.js App Router with providers, sessions, and middleware
Recipe
npm install next-auth@beta// auth.ts (root of project)
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!,
}),
],
});// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;// .env.local
AUTH_SECRET=your-random-secret-at-least-32-chars
AUTH_GITHUB_ID=...
AUTH_GITHUB_SECRET=...
AUTH_GOOGLE_ID=...
AUTH_GOOGLE_SECRET=...When to reach for this: You need OAuth login (GitHub, Google, etc.), session management, and route protection in a Next.js App Router application.
Working Example
// app/components/AuthButtons.tsx
import { auth, signIn, signOut } from "@/auth";
export async function SignInButton() {
const session = await auth();
if (session?.user) {
return (
<div className="flex items-center gap-3">
{session.user.image && (
<img
src={session.user.image}
alt=""
className="w-8 h-8 rounded-full"
/>
)}
<span className="text-sm">{session.user.name}</span>
<form
action={async () => {
"use server";
await signOut();
}}
>
<button className="text-sm text-gray-500 hover:text-gray-700">
Sign Out
</button>
</form>
</div>
);
}
return (
<div className="flex gap-2">
<form
action={async () => {
"use server";
await signIn("github");
}}
>
<button className="bg-gray-900 text-white px-4 py-2 rounded text-sm">
Sign in with GitHub
</button>
</form>
<form
action={async () => {
"use server";
await signIn("google");
}}
>
<button className="bg-blue-600 text-white px-4 py-2 rounded text-sm">
Sign in with Google
</button>
</form>
</div>
);
}// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session) {
redirect("/api/auth/signin");
}
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="mt-2">Welcome, {session.user?.name}!</p>
<pre className="mt-4 bg-gray-100 p-4 rounded text-sm">
{JSON.stringify(session, null, 2)}
</pre>
</div>
);
}// middleware.ts
import { auth } from "./auth";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isProtected = req.nextUrl.pathname.startsWith("/dashboard");
if (isProtected && !isLoggedIn) {
return Response.redirect(new URL("/api/auth/signin", req.nextUrl));
}
});
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*"],
};What this demonstrates:
- Auth.js v5 setup with GitHub and Google providers
- Server Component session access with
auth() - Server Action sign in/out forms
- Protected pages with redirect
- Middleware-based route protection
Deep Dive
How It Works
- Auth.js v5 exports
auth(),signIn(),signOut(), andhandlersfrom a single configuration auth()returns the current session in Server Components, Server Actions, API routes, and middleware- Sessions are JWT-based by default (no database required); add a database adapter for persistent sessions
- The
handlersexport provides GET and POST route handlers for the/api/auth/[...nextauth]catch-all route - OAuth flow: user clicks sign in, redirects to provider, provider redirects back with code, Auth.js exchanges code for tokens and creates a session
AUTH_SECRETis required in production for signing JWTs; generate withnpx auth secret
Variations
Credentials provider (email/password):
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials) => {
const parsed = z
.object({
email: z.string().email(),
password: z.string().min(8),
})
.safeParse(credentials);
if (!parsed.success) return null;
const user = await prisma.user.findUnique({
where: { email: parsed.data.email },
});
if (!user || !user.password) return null;
const valid = await bcrypt.compare(parsed.data.password, user.password);
if (!valid) return null;
return { id: String(user.id), name: user.name, email: user.email };
},
}),
],
});Prisma database adapter:
npm install @auth/prisma-adapterimport { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [GitHub, Google],
session: { strategy: "database" }, // Use database sessions instead of JWT
});Extending the session with custom fields:
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [GitHub],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role; // Add role from database user
}
return token;
},
async session({ session, token }) {
session.user.id = token.sub!;
session.user.role = token.role as string;
return session;
},
},
});// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession["user"];
}
}Client-side session access:
// app/providers.tsx
"use client";
import { SessionProvider } from "next-auth/react";
export function AuthProvider({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
// app/components/ClientProfile.tsx
"use client";
import { useSession } from "next-auth/react";
export function ClientProfile() {
const { data: session, status } = useSession();
if (status === "loading") return <p>Loading...</p>;
if (!session) return <p>Not signed in</p>;
return <p>Signed in as {session.user?.name}</p>;
}TypeScript Notes
- Extend the
SessionandJWTtypes via module augmentation intypes/next-auth.d.ts auth()returnsSession | null; always check for null before accessingsession.user- Provider-specific profile types are available but rarely needed
- The
Usertype can be extended for custom database fields
import type { Session } from "next-auth";
async function getUser(): Promise<Session["user"] | null> {
const session = await auth();
return session?.user ?? null;
}Gotchas
-
AUTH_SECRET missing — App crashes in production without
AUTH_SECRET. Fix: Generate it withnpx auth secretand add to your environment variables. Required for JWT signing. -
Callback URL mismatch — OAuth providers reject redirects if the callback URL doesn't match. Fix: Register
https://yourdomain.com/api/auth/callback/github(and/google) in each provider's developer settings. -
Session null in client components —
auth()only works server-side. Fix: UseSessionProvideranduseSession()hook for client components. Wrap your layout with<SessionProvider>. -
Middleware runs on every request — Without a
matcher, auth middleware runs on static assets, API routes, etc. Fix: Always configureconfig.matcherto limit middleware to protected paths. -
Credentials provider limitations — Credentials provider does not support database sessions out of the box (JWT only). Fix: Use JWT strategy with credentials. For database sessions, use OAuth providers with a database adapter.
-
Edge runtime compatibility — Some database adapters and bcrypt don't work in Edge runtime. Fix: Use
bcryptjsinstead ofbcrypt. Check adapter compatibility with Edge. Useruntime: "nodejs"in middleware if needed. -
v4 to v5 migration — Auth.js v5 has a significantly different API from NextAuth v4. Fix: Follow the official migration guide. Key changes:
NextAuth()returnsauthinstead of usinggetServerSession(), andnext-auth/reactis only for client components.
Alternatives
| Library | Best For | Trade-off |
|---|---|---|
| Auth.js / NextAuth v5 | Full OAuth + session management for Next.js | Complex setup for custom flows |
| Clerk | Drop-in auth UI components | Paid service, vendor lock-in |
| Supabase Auth | Supabase ecosystem integration | Tied to Supabase |
| Lucia | Lightweight, DIY auth | More manual setup, less abstraction |
| Kinde | B2B auth with organizations | Paid service |
| Custom JWT | Full control | Must handle security, refresh, revocation yourself |
FAQs
What does the auth() function return and where can I call it?
- Returns
Session | nullcontaining user info and expiry - Can be called in Server Components, Server Actions, API routes, and middleware
- Cannot be called in client components -- use
useSession()fromnext-auth/reactinstead - Always check for
nullbefore accessingsession.user
How does the OAuth flow work with Auth.js v5 and Next.js?
- User clicks sign-in, form action calls
signIn("github") - Next.js redirects to the OAuth provider's authorization page
- User grants permission, provider redirects back with a code
- Auth.js exchanges the code for tokens and creates a JWT session
- The session cookie is set and subsequent
auth()calls return the session
Gotcha: Why does my app crash in production with "AUTH_SECRET is missing"?
AUTH_SECRETis required for signing JWTs in production- Generate it with
npx auth secretand add to your environment variables - It must be at least 32 characters
- In development, Auth.js generates a temporary secret automatically
How do I protect routes with middleware?
// middleware.ts
import { auth } from "./auth";
export default auth((req) => {
if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) {
return Response.redirect(new URL("/api/auth/signin", req.nextUrl));
}
});
export const config = {
matcher: ["/dashboard/:path*"],
};Always set config.matcher to limit middleware to protected paths.
How do I access the session in a client component?
- Wrap your layout with
<SessionProvider>fromnext-auth/react - Use the
useSession()hook in client components useSession()returns{ data: session, status }where status is"loading","authenticated", or"unauthenticated"
How do I add a Credentials provider for email/password login?
- Import
Credentialsfromnext-auth/providers/credentials - Define
credentialsfields and anauthorizefunction - The
authorizefunction validates credentials and returns a user object ornull - Credentials provider only supports JWT sessions, not database sessions
How do I extend the session with custom fields like role?
callbacks: {
async jwt({ token, user }) {
if (user) token.role = user.role;
return token;
},
async session({ session, token }) {
session.user.role = token.role as string;
return session;
},
}Also extend the Session type via module augmentation in types/next-auth.d.ts.
How do I type-extend the Session interface in TypeScript?
// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession["user"];
}
}This adds id and role to session.user throughout your app.
Gotcha: Why does auth() return null in my client component?
auth()is a server-only function and cannot be called in client components- Use
<SessionProvider>anduseSession()hook for client-side session access - Make sure
SessionProviderwraps the component tree in your layout
How do I set up a Prisma database adapter for persistent sessions?
import { PrismaAdapter } from "@auth/prisma-adapter";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "database" },
providers: [GitHub, Google],
});Install @auth/prisma-adapter and set session.strategy to "database".
Gotcha: Why does my middleware run on every request including static assets?
- Without a
matcherconfig, middleware runs on all routes including_next/static, images, and API routes - Always configure
config.matcherto limit it to protected paths - Example:
matcher: ["/dashboard/:path*", "/settings/:path*"]
What are the key differences between NextAuth v4 and Auth.js v5?
- v5 exports
auth()instead of requiringgetServerSession(authOptions) - v5 uses a single
NextAuth()call that returnsauth,signIn,signOut, andhandlers next-auth/react(useSession,SessionProvider) is only for client components- The configuration file is typically
auth.tsat the project root
Related
- Prisma — Database adapter for persistent sessions and user storage
- Next.js Middleware — Route protection patterns
- Next.js Server Actions — Server-side sign in/out flows
- TanStack Query — Fetching user-specific data after authentication