Problem Summary
A Next.js application using Auth0 for authentication is deployed to a production domain. In development, auth works correctly at http://localhost:3000. After deployment to https://myapp.com, users who attempt to log in are redirected to Auth0 and then redirected back to the application, but Auth0 throws an error before completing the callback. The error page at Auth0's login domain (your-tenant.auth0.com) shows 'Oops, something went wrong callback_url_mismatch.' The URL in the Auth0 dashboard still has localhost:3000 as the only allowed callback URL.
Symptoms
- Auth0 error page: 'error=access_denied&error_description=callback_url_mismatch' in the redirect URL
- Error message: 'The redirect URI in the authorization request does not match any registered redirect URIs'
- The login flow works for developers (on localhost) but fails for all production users
- Auth0 logs in the Dashboard show 'Failed Login: Callback URL mismatch' events
- The application's AUTH0_BASE_URL environment variable points to the production URL but the Auth0 dashboard still has localhost
- Wildcard callback URL attempts like https://*.myapp.com are not matching as expected
- HTTPS production URLs fail but an HTTP version of the same domain (or a redirect chain) causes the URL Auth0 receives to not match the registered URL
- Staging and production deployments share the same Auth0 application and one environment's working config breaks the other
Business Impact
A callback URL mismatch is a hard authentication failure. No user can log in to the production application. New user sign-ups are impossible. Existing users cannot access their accounts. The application is effectively offline for all authenticated features. Depending on how quickly the issue is identified, this can represent hours of downtime during a product launch or after a routine deployment.
Root Cause
Auth0 implements the OAuth 2.0 redirect_uri validation as a security measure. When your application initiates a login flow, it sends a redirect_uri parameter telling Auth0 where to send the user after authentication. Auth0 compares this value against the list of Allowed Callback URLs registered in the application's settings. If the value sent by the application does not exactly match one of the registered URLs including protocol (http vs https), port, path, trailing slash, and query string Auth0 rejects the request with callback_url_mismatch.
Auth0's Allowed Callback URLs comparison is exact string matching, not prefix or pattern matching. https://myapp.com/api/auth/callback and https://myapp.com/api/auth/callback/ (with a trailing slash) are treated as different URLs. http://myapp.com/api/auth/callback and https://myapp.com/api/auth/callback are different URLs. The URL your application sends must be an exact byte-for-byte match of one entry in the allowed list.
Diagnosis
Step 1: Find the Exact Callback URL Your App Is Sending
The easiest way to find the exact redirect_uri your application is sending to Auth0 is to look at the Auth0 Dashboard logs. Navigate to Auth0 Dashboard > Monitoring > Logs and filter for 'Failed Login' events. Click a failed login event and look at the 'data.redirect_uri' field this is the exact URL that your application sent and that did not match.
# Using Auth0 Management API to fetch recent logs
# Replace YOUR_DOMAIN and YOUR_MGMT_TOKEN
curl -X GET "https://YOUR_DOMAIN.auth0.com/api/v2/logs?type=f" -H "Authorization: Bearer YOUR_MGMT_TOKEN" -H "Content-Type: application/json" | jq '.[0].data.redirect_uri'
# Or use the Auth0 CLI:
auth0 logs list --filter type:f --number 5Step 2: Update Allowed Callback URLs in the Auth0 Dashboard
Navigate to Auth0 Dashboard > Applications > Your Application > Settings. Find the 'Allowed Callback URLs' field. This field accepts a comma-separated list of URLs. Add your production callback URL exactly as it appears in the Auth0 logs from Step 1.
# Add all environments you need to support
# Comma-separate each URL on the same line or separate lines
http://localhost:3000/api/auth/callback,
https://myapp.com/api/auth/callback,
https://staging.myapp.com/api/auth/callback
# For Next.js with auth0-nextjs-sdk (v3+), the callback path is:
# /api/auth/callback
# For Next.js with @auth0/nextjs-auth0 (older), it may be:
# /api/auth/callback/auth0 (if using NextAuth.js under the hood)
# Also update these fields in the same Settings page:
# Allowed Logout URLs:
http://localhost:3000,https://myapp.com,https://staging.myapp.com
# Allowed Web Origins (for silent auth / refresh tokens):
http://localhost:3000,https://myapp.com,https://staging.myapp.comStep 3: Verify Environment Variables Match the Registered URL
The auth0-nextjs-sdk constructs the redirect_uri from your AUTH0_BASE_URL environment variable. If AUTH0_BASE_URL is set to http://myapp.com (HTTP) but your application is served over HTTPS, the redirect_uri sent to Auth0 will use HTTP, which will not match an HTTPS entry in the allowed list. Check that AUTH0_BASE_URL uses the same protocol as your production URL.
# Auth0 SDK environment variables
AUTH0_SECRET=your-long-random-secret-min-32-chars
AUTH0_BASE_URL=https://myapp.com # Must use HTTPS in production
AUTH0_ISSUER_BASE_URL=https://your-tenant.auth0.com
AUTH0_CLIENT_ID=your-auth0-client-id
AUTH0_CLIENT_SECRET=your-auth0-client-secret
# The SDK constructs the callback URL as:
# AUTH0_BASE_URL + "/api/auth/callback"
# So with the above: https://myapp.com/api/auth/callback
# This must appear in Auth0 Dashboard > Allowed Callback URLsStep 4: Handle Wildcard URLs for Preview Deployments
Vercel creates unique preview URLs for each deployment (myapp-git-branch-team.vercel.app). You cannot pre-register all possible preview URLs. Auth0 supports wildcard entries in Allowed Callback URLs but with important restrictions: wildcards can only appear at the beginning of the URL and only as a subdomain wildcard.
# VALID wildcard matches any subdomain of vercel.app ending in myapp
https://*.vercel.app/api/auth/callback
# VALID matches myapp-*.vercel.app
https://myapp-*.vercel.app/api/auth/callback
# INVALID wildcards cannot appear in the path
https://myapp.vercel.app/*/auth/callback
# INVALID wildcards cannot appear in the protocol or domain TLD
https://*myapp*/api/auth/callback
# For Vercel specifically, use a more targeted pattern:
# Auth0 recommends: https://myapp-git-*-myteam.vercel.app/api/auth/callback
# where myteam is your Vercel team slugFor production applications, never use wildcard callback URLs in production Auth0 applications. Use wildcards only in a separate development or staging Auth0 application. Production applications should have an explicit, finite list of allowed callback URLs.
Step 5: Use Separate Auth0 Applications per Environment
The cleanest long-term solution is a separate Auth0 Application (client ID / client secret pair) for each environment: development, staging, and production. Each application has its own Allowed Callback URLs list with no overlap. This prevents production deployments from accidentally being broken by development configuration changes.
# .env.development.local
AUTH0_CLIENT_ID=dev_client_id_here
AUTH0_CLIENT_SECRET=dev_client_secret_here
AUTH0_ISSUER_BASE_URL=https://your-tenant.auth0.com
AUTH0_BASE_URL=http://localhost:3000
# .env.staging (Vercel environment: Preview)
AUTH0_CLIENT_ID=staging_client_id_here
AUTH0_CLIENT_SECRET=staging_client_secret_here
AUTH0_ISSUER_BASE_URL=https://your-tenant.auth0.com
AUTH0_BASE_URL=https://staging.myapp.com
# .env.production (Vercel environment: Production)
AUTH0_CLIENT_ID=prod_client_id_here
AUTH0_CLIENT_SECRET=prod_client_secret_here
AUTH0_ISSUER_BASE_URL=https://your-tenant.auth0.com
AUTH0_BASE_URL=https://myapp.comProduction Failure Scenarios
Scenario 1: Custom Domain Added Without Updating Callback URLs
The app is initially deployed to myapp.vercel.app and Auth0 has that URL in Allowed Callback URLs. The team adds a custom domain myapp.com, updates DNS, but forgets to add https://myapp.com/api/auth/callback to Auth0. The application now serves traffic on the custom domain but Auth0 still only knows about the Vercel URL. Every login from the custom domain fails with callback_url_mismatch. Fix: any time the application's domain changes, Auth0 Allowed Callback URLs, Allowed Logout URLs, and Allowed Web Origins must all be updated.
Scenario 2: Load Balancer Strips HTTPS, App Reports HTTP
A Node.js application behind a load balancer receives requests as HTTP internally (the LB terminates HTTPS). The Auth0 SDK reads req.protocol to construct the redirect_uri. req.protocol returns 'http' because the app sees the internal HTTP request. Auth0 receives http://myapp.com/api/auth/callback, which does not match the registered https://myapp.com/api/auth/callback. Fix: configure the application to trust the X-Forwarded-Proto header from the load balancer by setting app.set('trust proxy', true) in Express or the equivalent in your framework.
import express from "express";
const app = express();
// Trust the X-Forwarded-Proto header from your load balancer
// so req.protocol correctly returns "https" even though the app
// receives HTTP internally
app.set("trust proxy", true);
// For the Auth0 SDK, also set:
// AUTH0_BASE_URL=https://myapp.com (with HTTPS)
// The SDK will use this value rather than computing from req.protocolScenario 3: Trailing Slash Mismatch from Reverse Proxy
A reverse proxy adds a trailing slash to the callback URL (https://myapp.com/api/auth/callback/). The Auth0 dashboard has https://myapp.com/api/auth/callback (no trailing slash) registered. Auth0's exact match comparison rejects the request. Fix: either normalize the URL in the reverse proxy configuration to strip trailing slashes, or add both variants to the Auth0 Allowed Callback URLs list.
Prevention Checklist
- Add production callback URLs to Auth0 Allowed Callback URLs as part of the deployment runbook, not after deployment
- Use separate Auth0 Applications for development, staging, and production environments
- Store AUTH0_BASE_URL in environment-specific .env files and verify it uses HTTPS in production
- Update Allowed Callback URLs, Allowed Logout URLs, and Allowed Web Origins simultaneously when adding a new domain
- Set up trust proxy configuration on any Node.js server behind a load balancer that terminates HTTPS
- After every domain change (custom domain, CDN, etc.), test the login flow from an incognito window immediately
- Monitor Auth0 Dashboard Logs for 'Failed Login' events and set up alert notifications via Auth0 Log Streams
- Document all registered callback URLs and which environments they belong to in your team's infrastructure documentation
Resolution Summary
The production login failure was caused by AUTH0_BASE_URL being set to the production HTTPS domain while the Auth0 dashboard Allowed Callback URLs still contained only http://localhost:3000/api/auth/callback from initial development setup. Adding https://myapp.com/api/auth/callback to the Allowed Callback URLs in the Auth0 dashboard along with https://myapp.com to Allowed Logout URLs and Allowed Web Origins resolved the mismatch immediately. No code change was required; it was a configuration-only fix.
Lessons Learned
- Auth0 callback URL validation is exact string matching protocol, host, port, path, and trailing slash must all match exactly
- The Auth0 dashboard Allowed Callback URLs field is a deployment-time artifact that must be treated the same as environment variables update it as part of every deployment to a new domain
- A reverse proxy or load balancer that terminates HTTPS changes the protocol seen by the application and can cause URL mismatches unless trust proxy is configured
- Separate Auth0 Applications per environment eliminate an entire category of cross-environment configuration interference
- Auth0 Dashboard > Monitoring > Logs shows the exact redirect_uri that failed always start diagnosis there, not in application code
Conclusion
Auth0 callback URL mismatches are frustrating because the fix is trivial once identified add the URL to the allowed list but the diagnosis can take time if you do not know where to look. The Auth0 Dashboard logs show the exact URL that failed within seconds. From there, the fix is a one-minute dashboard update. The real investment is in preventive infrastructure: separate Auth0 apps per environment and a deployment runbook that includes updating Auth0 configuration alongside environment variables.