All Docs
FeaturesAgentOS WorkUpdated March 11, 2026

Verifying Auth Middleware Root-Path Access in Next.js (v1.0.14)

Verifying Auth Middleware Root-Path Access in Next.js

Release: v1.0.14

Background

Our platform uses next-auth v5's auth middleware to protect routes and redirect unauthenticated users to /sign-in. The middleware is configured in src/middleware.ts with a matcher pattern that whitelists certain public paths.

In v1.0.14 we identified a subtle but important issue in that matcher pattern that could affect whether unauthenticated users can reach the root path (/) of the application.


The Issue

The matcher pattern in use was:

/((?!api/auth|_next/static|_next/image|favicon.ico|sign-in|sign-up|$).*)

This is a negative lookahead that tells Next.js middleware to run on every path except those matching the listed alternatives. The intent of the trailing $ alternative is to exclude the root path /.

However, $ used inside a regex alternation group in this position is non-standard. Depending on the regex engine and how Next.js parses the matcher, $ may not reliably match the root path, causing the middleware to execute on GET / and redirect unauthenticated visitors to /sign-in.


Why This Matters

next-auth v5's auth() middleware redirects all matched, unauthenticated requests to /sign-in. If the root path / is unintentionally matched:

  • Unauthenticated users see a login page instead of a public homepage or landing page.
  • SEO crawlers and link-preview bots may be incorrectly bounced.
  • Any public-facing marketing content at / becomes inaccessible without a session.

How to Verify

Before applying any code change, confirm whether the issue is actually present in your deployment:

  1. Manual test: Open a private/incognito browser window and navigate to /. If you are redirected to /sign-in, the root path is being protected.
  2. Automated test: Add an integration test that issues an unauthenticated GET / and asserts a 200 status (not a 302 redirect).
// Example using Playwright
test('root path is accessible without auth', async ({ page }) => {
  // Start with no session cookies
  await page.context().clearCookies();
  const response = await page.goto('/');
  expect(response?.status()).toBe(200);
  expect(page.url()).not.toContain('/sign-in');
});

Recommended Fix

If verification confirms the root path is being protected, update src/middleware.ts using one of the following approaches:

Option A — Replace $ with ^/$

// src/middleware.ts
export const config = {
  matcher: [
    '/((?!api/auth|_next/static|_next/image|favicon.ico|sign-in|sign-up|^/$).*)',
  ],
};

^/$ is explicit: it anchors to the start and end of the string, unambiguously matching only the root path.

Option B — Exclude / inside the auth() callback

Alternatively, keep the matcher as-is and handle the root path as a public route in the auth callback:

// src/middleware.ts
import { auth } from '@/auth';

export default auth((req) => {
  const { nextUrl, auth: session } = req;

  // Allow unauthenticated access to the root path
  if (nextUrl.pathname === '/') return;

  if (!session) {
    return Response.redirect(new URL('/sign-in', nextUrl));
  }
});

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

Option B is often preferable when you need fine-grained, per-path logic beyond what the matcher regex can easily express.


Summary

BeforeAfter (recommended)
Matcher $ positionBare $ in alternation^/$ or explicit callback check
Root path for unauth usersMay redirect to /sign-inServes the root page correctly
Other protected routesUnchangedUnchanged

This is a low-risk, high-impact fix. If your root path is intended to be fully public, apply the change and add a regression test to prevent future middleware adjustments from accidentally re-protecting it.