From 75d156a2eeeccb44fb09f596783ec525cf170f19 Mon Sep 17 00:00:00 2001 From: Bailey Eaton Date: Thu, 9 Jan 2025 12:34:59 +1000 Subject: [PATCH 1/2] fix: move internal kinde path bailout towards top prevents certain edge cases (primarily in Next13) that can cause redirect loops --- src/authMiddleware/authMiddleware.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/authMiddleware/authMiddleware.ts b/src/authMiddleware/authMiddleware.ts index debb183..9566a77 100644 --- a/src/authMiddleware/authMiddleware.ts +++ b/src/authMiddleware/authMiddleware.ts @@ -16,6 +16,12 @@ const handleMiddleware = async (req, options, onSuccess) => { const isReturnToCurrentPage = options?.isReturnToCurrentPage; const loginPage = options?.loginPage || "/api/auth/login"; + const callbackPage = "/api/auth/kinde_callback"; + const registerPage = "/api/auth/register"; + + if(loginPage == pathname || callbackPage == pathname || registerPage == pathname) { + return NextResponse.next(); + } let publicPaths = ["/_next", "/favicon.ico"]; if (options?.publicPaths !== undefined) { @@ -28,7 +34,7 @@ const handleMiddleware = async (req, options, onSuccess) => { ? `${loginPage}?post_login_redirect_url=${pathname}` : loginPage; - const isPublicPath = loginPage == pathname || publicPaths.some((p) => pathname.startsWith(p)); + const isPublicPath = publicPaths.some((p) => pathname.startsWith(p)); // getAccessToken will validate the token let kindeAccessToken = await getAccessToken(req); From a58a7627c028b65e025651836381b7640f71f8d0 Mon Sep 17 00:00:00 2001 From: Bailey Eaton Date: Thu, 9 Jan 2025 12:48:50 +1000 Subject: [PATCH 2/2] fix: rework prefetch check The current logic assumes headers is a map (which is true for Next >14), but in Next 13 the request headers are an object. This causes errors on Next 13 like so: TypeError: e.headers.get is not a function Given the headers function was added in 13, we should resort to using that instead as it provides a stable and consistent API for accessing headers in all versions greater than Next 13. --- src/handlers/login.ts | 4 +++- src/handlers/logout.ts | 4 +++- src/handlers/register.ts | 4 +++- src/utils/isPreFetch.test.ts | 36 +++++++++++++----------------------- src/utils/isPreFetch.ts | 10 +++++----- 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/handlers/login.ts b/src/handlers/login.ts index d3a5701..55611dc 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -1,3 +1,4 @@ +import { headers } from "next/headers"; import RouterClient from "../routerClients/RouterClient"; import { isPreFetch } from "../utils/isPreFetch"; @@ -6,7 +7,8 @@ import { isPreFetch } from "../utils/isPreFetch"; * @param {RouterClient} routerClient */ export const login = async (routerClient: RouterClient) => { - if (isPreFetch(routerClient.req)) { + const heads = await headers(); + if (isPreFetch(heads)) { return null; } diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts index f4fb316..ebab903 100644 --- a/src/handlers/logout.ts +++ b/src/handlers/logout.ts @@ -1,3 +1,4 @@ +import { headers } from "next/headers"; import { config } from "../config/index"; import RouterClient from "../routerClients/RouterClient"; import { isPreFetch } from "../utils/isPreFetch"; @@ -7,7 +8,8 @@ import { isPreFetch } from "../utils/isPreFetch"; * @param {RouterClient} routerClient */ export const logout = async (routerClient: RouterClient) => { - if (isPreFetch(routerClient.req)) { + const heads = await headers(); + if (isPreFetch(heads)) { return null } diff --git a/src/handlers/register.ts b/src/handlers/register.ts index 8573f4e..132cf45 100644 --- a/src/handlers/register.ts +++ b/src/handlers/register.ts @@ -1,3 +1,4 @@ +import { headers } from "next/headers"; import RouterClient from "../routerClients/RouterClient"; import { isPreFetch } from "../utils/isPreFetch"; @@ -6,7 +7,8 @@ import { isPreFetch } from "../utils/isPreFetch"; * @param {RouterClient} routerClient */ export const register = async (routerClient: RouterClient) => { - if (isPreFetch(routerClient.req)) { + const heads = await headers(); + if (isPreFetch(heads)) { return null } diff --git a/src/utils/isPreFetch.test.ts b/src/utils/isPreFetch.test.ts index a09b1cb..4336a75 100644 --- a/src/utils/isPreFetch.test.ts +++ b/src/utils/isPreFetch.test.ts @@ -1,50 +1,40 @@ // isPrefetch.test.ts +import { ReadonlyHeaders } from 'next/dist/server/web/spec-extension/adapters/headers'; import { isPreFetch } from './isPreFetch'; import { NextRequest } from 'next/server'; import { describe, expect, it } from 'vitest'; describe('isPreFetch', () => { - const mockNextRequest = (headers: Record) => { - return { - headers: new Headers(headers) - } as NextRequest; + const mockHeaders = (headers: Record) => { + return new Headers(headers) as ReadonlyHeaders }; it('returns true when purpose header is prefetch', () => { - const req = mockNextRequest({ purpose: 'prefetch' }); - expect(isPreFetch(req)).toBe(true); + const headers = mockHeaders({ purpose: 'prefetch' }); + expect(isPreFetch(headers)).toBe(true); }); it('returns true when x-purpose header is prefetch', () => { - const req = mockNextRequest({ 'x-purpose': 'prefetch' }); - expect(isPreFetch(req)).toBe(true); + const headers = mockHeaders({ 'x-purpose': 'prefetch' }); + expect(isPreFetch(headers)).toBe(true); }); it('returns true when x-moz header is prefetch', () => { - const req = mockNextRequest({ 'x-moz': 'prefetch' }); - expect(isPreFetch(req)).toBe(true); + const headers = mockHeaders({ 'x-moz': 'prefetch' }); + expect(isPreFetch(headers)).toBe(true); }); it('returns false when no prefetch headers are present', () => { - const req = mockNextRequest({}); - expect(isPreFetch(req)).toBe(false); + const headers = mockHeaders({}); + expect(isPreFetch(headers)).toBe(false); }); it('returns false when headers have different values', () => { - const req = mockNextRequest({ + const headers = mockHeaders({ purpose: 'navigation', 'x-purpose': 'fetch', 'x-moz': 'load' }); - expect(isPreFetch(req)).toBe(false); - }); - - it('handles undefined request gracefully', () => { - expect(isPreFetch(undefined as unknown as NextRequest)).toBe(false); - }); - - it('handles null headers gracefully', () => { - const req = { headers: null } as unknown as NextRequest; - expect(isPreFetch(req)).toBe(false); + expect(isPreFetch(headers)).toBe(false); }); }); \ No newline at end of file diff --git a/src/utils/isPreFetch.ts b/src/utils/isPreFetch.ts index 41c08c1..e740626 100644 --- a/src/utils/isPreFetch.ts +++ b/src/utils/isPreFetch.ts @@ -1,9 +1,9 @@ -import { NextRequest } from "next/server"; +import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; -export function isPreFetch(req: NextRequest): boolean { - const isPrefetch = req?.headers && (req?.headers.get('purpose') === 'prefetch' || - req?.headers.get('x-purpose') === 'prefetch' || - req?.headers.get('x-moz') === 'prefetch'); +export function isPreFetch(headers: ReadonlyHeaders): boolean { + const isPrefetch = headers.get('purpose') === 'prefetch' || + headers.get('x-purpose') === 'prefetch' || + headers.get('x-moz') === 'prefetch'; return !!isPrefetch; } \ No newline at end of file