NextAuth + Turnstile Redirect Loops (2026 Fix)

Yatish Goel

Yatish Goel

Co-Founder & CTO

February 15, 2026
#nextjs#nextauth#cloudflare-turnstile#middleware#auth#security#app-router

This post is about a very specific bug that keeps showing up in 2025-2026 Next.js SaaS builds: you add Cloudflare Turnstile to your login or signup form, you protect routes with NextAuth middleware, you deploy to Vercel (or anything with an edge-ish layer), and suddenly you get an infinite redirect loop.

It usually looks like: /login -> /api/auth/signin -> /login -> /api/auth/signin. The tab spins, users bounce, and you start blaming Turnstile.

Turnstile is rarely the real problem. The real problem is your auth flow has one unstable decision point, and middleware keeps re-evaluating it.

If you are a founder in the US/UK/Europe and you just want people to log in, this is the checklist I wish existed.

The exact failure mode (why this loop happens)

Redirect loops happen when something keeps telling Next.js: "you are not authenticated" even though you just did the work to authenticate. That "something" is almost always middleware.

The loop tends to appear when one or more of these is true:

1) Your middleware protects too much, including NextAuth endpoints or your Turnstile verify endpoint.
2) You redirect inside a server action or route handler before NextAuth finishes setting cookies.
3) Your callback URL gets lost or rewritten (common behind proxies, custom domains, www vs apex).
4) Cookie attributes (SameSite, Secure, domain) differ between localhost and production, so the session cookie never sticks.
5) You are mixing runtimes (Edge middleware, Node route handlers) and assuming headers behave the same.

What makes it expensive is that it often works locally. Then you ship and it breaks. A US-based team can burn 1-2 engineer-days if they debug blindly.

Pick a sane architecture (App Router + Turnstile + NextAuth)

Here is the setup that has the fewest weird edge cases in 2026:

- Turnstile widget runs on the client
- Token is verified server-side in a route handler: POST /api/turnstile/verify
- Only after verification, you call NextAuth signIn()
- Middleware protects only real app pages, not auth pages, not Turnstile routes

Yes, it is two steps. No, it is "extra". It is the difference between a predictable auth flow and a support ticket farm.

Step 1: verify Turnstile on the server (do not trust the client)

Cloudflare is pretty clear about this: the token must be verified server-side. The Turnstile token is short-lived and single-use. Treat it like a one-time password.

Minimal App Router route handler:

```ts
// app/api/turnstile/verify/route.ts
import { NextResponse } from "next/server";

export async function POST(req: Request) {
const { token } = await req.json();

const secret = process.env.TURNSTILE_SECRET_KEY;
if (!secret) return NextResponse.json({ ok: false }, { status: 500 });

const form = new FormData();
form.append("secret", secret);
form.append("response", token);

const resp = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
body: form,
});

const data = await resp.json();
return NextResponse.json({ ok: !!data.success, data });
}
```

Cloudflare describes Turnstile as a smart CAPTCHA alternative that can run without showing users a puzzle. That is true. But the server-side verification step is still required.

Common Turnstile mistakes that trigger auth chaos

These mistakes do not always break auth, but they create the conditions for a loop:

- You store the Turnstile token in localStorage and reuse it. Tokens are single-use.
- You let the login form submit twice (double click, Enter key + button, React strict mode effects).
- You verify Turnstile in the browser, then call signIn, but your server never sees the token.
- You accidentally run the verify route on Edge and hit a fetch or FormData difference you did not test.

The fix is boring: verify on the server, and reset the widget after any failure.

Step 2: only call NextAuth signIn after Turnstile success

A common mistake is wiring Turnstile into the same submit handler that calls signIn and letting both happen at once. That creates races. In the worst case, middleware runs before cookies are set, decides "not signed in", and bounces you back to /login.

Do this instead:

```ts
const onSubmit = async (e) => {
e.preventDefault();

setSubmitting(true);
try {
const token = await getTurnstileToken();

const verify = await fetch('/api/turnstile/verify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ token }),
}).then(r => r.json());

if (!verify.ok) {
setError('Try again.');
resetTurnstile();
return;
}

await signIn('credentials', {
email,
password,
callbackUrl: '/app',
});
} finally {
setSubmitting(false);
}
};
```

Opinion: if you try to compress this into one request because it feels "clean", you are trading cleanliness for unpredictability.

If you use OAuth providers (Google, GitHub)

Turnstile is mainly useful on email/password forms. For OAuth, it is awkward because the user gets bounced to a third-party domain anyway. If you are seeing loops with OAuth, the root cause is usually callback URL rewriting or cookie persistence. Skip Turnstile for OAuth and focus on rate limiting and bot rules on your own endpoints.

Step 3: fix your middleware matcher (the loop factory)

Most redirect loops are self-inflicted by middleware. People copy a matcher from a random snippet, then they add Turnstile, then the middleware starts blocking auth endpoints.

Your middleware should ignore at least:
- /api/auth (NextAuth)
- /api/turnstile/verify (your verify endpoint)
- /_next (Next assets)
- static files

