Middleware-Based Auth
Middleware is the first line of defense for authentication in Next.js. It runs before every request, making it perfect for route protection.
Basic Auth Middleware
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value
// Check if accessing protected route
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
// Redirect to login with return URL
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/api/user/:path*'],
}
JWT Verification in Middleware
Verify tokens at the edge:
// middleware.ts
import { jwtVerify } from 'jose'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, secret)
return payload
} catch {
return null
}
}
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
const payload = await verifyToken(token)
if (!payload) {
// Invalid or expired token
const response = NextResponse.redirect(new URL('/login', request.url))
response.cookies.delete('token')
return response
}
// Optionally add user info to headers for downstream use
const response = NextResponse.next()
response.headers.set('x-user-id', payload.userId as string)
return response
}
return NextResponse.next()
}
Role-Based Access Control
Check user roles in middleware:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
type Role = 'user' | 'admin' | 'super-admin'
interface TokenPayload {
userId: string
role: Role
}
const routePermissions: Record<string, Role[]> = {
'/admin': ['admin', 'super-admin'],
'/settings/billing': ['admin', 'super-admin'],
'/dashboard': ['user', 'admin', 'super-admin'],
}
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
const payload = await verifyToken(token) as TokenPayload | null
if (!payload) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Check route permissions
for (const [route, allowedRoles] of Object.entries(routePermissions)) {
if (request.nextUrl.pathname.startsWith(route)) {
if (!allowedRoles.includes(payload.role)) {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
}
}
return NextResponse.next()
}
Handling Auth State Changes
Redirect authenticated users away from auth pages:
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
const isAuthPage = request.nextUrl.pathname.startsWith('/login') ||
request.nextUrl.pathname.startsWith('/signup')
if (isAuthPage && token) {
// Already logged in - redirect to dashboard
return NextResponse.redirect(new URL('/dashboard', request.url))
}
if (!isAuthPage && request.nextUrl.pathname.startsWith('/dashboard') && !token) {
// Not logged in - redirect to login
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
Session Refresh Pattern
Extend sessions on activity:
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
if (!token) {
return NextResponse.next()
}
const payload = await verifyToken(token)
if (!payload) {
return NextResponse.redirect(new URL('/login', request.url))
}
const response = NextResponse.next()
// Refresh token if it's close to expiring
const exp = payload.exp as number
const now = Math.floor(Date.now() / 1000)
const fifteenMinutes = 15 * 60
if (exp - now < fifteenMinutes) {
const newToken = await generateToken(payload.userId as string)
response.cookies.set('token', newToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
})
}
return response
}
Middleware Limitations for Auth
Middleware runs on the Edge, so:
- No direct database access (use JWT or API calls)
- Limited to Edge-compatible packages
- Should be fast (runs on every request)
- Can't access Node.js APIs
For complex auth logic, verify the token in middleware but do authorization checks in Server Components or API routes.
Summary
- Use middleware as the first auth checkpoint
- Verify tokens before any page code runs
- Implement role-based access with route mappings
- Redirect authenticated users away from auth pages
- Refresh sessions to prevent unnecessary logouts
- Keep middleware fast - defer complex logic to Server Components

