I am facing a challenging debugging journey and its solution for an issue with @auth0/nextjs-auth0 on Vercel with the Next.js App Router.
Core Framework & Auth Libraries:
Database & API Libraries:
My application worked perfectly on localhost. However, when deployed to production on Vercel, the login flow would fail silently.
/dashboard
).307
redirect to /api/auth/login
.We exhaustively checked every common cause of this issue:
AUTH0_SECRET
, AUTH0_BASE_URL
, AUTH0_CLIENT_ID
, AUTH0_CLIENT_SECRET
, AUTH0_ISSUER_BASE_URL
, AUTH0_AUDIENCE
) were confirmed to be present and correct in the Vercel production environment.Allowed Callback URLs
, Allowed Logout URLs
, Allowed Web Origins
) were confirmed to be correct.Application Login URI
was correctly left blank.matcher
Configuration: We found and fixed a secondary issue where our matcher
was incorrectly running on /api
routes, causing a loop. Fixing this did not solve the original silent crash.To isolate the issue, we were forced to implement a fully manual authentication flow, completely bypassing the SDK's handleLogin
and handleCallback
functions. This manual process, while complex, is the only thing that works in our production environment.
Our manual flow consists of:
/api/auth/manual-login
): This route manually constructs the full Auth0 /authorize
URL, encodes our desired returnTo
path into the state
parameter, and redirects the user. This step was successful./api/auth/manual-callback
): This route receives the code
from Auth0, manually performs the server-to-server POST request to the /oauth/token
endpoint, and upon success, uses the jose
library to create its own secure, JWE-encrypted session cookie. Finally, it decodes the state
parameter and performs the redirect to the user's original destination.We are reaching out to the community because this solution feels unsustainable and overly complex. It required us to essentially reinvent the core functionality of the SDK, including session management and encryption, just to get a basic login to work on Vercel.
Throughout this process, two key behaviors made debugging nearly impossible:
Absence of Auth0 Logs: Despite countless failed login attempts, our Auth0 Dashboard never once showed a "Failed Exchange" or any error log related to the callback. We only ever saw a "Success Login" event (after the manual login implementation). This is the critical evidence proving the Vercel function was crashing silently before it could ever make the server-to-server request to exchange the token. The failure was a complete black box between Auth0-to-Vercel side.
The Silent Redirect Loop: The user-facing symptom was a silent failure. The browser would be redirected to an SDK route like /api/auth/login
or /api/auth/callback
, and the request would simply hang, eventually re-rendering the previous page without any error message in the console. This created a frustrating loop with no feedback for the user or our developers.
Any help we can get to look into this to know what could be the issue that we missed out? Appreciate the help.
After hours of debugging and narrowing down the issues, ended up it was because of vercel.json
which unknowingly trying to rewrite all paths..."catch-all" rewrite rule could be intercepting requests to /api/auth/login and incorrectly serving the main application page
{ "rewrites": [ { "source": "/:path*", "destination": "/" } ]}
We were checking earlier with build logs show the functions being created, hence we didn't expect this. Didn't realise that during the npm run build
process, Vercel's system correctly analyzes our Next.js project. It sees our app/api/...
files and successfully builds them into Serverless Functions. The build process does not look at the vercel.json
rewrites. This is why the build log looks perfect.
and this is different in Runtime: When a user's browser makes a request to https://<our-domain>/api/auth/login
, it hits Vercel's Edge Network. The very first thing Vercel does is process the rules in vercel.json
.
It sees the incoming path /api/auth/login
> checks rewrites rule. The rule {"source": "/(.*)"}
matches perfectly.
Vercel's router then says, "Okay, rule matched. I will now serve the content from the destination /
."
The request is handled by serving the homepage before it ever gets a chance to be routed to the Serverless Function that was created during the build. ¯\_(ツ)_/¯
So lesson learned: Check your vercel rewrite rules!