Skip to main content

Next.js Auth Middleware

Authentication middleware is a crucial component in Next.js applications that helps protect routes, manage user sessions, and control access to your application's resources. In this guide, you'll learn how to implement and customize authentication middleware in your Next.js projects.

Introduction to Auth Middleware

Next.js middleware runs before a request is completed, allowing you to modify the response by rewriting, redirecting, or enhancing the request. For authentication purposes, middleware lets you:

  • Protect routes based on user authentication status
  • Redirect unauthenticated users to login pages
  • Validate user sessions on every request
  • Apply role-based access controls
  • Manage authentication tokens and cookies

Middleware runs on the Edge, making it extremely fast and perfect for authentication checks that need to happen on every request.

Getting Started with Next.js Middleware

Basic Middleware Structure

To create authentication middleware in Next.js, you'll need to create a file named middleware.js or middleware.ts in your project's root directory:

typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Your authentication logic here

return NextResponse.next();
}

Configuring Middleware for Specific Routes

You can specify which routes the middleware should run on using the config export:

typescript
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'],
};

Implementing Basic Auth Protection

Let's create a simple authentication middleware that checks for a session token and redirects unauthenticated users:

typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Get the pathname of the request
const path = request.nextUrl.pathname;

// Define public paths that don't require authentication
const isPublicPath = path === '/login' || path === '/register';

// Get the token from cookies
const token = request.cookies.get('authToken')?.value || '';

// Redirect logic
if (!isPublicPath && !token) {
// Redirect to login if trying to access a protected route without a token
return NextResponse.redirect(new URL('/login', request.url));
}

if (isPublicPath && token) {
// Redirect to dashboard if user is already logged in
return NextResponse.redirect(new URL('/dashboard', request.url));
}

return NextResponse.next();
}

export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

This middleware:

  1. Checks if the current path is public (login or register page)
  2. Looks for an authentication token in the cookies
  3. Redirects unauthenticated users to the login page when they try to access protected routes
  4. Redirects authenticated users away from login/register pages if they're already logged in

Working with JWT Tokens

For more secure applications, you'll often use JWT (JSON Web Tokens) for authentication. Here's how to verify a JWT token in your middleware:

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

export async function middleware(request: NextRequest) {
const token = request.cookies.get('authToken')?.value;

// If there's no token and we're not on a public route, redirect to login
if (!token) {
const url = new URL('/login', request.url);
url.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(url);
}

try {
// Verify the JWT token
const secretKey = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secretKey);

// Add user information to headers for use in route handlers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.sub as string);
requestHeaders.set('x-user-role', payload.role as string);

return NextResponse.next({
request: {
headers: requestHeaders,
},
});
} catch (error) {
// Token is invalid, redirect to login
const url = new URL('/login', request.url);
url.searchParams.set('error', 'Your session has expired. Please log in again.');
return NextResponse.redirect(url);
}
}

export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/admin/:path*'],
};

In this example, we:

  1. Extract the JWT token from cookies
  2. Verify the token using the jose library
  3. Extract user information from the token payload
  4. Add this information to request headers for use in API routes
  5. Handle cases where the token is invalid or expired

Role-Based Access Control

You can extend your middleware to implement role-based access control:

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

export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
const token = request.cookies.get('authToken')?.value;

if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}

try {
const secretKey = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secretKey);
const userRole = payload.role as string;

// Check role-based permissions
if (path.startsWith('/admin') && userRole !== 'admin') {
// Redirect non-admins trying to access admin routes
return NextResponse.redirect(new URL('/dashboard', request.url));
}

if (path.startsWith('/moderator') && !['admin', 'moderator'].includes(userRole)) {
// Redirect users without moderator privileges
return NextResponse.redirect(new URL('/dashboard', request.url));
}

return NextResponse.next();
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url));
}
}

export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/admin/:path*', '/moderator/:path*'],
};

This middleware enforces role-based permissions by checking the user's role against the requested path, ensuring users can only access routes appropriate for their permission level.

Real-World Example: Integration with NextAuth.js

Many Next.js applications use NextAuth.js for authentication. Here's how to integrate NextAuth.js with middleware:

typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;

// Check if the path is a protected route
const isProtected =
path.startsWith('/dashboard') ||
path.startsWith('/profile') ||
path.startsWith('/admin');

if (isProtected) {
// Get the NextAuth.js token
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET
});

if (!token) {
const url = new URL('/api/auth/signin', request.url);
url.searchParams.set('callbackUrl', request.nextUrl.pathname);
return NextResponse.redirect(url);
}

// Role-based access control
if (path.startsWith('/admin') && token.role !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}

return NextResponse.next();
}

export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

This middleware seamlessly integrates with NextAuth.js by using its built-in getToken function to verify the user's session.

Performance Considerations

Authentication middleware executes on every request to matched routes, so it's important to keep it efficient:

  1. Be selective with matchers: Only run middleware on routes that need authentication
  2. Minimize dependencies: Use lightweight token verification when possible
  3. Consider caching: For complex permission checks, implement caching strategies
  4. Use Edge runtime: Next.js middleware runs on the Edge by default, which is already optimized for speed

Debugging Authentication Middleware

When your authentication middleware isn't working as expected, here are some debugging tips:

typescript
export function middleware(request: NextRequest) {
console.log('Middleware executing on:', request.nextUrl.pathname);

const token = request.cookies.get('authToken')?.value;
console.log('Token exists:', !!token);

// Rest of your middleware code...
}

You can see these logs in your server console to understand what's happening during the middleware execution.

Summary

Next.js Auth Middleware provides a powerful way to protect your application routes and implement authentication logic at the edge. Key takeaways include:

  • Middleware runs before page rendering, making it ideal for authentication checks
  • You can redirect unauthenticated users, verify tokens, and implement role-based access control
  • The middleware configuration lets you specify which routes should be protected
  • Integration with libraries like NextAuth.js can simplify your authentication implementation

By implementing proper authentication middleware, you can ensure that your Next.js application is secure and that users can only access the resources they're authorized to use.

Additional Resources

Exercises

  1. Implement a basic auth middleware that redirects unauthenticated users to a login page.
  2. Extend your middleware to handle token expiration and refresh tokens.
  3. Create role-based middleware that restricts access to admin routes.
  4. Implement a middleware that adds custom headers based on user permissions.
  5. Build a complete authentication system with Next.js, NextAuth.js, and custom middleware for different user roles.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)