next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

provide an example with i18n

Open stefanprobst opened this issue 1 year ago • 8 comments

What is the improvement or update you wish to see?

it would be super helpful to have an official example repo which shows how to combine next-auth v5 with an i18n library like next-intl.

from the current docs, it is not clear to me

  • how to correctly compose auth and i18n middleware
  • how to get translated sign-in/sign-out pages

thanks :pray:

Is there any context that might help us understand?

  • https://github.com/nextauthjs/next-auth/discussions?discussions_q=is%3Aopen+i18n
  • https://github.com/amannn/next-intl/issues/596
  • https://github.com/amannn/next-intl/pull/598

Does the docs page already exist? Please link to it.

No response

stefanprobst avatar Jan 04 '24 19:01 stefanprobst

Hi! After many hours I've managed to create my own auth middleware to work properly alongside next-intl. Here is the code:

Middleware

import { auth } from "@/auth";
import pages from "./lib/pages";
import { NextRequest, NextResponse } from "next/server";
import createIntlMiddleware from "next-intl/middleware";

const locales = ["en", "es"];
const protectedPages = ["/dashboard/*"];
const authPages = ["/auth/signin", "/auth/signup"];

const intlMiddleware = createIntlMiddleware({
  locales,
  defaultLocale: "es",
  localePrefix: "as-needed",
});

const testPagesRegex = (pages: string[], pathname: string) => {
  const regex = `^(/(${locales.join("|")}))?(${pages
    .map((p) => p.replace("/*", ".*"))
    .join("|")})/?$`;
  return new RegExp(regex, "i").test(pathname);
};

const handleAuth = async (
  req: NextRequest,
  isAuthPage: boolean,
  isProtectedPage: boolean,
) => {
  const session = await auth();
  const isAuth = !!session?.user;

  if (!isAuth && isProtectedPage) {
    let from = req.nextUrl.pathname;
    if (req.nextUrl.search) {
      from += req.nextUrl.search;
    }

    return NextResponse.redirect(
      new URL(
        `${pages.auth.signin()}?from=${encodeURIComponent(from)}`,
        req.url,
      ),
    );
  }

  if (isAuth && isAuthPage) {
    return NextResponse.redirect(new URL(pages.dashboard.root, req.nextUrl));
  }

  return intlMiddleware(req);
};

export default async function middleware(req: NextRequest) {
  const isAuthPage = testPagesRegex(authPages, req.nextUrl.pathname);
  const isProtectedPage = testPagesRegex(protectedPages, req.nextUrl.pathname);

  return await handleAuth(req, isAuthPage, isProtectedPage);
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

use of signIn()

  const searchParams = useSearchParams();

  const handleGoogle = async () => {
    setLoading(true);
    return await signIn("google", {
      callbackUrl: searchParams?.get("from") || pages.dashboard.root,
    });
  };

jero237 avatar Jan 07 '24 21:01 jero237

@jero237 How to deal with callbackUrl in signin?

signin("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: true,
      callbackUrl: searchParams?.get("callbackUrl") || "/",
    });

this code alwalys redirect pages.dashboard.root

Adherentman avatar Jan 16 '24 09:01 Adherentman

@jero237 How to deal with callbackUrl in signin?

signin("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: true,
      callbackUrl: searchParams?.get("callbackUrl") || "/",
    });

this code alwalys redirect pages.dashboard.root

great question, I've just updated the comment above

jero237 avatar Jan 16 '24 13:01 jero237

@jero237 I change some code with signin after redirect by callbackUrl

  if (isAuth && isAuthPage) {
    const url = req.nextUrl.clone();
    const fromValue = url.searchParams.get("from");
    return NextResponse.redirect(new URL(fromValue ?? "/", req.nextUrl));
  }

This is my all code


const handleAuth = async (req: NextRequest, isAuthPage: boolean, isProtectedPage: boolean) => {
  const session = await auth();
  const isAuth = !!session?.user;

  if (!isAuth && isProtectedPage) {
    let from = req.nextUrl.pathname;
    if (req.nextUrl.search) {
      from += req.nextUrl.search;
    }
    return NextResponse.redirect(new URL(`/auth/signin?from=${encodeURIComponent(from)}`, req.url));
  }

  if (isAuth && isAuthPage) {
    const url = req.nextUrl.clone();
    const fromValue = url.searchParams.get("from");
    return NextResponse.redirect(new URL(fromValue ?? "/", req.nextUrl));
  }

  return intlMiddleware(req);
};

