Next.js + Vercel Stack6 min read

Next.js Middleware Authentication: Protecting Routes in 2025

Learn how to implement Next.js middleware authentication to protect routes with JWT validation, role-based access control, and proper redirects in 2025.

By John Hashem

Next.js Middleware Authentication: Protecting Routes in 2025

Protecting routes in your Next.js application is crucial for any production app that handles user data or requires access control. Next.js middleware provides a powerful way to implement authentication checks before requests reach your pages or API routes. This approach runs at the edge, making it faster and more efficient than traditional client-side or server-side authentication guards.

In this guide, you'll learn how to implement robust middleware authentication in Next.js 13+ using the App Router. We'll cover JWT token validation, role-based access control, and proper redirect handling for both authenticated and unauthenticated users. By the end, you'll have a production-ready authentication system that protects your routes efficiently.

Prerequisites

Before implementing nextjs middleware authentication, ensure you have:

  • Next.js 13+ with App Router configured
  • Basic understanding of JWT tokens
  • An authentication system that issues tokens (NextAuth.js, custom JWT, or third-party service)
  • Node.js 18+ for the latest middleware features

Step 1: Create the Middleware File

Start by creating a middleware.ts file in your project root (same level as your app directory). This file will intercept all requests and run your authentication logic.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET)

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  
  // Define protected routes
  const protectedRoutes = ['/dashboard', '/profile', '/admin']
  const isProtectedRoute = protectedRoutes.some(route => 
    pathname.startsWith(route)
  )
  
  if (!isProtectedRoute) {
    return NextResponse.next()
  }
  
  // Continue to authentication logic
  return NextResponse.next()
}

This basic structure identifies protected routes and allows unprotected routes to pass through immediately. The middleware runs on every request, so it's important to return early for routes that don't need authentication.

Step 2: Implement JWT Token Validation

Add JWT validation logic to verify tokens from cookies or headers. This step ensures only users with valid tokens can access protected routes.

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  
  const protectedRoutes = ['/dashboard', '/profile', '/admin']
  const isProtectedRoute = protectedRoutes.some(route => 
    pathname.startsWith(route)
  )
  
  if (!isProtectedRoute) {
    return NextResponse.next()
  }
  
  // Get token from cookie or Authorization header
  const token = request.cookies.get('auth-token')?.value || 
    request.headers.get('authorization')?.replace('Bearer ', '')
  
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  try {
    const { payload } = await jwtVerify(token, JWT_SECRET)
    
    // Add user info to request headers for use in components
    const response = NextResponse.next()
    response.headers.set('x-user-id', payload.userId as string)
    response.headers.set('x-user-role', payload.role as string)
    
    return response
  } catch (error) {
    // Invalid token - redirect to login
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

The verification process checks for tokens in both cookies and Authorization headers, providing flexibility for different authentication flows. When a token is valid, user information is passed to your components through custom headers.

Step 3: Add Role-Based Access Control

Implement role-based permissions to restrict certain routes based on user roles. This is essential for admin panels or premium features.

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  
  // Define route permissions
  const routePermissions = {
    '/dashboard': ['user', 'admin'],
    '/profile': ['user', 'admin'],
    '/admin': ['admin'],
    '/admin/users': ['admin'],
    '/premium': ['premium', 'admin']
  }
  
  const protectedRoute = Object.keys(routePermissions).find(route => 
    pathname.startsWith(route)
  )
  
  if (!protectedRoute) {
    return NextResponse.next()
  }
  
  const token = request.cookies.get('auth-token')?.value || 
    request.headers.get('authorization')?.replace('Bearer ', '')
  
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  try {
    const { payload } = await jwtVerify(token, JWT_SECRET)
    const userRole = payload.role as string
    const requiredRoles = routePermissions[protectedRoute]
    
    if (!requiredRoles.includes(userRole)) {
      return NextResponse.redirect(new URL('/unauthorized', request.url))
    }
    
    const response = NextResponse.next()
    response.headers.set('x-user-id', payload.userId as string)
    response.headers.set('x-user-role', userRole)
    
    return response
  } catch (error) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

This role-based system allows granular control over who can access specific routes. Users with insufficient permissions are redirected to an unauthorized page instead of the login page.

Step 4: Handle Public Routes and Login Redirects

Configure proper handling for users who are already authenticated but try to access login or registration pages. This prevents confusion and improves user experience.

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  
  // Routes that redirect authenticated users
  const authRoutes = ['/login', '/register', '/forgot-password']
  const isAuthRoute = authRoutes.includes(pathname)
  
  const routePermissions = {
    '/dashboard': ['user', 'admin'],
    '/profile': ['user', 'admin'],
    '/admin': ['admin']
  }
  
  const protectedRoute = Object.keys(routePermissions).find(route => 
    pathname.startsWith(route)
  )
  
  const token = request.cookies.get('auth-token')?.value
  
  // Handle authenticated users on auth routes
  if (isAuthRoute && token) {
    try {
      await jwtVerify(token, JWT_SECRET)
      return NextResponse.redirect(new URL('/dashboard', request.url))
    } catch (error) {
      // Invalid token, let them access auth routes
      return NextResponse.next()
    }
  }
  
  // Handle protected routes
  if (protectedRoute) {
    if (!token) {
      const loginUrl = new URL('/login', request.url)
      loginUrl.searchParams.set('callbackUrl', pathname)
      return NextResponse.redirect(loginUrl)
    }
    
    try {
      const { payload } = await jwtVerify(token, JWT_SECRET)
      const userRole = payload.role as string
      const requiredRoles = routePermissions[protectedRoute]
      
      if (!requiredRoles.includes(userRole)) {
        return NextResponse.redirect(new URL('/unauthorized', request.url))
      }
      
      const response = NextResponse.next()
      response.headers.set('x-user-id', payload.userId as string)
      response.headers.set('x-user-role', userRole)
      
      return response
    } catch (error) {
      const loginUrl = new URL('/login', request.url)
      loginUrl.searchParams.set('callbackUrl', pathname)
      return NextResponse.redirect(loginUrl)
    }
  }
  
  return NextResponse.next()
}

