Programming

【Next.js】Configuring NextAuth with Multilingual Support

Overview

In a previous article, I explained how to implement multilingual (i18n) support in Next.js.

This article focuses on implementing NextAuth while maintaining the multilingual setup described earlier. Before diving into this article, I recommend reviewing and implementing the steps in the previous article to ensure a solid foundation.

My goal with this article is to help anyone struggling with setting up NextAuth alongside multilingual support.

What is NextAuth?

NextAuth is a library that simplifies the implementation of authentication (login and logout) in Next.js applications. You can find more details on the official NextAuth website:

NextAuth Official Website

Using NextAuth eliminates the need to build authentication from scratch, allowing developers to focus more on app development. Additionally, it supports many authentication providers (Google, Facebook, GitHub, Twitter, etc.), making it easier to implement the desired authentication methods for your app.

Integrating NextAuth

To start, navigate to your project directory and run the following command to install NextAuth (if you're using npm, replace yarn add with npm install):

yarn add next-auth

After installation, paste the following code into src/lib/auth.ts.

Normally, the authorize and signOut functions would include processes to call backend login and sign-out APIs. However, for this explanation focusing on the integration with multilingual support, we’ll implement a simple frontend-only version without backend interactions.

import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export const authOptions: NextAuthOptions = {
  pages: {
    signIn: "/",
  },
  session: {
    strategy: "jwt",
    maxAge: 24 * 60 * 60, // 1日 (秒単位)
  },
  providers: [
    CredentialsProvider({
      name: "credentials",
      credentials: {
        username: {
          label: "Username",
          type: "text",
        },
        password: {
          label: "Password",
          type: "password",
        },
      },
      async authorize(credentials) {
        return { id: `${credentials?.username}:${credentials?.password}` };
      },
    }),
  ],
};

Next, create a folder at src/app/api/auth/[...nextauth] and create a route.ts file with the following content.

import { authOptions } from '@/lib/auth';
import NextAuth from 'next-auth';

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

With this, the NextAuth setup is complete.

Let’s integrate it with multilingual support!

When combining with the multilingual support shared above, one challenging aspect is configuring Next.js middleware.

You’ll need to write code to chain and execute each configured middleware. Create a folder named src/middlewares and then create a chain.ts file with the following code.

import { NextMiddlewareResult } from "next/dist/server/web/types";
import type { NextFetchEvent, NextRequest } from "next/server";
import { NextResponse } from "next/server";

export type CustomMiddleware = (
  request: NextRequest,
  event: NextFetchEvent,
  response: NextResponse
) => NextMiddlewareResult | Promise<NextMiddlewareResult>;

type MiddlewareFactory = (middleware: CustomMiddleware) => CustomMiddleware;
export function chain(
  functions: MiddlewareFactory[],
  index = 0
): CustomMiddleware {
  const current = functions[index];
  if (current) {
    const next = chain(functions, index + 1);
    return current(next);
  }

  return (
    request: NextRequest,
    event: NextFetchEvent,
    response: NextResponse
  ) => {
    return response;
  };
}

Next, let's configure AuthNext. Create the following withAuthMiddleware.ts file in the src/middlewares directory.

import { i18nConfig } from "@/i18n/config";
import { getToken } from "next-auth/jwt";
import type { NextFetchEvent, NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { CustomMiddleware } from "./chain";
import { getLocale, isMissingLocale } from "./withI18nMiddleware";

const authRoutes = ["/home"];
const guestRoutes = ["/"];
export function withAuthMiddleware(
  middleware: CustomMiddleware
): CustomMiddleware {
  return async (
    request: NextRequest,
    event: NextFetchEvent,
    response: NextResponse
  ) => {
    const token = await getToken({
      req: request,
      secret: process.env.NEXTAUTH_SECRET,
    });

    const pathname = request.nextUrl.pathname;
    const isIndexPage = pathname === getPathWithLocale(request, "/");
    const isAuthRoute = authRoutes.some((route) =>
      pathname.startsWith(getPathWithLocale(request, route))
    );
    const isGuestRoute = guestRoutes.some(
      (route) => pathname === getPathWithLocale(request, route)
    );

    if (!token && isAuthRoute) {
      const redirectUrl = new URL(getPathWithLocale(request, "/"), request.url);
      redirectUrl.searchParams.set("callbackUrl", request.nextUrl.href);
      return NextResponse.redirect(redirectUrl);
    }

    if (token) {
      if (isIndexPage || isGuestRoute) {
        return NextResponse.redirect(
          new URL(getPathWithLocale(request, "/home"), request.url)
        );
      }
    }

    return middleware(request, event, response);
  };
}

export function getPathWithLocale(request: NextRequest, path: string): string {
  const pathname = request.nextUrl.pathname;
  const locale = getLocale(request);
  if (isMissingLocale(pathname) && locale !== i18nConfig.defaultLocale) {
    return `/${locale}${path}`;
  }
  return path;
}

Next, let's configure the multilingual support. Create the following withI18nMiddleware.ts file in the src/middlewares directory.

import { i18nConfig } from "@/i18n/config";
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
import { i18nRouter } from "next-i18n-router";
import type { NextFetchEvent, NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { CustomMiddleware } from "./chain";

export function withI18nMiddleware(middleware: CustomMiddleware) {
  return async (
    request: NextRequest,
    event: NextFetchEvent,
    response: NextResponse
  ) => {
    const pathname = request.nextUrl.pathname;
    const locale = getLocale(request);
    if (isMissingLocale(pathname) && locale !== i18nConfig.defaultLocale) {
      const redirectURL = new URL(request.url);
      if (locale) {
        redirectURL.pathname = `/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`;
      }

      // Saving Query Parameters
      redirectURL.search = request.nextUrl.search;

      return NextResponse.redirect(redirectURL.toString());
    }

    return middleware(request, event, i18nRouter(request, i18nConfig));
  };
}

export function isMissingLocale(pathname: string): boolean {
  return i18nConfig.locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );
}

export function getLocale(request: NextRequest): string | undefined {
  if (isMissingLocale(request.nextUrl.pathname)) {
    return i18nConfig.defaultLocale;
  } else {
    const negotiatorHeaders: Record<string, string> = {};
    request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));

    const locales: string[] = i18nConfig.locales;
    const languages = new Negotiator({
      headers: negotiatorHeaders,
    }).languages();

    const locale = matchLocale(languages, locales, i18nConfig.defaultLocale);
    return locale;
  }
}

