a black and white image of set of keys on a table.

How to Add Route Protection to Next.js with Auth.js

You’ve decided to add authorization to your Next.js app using Auth.js, but you’re not sure where to start. No problem I’ve got you covered. Before adding authorization to your, app you must first have authentication setup.

If you don’t already have this set up you can check out my article on this topic: How To Set Up Auth.js In Next.js.

If you already have user Authentication setup in your app, great we can get started. As discussed in a previous article Authentication vs Authorization; authorization is about deciding what a user is allowed to access once they are in our app or system.

In this Article I will break down the steps to adding authorization to your app using Auth.js, however I want to cover some critical info first.

What We’re Building

We’ll protect routes in your Next.js app so only authenticated users can access them. Users who aren’t logged in will be redirected to the login page.

🚨 Critical Heads Up

Never use Next.js Middleware as your only line of defense. Always use a layered approach:

  1. Middleware for initial checks
  2. Server-side session validation in protected pages
  3. API route protection

Next.js had a vulnerability (now patched) that allowed bypassing middleware.

Read more: Protects against vulnerability.

🛑 2025 Security Update: Proximity Principle (12-01-2025)

While using Next.js Middleware for redirects is still common, official security guidance now strongly recommends deferring all critical authentication checks (like calling await auth()) to the final destination:

  • Server Components (page.tsx)
  • Server Actions ('use server')
  • API Routes (route.ts)

Middleware should only be used for optimistic, lightweight checks and redirects. The Server Component check in Step 4 is your primary, non-bypassable security defense.

Step 1: Create the Middleware File

Create a file at the root of your project: /src/middleware.ts (or /middleware.ts if not using src).

Import Required Dependencies

JavaScript
// step 2: tools from Next.js
import { NextRequest, NextResponse } from 'next/server';

// Grab our authentication toolkit
import { auth } from '@lib/auth';

Create the Middleware Function

JavaScript
export default async function middleware(req: NextRequest) {
  // We'll add logic here
  return NextResponse.next();
}

Step 2: Configure Which Routes to Protect

Next.js middleware uses a matcher configuration to determine which routes it should run on. This is efficient because the matcher runs at the edge before your middleware function even executes.

Add this configuration below your middleware function:

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

What this does:

  • Middleware only runs on these specific routes
  • The :path* syntax protects nested routes automatically
    • Example: /dashboard, /dashboard/analytics, /dashboard/settings/billing are all protected
  • Routes not in this list (like /, /about, /login) won’t trigger the middleware at all

To add more protected routes, simply add them to the array:

JavaScript
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/settings/:path*',
    '/admin/:path*',      // ← Add new protected sections here
    '/projects/:path*',
  ],
};

Advanced: Default-Deny with Regex Matcher

If you want to protect all routes by default except specific ones, use this alternative matcher:

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

What this does:

  • Runs middleware on all routes except:
    • /api/* – API routes
    • /_next/static/* – Static files
    • /_next/image/* – Image optimization
    • *.png – PNG images

Step 3: Add Authentication Logic

🛑 2025 Security Update: Skip the Heavy Lifting! (update: 12-01-25)

The Approach is Valid: This logic successfully redirects unauthenticated users fast.

But Don’t Rely on It: Next.js Middleware runs at the Edge, and past vulnerabilities (like CVE-2025-29927) prove it can be bypassed.

The Best Practice:

Skip await auth() here. For security and performance, let Middleware handle the quick redirect (UX), and move the mandatory check to Step 4.

Now we’ll check if the user has a valid session. If not, redirect them to login.

Since the matcher already filters which routes this runs on, our middleware can be simple and focused:

JavaScript
export default async function middleware(req: NextRequest) {
  // Get the user's session
  const session = await auth();
  
  // If no session exists, redirect to login
  if (!session?.user) {
    return NextResponse.redirect(new URL('/login', req.nextUrl));
  }

  // User is authenticated, allow them through
  return NextResponse.next();
}

Complete Middleware Example

Here’s the full middleware file:

JavaScript
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';

export default async function middleware(req: NextRequest) {
  // This only runs for routes matched in config.matcher
  const session = await auth();
  
  if (!session?.user) {
    return NextResponse.redirect(new URL('/login', req.nextUrl));
  }

  return NextResponse.next();
}

// choose the matcher option:
// matcher option 1.  
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/settings/:path*',
    // add any other protected sections here
  ],
};

// or matcher option 2.
export const config = {
  matcher: [
    '/((?:(?!api|_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|gif|svg|webp|ico)$).*)?)/',
  ],
};

That’s it! Your protected routes are now secured with just a few lines of code.

Note

  • You can extend your session with more fields from your OAuth provider. “extending the session” guide.
  • By default, GET requests to the session endpoint will automatically return the headers to prevent caching.

Step 4: THE PRIMARY Security Check (Server-Side Protection)

🚨 Important

Always validate sessions on the server side.

This is your ultimate security check. It cannot be bypassed. The validation in your Server Components or API Routes is the final, non-negotiable step, while middleware only handles quick redirects.

Validate the session here before fetching data. For Server Actions, Pages, and API Routes, make the session check the very first line of code!

For Server Components

In your protected route (e.g., app/dashboard/page.tsx):

JavaScript
import { auth } from "@/lib/auth";

export default async function Dashboard() {
  const session = await auth();
  
  if (!session?.user) {
    return <div>Not authenticated</div>;
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      {/* Your protected content */}
    </div>
  );
}

For API Routes

In your API routes (e.g., app/api/data/route.ts):

JavaScript
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";

export async function GET() {
  const session = await auth();
  
  if (!session?.user) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    );
  }

  // Your protected API logic
  return NextResponse.json({ data: "Protected data" });
}

By following these steps, you now have the knowledge to implement authorization logic in your Next.js app using Auth.js. Just remember the words from Spider-Man’s Uncle Ben, “With great power comes great responsibility”.

Code responsibly, and if you enjoyed this article, feel free to give me a shout on one of my social media channels.