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.


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-ad vs /azure-ad/
  • Wrong subdomain — www.yourdomain.com vs yourdomain.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 code
  • proxy_buffering off — Prevents NGINX from buffering the response from NextAuth.js before forwarding it to the client
  • proxy_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_upstream enabled — 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_URL in 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 off for 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 off proxy_next_upstream globally; only auth callback routes need this treatment
  • Set NEXTAUTH_URL explicitly 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 -t before 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.

f X W