Create a src/middleware.ts file and implement the following code.

import { chain } from "./middlewares/chain";
import { withAuthMiddleware } from "./middlewares/withAuthMiddleware";
import { withI18nMiddleware } from "./middlewares/withI18nMiddleware";

export default chain([withI18nMiddleware, withAuthMiddleware]);

// only applies this middleware to files in the app directory
export const config = {
  matcher: "/((?!api|static|.*\\..*|_next).*)",
};

Create a src/[lang]/page.tsx file and implement the following code.

"use client";

import { initI18n } from "@/i18n/config";
import { t } from "i18next";
import { signIn } from "next-auth/react";

const i18n = initI18n();
export default function Page({
  params: { lang },
}: {
  params: {
    lang: string;
  };
}) {
  // Multilingual settings
  i18n.changeLanguage(lang);
  const hello = t("hello");
  // Login process
  const clickHandler = async () => {
    await signIn("credentials", {
      username: "",
      password: "",
      callbackUrl: "/home",
      redirect: true,
    });
  };
  return (
    <body>
      <h1>{hello}</h1>
      <button onClick={clickHandler}>login</button>
    </body>
  );
}

Create a src/[lang]/home/page.tsx file and implement the following code.

"use client";

import { signOut } from "next-auth/react";

export default function Page() {
  // Logout process
  const clickHandler = async () => {
    signOut({ callbackUrl: "/" });
  };
  return (
    <body>
      <h1>home</h1>
      <button onClick={clickHandler}>logout</button>
    </body>
  );
}

Finally, add the following settings to .env.local, and you're all set!

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=<A random string generated by executing openssl rand -base64 32>

Let's give it a try!

First, navigate to localhost:3000/home. Since you're not logged in, you should be redirected to the top screen as shown below. (In the previous multilingual setup, the default language was automatically set to Japanese based on the browser's language settings. However, this time, the default language is set to English.)

From there, click the login button on the top screen, and you will be redirected to the home screen!
(The text in the attached image is still in Japanese; I apologize for that.)

Additionally, if you navigate to localhost:3000/ja, you will see the login screen in Japanese! (To switch back to English, simply enter /en in the URL.)

Finally

With this setup, you've created an application that combines multilingual settings with NextAuth! I hope this helps those who were stuck at this stage.

In this implementation, we skipped the backend integration (since the focus is on multilingual settings and NextAuth). When incorporating this into a real application, you will need to implement backend communication to store values in the session as needed.

Sponsored Link

  • Author

kaz

Full-stack Engineer specializing in Backend/Frontend/Cloud Infrastructure | Digital Nomad since June 2023, traveling the world | Sharing programming tips and insights | Posting travel updates on X

-Programming
-,