How to Fix AADSTS54005 in NextAuth.js Azure AD
Introduction
If you are building a Next.js application with NextAuth.js and Azure Active Directory authentication, there is one error that will stop your entire login flow cold — especially after deployment:
AADSTS54005: OAuth2 Authorization code was already used
No warning before it happens. No obvious cause in the application code. Just a broken authentication callback and users who cannot log in.
I hit this exact error while deploying a production application using Next.js, NextAuth.js, and Azure AD OAuth authentication behind an NGINX reverse proxy on Linux. Development worked perfectly. The moment it went live, the login flow started failing silently.
This guide covers exactly what AADSTS54005 means, every real-world cause behind it, and the precise fixes that resolved it in production — including the NGINX configuration change that was the actual root cause in my case.
- Introduction
- What AADSTS54005 Actually Means
- What Happened in My Production Deployment
- Root Causes of AADSTS54005 in NextAuth.js Azure AD
- How I Fixed AADSTS54005 in NextAuth.js — Step by Step
- Root Cause Summary — AADSTS54005 in NextAuth.js Production
- Common Mistakes That Cause AADSTS54005
- Best Practices for NextAuth.js Azure AD in Production
- Conclusion
What AADSTS54005 Actually Means
Before jumping into fixes, understanding what this error is saying saves you hours of debugging in the wrong direction.
When a user signs in through Azure Active Directory, the OAuth 2.0 Authorization Code flow works like this:
- User clicks login — Azure AD generates a one-time authorization code
- Azure redirects the user back to your app's callback URL with that code
- Your NextAuth.js backend exchanges that code for an access token
- The access token completes the login and creates the session
The critical rule in the OAuth 2.0 spec is that each authorization code can only be used once. Azure AD enforces this strictly. The moment that code is exchanged for a token, it is immediately invalidated.
So when AADSTS54005 appears, it means one thing with absolute certainty:
Your application sent the same authorization code to Azure AD more than once. The first exchange succeeded. The second attempt hit an already-invalidated code — and Azure returned the error.
What Happened in My Production Deployment
The setup was a standard production stack:
- Next.js application running on Node.js
- NextAuth.js handling Azure AD OAuth authentication
- NGINX acting as a reverse proxy on Ubuntu Linux
- Azure App Registration with production credentials
Development worked without a single issue. After deploying to the Linux server, users started hitting authentication failures immediately. Checking the application logs, I found that the NextAuth.js callback endpoint:
/api/auth/callback/azure-ad
was being called twice per login attempt. The first call succeeded and consumed the authorization code. The second call — arriving milliseconds later — tried to use the same invalidated code, which caused Azure AD to return AADSTS54005.
The application code was completely correct. The problem was entirely in the infrastructure layer — specifically in how NGINX was handling requests to the authentication callback route.
Root Causes of AADSTS54005 in NextAuth.js Azure AD
1. NGINX Reverse Proxy Retrying Failed Requests
This was the root cause in my case — and it is the most common cause of AADSTS54005 in self-hosted Next.js deployments on Linux.
NGINX has a default behavior called proxy_next_upstream which automatically retries requests to the upstream server when certain error conditions occur. For most routes this is harmless. For OAuth callback routes it is catastrophic — NGINX retries the callback request with the same authorization code that was already consumed by the first attempt.
2. Duplicate Frontend Login Triggers
React's rendering behavior — especially in development mode with Strict Mode enabled, or in production with certain useEffect patterns — can trigger the signIn() function more than once. Each call initiates a separate OAuth flow, which can result in multiple callback requests arriving at the server in quick succession.
3. Mixed Development and Production Azure AD Credentials
Using the same Azure App Registration client ID and client secret across development and production environments causes OAuth state mismatches. Azure AD tracks authorization codes per application registration — mixing environments breaks the expected flow and can produce AADSTS54005 alongside other authentication errors.
4. Redirect URI Mismatch in Azure App Registration
Azure Active Directory requires an exact match between the redirect URI your application sends during the OAuth request and the URI registered in your Azure App Registration. Any difference — trailing slash, HTTP vs HTTPS, wrong subdomain — causes the callback to fail, which can trigger retry behavior and produce duplicate code usage.
5. Browser or Network Retry Behavior
Some network conditions — slow responses from the upstream server, connection timeouts during the callback — cause browsers or intermediate network layers to retry the callback GET request automatically. Each retry carries the same authorization code from the original Azure redirect.
How I Fixed AADSTS54005 in NextAuth.js — Step by Step
Step 1: Verify Production Environment Variables
Before touching NGINX or Azure configuration, confirm that your production environment has completely separate credentials from development. Mixing these is a silent killer of OAuth flows.
# Development environment
NEXTAUTH_URL=http://localhost:3000
AZURE_AD_CLIENT_ID=dev-client-id
AZURE_AD_CLIENT_SECRET=dev-client-secret
AZURE_AD_TENANT_ID=dev-tenant-id
# Production environment
NEXTAUTH_URL=https://yourdomain.com
AZURE_AD_CLIENT_ID=prod-client-id
AZURE_AD_CLIENT_SECRET=prod-client-secret
AZURE_AD_TENANT_ID=prod-tenant-id
Critical: NEXTAUTH_URL must exactly match your production domain including the protocol. NextAuth.js uses this value to construct the callback URL it sends to Azure AD. A mismatch here causes every callback to land on the wrong address.
Step 2: Verify the Redirect URI in Azure App Registration
Log into the Azure Portal and navigate to your App Registration. Under Authentication → Redirect URIs, confirm this exact URI is listed:
https://yourdomain.com/api/auth/callback/azure-ad
Check for these common mistakes that cause silent failures:
- HTTP instead of HTTPS in production
- Trailing slash difference —
/azure-advs/azure-ad/ - Wrong subdomain —
www.yourdomain.comvsyourdomain.com - Development URI accidentally set as the only registered URI
Azure AD performs an exact string match. There is no fuzzy matching or partial URI acceptance.
Step 3: Fix NGINX Configuration for NextAuth.js OAuth Callback
This was the fix that resolved the issue entirely in my deployment. NGINX's default proxy_next_upstream directive was silently retrying the OAuth callback, consuming the authorization code twice on every login attempt.
Update your NGINX configuration for the NextAuth.js authentication routes:
location /api/auth/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Disable buffering for auth callbacks
proxy_buffering off;
proxy_request_buffering off;
# Critical — disable automatic retries on auth routes
proxy_next_upstream off;
}
The three critical directives for fixing AADSTS54005 are:
proxy_next_upstream off— Stops NGINX from retrying failed requests to the upstream server. Without this, any upstream error during the callback causes NGINX to retry — reusing the already-consumed authorization codeproxy_buffering off— Prevents NGINX from buffering the response from NextAuth.js before forwarding it to the clientproxy_request_buffering off— Prevents NGINX from buffering the incoming request body before forwarding it upstream
After updating the NGINX config, always validate and reload:
nginx -t
systemctl reload nginx
Step 4: Ensure signIn() Is Called Only Once on the Frontend
Audit your login trigger code to confirm the signIn() function cannot be called multiple times from the same user interaction. A common mistake is placing the call inside a useEffect without a proper dependency array, or inside an event handler that fires more than once.
Correct implementation:
// Single controlled login trigger
const handleLogin = async () => {
await signIn("azure-ad", { callbackUrl: "/" });
};
// Button — ensure no double-click is possible
<button onClick={handleLogin} disabled={isLoading}>
Sign in with Azure AD
</button>
Disabling the button after the first click prevents rapid double-clicks from firing two separate OAuth flows simultaneously.
Step 5: Add Callback Logging to Confirm the Fix
After applying the NGINX fix, add a temporary log to your NextAuth.js configuration to confirm the callback is now being triggered exactly once per login:
// pages/api/auth/[...nextauth].ts
export default NextAuth({
providers: [...],
callbacks: {
async signIn({ user, account }) {
console.log(`[Auth] signIn triggered — user: ${user.email}`);
return true;
},
async session({ session, token }) {
return session;
}
}
});
Before the fix, this log appeared twice per login in the server output. After applying proxy_next_upstream off in NGINX, it appears exactly once — confirming the duplicate request issue is resolved.
Root Cause Summary — AADSTS54005 in NextAuth.js Production
- NGINX
proxy_next_upstreamenabled — Silently retries OAuth callback, consuming auth code twice - Mixed dev/prod Azure AD credentials — OAuth state mismatch breaks the token exchange
- Redirect URI mismatch in Azure Portal — Callback lands on wrong address, triggers retries
signIn()called multiple times — Multiple parallel OAuth flows with same code- Wrong
NEXTAUTH_URLin production — Callback URL mismatch breaks Azure AD redirect
Common Mistakes That Cause AADSTS54005
- Not separating Azure App Registrations per environment — Always create a dedicated App Registration for production with its own client ID, secret, and redirect URIs
- Forgetting
proxy_next_upstream offfor auth routes — This single missing directive causes the majority of AADSTS54005 errors in NGINX-hosted Next.js applications - Using HTTP redirect URI in Azure for a production HTTPS domain — Azure AD rejects HTTP redirect URIs for production applications and the fallback behavior causes duplicate requests
- Not disabling the login button after first click — Users who click login twice before the redirect completes generate two simultaneous OAuth flows
Best Practices for NextAuth.js Azure AD in Production
- Always use separate Azure App Registrations for dev and production — never share client IDs or secrets across environments
- Scope NGINX retry-disable rules to
/api/auth/only — don't turn offproxy_next_upstreamglobally; only auth callback routes need this treatment - Set
NEXTAUTH_URLexplicitly in every environment — never rely on auto-detection in production deployments - Monitor your NextAuth.js callback logs after every deployment — a callback firing twice is always a proxy or frontend trigger issue, never an Azure AD issue
- Run
nginx -tbefore every reload — a broken NGINX config in production takes down authentication for every user
Conclusion
The AADSTS54005 error in NextAuth.js with Azure AD is not random and it is not caused by Azure AD behaving incorrectly. It is always caused by the same authorization code being sent to Azure more than once — whether through NGINX retry behavior, a frontend double-trigger, a credential mismatch, or a redirect URI misconfiguration.
In production deployments behind NGINX on Linux, the fix almost always comes down to one directive:
proxy_next_upstream off;
Add it to your NGINX location block for /api/auth/, reload NGINX, and your NextAuth.js Azure AD authentication will work reliably on every login attempt.
Fix the infrastructure layer first. The application code is almost never the problem.