| name | nextauth |
| description | Implements authentication with Auth.js/NextAuth.js v5 including OAuth providers, credentials, sessions, and route protection. Use when adding authentication to Next.js, configuring OAuth providers, or protecting routes. |
NextAuth.js / Auth.js
Flexible authentication library for Next.js with 80+ OAuth providers and database adapters.
Quick Start
Install:
npm install next-auth@beta
Generate secret:
npx auth secret
This adds AUTH_SECRET to .env.local.
Configuration
Auth Config
// auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
GitHub,
Google,
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
// Validate credentials against database
const user = await getUserFromDb(
credentials.email as string,
credentials.password as string
);
if (!user) return null;
return {
id: user.id,
email: user.email,
name: user.name,
};
},
}),
],
});
Route Handler
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
Environment Variables
# .env.local
AUTH_SECRET=your-generated-secret
# OAuth Providers
AUTH_GITHUB_ID=your-github-client-id
AUTH_GITHUB_SECRET=your-github-client-secret
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret
OAuth Providers
GitHub
import GitHub from 'next-auth/providers/github';
export const { handlers, auth } = NextAuth({
providers: [
GitHub({
clientId: process.env.AUTH_GITHUB_ID,
clientSecret: process.env.AUTH_GITHUB_SECRET,
}),
],
});
import Google from 'next-auth/providers/google';
export const { handlers, auth } = NextAuth({
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
authorization: {
params: {
prompt: 'consent',
access_type: 'offline',
response_type: 'code',
},
},
}),
],
});
Discord
import Discord from 'next-auth/providers/discord';
export const { handlers, auth } = NextAuth({
providers: [Discord],
});
Credentials Provider
import Credentials from 'next-auth/providers/credentials';
import { compare } from 'bcryptjs';
export const { handlers, auth } = NextAuth({
providers: [
Credentials({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
});
if (!user || !user.password) {
return null;
}
const isValid = await compare(
credentials.password as string,
user.password
);
if (!isValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
};
},
}),
],
pages: {
signIn: '/login',
},
});
Session Management
Get Session (Server)
// In Server Components
import { auth } from '@/auth';
export default async function Page() {
const session = await auth();
if (!session) {
return <div>Please sign in</div>;
}
return (
<div>
<p>Welcome, {session.user?.name}</p>
<p>Email: {session.user?.email}</p>
</div>
);
}
Get Session (Client)
'use client';
import { useSession } from 'next-auth/react';
export function UserInfo() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <div>Loading...</div>;
}
if (status === 'unauthenticated') {
return <div>Not signed in</div>;
}
return (
<div>
<p>Signed in as {session?.user?.name}</p>
</div>
);
}
Session Provider
// app/layout.tsx
import { SessionProvider } from 'next-auth/react';
import { auth } from '@/auth';
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
return (
<html lang="en">
<body>
<SessionProvider session={session}>
{children}
</SessionProvider>
</body>
</html>
);
}
Sign In / Sign Out
Server Actions
// app/actions.ts
'use server';
import { signIn, signOut } from '@/auth';
export async function handleSignIn(provider: string) {
await signIn(provider, { redirectTo: '/dashboard' });
}
export async function handleSignOut() {
await signOut({ redirectTo: '/' });
}
UI Components
'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>
);
}
// Or using server actions
import { handleSignIn, handleSignOut } from './actions';
export function AuthButtons() {
return (
<div>
<form action={() => handleSignIn('github')}>
<button type="submit">Sign in with GitHub</button>
</form>
<form action={handleSignOut}>
<button type="submit">Sign out</button>
</form>
</div>
);
}
Route Protection
Middleware
// middleware.ts
export { auth as middleware } from '@/auth';
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
With Custom Logic
// middleware.ts
import { auth } from '@/auth';
import { NextResponse } from 'next/server';
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard');
const isOnAuth = req.nextUrl.pathname.startsWith('/login');
if (isOnDashboard && !isLoggedIn) {
return NextResponse.redirect(new URL('/login', req.url));
}
if (isOnAuth && isLoggedIn) {
return NextResponse.redirect(new URL('/dashboard', req.url));
}
return NextResponse.next();
});
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Authorized Callback
// auth.ts
export const { handlers, auth } = NextAuth({
providers: [...],
callbacks: {
authorized: async ({ auth, request }) => {
const isLoggedIn = !!auth?.user;
const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !isLoggedIn) {
return false; // Redirects to signIn page
}
return true;
},
},
});
API Route Protection
// app/api/protected/route.ts
import { auth } from '@/auth';
import { NextResponse } from 'next/server';
export const GET = auth(function GET(req) {
if (!req.auth) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
return NextResponse.json({
user: req.auth.user,
message: 'Protected data',
});
});
Callbacks
JWT & Session Callbacks
// auth.ts
export const { handlers, auth } = NextAuth({
providers: [...],
callbacks: {
jwt: async ({ token, user, account }) => {
// Add user data to token on sign in
if (user) {
token.id = user.id;
token.role = user.role;
}
// Add access token from OAuth
if (account) {
token.accessToken = account.access_token;
}
return token;
},
session: async ({ session, token }) => {
// Add token data to session
if (token) {
session.user.id = token.id as string;
session.user.role = token.role as string;
session.accessToken = token.accessToken as string;
}
return session;
},
},
});
Sign In Callback
callbacks: {
signIn: async ({ user, account, profile }) => {
// Allow OAuth sign in
if (account?.provider !== 'credentials') {
return true;
}
// Check if user is verified
const existingUser = await getUserById(user.id);
if (!existingUser?.emailVerified) {
return false;
}
return true;
},
}
Database Adapters
Prisma Adapter
npm install @auth/prisma-adapter
// auth.ts
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';
export const { handlers, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [...],
session: {
strategy: 'database', // or 'jwt'
},
});
Drizzle Adapter
npm install @auth/drizzle-adapter
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from '@/db';
export const { handlers, auth } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [...],
});
TypeScript Extensions
// types/next-auth.d.ts
import { DefaultSession } from 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession['user'];
accessToken?: string;
}
interface User {
role: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
id: string;
role: string;
accessToken?: string;
}
}
Custom Pages
// auth.ts
export const { handlers, auth } = NextAuth({
providers: [...],
pages: {
signIn: '/login',
signOut: '/logout',
error: '/auth/error',
verifyRequest: '/auth/verify',
newUser: '/onboarding',
},
});
// app/login/page.tsx
import { signIn } from '@/auth';
export default function LoginPage() {
return (
<div className="flex flex-col gap-4 max-w-md mx-auto mt-20">
<h1 className="text-2xl font-bold">Sign In</h1>
<form
action={async () => {
'use server';
await signIn('github', { redirectTo: '/dashboard' });
}}
>
<button className="w-full p-2 bg-gray-900 text-white rounded">
Sign in with GitHub
</button>
</form>
<form
action={async () => {
'use server';
await signIn('google', { redirectTo: '/dashboard' });
}}
>
<button className="w-full p-2 bg-blue-600 text-white rounded">
Sign in with Google
</button>
</form>
</div>
);
}
Best Practices
- Use middleware for protection - Centralized auth checks
- Verify near data layer - Don't rely only on middleware
- Extend session types - Add custom user properties
- Use database sessions for sensitive apps - More secure than JWT
- Handle errors gracefully - Custom error pages
Common Mistakes
| Mistake | Fix |
|---|---|
| Missing AUTH_SECRET | Run npx auth secret |
| Exposed credentials | Use environment variables |
| JWT-only with credentials | Consider database sessions |
| Missing session provider | Wrap app with SessionProvider |
| No redirect after auth | Set redirectTo option |
Reference Files
- references/providers.md - All OAuth providers
- references/adapters.md - Database adapters
- references/callbacks.md - Callback patterns