Closed seanaguinaga closed 2 months ago
I have used this library before with amplify-auth and next-intl
https://github.com/kj455/next-compose-middleware
not sure how to do that with this though
https://github.com/z4nr34l/next-easy-middlewares
This one looks better?
This somewhat works?
import { authConfig } from '@right-lane/auth/edge';
import { authMiddleware } from 'next-firebase-auth-edge';
import { NextRequest, NextResponse } from 'next/server';
const PUBLIC_PATHS = ['/', '/register', '/login', '/reset-password'];
export async function middleware(request: NextRequest) {
return authMiddleware(request, {
loginPath: '/api/login',
logoutPath: '/api/logout',
apiKey: authConfig.apiKey,
cookieName: authConfig.cookieName,
cookieSerializeOptions: authConfig.cookieSerializeOptions,
cookieSignatureKeys: authConfig.cookieSignatureKeys,
serviceAccount: authConfig.serviceAccount,
handleValidToken: async ({ token, decodedToken }, headers) => {
// console.info('Got a valid token', { token, decodedToken });
const url = request.nextUrl.clone();
const locale = url.locale || 'en'; // Default to 'en' if no locale is found
// Authenticated user should not be able to access /, /login, /register and /reset-password routes
if (
PUBLIC_PATHS.includes(
request.nextUrl.pathname.replace(`/${locale}`, ''),
)
) {
return redirectToHome(request);
}
return NextResponse.next({
request: {
headers,
},
});
},
handleInvalidToken: async (reason) => {
// console.info('Missing or malformed credentials', { reason });
return redirectToLogin(request, {
path: '/',
publicPaths: PUBLIC_PATHS,
});
},
handleError: async (error) => {
console.error('Unhandled authentication error', { error });
return redirectToLogin(request, {
path: '/',
publicPaths: PUBLIC_PATHS,
});
},
});
}
export const config = {
matcher: [
'/',
'/((?!_next|favicon.ico.*\\.).*)',
'/api/login',
'/api/logout',
],
};
function redirectToHome(request: NextRequest) {
const url = request.nextUrl.clone();
const locale = url.locale || 'en'; // Default to 'en' if no locale is found
url.pathname = '/home';
url.search = '';
return NextResponse.redirect(new URL(`/${locale}/home`, url));
}
interface RedirectToLoginOptions {
path: string;
publicPaths: string[];
redirectParamKeyName?: string;
}
export function redirectToLogin(
request: NextRequest,
options: RedirectToLoginOptions = {
path: '/login',
publicPaths: ['/login'],
},
) {
const url = request.nextUrl.clone();
const locale = url.locale || 'en'; // Default to 'en' if no locale is found
const redirectKey = options.redirectParamKeyName || 'redirect';
if (
options.publicPaths.includes(
request.nextUrl.pathname.replace(`/${locale}`, ''),
)
) {
return NextResponse.next();
}
url.pathname = options.path;
url.search = `${redirectKey}=${request.nextUrl.pathname}${url.search}`;
return NextResponse.redirect(new URL(`/${locale}/login`, url));
}
Actually just this?
import { authConfig } from '@right-lane/auth/edge';
import { authMiddleware } from 'next-firebase-auth-edge';
import { NextRequest } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { defaultLocale, localePrefix, locales, pathnames } from './config';
const PUBLIC_PATHS = ['/', '/register', '/login', '/reset-password'];
const intlMiddleware = createIntlMiddleware({
defaultLocale,
locales,
pathnames,
localePrefix,
});
const publicPathnameRegex = RegExp(
`^(/(${locales.join('|')}))?(${PUBLIC_PATHS.flatMap((p) =>
p === '/' ? ['', '/'] : p,
).join('|')})/?$`,
'i',
);
export async function middleware(request: NextRequest) {
const isPublicPage = publicPathnameRegex.test(request.nextUrl.pathname);
return authMiddleware(request, {
loginPath: '/api/login',
logoutPath: '/api/logout',
apiKey: authConfig.apiKey,
cookieName: authConfig.cookieName,
cookieSerializeOptions: authConfig.cookieSerializeOptions,
cookieSignatureKeys: authConfig.cookieSignatureKeys,
serviceAccount: authConfig.serviceAccount,
handleValidToken: async (tokens) => {
if (isPublicPage) {
request.nextUrl.pathname = '/home';
}
return intlMiddleware(request);
},
handleInvalidToken: async (reason) => {
if (!isPublicPage) {
request.nextUrl.pathname = '/login';
}
return intlMiddleware(request);
},
handleError: async (error) => {
if (!isPublicPage) {
request.nextUrl.pathname = '/login';
}
return intlMiddleware(request);
},
});
}
export const config = {
matcher: [
// for next-firebase-auth-edge
'/api/login',
'/api/logout',
// Enable a redirect to a matching locale at the root
'/',
// Set a cookie to remember the previous locale for
// all requests that have a locale prefix
`/${locales.join('|')}/:path*`,
// Enable redirects that add missing locales
// (e.g. `/pathnames` -> `/en/pathnames`)
'/((?!_next|_vercel|.*\\..*).*)',
],
};
This is for the app router example that next-intl has which is well-featured
Hey @seanaguinaga!
Thanks for reporting!
The example you mentioned (https://next-firebase-auth-edge-docs.vercel.app/docs/usage/middleware#usage-with-next-intl-and-other-middlewares) is very simplified version without any redirection mechanism.
You're right. Making next-firebase-auth-edge
work with next-intl
in more complex scenarios requires some custom, locale-detection logic.
In some face-to-face conversations people also told me it's hard to compose the library with next-international
due to Modified Request Headers
feature.
I will be re-thinking the API to allow integration with libraries that you've mentioned and more.
Here's how I handle integration with next-intl
and next-firebase-auth-edge
. It's pretty similar to what you've come up with:
// middleware.ts
import { AUTH_TOKENS_COOKIE_NAME } from '@ensite/auth/lib/consts';
import { cookieOptions, serverFirebaseOptions } from './config';
import { clientFirebaseOptions } from './app/config';
import { authMiddleware } from 'next-firebase-auth-edge';
import { NextRequest, NextResponse } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { defaultLocale, locales } from '@ensite/i18n/lib/consts';
import { Locale } from '@ensite/domain/lib/locale';
const LOCALE_LIST = ['en', 'pl', 'de'];
const LOCALE_REGEXP = /^\/([a-z]{2})(\/.+)?$/;
const LOCALE_SET = new Set(LOCALE_LIST);
export function extractLocale(pathname: string): Locale | null {
const match = pathname.match(LOCALE_REGEXP);
if (!match) {
return null;
}
const locale = match[1];
if (!locale || !LOCALE_SET.has(locale)) {
return null;
}
return locale as Locale;
}
export function removeLocale(pathname: string, locale: Locale | null) {
if (!locale) {
return pathname;
}
if (pathname === `/${locale}`) {
return '/';
}
return pathname.replace(`/${locale}/`, '/');
}
export function withoutLocale(pathname: string): string {
const locale = extractLocale(pathname);
return removeLocale(pathname, locale);
}
const handleI18nRouting = createIntlMiddleware({
localePrefix: 'as-needed',
locales,
defaultLocale,
});
function redirectToLogin(request: NextRequest) {
if (withoutLocale(request.nextUrl.pathname) === '/login') {
return handleI18nRouting(request);
}
const url = request.nextUrl.clone();
url.pathname = `/login`;
url.searchParams.set('redirect', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
export async function middleware(request: NextRequest) {
return authMiddleware(request, {
loginPath: '/api/login',
logoutPath: '/api/logout',
apiKey: clientFirebaseOptions.apiKey,
cookieName: AUTH_TOKENS_COOKIE_NAME,
cookieSignatureKeys: cookieOptions.keys,
cookieSerializeOptions: {
path: cookieOptions.path,
httpOnly: cookieOptions.httpOnly,
secure: cookieOptions.secure,
sameSite: cookieOptions.sameSite,
maxAge: cookieOptions.maxAge,
},
serviceAccount: serverFirebaseOptions,
handleValidToken: async ({ decodedToken }) => {
if (!decodedToken.email_verified) {
return redirectToLogin(request);
}
return handleI18nRouting(request);
},
handleInvalidToken: async () => {
return redirectToLogin(request);
},
handleError: async () => {
return redirectToLogin(request);
},
});
}
export const config = {
matcher: [
'/',
'/((?!_next|api/instagram|api/templates|api/revalidate|api/reauth|assets|health-check|favicon.ico|logo.svg).*)',
],
};
@seanaguinaga by the way, you mentioned using https://github.com/kj455/next-compose-middleware with amplify-auth and next-intl. Is there a chance you could share some code examples? It will help me to gather some hints about the approach I should take
By doing this, the token will be re-checked on the subsequent calls (AFAIK):
handleValidToken: async (tokens) => {
if (isPublicPage) {
request.nextUrl.pathname = '/home';
}
return intlMiddleware(request);
},
Would it be ok to modify the request with the new headers? Maybe something like this?
handleValidToken: async (tokens, headers) => {
if (isPublicPage) {
request.nextUrl.pathname = "/home";
}
headers?.forEach((value, key) => {
request.headers.set(key, value);
});
return intlMiddleware(request);
},
Hey @saulogt!
By doing this, the token will be re-checked on the subsequent calls (AFAIK):
Is this actually the behavior you're experiencing?
handleValidToken: async (tokens) => {
if (isPublicPage) {
request.nextUrl.pathname = '/home';
}
return intlMiddleware(request);
},
This should work just fine.
headers?.forEach((value, key) => {
request.headers.set(key, value);
});
This is actually done by authMiddleware
just before handleValidRequest
is called.
I inspected intlMiddleware
implementation and when it's returning a response, it automatically decorates request headers (See this link), so you don't need to pass headers by yourself.
To add on that, there is no way to Modify Request Headers other than passing headers
directly to NextResponse.next({ request: { headers } })
or NextResponse.rewrite({ request: { headers } })
at the moment
I will close the issue for now.
Due to Next.js limitation, we cannot set Modified Request Headers on existing response, which requires authMiddleware
to be called at the top level, if we want to provide different behaviour depending on user context.
Simply put: If you want to use handleValidToken
, handleInvalidToken
or handleError
, you will have hard time composing the middleware with next-compose-middleware
When Next.js upgrades it's API to allow to update modified request headers on response instance, I will reopen the issue and introduce updates to make it compatible with middleware composers
According to this:
https://next-firebase-auth-edge-docs.vercel.app/docs/usage/middleware#usage-with-next-intl-and-other-middlewares
You don't need to do anything but make it look like that?
I am confused as to how you would make that work with the example middleware: