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