Example matcher that is hard to mess up:

```ts
// middleware.ts
export const config = {
matcher: [
'/((?!api/auth|api/turnstile|_next/static|_next/image|favicon.ico).*)',
],
};
```

If you are only protecting /app, do not protect the whole site. Protect less, not more.

The auth gate logic should be dumb

If your middleware tries to be clever, it will break. I like middleware that does one thing:

- if path starts with /app and user is not signed in: redirect to /login
- else: let it through

Do not call your database from middleware. Do not verify Turnstile from middleware. Middleware is not your backend. It is a traffic cop.

Step 4: stop rewriting callback URLs (proxy + custom domain gotcha)

If you run behind a load balancer, reverse proxy, Cloudflare, or you have both www and apex domains, your callback URLs can get rewritten in ways that NextAuth does not like. That can bounce users back to /login even after a successful sign-in. It looks like a Turnstile bug, but it is not.

Checklist:

- Set NEXTAUTH_URL to the exact public URL you want (https, correct host).
- Ensure x-forwarded-proto is set correctly when TLS terminates upstream.
- Do not mix http and https in production.
- Pick one canonical host (www or apex). Redirect the other at the edge.

A real client story (anonymized): a founder in the UK had NEXTAUTH_URL set to the apex domain, but marketing used www in ads. Half the logins looped, half worked. The fix took 20 minutes once we noticed it. The discovery took 6 hours.

Step 5: cookies that bite in 2026 (Safari and mobile)

When this breaks only in Safari or only on iOS, think cookies.

Three things to watch:

1) SameSite: strict breaks more flows than people admit. lax is usually fine for same-site apps.
2) Secure: in production, cookies must be Secure. In dev, they often are not.
3) Domain: setting cookie domain wrong (like .example.com vs example.com) creates ghost sessions.

If you do not need cross-site auth, keep it boring. Boring auth is good auth.

Do not ship a different cookie story to production

The classic trap is: localhost works because everything is http and one host. Production breaks because you added a second host, or you are on https, or you are behind a proxy that drops a header.

Before you ship, test on the real production domain with HTTPS. Not the preview URL. Not localhost. The real thing.

Debugging: prove where the loop starts

Here is how I debug this fast:

1) Open DevTools Network tab and preserve log.
2) Refresh once and watch the first 302.
3) Identify which component returned the redirect.

Rules of thumb:

- If the first 302 is from middleware: your matcher or auth gate logic is wrong.
- If the first 302 is from /api/auth: it is NextAuth callback or cookie persistence.
- If you never hit /api/turnstile/verify: your frontend never verified, or middleware blocked it.

Also log in middleware. People avoid logging because it feels messy. Print the pathname and decision. You are debugging a loop.

A simple middleware logger

You can do this safely without leaking secrets:

```ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
const path = req.nextUrl.pathname;
const hasSessionCookie = Boolean(
req.cookies.get('next-auth.session-token') ||
req.cookies.get('__Secure-next-auth.session-token')
);

if (path.startsWith('/app') && !hasSessionCookie) {
console.log('MW redirect', { path, hasSessionCookie });
const url = req.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('from', path);
return NextResponse.redirect(url);
}

return NextResponse.next();
}
```

Do not overfit the cookie name. It depends on your NextAuth config. This is just to prove the decision point.

Real-world timeline and cost (US/UK teams)

For a typical Next.js SaaS in 2026:

- If the bug is only a matcher issue: 30-90 minutes.
- If it is callback URL + proxy headers: 2-4 hours.
- If it is cookie weirdness across subdomains + Safari: 4-8 hours.
- If it is a mixed runtime issue (Edge vs Node) plus logging gaps: a full day.

At US agency rates ($150-$250/hr), this tiny auth loop can cost $600 to $2,000. For a bootstrapped founder, that is brutal. That is why I tell teams to set the architecture right before they ship.

The quick fix checklist (print this)

If you just want the loop gone:

- Middleware matcher excludes /api/auth and your Turnstile verify endpoint
- Turnstile verified server-side before calling signIn
- NEXTAUTH_URL set to the correct https domain
- One canonical host (www or apex), not both
- Cookies not set to strict without a reason
- No redirects in server actions that run before cookies are set

If you do those, most loops disappear.

When you should ditch Turnstile (hot take)

Turnstile is great for form spam and basic bot pressure. But if your real problem is credential stuffing, you also need rate limits, per-user throttles, and maybe passkeys. Turnstile is a seatbelt, not an airbag.

If you are using Turnstile to stop bots from hammering /api/auth/callback/credentials, you are late. Put rate limiting in front of it too.

If you want help untangling an auth flow like this, HeyDev does rescue work for apps that are stuck in the "it works locally" phase. Keep your middleware boring and your callbacks explicit.

---

Related reading

Yatish Goel

Yatish Goel

Co-Founder & CTO

US Startup ExperienceIIT Kanpur

Full-stack architect with US startup experience and an IIT Kanpur degree. Yatish drives the technical vision at HeyDev, designing robust architectures and leading development across web, mobile, and AI projects.

Related Articles