next-auth
next-auth copied to clipboard
provide an example with i18n
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
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 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
@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 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);
}
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`,
},
}));
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;
},
},
});
@balazsorban44 save us please 🥺
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)
+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!
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 :)
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!
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!
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)$).)", ], }