export default async function middleware(req: NextRequest) {
  const isAuthPage = testPagesRegex(authPages, req.nextUrl.pathname);
  const isProtectedPage = testPagesRegex(protectedPages, req.nextUrl.pathname);

  return await handleAuth(req, isAuthPage, isProtectedPage);
}

Adherentman avatar Jan 18 '24 01:01 Adherentman

Same requirement from i18next

import { withAuth } from 'next-auth/middleware';
import acceptLanguage from 'accept-language';

const cookieName = 'i18next';
const fallbackLng = 'en';
const languages = ['en', 'zh'];

// Language from cookie/headers/fallback
function getLng(req: NextRequest) {
  if (req.cookies.has(cookieName))
    return acceptLanguage.get(req.cookies.get(cookieName)?.value);

  return acceptLanguage.get(req.headers.get('Accept-Language')) ?? fallbackLng;
};

// Append i18n language to path like: app/[lng]/social-login/page.tsx
// example: /social-login -> /en/social-login
function handleI18n(req: NextRequest) {
  if (req.nextUrl.pathname.startsWith('/api')) return;

  if (
    !languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith('/_next')
  ) {
    const lng = getLng(req);
    return NextResponse.redirect(
      new URL(`/${lng}${req.nextUrl.pathname}`, req.url)
    );
  }

  if (req.headers.has('referer')) {
    const refererUrl = new URL(req.headers.get('referer')!);
    const lngInReferer = languages.find(l =>
      refererUrl.pathname.startsWith(`/${l}`)
    );
    const response = NextResponse.next();
    if (lngInReferer)
      response.cookies.set(cookieName, lngInReferer, { sameSite: 'lax' });
    return response;
  }
}

function middleware(req: NextRequest) {
  console.log('Running middleware!'); // <--------- This will not run if not authed
  return handleI18n(req) ?? NextResponse.next();
}

export default withAuth(middleware, {
  pages: {
    signIn: '/social-login', // <--------- This will not redirect to /en/social-login
    signOut: '/social-login',
  },
});

export const config = {
  matcher: ['/((?!_next/static|_next/image|assets|favicon.ico|sw.js|fonts).*)'],
};

will be nice if we can access req in NextAuthMiddlewareOptions

export default withAuth(middleware, (req) => ({
  pages: {
    signIn: `${getLng(req)}/social-login`,
    signOut: `${getLng(req)}/social-login`,
  },
}));

soapproject avatar May 28 '24 11:05 soapproject

update: I just notic v5 comming soon and it will be much easyer. But so far this works with v4.

import acceptLanguage from 'accept-language';
import { withAuth, type NextRequestWithAuth } from 'next-auth/middleware';
import { NextResponse, type NextRequest } from 'next/server';
import { cookieName, fallbackLng, languages } from './i18n';

acceptLanguage.languages([...languages]);

export const config = {
matcher: [
  '/((?!_next/static|_next/image|assets|favicon.ico|sw.js|manifest.json|fonts).*)',
],
};

const getLng = (req: NextRequest) => {
if (req.cookies.has(cookieName))
  return acceptLanguage.get(req.cookies.get(cookieName)?.value);

return acceptLanguage.get(req.headers.get('Accept-Language')) ?? fallbackLng;
};

const handleI18n = (req: NextRequest) => {
if (req.nextUrl.pathname.startsWith('/api')) return;

const pathnameStartsWithLanguage = languages.some(loc =>
  req.nextUrl.pathname.startsWith(`/${loc}`)
);
if (
  !pathnameStartsWithLanguage &&
  !req.nextUrl.pathname.startsWith('/_next')
) {
  const lng = getLng(req);
  return NextResponse.redirect(
    new URL(`/${lng}${req.nextUrl.pathname}`, req.url)
  );
}

if (req.headers.has('referer')) {
  const refererUrl = new URL(req.headers.get('referer')!);
  const lngInReferer = languages.find(l =>
    refererUrl.pathname.startsWith(`/${l}`)
  );
  const response = NextResponse.next();
  if (lngInReferer) {
    response.cookies.set(cookieName, lngInReferer, { sameSite: 'lax' });
  }
  return response;
}

return;
};

