Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 26 additions & 19 deletions src/authMiddleware/authMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,23 @@ import { OAuth2CodeExchangeResponse } from "@kinde-oss/kinde-typescript-sdk";
import { copyCookiesToRequest } from "../utils/copyCookiesToRequest";
import { getStandardCookieOptions } from "../utils/cookies/getStandardCookieOptions";
import { isPublicPathMatch } from "../utils/isPublicPathMatch";
import { TWENTY_NINE_DAYS } from "src/utils/constants";

/**
* Handles invitation code redirect logic.
* Redirects to the register page with the invitation code, or to login on error.
*/
const handleInvitationCodeRedirect = (
req,
invitationCode: string,
registerPage: string,
loginRedirectUrl: string,
redirectURLBase: string | undefined,
): NextResponse => {
const method = req.method?.toUpperCase() ?? "GET";
if (method !== "GET" && method !== "HEAD") {
return NextResponse.json({ statusCode: 401, message: "Unauthorized" });
}

try {
const params = new URLSearchParams();
params.set("invitation_code", invitationCode);
Expand All @@ -50,6 +55,20 @@ const handleInvitationCodeRedirect = (
}
};

const loginRedirect = (
req,
loginRedirectUrl: string,
redirectURLBase: string | undefined,
): NextResponse => {
const method = req.method?.toUpperCase() ?? "GET";
if (method !== "GET" && method !== "HEAD") {
return NextResponse.json({ statusCode: 401, message: "Unauthorized" });
}
return NextResponse.redirect(
new URL(loginRedirectUrl, redirectURLBase || config.redirectURL),
);
};
Comment on lines +58 to +70
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Next.js 15 middleware, what HTTP status code does NextResponse.json return by default when no status option is provided?

💡 Result:

In Next.js 15, when you use NextResponse.json without providing a status option, it returns an HTTP 200 status code by default [1][2]. This behavior aligns with the standard Web Response API, which NextResponse extends [3][2].

Citations:


Fix 401 response in loginRedirect (and ensure invitation-code doesn’t bypass it)

loginRedirect returns NextResponse.json(...) without a { status: 401 } option, which results in HTTP 200 by default—so the transport-level 401 contract isn’t met.

-    return NextResponse.json({ statusCode: 401, message: "Unauthorized" });
+    return NextResponse.json(
+      { statusCode: 401, message: "Unauthorized" },
+      { status: 401 },
+    );

Also, the invitation-code flow still returns NextResponse.redirect(...) directly and appears to bypass the non-GET/HEAD method gate; ensure it goes through the same loginRedirect logic (or otherwise enforces 401 for non-GET/HEAD).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const loginRedirect = (
req,
loginRedirectUrl: string,
redirectURLBase: string | undefined,
): NextResponse => {
const method = req.method?.toUpperCase() ?? "GET";
if (method !== "GET" && method !== "HEAD") {
return NextResponse.json({ statusCode: 401, message: "Unauthorized" });
}
return NextResponse.redirect(
new URL(loginRedirectUrl, redirectURLBase || config.redirectURL),
);
};
const loginRedirect = (
req,
loginRedirectUrl: string,
redirectURLBase: string | undefined,
): NextResponse => {
const method = req.method?.toUpperCase() ?? "GET";
if (method !== "GET" && method !== "HEAD") {
return NextResponse.json(
{ statusCode: 401, message: "Unauthorized" },
{ status: 401 },
);
}
return NextResponse.redirect(
new URL(loginRedirectUrl, redirectURLBase || config.redirectURL),
);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/authMiddleware/authMiddleware.ts` around lines 52 - 64, The loginRedirect
function currently returns NextResponse.json({ statusCode: 401, ... }) which
sends HTTP 200; change it to return NextResponse.json({ statusCode: 401,
message: "Unauthorized" }, { status: 401 }) so the transport-level status is
correct, and ensure any invitation-code handling path uses the same
loginRedirect flow (or explicitly enforces the GET/HEAD check and returns the
same NextResponse.json(..., { status: 401 })) instead of directly calling
NextResponse.redirect; update references to loginRedirect and the
invitation-code branch so non-GET/HEAD requests always hit the 401 response.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Next.js middleware, does NextResponse.redirect default to HTTP 307 and preserve the original HTTP method for POST requests?

💡 Result:

Yes. 1) Default status code: In Next.js middleware, NextResponse.redirect defaults to an HTTP 307 Temporary Redirect when you don’t specify a status code. This is shown in the Next.js implementation where it uses init?.status?? 307 [1]. Next.js docs also state that redirect returns a 307 by default [2]. 2) Preservation of method for POST: With an HTTP 307 Temporary Redirect, HTTP semantics require the client to repeat the request with the same method (so a POST stays a POST). MDN describes 307 as “Method and body not changed” [3]. Therefore, when NextResponse.redirect defaults to 307, it preserves the original HTTP method for POST requests in practice/semantics. Note: Next.js redirect helpers can be overridden if you pass a different status (e.g., { status: 308 } for permanent redirect) [1][4], but the default behavior is 307, which preserves method per HTTP semantics [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/authMiddleware/authMiddleware.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "=== File header/structure ==="
sed -n '1,120p' "$FILE" | nl -ba

echo "=== Search for handleMiddleware and handleInvitationCodeRedirect ==="
rg -n "handleMiddleware|handleInvitationCodeRedirect|handleInvitationCode|invitation_code|loginRedirect" "$FILE" -n

echo "=== Show region around handleMiddleware (lines ~1-220) ==="
sed -n '1,240p' "$FILE" | nl -ba

echo "=== Show region around loginRedirect (lines ~1-120) ==="
rg -n "const loginRedirect" "$FILE"
START=$(rg -n "const loginRedirect" "$FILE" | head -n1 | cut -d: -f1)
START=$((START-30))
END=$((START+60))
sed -n "${START},${END}p" "$FILE" | nl -ba

echo "=== Show region around handleInvitationCodeRedirect (lines ~1-240) ==="
rg -n "const handleInvitationCodeRedirect" "$FILE" || true
START2=$(rg -n "const handleInvitationCodeRedirect" "$FILE" | head -n1 | cut -d: -f1)
START2=$((START2-40))
END2=$((START2+120))
sed -n "${START2},${END2}p" "$FILE" | nl -ba

Repository: kinde-oss/kinde-auth-nextjs

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/authMiddleware/authMiddleware.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

nl_awk() { awk '{printf "%6d %s\n", NR, $0}'; }

echo "=== File header/structure (first 120 lines) ==="
sed -n '1,120p' "$FILE" | nl_awk

echo "=== Search for handleMiddleware and handleInvitationCodeRedirect ==="
rg -n "handleMiddleware|handleInvitationCodeRedirect|invitation_code|loginRedirect" "$FILE"

echo "=== Region around handleMiddleware (1-240) ==="
sed -n '1,240p' "$FILE" | nl_awk

echo "=== Region around loginRedirect definition ==="
start=$(rg -n "const loginRedirect" "$FILE" | head -n1 | cut -d: -f1)
start=$((start-30))
end=$((start+100))
if [ "$start" -lt 1 ]; then start=1; fi
sed -n "${start},${end}p" "$FILE" | nl_awk

echo "=== Region around handleInvitationCodeRedirect definition ==="
start2=$(rg -n "const handleInvitationCodeRedirect" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -z "${start2}" ]; then
  echo "No handleInvitationCodeRedirect definition found"
  exit 0
fi
start2=$((start2-40))
end2=$((start2+160))
if [ "$start2" -lt 1 ]; then start2=1; fi
sed -n "${start2},${end2}p" "$FILE" | nl_awk

Repository: kinde-oss/kinde-auth-nextjs

Length of output: 25355


Guard invitation-code redirects against non-GET/HEAD (default 307 preserves POST method)

handleMiddleware immediately returns handleInvitationCodeRedirect(...) when invitation_code is present (bypassing loginRedirect(req, ...)), and handleInvitationCodeRedirect performs a bare NextResponse.redirect(...) with no method check. Since NextResponse.redirect defaults to HTTP 307, a POST /protected?invitation_code=... will keep the POST method and be repeated to the register URL (and to the login URL in the error path).

One way to close the gap
-const handleInvitationCodeRedirect = (
+const handleInvitationCodeRedirect = (
+  req,
   invitationCode: string,
   registerPage: string,
   loginRedirectUrl: string,
   redirectURLBase: string | undefined,
 ): NextResponse => {
+  const method = req.method?.toUpperCase() ?? "GET";
+  if (method !== "GET" && method !== "HEAD") {
+    return NextResponse.json(
+      { statusCode: 401, message: "Unauthorized" },
+      { status: 401 },
+    );
+  }
+
   try {
     const params = new URLSearchParams();
     params.set("invitation_code", invitationCode);
@@
     return handleInvitationCodeRedirect(
+      req,
       invitationCode,
       registerPage,
       loginRedirectUrl,
       options?.redirectURLBase,
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/authMiddleware/authMiddleware.ts` around lines 52 - 64, The
invitation-code redirect path lacks the GET/HEAD guard and so can forward
non-GET methods as a 307; update handleInvitationCodeRedirect to mirror
loginRedirect: read req.method (uppercase, default "GET"), and if it's not "GET"
or "HEAD" return a 401 NextResponse.json({ statusCode: 401, message:
"Unauthorized" }); otherwise perform the redirect (or explicitly use a 303/302
redirect) to the registration/login URL; ensure you change only
handleInvitationCodeRedirect (and any callers in handleMiddleware if needed) so
invitation_code redirects cannot replay POSTs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix 401 response in loginRedirect: the finding is technically correct (HTTP 200 with statusCode: 401 in the body is wrong), but fixing it in isolation would make loginRedirect and handleInvitationCodeRedirect diverge from the identical pattern used across all 5 call sites in protect.js. We'd be creating a new inconsistency rather than resolving one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shafaladhikari, that's a fair point — patching just loginRedirect here while leaving the identical { statusCode: 401 } + HTTP 200 pattern intact across protect.js's five call sites would create a worse inconsistency than the one it solves. Deferring to a single coordinated follow-up across both files makes sense.

Would you like me to open a follow-up GitHub issue to track the holistic fix (correcting the HTTP status to 401 across authMiddleware.ts and protect.js)?


✏️ Learnings added
Learnt from: shafaladhikari
Repo: kinde-oss/kinde-auth-nextjs PR: 526
File: src/authMiddleware/authMiddleware.ts:52-64
Timestamp: 2026-06-02T02:42:59.276Z
Learning: In kinde-auth-nextjs, the pattern of returning `NextResponse.json({ statusCode: 401, message: "Unauthorized" })` without `{ status: 401 }` (resulting in HTTP 200 with a 401 status code in the body) is intentionally consistent across `loginRedirect` in `src/authMiddleware/authMiddleware.ts` and five call sites in `protect.js`. The maintainer (shafaladhikari) has explicitly deferred fixing the HTTP transport-level status to a follow-up that will address all locations holistically to avoid introducing new inconsistencies. Do not flag individual instances of this pattern as bugs in isolation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


const handleMiddleware = async (req, options, onSuccess) => {
const { pathname, search } = req.nextUrl;

Expand Down Expand Up @@ -95,6 +114,7 @@ const handleMiddleware = async (req, options, onSuccess) => {

if (hasInvitationCode) {
return handleInvitationCodeRedirect(
req,
invitationCode,
registerPage,
loginRedirectUrl,
Expand Down Expand Up @@ -122,9 +142,7 @@ const handleMiddleware = async (req, options, onSuccess) => {
"authMiddleware: no access or id token, redirecting to login",
);
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
return loginRedirect(req, loginRedirectUrl, options?.redirectURLBase);
}

const session = await sessionManager(req);
Expand All @@ -145,12 +163,7 @@ const handleMiddleware = async (req, options, onSuccess) => {
console.error(debugMessage);
}
if (!isPublicPath) {
return NextResponse.redirect(
new URL(
loginRedirectUrl,
options?.redirectURLBase || config.redirectURL,
),
);
return loginRedirect(req, loginRedirectUrl, options?.redirectURLBase);
}
return undefined;
};
Expand Down Expand Up @@ -247,9 +260,7 @@ const handleMiddleware = async (req, options, onSuccess) => {
"authMiddleware: access token decode failed, redirecting to login",
);
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
return loginRedirect(req, loginRedirectUrl, options?.redirectURLBase);
}

try {
Expand All @@ -260,9 +271,7 @@ const handleMiddleware = async (req, options, onSuccess) => {
"authMiddleware: id token decode failed, redirecting to login",
);
}
return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
return loginRedirect(req, loginRedirectUrl, options?.redirectURLBase);
}

const customValidationValid = options?.isAuthorized
Expand Down Expand Up @@ -327,9 +336,7 @@ const handleMiddleware = async (req, options, onSuccess) => {
console.log("authMiddleware: default behaviour, redirecting to login");
}

return NextResponse.redirect(
new URL(loginRedirectUrl, options?.redirectURLBase || config.redirectURL),
);
return loginRedirect(req, loginRedirectUrl, options?.redirectURLBase);
};

/**
Expand Down