next-auth
Authentication for Next.js apps, now part of the wider Auth.js project. It handles the OAuth dance for dozens of built-in providers (Google, GitHub, and many more), plus email magic links, passkeys/WebAuthn, and encrypted JWT or database-backed sessions, with CSRF protection and sane cookie defaults out of the box. You mount one API route, wrap your app in a session provider, and get signIn/signOut/useSession. Important context: the project's own README now says Auth.js has joined Better Auth and recommends new projects start with Better Auth unless they specifically need stateless, database-free sessions.
Still fine to keep running in existing Next.js apps, and the fastest free path to OAuth sessions on v4. But with v5 perpetually in beta and the maintainers pointing new projects at Better Auth, starting fresh on next-auth today is choosing a library on its way out.
Use it if
- You need OAuth sign-in (Google, GitHub, etc.) in an existing Next.js app quickly, with CSRF and cookie handling done for you
- You want stateless JWT sessions with no database at all; encrypted JWTs are the default and this remains the feature the maintainers say Better Auth lacks
- You are on a Next.js codebase that already uses next-auth and needs providers added or sessions extended, where migrating auth is not on the roadmap
- You want to own your auth and user data on your own infrastructure instead of paying a hosted provider like Clerk or Auth0
- You are starting a new project: the maintainers themselves now recommend Better Auth for new apps since Auth.js joined that project, so building new on next-auth means building on a library in wind-down
- You need email/password auth with proper flows: the Credentials provider is deliberately bare (no registration, hashing, rate limiting, or password reset) and the docs discourage it
- You want v5: it has been stuck in beta (5.0.0-beta.32) for years while npm latest is still v4, so every tutorial you find targets one of two incompatible APIs and half will not match your install
- You need fine-grained authorization, organizations, or roles built in: it does sign-in and sessions only, everything else is yours to build in callbacks
- You are not on Next.js: despite the Auth.js rebrand, the non-Next framework packages (@auth/sveltekit and friends) are younger and thinner than the Next.js path
Setup reality
Install is one package, but configuration is where the time goes: a [...nextauth] catch-all route, provider client IDs and secrets from each OAuth console, NEXTAUTH_SECRET and NEXTAUTH_URL env vars (deployments break in confusing ways when these are wrong), and a SessionProvider wrapper for client components. The real tax is version confusion: v4 (stable, pages- and app-router capable) and v5 beta (auth() everywhere, different config file, AUTH_* env names) coexist, docs at authjs.dev lean v5 while npm installs v4, so copy-pasted snippets routinely target the wrong major. Database sessions add an adapter package and schema migrations on top.
Patterns
Set up next-auth v4 with an OAuth providerbasic-setup
// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
export const authOptions = {
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
],
};
export default NextAuth(authOptions);Also set NEXTAUTH_SECRET (and NEXTAUTH_URL outside Vercel) or production logins fail with JWT decryption errors.
Mount the v4 handler in the App Routerapp-router-setup
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };Keep authOptions in a separate file so getServerSession can import it; exporting it from route.ts breaks Next.js route type checks.
Read the session in a client componentclient-session
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
export function UserButton() {
const { data: session, status } = useSession();
if (status === 'loading') return <span>...</span>;
if (!session) return <button onClick={() => signIn('github')}>Sign in</button>;
return <button onClick={() => signOut()}>Sign out {session.user?.email}</button>;
}useSession only works under a <SessionProvider>, which must live in a client component wrapper in the App Router.
Read the session on the serverserver-session
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export default async function Page() {
const session = await getServerSession(authOptions);
if (!session) return <p>Not signed in</p>;
return <p>Hello {session.user?.name}</p>;
}Always pass authOptions; calling getServerSession without it silently loses your callbacks and custom session shape.
Protect routes with middlewareprotect-middleware
// middleware.ts
export { default } from 'next-auth/middleware';
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*'],
};The bundled middleware only works with JWT session strategy, not database sessions.
Email/password with the Credentials providercredentials-provider
import CredentialsProvider from 'next-auth/providers/credentials';
CredentialsProvider({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
const user = await verifyUser(credentials!.email, credentials!.password);
return user ?? null; // null -> sign-in rejected
},
});You must supply hashing, rate limiting, and registration yourself; Credentials also forces JWT sessions even with a database adapter.
Add the user id to the sessionextend-session
export const authOptions = {
// ...providers,
callbacks: {
async jwt({ token, user }) {
if (user) token.id = user.id;
return token;
},
async session({ session, token }) {
(session.user as any).id = token.id;
return session;
},
},
};The jwt callback runs first and only gets user on initial sign-in; persist anything you need onto the token there.
Persist users with a database adapterdatabase-adapter
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';
export const authOptions = {
adapter: PrismaAdapter(prisma),
session: { strategy: 'database' },
// ...providers,
};Adapters live in separate @auth/* packages and expect a specific schema (users, accounts, sessions, verification tokens); copy it from the adapter docs exactly.
Use your own sign-in pagecustom-signin-page
export const authOptions = {
// ...providers,
pages: {
signIn: '/login',
error: '/login', // OAuth errors land here as ?error=
},
};Your page must call signIn(provider) itself; check the error query param to surface OAuth failures to users.
Sign in and control the redirectsignin-redirect
import { signIn } from 'next-auth/react';
await signIn('google', { callbackUrl: '/dashboard' });
// credentials without a full redirect:
const res = await signIn('credentials', {
redirect: false,
email, password,
});
if (res?.error) setError('Invalid login');callbackUrl must be same-origin or whitelisted in the redirect callback, otherwise next-auth falls back to the base URL.
Verify the JWT in an API routeget-token-api-route
import { getToken } from 'next-auth/jwt';
export default async function handler(req, res) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
if (!token) return res.status(401).end();
res.json({ userId: token.id });
}getToken reads and decrypts the session cookie directly; cheaper than getServerSession when you only need claims.
Guard a server actionprotect-server-action
'use server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export async function deletePost(id: string) {
const session = await getServerSession(authOptions);
if (!session) throw new Error('Unauthorized');
await db.delete(posts).where(eq(posts.id, id));
}Middleware does not cover server actions invoked from cached pages; check the session inside every mutating action.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| better-auth | npm | New projects; the Auth.js README itself now points here, and it ships email/password, organizations, and 2FA in the box |
| @clerk/nextjs | npm | You will pay for hosted auth to get polished prebuilt UI, user management, and orgs without building them |
| iron-session | npm | You just need small encrypted cookie sessions and want to wire up OAuth or passwords yourself |
| lucia | npm | You want to learn how sessions actually work and hand-roll auth; note it is now a learning resource rather than a maintained package |