next.jsauthorizationmiddlewarenext-auth

Middleware not triggering in Next.js app using next-auth


I’m working on a Next.js 14 app with the App Router and next-auth@5. I followed the official Next.js Learn guide on authentication, except I replaced email with username in the database.

I set up middleware.ts, auth.config.ts, and auth.ts (merged the last two into one file).

However, the middleware never triggers — I placed console.log statements inside it and none run. There’s no error in the browser or the server, just no effect.

I suspect I’m missing a required export or config somewhere, but I can’t pinpoint it.


Here’s a simplified version of my setup:

// middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';

export default NextAuth(authConfig).auth;

export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
// auth.ts
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import NextAuth from 'next-auth';
import sql from "@/app/lib/data";
import bcrypt from 'bcrypt';
import { z } from 'zod';

async function getUser(email: string) {
  try {
    const user = await sql`SELECT * FROM users WHERE username=${email}`;
    return user[0];
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw new Error('Failed to fetch user.');
  }
}

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);

        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await getUser(email);
          if (!user) return null;
          const passwordsMatch = await bcrypt.compare(password, user.password);

          if (passwordsMatch) return user;
        }

        console.log('Invalid credentials');
        return null;
      },
    }),
  ],
});
//auth.config.ts
import type { NextAuthConfig } from 'next-auth';

export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    },
  },
  providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
// /src/app/dashboard/page.tsx
export default function Page() {
    return (
        <>
            <p>Dashboard page</p>
        </>
    )
}
// /src/app/login/page.tsx
"use client";

import { useActionState } from "react";
import { authenticate } from "@/app/lib/actions";
import { useSearchParams } from "next/navigation";

export default function LoginForm() {
  const searchParams = useSearchParams();
  const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";
  const [errorMessage, formAction, isPending] = useActionState(
    authenticate,
    undefined,
  );

  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>

      <input
        id="email"
        type="email"
        name="email"
        placeholder="Enter your email address"
        required
      />

      <label htmlFor="password">Password</label>

      <input
        id="password"
        type="password"
        name="password"
        placeholder="Enter password"
        required
        minLength={6}
      />

      <input type="hidden" name="redirectTo" value={callbackUrl} />
      <button aria-disabled={isPending}>Log in</button>
      {errorMessage && (
        <>
          <p>{errorMessage}</p>
        </>
      )}
    </form>
  );
}

Solution

  • resolved:
    change location root/middleware.ts -> src/middleware.ts