The callback URL parameter ensures users are redirected to their intended destination after successful login. This creates a seamless authentication flow.

Step 5: Configure Middleware Matcher

Optimize performance by specifying which routes should trigger middleware execution. This prevents unnecessary runs on static assets and API routes that don't need authentication.

export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - public folder files
     */
    '/((?!_next/static|_next/image|favicon.ico|public/).*)',
  ],
}

Place this config export at the bottom of your middleware file. The matcher pattern ensures middleware only runs on actual page routes and API endpoints that might need authentication.

Step 6: Access User Data in Components

Retrieve user information in your React components using the headers set by middleware. This approach works with both Server and Client Components.

// app/dashboard/page.tsx
import { headers } from 'next/headers'

export default function Dashboard() {
  const headersList = headers()
  const userId = headersList.get('x-user-id')
  const userRole = headersList.get('x-user-role')
  
  return (
    <div>
      <h1>Dashboard</h1>
      <p>User ID: {userId}</p>
      <p>Role: {userRole}</p>
      {userRole === 'admin' && (
        <a href="/admin">Admin Panel</a>
      )}
    </div>
  )
}

For Client Components, you'll need to pass this data as props from a Server Component or fetch it separately. The middleware headers provide a reliable way to access authenticated user data throughout your application.

Common Mistakes and Troubleshooting

Avoid these frequent issues when implementing Next.js middleware authentication:

Middleware running on static assets: Always use the matcher config to exclude static files. Running authentication logic on CSS, JS, and image files wastes resources and can cause unexpected behavior.

Infinite redirect loops: Be careful with redirect logic, especially when handling authentication routes. Always check if a user is already authenticated before redirecting them away from login pages.

JWT secret handling: Never hardcode JWT secrets. Use environment variables and ensure they're properly loaded in your middleware environment. The jose library requires TextEncoder format for secrets.

If you're building authentication systems with AI assistance, check out our Claude Code Authentication: JWT vs NextAuth Implementation Guide for comparing different approaches. For production deployment considerations, our Claude Code Production Deployment: Complete Pipeline Setup Guide covers security best practices.

Next Steps

After implementing middleware authentication, consider these enhancements:

Add token refresh logic to handle expired JWTs gracefully. Implement session management with automatic logout for inactive users. Set up proper error handling and logging for authentication failures.

Consider implementing rate limiting in your middleware to prevent brute force attacks. You can also add device tracking and suspicious activity detection for enhanced security.

For comprehensive security practices, review our Claude Code Security Checklist: Protect Your AI-Generated MVP to ensure your authentication system meets production standards. When building complex applications, our Next.js Server Actions with Prisma: Complete CRUD Tutorial shows how to integrate authenticated routes with database operations.

Ready to build something great?

Let's talk about your project. I offer 1-week MVP sprints, fractional CTO services, and Claude Code consulting.

View All Services