const handleAuth = (req: NextRequestWithAuth) => {
if (req.nextUrl.pathname.includes('/dmz')) return NextResponse.next();

const token = req.nextauth.token;
if (!token)
  return NextResponse.redirect(new URL(`/dmz/social-login`, req.url));

return;
};

const middleware = (req: NextRequestWithAuth) => {
const i18nRes = handleI18n(req);
if (i18nRes) return i18nRes;

const authRes = handleAuth(req);
if (authRes) return authRes;

return NextResponse.next();
};

export default withAuth(middleware, {
callbacks: {
  authorized() {
    /**
     * Trick, tell next-auth we are always authorized, so middleware callback will not skip.
     * then we handle token check and redirect to login page our self.
     */
    return true;
  },
},
});

soapproject avatar May 31 '24 07:05 soapproject

@balazsorban44 save us please 🥺

ScreamZ avatar Jun 25 '24 15:06 ScreamZ

There is an example by the maintainer of next-intl https://github.com/amannn/next-intl/blob/main/examples/example-app-router-next-auth/src/middleware.ts (for v4)

phibo23 avatar Aug 05 '24 16:08 phibo23

+1

Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote 👍 on the issue description or subscribe to the issue for updates. Thanks!

barlas avatar Nov 14 '24 19:11 barlas

There is an example by the maintainer of next-intl amannn/next-intl@main/examples/example-app-router-next-auth/src/middleware.ts (for v4)

I left the ship for Lucia Auth, no more issue but thanks :)

ScreamZ avatar Nov 15 '24 08:11 ScreamZ

It looks like this issue did not receive any activity for 60 days. It will be closed in 7 days if no further activity occurs. If you think your issue is still relevant, commenting will keep it open. Thanks!

stale[bot] avatar Jan 21 '25 23:01 stale[bot]

To keep things tidy, we are closing this issue for now. If you think your issue is still relevant, leave a comment and we might reopen it. Thanks!

stale[bot] avatar Jan 31 '25 22:01 stale[bot]

404 Error on Root URL After Locale Redirection in Next.js Middleware

import { checkServiceability, CryptoChainCode, CurrencyCode, getPostRampOrderFlow, getPreRampOrderFlow, OrderType, } from "client" import { DEFAULT_PAGE, ERROR_PAGE, ORDER_CONFIRM_PAGE, PAYMENT_PROCESSING_PAGE, QUOTE_PAGE, WALLET_ADDRESS_PAGE, } from "constants/routes" import { ACTION_REDIRECT_MAP, BANNER_PARAMS } from "lib/utils" import { type JWT } from "next-auth/jwt" import { withAuth } from "next-auth/middleware" import { NextRequest, NextResponse } from "next/server"

import { match as matchLocale } from "@formatjs/intl-localematcher" import { i18n } from "i18n-config" import Negotiator from "negotiator"

export function getLocale(request: NextRequest): string | undefined { const negotiatorHeaders: Record<string, string> = {} request.headers.forEach((value, key) => (negotiatorHeaders[key] = value))

const locales = i18n.locales ?? "en"

let languages = new Negotiator({ headers: negotiatorHeaders }).languages( locales as any, )

const locale = matchLocale(languages, locales, i18n.defaultLocale)

return locale }

function getAuthToken(request: NextRequest) { let authToken = request.cookies.get("__Secure-next-auth.session-token")?.value

if (!authToken) { authToken = request.cookies.get("next-auth.session-token")?.value }

return authToken }

async function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname

const pathnameIsMissingLocale = i18n.locales.every( (locale) => !pathname.startsWith(/${locale}/) && pathname !== /${locale}, )

if (pathnameIsMissingLocale) { const locale = getLocale(request) return NextResponse.redirect( new URL( /${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}, request.url, ), ) } const isLocaleRoot = i18n.locales.some((locale) => pathname === /${locale})

try { if (isLocaleRoot || pathname === DEFAULT_PAGE) { const newUrl = new URL(QUOTE_PAGE, request.url) return NextResponse.redirect(newUrl) }

if (pathname.endsWith(QUOTE_PAGE)) {
  console.log("Redirecting to:", pathname === DEFAULT_PAGE)
  return await handleCheckServiceability(request)
}

if (pathname.endsWith(ORDER_CONFIRM_PAGE)) {
  console.log("Redirecting to:", pathname === DEFAULT_PAGE)
  return await handlePreOrderFlow(request)
}

if (pathname.endsWith(PAYMENT_PROCESSING_PAGE)) {
  console.log("Redirecting to:", pathname === DEFAULT_PAGE)
  return await handlePostOrderFlow(request)
}

} catch (error) { console.error("Request failed:", error) return NextResponse.redirect(new URL(ERROR_PAGE, request.url)) }

