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:
- Manual test: Open a private/incognito browser window and navigate to
/. If you are redirected to/sign-in, the root path is being protected. - Automated test: Add an integration test that issues an unauthenticated
GET /and asserts a200status (not a302redirect).
// 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
| Before | After (recommended) | |
|---|---|---|
Matcher $ position | Bare $ in alternation | ^/$ or explicit callback check |
| Root path for unauth users | May redirect to /sign-in | Serves the root page correctly |
| Other protected routes | Unchanged | Unchanged |
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.