Clerk: redirect loop after authentication [2026 fix]

Your redirect URI doesn't match Clerk's allowlist or middleware runs before auth state syncs. Add exact URI to dashboard + ensure getAuth() completes before redirect.

Clerk: Redirect Loop After Authentication [2am Emergency Fix]

TL;DR

Cause: Your application's redirect URI doesn't match Clerk's configured allowlist, OR your middleware redirects before the session is fully synced to the client.

Fix: Add your exact callback URL to Clerk Dashboard → Applications → Allowed Redirect URIs, AND ensure getAuth() completes before executing any redirects in middleware.

---

Real Console Error Messages

``` 1. "Error: Redirect URI mismatch. Received: http://localhost:3000/dashboard, Allowed: http://localhost:3000/auth/callback"

2. "Clerk: The redirect loop was caused by an incomplete session. Make sure your auth state is fully loaded before redirecting."

3. "TypeError: Cannot read property 'sessionId' of undefined at /app/.next/server/app/middleware.js:45:12"

4. "[auth] Caught exception in Clerk middleware: Invariant: sessionClaims should exist when isSignedIn is true"

5. "POST /api/auth/callback?code=xxxx 307 redirect loop detected - redirecting back to /sign-in infinitely" ```

---

Broken Code → Fixed Code

Issue #1: Missing/Mismatched Redirect URI

BROKEN: ```typescript // middleware.ts - redirects without checking allowlist import { authMiddleware } from "@clerk/nextjs";

export const middleware = authMiddleware({ publicRoutes: ["/", "/sign-in"], async afterAuth(auth, req) { if (!auth.userId) { return redirectToSignIn({ returnBackUrl: req.url }); } // Redirects to /dashboard, but Dashboard NOT in Clerk allowlist if (req.nextUrl.pathname === "/") { return NextResponse.redirect(new URL("/dashboard", req.url)); } }, }); ```

FIXED: ```typescript // middleware.ts - with proper URI configuration import { authMiddleware, redirectToSignIn } from "@clerk/nextjs"; import { NextResponse } from "next/server";

export const middleware = authMiddleware({ publicRoutes: ["/", "/sign-in"], async afterAuth(auth, req) { if (!auth.userId) { return redirectToSignIn({ returnBackUrl: req.url }); } if (req.nextUrl.pathname === "/") { return NextResponse.redirect(new URL("/dashboard", req.url)); } }, });

// Action: In Clerk Dashboard → Applications → Allowed Redirect URIs, add: // Production: https://yourdomain.com/dashboard // Dev: http://localhost:3000/dashboard ```

Issue #2: Middleware Runs Before Auth State Syncs

BROKEN: ```typescript // middleware.ts - checks auth before session loads export const middleware = authMiddleware({ publicRoutes: ["/"], async afterAuth(auth, req) { // ❌ sessionId might be undefined here const sessionId = auth.sessionId; if (sessionId && req.nextUrl.pathname === "/") { return NextResponse.redirect(new URL("/dashboard", req.url)); } }, }); ```

FIXED: ```typescript // middleware.ts - waits for full auth state export const middleware = authMiddleware({ publicRoutes: ["/"], async afterAuth(auth, req) { // ✅ Explicit check for loaded session if (auth.isLoaded && auth.userId) { if (req.nextUrl.pathname === "/") { return NextResponse.redirect(new URL("/dashboard", req.url)); } } else if (auth.isLoaded && !auth.userId) { // Only redirect unauthenticated users if fully loaded return redirectToSignIn({ returnBackUrl: req.url }); } // Return undefined to continue without redirect while loading return undefined; }, }); ```

Issue #3: Callback Route Missing

BROKEN: ```typescript // pages/api/auth/callback.ts - file doesn't exist // Clerk tries to POST here after OAuth, gets 404, loops back ```

FIXED: ```typescript // pages/api/auth/callback.ts - create this file import { handleCallback } from "@clerk/nextjs/api";

export default handleCallback();

// OR in App Router (app/api/auth/callback/route.ts): import { handleCallback } from "@clerk/nextjs/api"; export const GET = handleCallback(); export const POST = handleCallback(); ```

---

Still Broken? Check These Too

1. CORS/CSP Headers Blocking Clerk: If using Content-Security-Policy, ensure https://clerk.com and your domain are whitelisted. Check Network tab for blocked requests.

2. Environment Variables Mismatch: Verify NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY match your Clerk dashboard environment (dev vs. production). Mismatched keys cause session validation failures.

3. Multiple Auth Providers Conflicting: If using NextAuth.js + Clerk, middleware order matters. [Check Clerk + NextAuth integration guide](/?guide=clerk-nextauth). One library's redirect can trigger the other's middleware, creating loops.

---

Clerk Version Note

Behavior described is consistent with @clerk/nextjs v4.24.0+. If using v4.10-v4.23, the isLoaded flag worked differently—check your package.json and upgrade if stale: npm install @clerk/nextjs@latest.

---

Related Guides

  • [Clerk Middleware Configuration](/?guide=clerk-middleware-setup)
  • [NextAuth + Clerk Conflicts](/?guide=nextauth-clerk-redirect)
  • Official Documentation

  • [Clerk Redirect URI Configuration](https://clerk.com/docs/deployments/clerk-on-your-domain)
  • [Clerk Middleware Reference](https://clerk.com/docs/references/nextjs/auth-middleware)
  • ---

    Found a different variation? Drop it in the comments—especially if you hit this at 3am with a different stack (Remix, SvelteKit, etc.)

    🔥 0d
    LIVE
    ↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising •↑ Resend loved by devs •↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising
    DEVELOPER PAIN RADAR // Loading...

    Developers complain.
    Opportunities appear.

    We track what developers are struggling with today — and what opportunities that creates.

    —
    guides today
    —
    avg happiness
    🔥 Pain
    📖 Guides
    🔭 Explore
    👤 Mine
    🔥 Pain Radar — rage scores today
    ↗ share
    💡 Opportunity Feed — pain = market gap
    📈 Tool Momentum
    all scores →
    📖 Latest Guide
    all guides →
    📖 All Guides
    📊 Tool Scores
    + Submit
    📰 Hacker News
    ➕ Submit a Tool
    ← back