Fixing the “Authentication Checking” Loop in Next.js + Auth0 (SSR Issue)

🧩 Fixing the “Authentication Checking” Loop in Next.js + Auth0 (SSR Issue)
Context
While building a Next.js (App Router) dashboard that uses Auth0 for authentication,
we encountered a recurring issue where the login flow froze on the message:
“Checking authentication…”
This turned out not to be an API or credential problem — it was caused by
server-side rendering (SSR) in Next.js trying to execute Auth0’s browser-only logic.
🚨 The Issue
After a user logged in via Auth0 (/login), the redirect worked correctly,
but the app then stalled on a blank screen showing “Checking authentication…” indefinitely.
Symptoms:
- DevTools showed repeated network requests to Auth0’s
/authorizeendpoint. - Clearing browser cache temporarily fixed the problem.
- After refreshing, the issue returned.
🔍 Root Cause: SSR + window Reference
The Auth0 React SDK (@auth0/auth0-react) relies on browser APIs like window and localStorage.
However, Next.js App Router initially renders pages on the server, where window doesn’t exist.
When code like this executes on the server:
const domain = window.location.origin;
…it throws an invisible error during SSR,
causing the app to hang during authentication checks.
💡 The Fix: Client-Side Auth0 Provider Wrapper
We fixed this by creating a client-only wrapper that safely initializes Auth0
only when running in the browser, not during SSR.
/app/providers/Auth0Provider.tsx
"use client";
import { Auth0Provider } from "@auth0/auth0-react";
import { useRouter } from "next/navigation";
export default function Auth0ProviderWrapper({ children }: { children: React.ReactNode }) {
const router = useRouter();
// ✅ Prevent SSR execution
if (typeof window === "undefined") return null;
const onRedirectCallback = (appState?: { returnTo?: string }) => {
router.push(appState?.returnTo || "/dashboard");
};
return (
<Auth0Provider
domain="auth0-demo.us.auth0.com"
clientId="your-auth0-client-id"
authorizationParams={{
redirect_uri: window.location.origin + "/dashboard",
audience: "https://auth0-demo-api",
scope: "openid profile email",
}}
onRedirectCallback={onRedirectCallback}
cacheLocation="localstorage"
useRefreshTokens={true}
>
{children}
</Auth0Provider>
);
}
⚙️ Applying the Wrapper in Layout
Previously, the layout directly included its own Auth0Provider,
creating duplicate providers that caused token mismatches.
The fix was to use the unified wrapper instead.
/app/dashboard/layout.tsx
"use client";
import "@/app/globals.css";
import Header from "@/app/_components/Header";
import { LogOut, LifeBuoy } from "lucide-react";
import { useRouter } from "next/navigation";
import { useAuth0 } from "@auth0/auth0-react";
import Auth0ProviderWrapper from "@/app/providers/Auth0Provider";
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<Auth0ProviderWrapper>
<DashboardInnerLayout>{children}</DashboardInnerLayout>
</Auth0ProviderWrapper>
);
}
function DashboardInnerLayout({ children }: { children: React.ReactNode }) {
const { logout } = useAuth0();
const router = useRouter();
return (
<>
{/* 🟦 Fixed Header */}
<header className="fixed top-0 left-0 w-full bg-white/90 backdrop-blur border-b z-50 flex items-center justify-between px-6 py-3">
<Header />
<div className="flex gap-3 items-center">
<button
onClick={() => router.push("/dashboard/support")}
className="flex items-center gap-1 px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-100 transition"
>
<LifeBuoy className="w-4 h-4" /> Support
</button>
<button
onClick={() =>
logout({
logoutParams: {
returnTo: "https://your-app-domain.com/logout",
},
})
}
className="flex items-center gap-1 px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-100 transition"
>
<LogOut className="w-4 h-4" /> Logout
</button>
</div>
</header>
<main className="pt-20">{children}</main>
</>
);
}
🧠 Why This Works
✅ SSR-Safe: The provider only initializes on the client (after window exists).
✅ Single Auth0 Context: Avoids duplicate providers and token mismatches.
✅ Persistent Sessions: cacheLocation="localstorage" keeps users logged in across reloads.
✅ Automatic Refresh: useRefreshTokens={true} ensures long-lived sessions.
🧩 Key Takeaways
ProblemSolution
Auth0 stuck on “Checking authentication…”
Guard Auth0Provider with "use client" and typeof window
Lost session after page reload
Use cacheLocation="localstorage"
Frequent re-login required
Enable useRefreshTokens={true}
Duplicate Auth0 contexts
Use a single global provider wrapper
🧰 Implementation Checklist
✅ Result
After applying this fix:
- The login no longer freezes on “Checking authentication…”
- Browser cache clearing is no longer necessary
- Redirect to
/dashboardis instant after login - Sessions persist even after browser restarts
💬 Notes
If the issue persists:
- Ensure
redirect_urimatches your deployed URL (e.g.https://your-app-domain.com/dashboard). - Confirm
domainandclientIdmatch your Auth0 app settings. - Remove any extra Auth0Provider definitions in nested components.
Would you like me to append a short Japanese summary section (for internal sharing, like “社内向け要約”)?
It can summarize this English article in concise Japanese for your Solize dev team.
