Middleware for next-auth and i18n (next.js 13 with app router) – Next.js

by
Ali Hasan
next-auth next.js

Quick Fix: Combine both middleware into a single function that performs locale checking and user authentication. Use match() function to determine a locale from accept-language header and redirect if a supported locale is missing in the pathname.

The Problem:

A user has a Next.js 13 project with App Router and NextAuth set up. They aim to incorporate internationalization (i18n) using the method outlined in the Next.js documentation. The challenge arises in merging the middleware for NextAuth, which protects specific routes, with the middleware for i18n, which handles locale detection and redirection. The existing i18n middleware appears functional, but the NextAuth routes are no longer protected. The user seeks assistance in combining these two middleware solutions effectively.

The Solutions:

Solution 1: Combine next-auth and i18n middleware

The provided code combines the middleware for next-auth and i18n by first handling the i18n logic and then checking if authentication is required using the next-auth middleware.

Here’s a breakdown and explanation:

  1. Importing Necessary Libraries:

    import { NextResponse } from 'next/server';
    import { match } from '@formatjs/intl-localematcher';
    import Negotiator from 'negotiator';
    import nextAuthMiddleware from "next-auth/middleware";
    
  2. Defining Locale-Related Variables:

    let locales = ['en', 'de'];
    let defaultLocale = 'en';
    
    • locales: An array of supported locales.
    • defaultLocale: The default locale to use if no preferred locale is found.
  3. getLocale Function:

    function getLocale(request) {
        let headers = { 'accept-language': 'en' };
        let languages = new Negotiator({ headers }).languages();
        return match(languages, locales, defaultLocale); // -> 'en'
    }
    
    • This function determines the preferred locale based on the Accept-Language header in the request and returns the matched locale.
  4. middleware Function:

    export function middleware(request) {
        // cancel if exception
        const pathname = request.nextUrl.pathname;
        const isException = ['/img', '/preview', '/icons', '/logo.svg', '/api', '/manifest.json', '/sw.js'].some((allowedPath) =>
            pathname.startsWith(`${allowedPath}`),
        );
        if (isException) return;
    
        // Check if there is any supported locale in the pathname
        const pathnameIsMissingLocale = locales.every(
            (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
        );
    
        // Redirect if there is no locale
        if (pathnameIsMissingLocale) {
            const locale = getLocale(request);
            return NextResponse.redirect(
                new URL(`/${locale}/${pathname}`, request.url)
            );
        }
    
        // check if auth is required
        if (pathname.includes("/user")) {
            // check & handle if logged in
            const nextAuthResponse = nextAuthMiddleware(request);
            if (nextAuthResponse) {
                return nextAuthResponse;
            }
        }
    
        // Continue if no NextAuth middleware response
        return NextResponse.next();
    }
    
    • This function is the central middleware that handles both i18n and next-auth.
    • It first checks if the request path is an exception (e.g., image files, API endpoints, etc.) and skips further processing if it is.
    • If no locale is found in the pathname, it determines the preferred locale using getLocale and redirects the request to the correct version (e.g., /en/products).
    • If the path includes "/user", it checks if authentication is required using the nextAuthMiddleware. If authentication is required, it returns the response from nextAuthMiddleware.
    • If authentication is not required or already handled by nextAuthMiddleware, it continues the request by returning NextResponse.next().
  5. config Object:

    export const config = {
        matcher: [
            '/((?!_next).*)',
        ],
    };
    
    • This configures the middleware to match all routes except those that start with /_next. This ensures that the middleware is applied to all relevant pages in the application.

By combining the i18n and next-auth middleware in this manner, you can ensure that users are redirected to the correct locale version of the site and that authentication is handled appropriately for protected routes.

Solution 2: Using NextAuth and Next-i18n-Router Middleware

To combine the NextAuth and i18n middleware effectively, follow these steps:

  1. Install Dependencies:

    • Install next-auth and next-i18n-router:
      npm install next-auth next-i18n-router
      
  2. Configure Middleware:

    • Create a middleware.ts file to handle authorization and i18n:

      import NextAuth from "next-auth";
      import { authConfig } from "./auth.config";
      
      export default NextAuth(authConfig).auth;
      
      export const config = {
        matcher: "/((?!api|static|.*\\..*|_next).*)",
      };
      
  3. Create auth.config.ts File:

    • This file will combine the NextAuth and i18n configurations:

      import { i18nRouter } from "next-i18n-router";
      import type { NextAuthConfig } from "next-auth";
      import { i18nConfig } from "@/locale/config";
      
      const getLocaleFromPath = (pathname: string) => {
        const localeFromPathRegex = new RegExp(`^/(${i18nConfig.locales.join("|")})?`);
        const localeFromPath = pathname.match(localeFromPathRegex)?.[1];
        return { locale: localeFromPath, path: localeFromPath ? `/${localeFromPath}` : "" };
      };
      
      const checkCurrentRoute = (pathname: string, locale?: string) => {
        const checkPathnameRegex = (pattern: string | RegExp) => {
          const rootRegex = new RegExp(pattern);
          return Boolean(pathname.match(rootRegex));
        };
      
        return {
          root: checkPathnameRegex(`^/(${locale})?$`),
          dashboard: checkPathnameRegex(`^(/${locale})?/dashboard.*`),
          login: checkPathnameRegex(`^(/${locale})?/login.*`),
        };
      };
      
      export const authConfig = {
        pages: {
          signIn: "/login",
        },
        callbacks: {
          authorized({ auth, request }) {
            const { nextUrl } = request;
      
            const locale = getLocaleFromPath(nextUrl.pathname);
            const dashboardUrl = new URL(`${locale.path}/dashboard`, nextUrl);
      
            const { root: isOnRoot, dashboard: isOnDashboard, login: isOnLogin } =
              checkCurrentRoute(nextUrl.pathname, locale.locale);
      
            const isLoggedIn = !!auth?.user;
      
            if (isOnRoot || (isLoggedIn && !isOnDashboard)) {
              // If on root or logged in but not on dashboard, redirect to dashboard
              return Response.redirect(dashboardUrl);
            }
      
            if ((isOnLogin && !isLoggedIn) || (isOnDashboard && isLoggedIn)) {
              // Not logged in but on login OR logged in and on dashboard -> allow access
              return i18nRouter(request, i18nConfig);
            }
      
            // Not logged in and not on login or dashboard -> redirect to login page
            return false;
          },
        },
      
        providers: [],
      } satisfies NextAuthConfig;
      
  4. Create @/locale/config.ts File:

    • This file will contain the i18n configuration:

      export const i18nConfig = {
        locales: ["en", "es"],
        defaultLocale: "en",
      };
      
  5. Configure Middleware in pages/_middleware.js:

    • Add the middleware function to the pages/_middleware.js file:

      export { default } from "next-auth/middleware";
      

With this setup, you can effectively combine NextAuth for user authentication and next-i18n-router for internationalization in your Next.js 13 application.

Solution 3: Route and locale-based URL redirection

Create a single middleware file and specify two different matchers in the middleware configuration. The first matcher should match all requests that need to be protected by NextAuth, and the second matcher should match all requests that need to be redirected to the correct locale.

Inside the middleware function:

  1. Check if the pathname contains a supported locale. If not, redirect to the default locale.
  2. Determine the user’s locale and set it as the request’s pathname.
  3. Return the modified request object.

In the middleware.js file:

export function middleware(request) {
  let locales = ['en', 'fr'];
  let defaultLocale = 'en';

  const isPathAllowed = [
    '/img', 'img', '/api'
  ].some(allowedPath =>
    pathname.startsWith(`${allowedPath}`)
  );

  if (isPathAllowed) return;

  // Redirect if there is no locale
  const locale = defaultLocale;
  request.nextUrl.pathname = `/${locale}${pathname}`;
  return Response.redirect(request.nextUrl);
}

export const config = {
  matcher: [
    '//((?!_next).*)//',
    '//(/)//'
  ],
};

This middleware will redirect any request that does not have a supported locale in the pathname to the default locale. It will also redirect any request to the root URL to the default locale.

In your messages.js file, define the localized strings for each language:

import en from './en.json';
import fr from './fr.json';

export const getSelectedLanguage = (lang) => {
  switch (lang) {
    case 'en':
      return en;
    case 'fr':
      return fr;
    default:
      return null;
  }
};

In your layout component (layout.js), use the NextIntlClientProvider to provide the selected language and localized strings to the child components:

import { SessionProvider } from 'next-auth/react';
import { NextIntlClientProvider } from 'next-intl';
import { getSelectedLanguage } from '@/internationalization/messages/messages';

export default function RootLayout({ children, params: { lang } }) {
  const messages = getSelectedLanguage(lang);

  return (
    <html lang={lang}>
      <body>
        <NextIntlClientProvider locale={lang} messages={messages}>
          <SessionProvider>{children}</SessionProvider>
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

In your pages, use the useLocale and useTranslations hooks from next-intl to access the current locale and localized strings:

import Link from 'next/link';
import { useLocale, useTranslations } from 'next-intl';

const HomePage = () => {
  const t = useTranslations('Index');
  const locale = useLocale();

  return (
    <>
      <main>
          <div className="container">
            <h1>{t('title')}</h1>
          </div>
      </main>
   </>
  );
}

With this setup, your application will automatically redirect users to the correct locale based on their preferred language, and it will provide localized strings for the user interface.

Q&A

Combine next-auth & i18n (next.js 13) middleware to protect routes and enable internationalization.

Try leveraging NextAuth’s middleware configuration. Mix authorized handler and i18nRouter in the auth.config file.

I’ve tried combining next-auth & i18n middleware, but only i18n seems to be working. Why?

Make sure the paths for both middlewares don’t overlap. Use a different matcher regex for each middleware.

What if I want to protect the /user path and its sub-routes but allow other routes?

Use the matcher configuration in the next-auth middleware to specify the protected paths.

Video Explanation:

The following video, titled "Chain NextAuth and Internationalization Middlewares in NextJs 14 ...", provides additional insights and in-depth exploration related to the topics discussed in this post.

Play video

This video will look at chaining NextAuth and Internationalization (i18n) middleware functions in NextJs to run one after the other, ...