| name | auth-patterns |
| description | Next.js authentication and authorization patterns. Use when the user asks about "authentication in Next.js", "NextAuth.js", "Auth.js", "middleware auth guards", "protected routes", "session management", "role-based access", "login page", "signup form", "JWT sessions", "cookies auth", or needs guidance on implementing auth in App Router applications.
|
Authentication Patterns
Authentication in Next.js spans multiple layers: proxy (middleware), layouts, pages, Server Actions, and Route Handlers. This skill covers where and how to check auth at each layer.
Authentication Flow Overview
- User submits credentials → Server Action validates and creates session
- Session stored in encrypted cookie (stateless) or database (stateful)
- Proxy/middleware checks session on every request → redirects if unauthorized
- Server Components read session to render user-specific UI
- Server Actions/Route Handlers verify session before mutations
Sign-up / Login with Server Actions
1. Define Validation Schema
import { z } from 'zod'
export const SignupSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters').trim(),
email: z.string().email('Please enter a valid email').trim(),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
export const LoginSchema = z.object({
email: z.string().email('Please enter a valid email'),
password: z.string().min(1, 'Password is required'),
})
2. Server Action for Signup
'use server'
import { SignupSchema } from '@/lib/schemas'
import { createSession } from '@/lib/session'
import { redirect } from 'next/navigation'
import bcrypt from 'bcrypt'
export type AuthState = {
message: string
errors?: Record<string, string[]>
}
export async function signup(prevState: AuthState, formData: FormData): Promise<AuthState> {
const parsed = SignupSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
if (!parsed.success) {
return { message: 'Validation failed', errors: parsed..(). }
}
existingUser = db..({ : { : parsed.. } })
(existingUser) {
{ : , : { : [] } }
}
hashedPassword = bcrypt.(parsed.., )
user = db..({
: { : parsed.., : parsed.., : hashedPassword },
})
(user.)
()
}
3. Signup Form Component
'use client'
import { useActionState } from 'react'
import { signup, type AuthState } from '@/app/actions/auth'
const initialState: AuthState = { message: '' }
export default function SignupPage() {
const [state, formAction, pending] = useActionState(signup, initialState)
return (
<form action={formAction}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" type="text" required />
{state.errors?.name && <p className="text-red-500">{state.errors.name[0]}</p>}
</div>
<div>
<label =>Email
{state.errors?.email && {state.errors.email[0]}}
Password
{state.errors?.password && {state.errors.password[0]}}
{pending ? 'Creating account...' : 'Sign Up'}
{state.message && !state.errors && {state.message}}
)
}
Session Management
Stateless Sessions (JWT in Cookies)
Use jose for JWT encryption compatible with Edge Runtime:
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { cookies } from 'next/headers'
const secretKey = process.env.SESSION_SECRET!
const encodedKey = new TextEncoder().encode(secretKey)
export async function createSession(userId: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: ,
: process.. === ,
: ,
: expiresAt,
: ,
})
}
() {
cookieStore = ()
session = cookieStore.()?.
(!session)
{
{ payload } = (session, encodedKey, { : [] })
payload { : }
} {
}
}
() {
cookieStore = ()
cookieStore.()
}
Cookie options checklist:
httpOnly: true — prevents client-side JS access
secure: true — HTTPS only in production
sameSite: 'lax' — CSRF protection
path: '/' — available on all routes
Session Refresh
Extend session expiration when the user is active:
export async function refreshSession() {
const session = await getSession()
if (!session) return
await createSession(session.userId)
}
Proxy Auth Guard (Next.js 16)
Check session in proxy.ts to protect routes before they render:
import { NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/session'
const protectedRoutes = ['/dashboard', '/settings', '/account']
const authRoutes = ['/login', '/signup']
export async function proxy(request: NextRequest) {
const path = request.nextUrl.pathname
const isProtected = protectedRoutes.some(route => path.startsWith(route))
const isAuthRoute = authRoutes.some(route => path.startsWith(route))
const session = await getSession()
if (isProtected && !session) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', path)
return .(loginUrl)
}
(isAuthRoute && session) {
.( (, request.))
}
.()
}
Session in Server Components
import { getSession } from '@/lib/session'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const session = await getSession()
if (!session) {
redirect('/login')
}
const user = await db.user.findUnique({ where: { id: session.userId } })
return <h1>Welcome, {user?.name}</h1>
}
Protecting Server Actions
Always verify auth before mutations:
'use server'
import { getSession } from '@/lib/session'
export async function deletePost(postId: string) {
const session = await getSession()
if (!session) {
throw new Error('Unauthorized')
}
const post = await db.post.findUnique({ where: { id: postId } })
if (post?.authorId !== session.userId) {
throw new Error('Forbidden')
}
await db.post.delete({ where: { id: postId } })
}
Protecting Route Handlers
import { getSession } from '@/lib/session'
export async function GET() {
const session = await getSession()
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const posts = await db.post.findMany({ where: { authorId: session.userId } })
return Response.json(posts)
}
Role-Based Access Control (RBAC)
Extend session with roles:
export async function createSession(userId: string, role: string) {
const session = await new SignJWT({ userId, role })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(encodedKey)
}
import 'server-only'
import { getSession } from '@/lib/session'
export async function requireAdmin() {
const session = await getSession()
if (!session || session.role !== 'admin') {
throw new Error('Forbidden: Admin access required')
}
return session
}
export async function requireAuth() {
const session = await getSession()
if (!session) {
throw new Error('Unauthorized')
}
return session
}
import { requireAdmin } from '@/lib/dal'
export default async function AdminPage() {
const session = await requireAdmin()
return <h1>Admin Dashboard</h1>
}
Auth.js (NextAuth v5) Quick Setup
For a full-featured auth solution with OAuth providers:
npm install next-auth@beta
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [GitHub],
})
import { handlers } from '@/auth'
export const { GET, POST } = handlers
import { auth } from '@/auth'
export default async function Page() {
const session = await auth()
if (!session?.user) return <p>Not signed in</p>
return <p>Welcome, {session.user.name}</p>
}
Auth Check Layers — When to Use Which
| Layer | Purpose | Catches |
|---|
| Proxy | Redirect before render | Unauthenticated page visits |
| Layout | Shared auth UI (show/hide nav items) | Nothing — layouts don't block |
| Page | Server-side auth check + redirect | Direct page access |
| Server Action | Verify before mutation | Unauthorized form submissions |
| Route Handler | Verify before API response | Unauthorized API calls |
| Data Access Layer | Centralized auth + data | All data access points |
Best practice: Use proxy for redirects, DAL for data access. Don't rely solely on proxy — it can be bypassed by direct Server Action calls.
What This Skill Does NOT Cover
- Specific OAuth provider setup (Google, GitHub, etc.) — see Auth.js docs
- Database adapter configuration for Auth.js
- Multi-tenancy and organization-based access
- Two-factor authentication (2FA) implementation
- Social login UI components