ryotkim.com

News

NEWS

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

SSR

🧩 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 /authorize endpoint.
  • 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 /dashboard is instant after login
  • Sessions persist even after browser restarts

💬 Notes

If the issue persists:

  • Ensure redirect_uri matches your deployed URL (e.g. https://your-app-domain.com/dashboard).
  • Confirm domain and clientId match 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.