Introduction
NextAuth.js is the standard authentication solution for Next.js applications. It handles OAuth providers (Google, GitHub, Facebook), email magic links, credentials-based authentication, and more. Built on open standards like OAuth 2.0 and OpenID Connect, it provides enterprise-grade security with minimal setup.
Why Choose NextAuth.js?
- Zero-Configuration OAuth: Works out of the box with major providers (Google, GitHub, Facebook, etc.)
- Type-Safe: Full TypeScript support with excellent IDE autocomplete
- Flexible Sessions: Choose between JWT and database sessions based on your needs
- Middleware Protection: Secure pages with a single line of code
- Database Adapters: Works with PostgreSQL, MySQL, MongoDB, and more
- Callbacks: Extend functionality with custom logic at various authentication stages
Installation & Setup
Install NextAuth.js in your Next.js project:
npm install next-auth
Generate a secure NEXTAUTH_SECRET for token signing:
npx auth secret
Core Configuration
Create the authentication API route at app/api/auth/[...nextauth]/route.ts:
import NextAuth, { type NextAuthOptions } from "next-auth"
import GithubProvider from "next-auth/providers/github"
import GoogleProvider from "next-auth/providers/google"
import CredentialsProvider from "next-auth/providers/credentials"
export const authOptions: NextAuthOptions = {
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID || "",
clientSecret: process.env.GITHUB_SECRET || "",
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
// This is where you find the user from your database
// Hash passwords with bcrypt or similar!
const user = await db.user.findUnique({
where: { email: credentials?.email },
})
if (user && await verifyPassword(credentials?.password, user.password)) {
return user
}
return null
},
}),
],
session: {
strategy: "jwt", // or "database" for database sessions
maxAge: 30 * 24 * 60 * 60, // 30 days
},
secret: process.env.NEXTAUTH_SECRET,
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.role = user.role
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string
session.user.role = token.role as string
}
return session
},
},
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
Environment Variables
Configure your .env.local file with provider credentials:
# NextAuth Configuration
NEXTAUTH_SECRET=generated_secret_from_npx_auth_secret
NEXTAUTH_URL=http://localhost:3000
# GitHub OAuth
GITHUB_ID=your_github_oauth_app_id
GITHUB_SECRET=your_github_oauth_secret
# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Database (if using database sessions)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
Getting OAuth Credentials
GitHub: Go to Settings → Developer settings → OAuth Apps → Create New OAuth App
Google: Visit Google Cloud Console → Create Project → APIs & Services → Credentials → OAuth 2.0 Client ID
Authentication Strategies
JWT Sessions (Default)
JWTs are stateless and scale well for APIs and distributed systems:
// Pros: Stateless, scales well, works across subdomains
// Cons: Token is in localStorage (XSS vulnerability), requires refresh strategy
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
}
Database Sessions
Database sessions are more secure for sensitive applications:
// Pros: Secure, can revoke tokens, better for sensitive data
// Cons: Requires database, adds latency
session: {
strategy: "database",
maxAge: 24 * 60 * 60, // 24 hours
}
// Requires setting up a database adapter
import { PrismaAdapter } from "@next-auth/prisma-adapter"
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [ /* ... */ ],
}
Protecting Routes
Server-Side Protection with Middleware
Create middleware.ts at your project root to protect entire route groups:
import { withAuth } from "next-auth/middleware"
import { NextRequest } from "next/server"
export default withAuth(
function middleware(req: NextRequest) {
// Custom logic here
console.log("Protected route accessed by:", req.nextauth.token?.email)
},
{
callbacks: {
authorized: ({ token }) => !!token,
},
}
)
export const config = {
matcher: [
"/dashboard/:path*",
"/admin/:path*",
"/api/protected/:path*",
],
}
Component-Level Protection
Protect components with the SessionProvider and useSession hook:
import { SessionProvider } from "next-auth/react"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
)
}
Using Authentication in Components
Client-Side Session Access
"use client"
import { useSession, signIn, signOut } from "next-auth/react"
export default function UserMenu() {
const { data: session, status } = useSession()
if (status === "loading") {
return <div>Loading...</div>
}
if (status === "unauthenticated") {
return <button onClick={() => signIn()}>Sign In</button>
}
return (
<div>
<p>Welcome, {session?.user?.name}!</p>
<p>Email: {session?.user?.email}</p>
<button onClick={() => signOut()}>Sign Out</button>
</div>
)
}
Server-Side Session Access
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"
export default async function Dashboard() {
const session = await getServerSession(authOptions)
if (!session) {
redirect("/api/auth/signin")
}
return (
<div>
<h1>Dashboard for {session.user?.email}</h1>
{/* Protected content */}
</div>
)
}
Advanced Features
Role-Based Access Control (RBAC)
Extend the session with user roles for fine-grained access control:
// Extend NextAuth types
declare module "next-auth" {
interface Session {
user: {
id: string
email: string
name: string
role: "admin" | "user"
}
}
}
// In callbacks
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.role = token.role as string
}
return session
},
}
// Use in components
function AdminPanel() {
const { data: session } = useSession()
if (session?.user?.role !== "admin") {
return <div>Access denied</div>
}
return <div>Admin controls here</div>
}
Custom Sign-In Page
Create a custom authentication UI at app/auth/signin/page.tsx:
import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"
export default function SignIn() {
const router = useRouter()
return (
<div style={{ maxWidth: "400px", margin: "50px auto" }}>
<h1>Sign In</h1>
<button onClick={() => signIn("github")}>
Sign in with GitHub
</button>
<button onClick={() => signIn("google")}>
Sign in with Google
</button>
<button onClick={() => signIn("credentials", {
email: "user@example.com",
password: "password"
})}>
Sign in with Email
</button>
</div>
)
}
Email Provider with Magic Links
Send magic links via email without passwords:
import EmailProvider from "next-auth/providers/email"
import { resend } from "resend"
providers: [
EmailProvider({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
},
},
from: process.env.EMAIL_FROM,
}),
]
Security Best Practices
- HTTPS Only in Production: Set
NEXTAUTH_URLto HTTPS in production. - Secure Secrets: Use strong, random NEXTAUTH_SECRET. Regenerate with
npx auth secret. - Hash Passwords: Never store plain text passwords. Use bcrypt, argon2, or similar.
- Validate Inputs: Always validate email and password on the server side.
- Rate Limit: Implement rate limiting on signin/signup endpoints.
- CSRF Protection: NextAuth includes CSRF tokens by default.
- Secure Cookies: In production, cookies are automatically httpOnly and secure.
- Revoke Sessions: Implement logout that invalidates the session.
Common Integration Patterns
Redirect After Sign-In
signIn("github", {
callbackUrl: "/dashboard"
})
Check Auth in API Routes
import { getServerSession } from "next-auth/next"
export async function POST(req: Request) {
const session = await getServerSession()
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
// Process authenticated request
}
Refresh Token Strategy
// In JWT callback, handle token refresh
callbacks: {
async jwt({ token, account }) {
if (account?.expires_at) {
token.expiresAt = account.expires_at * 1000
}
// Check if token is expired
if (Date.now() > (token.expiresAt || 0)) {
return refreshAccessToken(token)
}
return token
},
}
Troubleshooting
Session not persisting: Ensure SessionProvider wraps your app and NEXTAUTH_SECRET is set.
OAuth callback fails: Check redirect URIs match exactly in your OAuth app configuration.
CSRF token mismatch: Clear cookies and cache, ensure NEXTAUTH_URL is correct.
Type errors with session: Extend NextAuth types as shown in the RBAC section.
Conclusion
NextAuth.js provides a secure, flexible, and production-ready authentication solution for Next.js applications. Whether you need simple OAuth authentication or complex multi-provider setups with role-based access control, NextAuth.js scales with your requirements. Its combination of zero-configuration simplicity and powerful customization options makes it the ideal choice for modern web applications.