Authentication middleware provides centralized route protection, checking user credentials before allowing access to protected pages. Implement session-based or JWT authentication, handle login redirects, manage user sessions, and enforce role-based access control. Master authentication middleware and secure your entire application with a single, powerful guard!
Session-Based Authentication
Basic Session Authentication
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Protected route patterns
const protectedRoutes = ['/dashboard', '/profile', '/settings'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if route is protected
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
);
if (isProtectedRoute) {
// Check for session cookie
const session = request.cookies.get('session');
if (!session) {
// No session - redirect to login
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnTo', pathname);
return NextResponse.redirect(loginUrl);
}
}
// Redirect logged-in users away from auth pages
const authPages = ['/login', '/register'];
const isAuthPage = authPages.includes(pathname);
if (isAuthPage) {
const session = request.cookies.get('session');
if (session) {
// Already logged in - go to dashboard
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/settings/:path*',
'/login',
'/register',
],
};
// ✅ Check session cookie existence
// ✅ Redirect unauthenticated users
// ✅ Preserve intended destination
// ✅ Prevent logged-in users from seeing auth pagesSession Verification with Helper
// Auth helper functions
export async function verifySession(sessionToken: string) {
try {
// Verify session with your auth system
// This could be:
// - Database lookup
// - Redis cache lookup
// - JWT verification
const session = await db.sessions.findUnique({
where: { token: sessionToken },
include: { user: true },
});
if (!session) {
return null;
}
// Check if session expired
if (session.expiresAt < new Date()) {
await db.sessions.delete({ where: { id: session.id } });
return null;
}
return {
userId: session.userId,
user: session.user,
expiresAt: session.expiresAt,
};
} catch (error) {
console.error('Session verification error:', error);
return null;
}
}
export function isSessionValid(session: any): boolean {
if (!session) return false;
if (!session.expiresAt) return false;
return new Date(session.expiresAt) > new Date();
}
// ✅ Centralized session verification
// ✅ Database validation
// ✅ Expiration check
// ✅ Error handlingUsing Session Verification in Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifySession } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isProtected = pathname.startsWith('/dashboard');
if (isProtected) {
const sessionToken = request.cookies.get('session')?.value;
if (!sessionToken) {
return redirectToLogin(request);
}
// Verify session is valid
const session = await verifySession(sessionToken);
if (!session) {
// Invalid or expired session
const response = redirectToLogin(request);
// Clear invalid session cookie
response.cookies.delete('session');
return response;
}
// Add user info to request headers for pages to use
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', session.userId);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
return NextResponse.next();
}
function redirectToLogin(request: NextRequest) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
export const config = {
matcher: '/dashboard/:path*',
};
// ✅ Verify session validity
// ✅ Clear expired sessions
// ✅ Pass user ID to pages
// ✅ Helper function for redirectsJWT-Based Authentication
JWT Verification Utility
import { jwtVerify, SignJWT } from 'jose';
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key'
);
export interface JWTPayload {
userId: string;
email: string;
role: string;
iat?: number;
exp?: number;
}
export async function verifyJWT(token: string): Promise<JWTPayload | null> {
try {
const verified = await jwtVerify(token, JWT_SECRET);
return verified.payload as JWTPayload;
} catch (error) {
console.error('JWT verification failed:', error);
return null;
}
}
export async function createJWT(payload: JWTPayload): Promise<string> {
const token = await new SignJWT({ ...payload })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(JWT_SECRET);
return token;
}
// ✅ Use jose library (Edge-compatible)
// ✅ Environment variable for secret
// ✅ Type-safe payload
// ✅ 7-day expirationJWT Authentication Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyJWT } from '@/lib/jwt';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isProtected = pathname.startsWith('/dashboard') ||
pathname.startsWith('/api/protected');
if (isProtected) {
// Get JWT from cookie or Authorization header
let token = request.cookies.get('token')?.value;
if (!token) {
const authHeader = request.headers.get('authorization');
if (authHeader?.startsWith('Bearer ')) {
token = authHeader.substring(7);
}
}
if (!token) {
return unauthorizedResponse(request, pathname);
}
// Verify JWT
const payload = await verifyJWT(token);
if (!payload) {
// Invalid or expired token
const response = unauthorizedResponse(request, pathname);
response.cookies.delete('token');
return response;
}
// Add user info to headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.userId);
requestHeaders.set('x-user-email', payload.email);
requestHeaders.set('x-user-role', payload.role);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
return NextResponse.next();
}
function unauthorizedResponse(request: NextRequest, pathname: string) {
// For API routes, return 401
if (pathname.startsWith('/api/')) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// For pages, redirect to login
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnTo', pathname);
return NextResponse.redirect(loginUrl);
}
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};
// ✅ Support cookie and header tokens
// ✅ Verify JWT signature
// ✅ Handle API vs page routes differently
// ✅ Pass user data to handlersRole-Based Access Control (RBAC)
Define Role Requirements
// Role definitions
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
MODERATOR = 'moderator',
}
// Route permissions
export const routePermissions: Record<string, UserRole[]> = {
'/dashboard': [UserRole.USER, UserRole.ADMIN, UserRole.MODERATOR],
'/admin': [UserRole.ADMIN],
'/moderator': [UserRole.ADMIN, UserRole.MODERATOR],
};
export function hasPermission(
userRole: string,
pathname: string
): boolean {
// Find matching route permission
const matchingRoute = Object.keys(routePermissions).find(route =>
pathname.startsWith(route)
);
if (!matchingRoute) {
// No specific permission required
return true;
}
const allowedRoles = routePermissions[matchingRoute];
return allowedRoles.includes(userRole as UserRole);
}
// ✅ Enum for type safety
// ✅ Route-to-role mapping
// ✅ Permission checker functionRBAC Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyJWT } from '@/lib/jwt';
import { hasPermission } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Protected routes
const isProtected = pathname.startsWith('/dashboard') ||
pathname.startsWith('/admin') ||
pathname.startsWith('/moderator');
if (isProtected) {
const token = request.cookies.get('token')?.value;
if (!token) {
return redirectToLogin(request);
}
// Verify JWT
const payload = await verifyJWT(token);
if (!payload) {
return redirectToLogin(request);
}
// Check role-based permissions
const hasAccess = hasPermission(payload.role, pathname);
if (!hasAccess) {
// User authenticated but doesn't have permission
return NextResponse.redirect(
new URL('/unauthorized', request.url)
);
}
// Pass user info to pages
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.userId);
requestHeaders.set('x-user-role', payload.role);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
return NextResponse.next();
}
function redirectToLogin(request: NextRequest) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*', '/moderator/:path*'],
};
// ✅ Verify authentication
// ✅ Check role permissions
// ✅ Redirect to unauthorized page
// ✅ Pass role to pagesUnauthorized Page
export default function UnauthorizedPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">403 - Unauthorized</h1>
<p className="text-gray-600 mb-8">
You don't have permission to access this page.
</p>
href="/dashboard"
className="text-blue-600 hover:underline"
>
Return to Dashboard
</a>
</div>
</div>
);
}
// ✅ Clear unauthorized message
// ✅ Link back to allowed page
// ✅ User-friendlyComplete Authentication Flow
Login API Route
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { createJWT } from '@/lib/jwt';
import bcrypt from 'bcryptjs';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const result = loginSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 400 }
);
}
const { email, password } = result.data;
// Find user
const user = await db.users.findUnique({
where: { email },
});
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Verify password
const validPassword = await bcrypt.compare(password, user.hashedPassword);
if (!validPassword) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Create JWT
const token = await createJWT({
userId: user.id,
email: user.email,
role: user.role,
});
// Set cookie
const response = NextResponse.json({
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
});
response.cookies.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
});
return response;
} catch (error) {
console.error('Login error:', error);
return NextResponse.json(
{ error: 'Login failed' },
{ status: 500 }
);
}
}
// ✅ Validate credentials
// ✅ Verify password
// ✅ Create JWT
// ✅ Set secure cookieLogout API Route
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
// Optional: Invalidate session in database
const token = request.headers.get('cookie')
?.split('; ')
.find(c => c.startsWith('token='))
?.split('=')[1];
if (token) {
// If using database sessions, delete it
// await db.sessions.delete({ where: { token } });
}
// Clear cookie
const response = NextResponse.json({
success: true,
message: 'Logged out successfully',
});
response.cookies.delete('token');
return response;
}
// ✅ Clear auth cookie
// ✅ Optional: invalidate in database
// ✅ Return successLogin Page with Form
'use client';
import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const returnTo = searchParams.get('returnTo') || '/dashboard';
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setLoading(true);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (!response.ok) {
setError(data.error || 'Login failed');
setLoading(false);
return;
}
// Redirect to intended page
router.push(returnTo);
router.refresh(); // Refresh to update auth state
} catch (err) {
setError('An error occurred');
setLoading(false);
}
}
return (
<div className="flex min-h-screen items-center justify-center">
<form onSubmit={handleSubmit} className="w-full max-w-md space-y-4">
<h1 className="text-2xl font-bold">Login</h1>
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded">
{error}
</div>
)}
<div>
<label className="block mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full border p-2 rounded"
required
/>
</div>
<div>
<label className="block mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full border p-2 rounded"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
);
}
// ✅ Login form
// ✅ Error handling
// ✅ Redirect to returnTo
// ✅ Loading stateAuthentication File Structure
Complete auth setup with middleware
Select a file or folder to see details
Reading User Data in Pages
Server Component Access
import { headers } from 'next/headers';
export default async function DashboardPage() {
// Read user info from headers set by middleware
const headersList = headers();
const userId = headersList.get('x-user-id');
const userRole = headersList.get('x-user-role');
if (!userId) {
// Should not happen if middleware is working
return <div>Unauthorized</div>;
}
// Fetch user data
const user = await db.users.findUnique({
where: { id: userId },
});
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.name}!</p>
<p>Role: {userRole}</p>
</div>
);
}
// ✅ Read headers from middleware
// ✅ Fetch user data
// ✅ Type-safeAPI Route Access
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// Read user info from headers
const userId = request.headers.get('x-user-id');
const userRole = request.headers.get('x-user-role');
if (!userId) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Use user info
const data = await fetchUserData(userId);
return NextResponse.json({
data,
user: { id: userId, role: userRole },
});
}
// ✅ Headers from middleware
// ✅ No additional auth check needed
// ✅ User info availableAdvanced Authentication Patterns
Refresh Token Pattern
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyJWT, createJWT } from '@/lib/jwt';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith('/dashboard')) {
const accessToken = request.cookies.get('accessToken')?.value;
const refreshToken = request.cookies.get('refreshToken')?.value;
if (!accessToken && !refreshToken) {
return redirectToLogin(request);
}
// Try to verify access token
let payload = accessToken ? await verifyJWT(accessToken) : null;
// Access token expired, try refresh token
if (!payload && refreshToken) {
const refreshPayload = await verifyJWT(refreshToken);
if (refreshPayload) {
// Create new access token
const newAccessToken = await createJWT({
userId: refreshPayload.userId,
email: refreshPayload.email,
role: refreshPayload.role,
});
const response = NextResponse.next();
response.cookies.set('accessToken', newAccessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 15, // 15 minutes
});
return response;
}
}
if (!payload) {
return redirectToLogin(request);
}
// Continue with user info
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.userId);
return NextResponse.next({
request: { headers: requestHeaders },
});
}
return NextResponse.next();
}
function redirectToLogin(request: NextRequest) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
// ✅ Short-lived access token
// ✅ Long-lived refresh token
// ✅ Automatic token refresh
// ✅ Better securityMulti-Tenant Authentication
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Extract tenant from subdomain or path
const hostname = request.headers.get('host') || '';
const subdomain = hostname.split('.')[0];
// Or from path: /tenant/acme/dashboard
const tenantFromPath = pathname.split('/')[2];
const tenantId = subdomain !== 'www' ? subdomain : tenantFromPath;
if (pathname.startsWith('/dashboard')) {
const token = request.cookies.get('token')?.value;
if (!token) {
return redirectToLogin(request);
}
const payload = await verifyJWT(token);
if (!payload) {
return redirectToLogin(request);
}
// Verify user belongs to tenant
const hasAccess = await userHasTenantAccess(
payload.userId,
tenantId
);
if (!hasAccess) {
return NextResponse.redirect(
new URL('/unauthorized', request.url)
);
}
// Pass tenant info
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-tenant-id', tenantId);
requestHeaders.set('x-user-id', payload.userId);
return NextResponse.next({
request: { headers: requestHeaders },
});
}
return NextResponse.next();
}
async function userHasTenantAccess(
userId: string,
tenantId: string
): Promise<boolean> {
const membership = await db.tenantMemberships.findFirst({
where: { userId, tenantId },
});
return !!membership;
}
// ✅ Extract tenant from subdomain/path
// ✅ Verify tenant access
// ✅ Pass tenant ID to pages
// ✅ Multi-tenant supportAuthentication Best Practices
1. Keep Middleware Fast
// ✅ GOOD: Simple JWT verification
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) return redirectToLogin(request);
const payload = await verifyJWT(token); // Fast
if (!payload) return redirectToLogin(request);
return NextResponse.next();
}
// ❌ BAD: Database queries in middleware
export async function middleware(request: NextRequest) {
const userId = await getUserFromToken(); // Slow!
const user = await db.users.findUnique({ where: { id: userId } }); // Slow!
const permissions = await db.permissions.findMany({ ... }); // Slow!
// Middleware runs on EVERY request - keep it fast!
}
// Do complex checks in pages/API routes, not middleware2. Use Secure Cookie Settings
// ✅ GOOD: Secure cookie settings
response.cookies.set('token', token, {
httpOnly: true, // Prevent JavaScript access
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
});
// ❌ BAD: Insecure settings
response.cookies.set('token', token, {
httpOnly: false, // Vulnerable to XSS!
secure: false, // Works on HTTP - dangerous!
sameSite: 'none', // CSRF vulnerable!
});
// Always use secure settings in production3. Don't Expose Sensitive Info
// ✅ GOOD: Generic error messages
if (!payload) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// ❌ BAD: Specific error messages
if (!payload) {
return NextResponse.json(
{ error: 'JWT signature verification failed' }, // Too specific!
{ status: 401 }
);
}
// Don't reveal implementation details4. Handle Edge Cases
// ✅ GOOD: Handle all cases
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Prevent redirect loops
if (pathname === '/login') {
return NextResponse.next();
}
// Handle missing token
const token = request.cookies.get('token')?.value;
if (!token) {
return redirectToLogin(request);
}
// Handle invalid token
const payload = await verifyJWT(token);
if (!payload) {
const response = redirectToLogin(request);
response.cookies.delete('token'); // Clear invalid token
return response;
}
// Handle expired token
if (payload.exp && payload.exp < Date.now() / 1000) {
const response = redirectToLogin(request);
response.cookies.delete('token');
return response;
}
return NextResponse.next();
}
// Handle all edge cases properly5. Use Environment Variables for Secrets
// ✅ GOOD: Environment variable
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
throw new Error('JWT_SECRET not configured');
}
// ❌ BAD: Hard-coded secret
const JWT_SECRET = 'my-secret-key-123'; // NEVER!
// Always use environment variables for secretsKey Takeaways
- Middleware runs first - perfect for authentication
- Session or JWT - both work in middleware
- Pass user data via headers - available in pages/routes
- Role-based access control - check permissions in middleware
- Secure cookies - httpOnly, secure, sameSite
- Keep it fast - no heavy operations
- Handle edge cases - expired tokens, missing cookies
- Generic error messages - don't expose internals
What's Next?
You've mastered authentication with middleware! Next, we'll explore Environment Variables and Configuration—managing secrets, configuring different environments, using NEXT_PUBLIC variables, and building secure, configurable applications. You'll learn proper configuration management!
We'll cover .env files, environment-specific configs, accessing variables in different contexts, and security best practices.
🔐 Security Reminder
Never store JWT secrets in code, always validate tokens before trusting them, use httpOnly cookies to prevent XSS, implement CSRF protection with sameSite, and rotate secrets regularly. Security is critical for authentication!