return NextResponse.next() }

async function handleCheckServiceability(request: NextRequest) { const forwardedFor = request.headers.get("x-forwarded-for") || "" const ipAddress = forwardedFor.split(",")[0].trim()

const response = await checkServiceability({ baseURL: process.env.API_BASE_URL, query: { ipAddress: ipAddress }, validateStatus: () => true, })

if (response.data?.code === 100019) { return handleActionFlow(request, "region_blocked", {}) }

if (response.data?.code !== 0) { throw response.data?.message ?? "Unknown error" }

return NextResponse.next() }

async function handlePreOrderFlow(request: NextRequest) { const searchParams = request.nextUrl.searchParams

const query = { orderType: (searchParams.get("orderType") as OrderType) ?? undefined, srcAmount: searchParams.get("srcAmount") ?? undefined, srcCurrency: (searchParams.get("srcCurrency") as CurrencyCode) ?? undefined, srcChain: (searchParams.get("srcChain") as CryptoChainCode) ?? undefined, dstCurrency: (searchParams.get("dstCurrency") as CurrencyCode) ?? undefined, dstChain: (searchParams.get("dstChain") as CryptoChainCode) ?? undefined, walletAddress: searchParams.get("walletAddress") ?? undefined, walletAddressTag: searchParams.get("walletAddressTag") ?? undefined, orderUid: searchParams.get("orderUid") ?? undefined, apiKey: searchParams.get("apiKey") ?? undefined, }

const response = await getPreRampOrderFlow({ baseURL: process.env.API_BASE_URL, headers: { Authorization: Bearer ${getAuthToken(request)} }, query, })

const flow = response?.data?.data?.flow ?? ""

let params: Record<string, string> = {}

const kycSessionData = response?.data?.data?.kycSessionData if (kycSessionData?.webData) { params.kycData = kycSessionData.webData params.kycLevel = String(kycSessionData.kycLevel) }

const walletUid = response?.data?.data?.walletUid if (walletUid) { params.walletUid = walletUid }

return handleActionFlow(request, flow, params) }

async function handlePostOrderFlow(request: NextRequest) { const response = await getPostRampOrderFlow({ baseURL: process.env.API_BASE_URL, headers: { Authorization: Bearer ${getAuthToken(request)} }, query: { orderUid: request.nextUrl.searchParams.get("orderUid") ?? "" }, })

const flow = response?.data?.data?.flow ?? ""

return handleActionFlow(request, flow, {}) }

async function handleActionFlow( request: NextRequest, flow: string, params: Record<string, string>, ) { if (flow == "proceed") { return NextResponse.next() }

const redirectPath = ACTION_REDIRECT_MAP[flow]

if (!redirectPath) { throw new Error(invalid_flow: ${flow}) }

if (request.nextUrl.pathname === redirectPath) { // Scenario: // - User is already in a redirected path // - Makes a call to the middleware again // - But the action flow is still the same return NextResponse.next() }

const nextUrl = new URL(redirectPath, request.url) const currentUrl = request.nextUrl.pathname + request.nextUrl.search

nextUrl.searchParams.append("redirectUrl", currentUrl)

Object.keys(params).forEach((key) => { nextUrl.searchParams.append(key, params[key]) })

if (BANNER_PARAMS[flow]) { nextUrl.searchParams.append("message", BANNER_PARAMS[flow]) }

return NextResponse.redirect(nextUrl) }

function authorizedCallback(params: { token: JWT | null req: NextRequest }): boolean { const pathname = params.req.nextUrl.pathname

if ( pathname === DEFAULT_PAGE || pathname.startsWith(QUOTE_PAGE) || pathname.startsWith(WALLET_ADDRESS_PAGE) || pathname.startsWith("/api/ramp") || pathname.startsWith(ERROR_PAGE) ) { return true }

return !!params.token }

export default withAuth(middleware, { callbacks: { authorized: authorizedCallback, }, })

export const config = { matcher: [ "/((?!_next/static|_next/image|favicon.ico|.\.(?:svg|png|jpg|jpeg|gif|webp)$).)", ], }

rohithroshan-r avatar May 22 '25 13:05 rohithroshan-r