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
Official Documentation
---
Found a different variation? Drop it in the comments—especially if you hit this at 3am with a different stack (Remix, SvelteKit, etc.)