Next Auth¶
Authentication in Next.js is a middleware dance , a session strategy , and a thousand ways to accidentally expose protected routes
Next.js doesn't ship auth. You bring your own. Auth.js (formerly NextAuth.js) is the most popular choice but the setup is non-trivial. One misconfigured middleware matcher and your admin panel is public. One wrong callback URL and your OAuth flow redirects to an attacker's domain. Get it right or get pwned
Auth.js v5 setup¶
npm install next-auth@beta @auth/core
Create the auth configuration:
// src/lib/auth.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Credentials from 'next-auth/providers/credentials'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
GitHub,
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type': 'password' }
},
async authorize(credentials) {
const user = await prisma.user.findUnique({
where: { email: credentials.email as string }
})
if (!user || !user.password) return null
// verify password with bcrypt
const valid = await bcrypt.compare(credentials.password as string, user.password)
if (!valid) return null
return user
}
})
],
session: {
strategy: 'jwt' // or 'database'
}
})
Create the route handler:
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/lib/auth'
export const { GET, POST } = handlers
Auth.js v5 vs v4¶
Auth.js v5 (the next-auth@beta / @auth/core package) is a complete rewrite
| Feature | v4 | v5 |
|---|---|---|
| Import | next-auth | @auth/core + next-auth |
| API route | pages/api/auth/[...nextauth].js | app/api/auth/[...nextauth]/route.ts |
| Session check | getServerSession() | auth() |
| Middleware | getToken() | auth() wrapper |
| Edge support | Limited | Full |
The auth() function is the v5 replacement for getServerSession(). It's simpler and runs on Edge
middleware-based protection¶
Protect routes using the auth() middleware wrapper
// src/middleware.ts
import { auth } from '@/lib/auth'
export default auth((req) => {
const { pathname } = req.nextUrl
const isLoggedIn = !!req.auth
// Public routes
const publicPaths = ['/', '/login', '/register', '/api/auth']
const isPublic = publicPaths.some(path => pathname.startsWith(path))
if (!isLoggedIn && !isPublic) {
return Response.redirect(new URL('/login', req.url))
}
// Role-based access
if (pathname.startsWith('/admin') && req.auth?.user?.role !== 'admin') {
return Response.redirect(new URL('/unauthorized', req.url))
}
})
export const config = {
matcher: [
// Match all routes except static files , images , and auth API
'/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)'
]
}
The matcher config is critical. If it's too broad , you block static assets. If it's too narrow , you miss protected routes. The pattern above is the standard safe configuration
session handling¶
Read the session in Server Components:
// src/app/dashboard/page.tsx
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const session = await auth()
if (!session) {
redirect('/login')
}
return (
<div>
<h1>Welcome, {session.user?.name}</h1>
<p>Email: {session.user?.email}</p>
<p>Role: {session.user?.role}</p>
</div>
)
}
Read session in Client Components:
'use client'
import { useSession } from 'next-auth/react'
export default function UserMenu() {
const { data: session, status } = useSession()
if (status === 'loading') return <div>Loading...</div>
if (status === 'unauthenticated') return <a href="/login">Login</a>
return <div>Signed in as {session.user?.name}</div>
}
Wrap your app with the SessionProvider:
// src/app/providers.tsx
'use client'
import { SessionProvider } from 'next-auth/react'
export default function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>
}
// src/app/layout.tsx
import Providers from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
JWT vs database sessions¶
JWT sessions: * Token stored in a cookie * No database lookup on every request * Can't invalidate individual sessions (token is valid until expiry) * Token size grows with session data
session: {
strategy: 'jwt',
maxAge: 60 * 60 * 24 * 7 // 7 days
}
Database sessions: * Session ID stored in cookie , data in database * Can invalidate sessions (delete from database) * Database lookup on every request * Works with PrismaAdapter automatically
session: {
strategy: 'database',
maxAge: 60 * 60 * 24 * 7
}
Pick JWT for serverless deployments where database lookups are expensive. Pick database sessions when you need to revoke access immediately (security incidents , account termination)
role-based access¶
Extend the JWT or session with custom fields:
// src/lib/auth.ts
export const { handlers, signIn, signOut, auth } = NextAuth({
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
token.id = user.id
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.role = token.role as string
session.user.id = token.id as string
}
return session
}
}
})
Then check roles in middleware or pages:
// middleware check
if (pathname.startsWith('/admin') && req.auth?.user?.role !== 'admin') {
return Response.redirect(new URL('/unauthorized', req.url))
}
// page-level check
const session = await auth()
if (session?.user?.role !== 'admin') {
redirect('/unauthorized')
}
OAuth callback security¶
OAuth callbacks are a common attack vector. Validate everything:
// Auth.js handles most of this , but configure the callback URL:
NextAuth({
// Restrict callback URLs (prevents open redirect attacks)
callbacks: {
async redirect({ url, baseUrl }) {
// Only allow redirects to your own domain
if (url.startsWith(baseUrl)) return url
// Or relative URLs
if (url.startsWith('/')) return new URL(url, baseUrl).toString()
return baseUrl
}
}
})
Never trust unvalidated callbackUrl parameters. That's how open redirect vulnerabilities work
prerequisites¶
next_08_api_routes.md - you should understand route handlers and middleware
next → next_10_security.md