Skip to content

fix: return 401 for non-GET requests instead of 307 redirect to login#526

Open
shafaladhikari wants to merge 4 commits into
kinde-oss:mainfrom
shafaladhikari:main
Open

fix: return 401 for non-GET requests instead of 307 redirect to login#526
shafaladhikari wants to merge 4 commits into
kinde-oss:mainfrom
shafaladhikari:main

Conversation

@shafaladhikari
Copy link
Copy Markdown

Explain your changes

Fixes #171

The middleware used NextResponse.redirect() which defaults to HTTP 307 (preserves request method). When a POST request hit a protected route with an expired session, the middleware redirected it as POST /api/auth/login, which returned 405 since the handler only exports GET.

Non-GET requests are fetch/XHR calls that cannot meaningfully follow an auth redirect, so the middleware now returns { statusCode: 401, message: "Unauthorized" } instead — matching the existing error format used in protectApi.

Note: The existing protectApi responses (and this fix) return HTTP 200 with statusCode: 401 in the body. The actual HTTP response status should be 401. This is a pre-existing inconsistency worth addressing across protect.js and authMiddleware.ts in a follow-up.

Checklist

🛟 If you need help, consider asking for advice over in the Kinde community.

@shafaladhikari shafaladhikari requested a review from a team as a code owner June 2, 2026 02:32
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 629af1d2-85f3-49ac-bc05-f96990c06c47

📥 Commits

Reviewing files that changed from the base of the PR and between fefba21 and 891f370.

📒 Files selected for processing (1)
  • src/authMiddleware/authMiddleware.ts

Walkthrough

This PR centralizes login redirect behavior: it adds a loginRedirect helper that permits only GET/HEAD (other methods return 401 JSON), updates the invitation-code redirect to accept req and enforce the same method gating, and replaces several inline redirect call sites with loginRedirect.

Changes

Login Redirect Centralization

Layer / File(s) Summary
Introduce loginRedirect and gate invitation handler
src/authMiddleware/authMiddleware.ts
Updated handleInvitationCodeRedirect(req, ...) to validate HTTP method (allow only GET/HEAD), added loginRedirect helper enforcing the same rule and redirect URL selection, and updated the invitation-code call site to pass req.
Replace inline redirects with loginRedirect across auth failure paths
src/authMiddleware/authMiddleware.ts
Replaced inline NextResponse.redirect(new URL(...)) usages with loginRedirect for missing/expired tokens, token refresh failure paths, access-token and id-token decode failures, and the default unauthenticated redirect behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: returning 401 for non-GET requests instead of 307 redirects.
Description check ✅ Passed The description is directly related to the changeset, explaining the issue fix and the implementation approach.
Linked Issues check ✅ Passed The PR successfully addresses issue #171 by preventing non-GET requests from being redirected and returning a 401 JSON error response instead.
Out of Scope Changes check ✅ Passed All changes in the PR are directly related to fixing the non-GET request handling in the authentication middleware.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/authMiddleware/authMiddleware.ts`:
- Around line 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.
- Around line 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.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a62922c8-71ea-440e-8595-1a113d46a873

📥 Commits

Reviewing files that changed from the base of the PR and between d2e1f94 and fefba21.

📒 Files selected for processing (1)
  • src/authMiddleware/authMiddleware.ts

Comment on lines +52 to +64
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),
);
};
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: 405 method not allowed - Middleware + handler don't handle non-GET requests gracefully

2 participants