Fix NextAuth / Auth.js OAuth Errors — invalid_grant, CORS, Redirect URI
Updated April 2026
Reading this? Verify your fix live. Debug your OAuth error → OAuthFixer
NextAuth.js (now Auth.js) adds an abstraction layer over OAuth. When things break, the error message often points at the library rather than the underlying OAuth issue. Here is how to read through it.
The most common Auth.js errors
Error: OAuthCallbackError — "State" mismatch
# Cause: NEXTAUTH_URL does not match your actual deployment URL # The state parameter is validated against the originating URL # Fix — set in your deployment environment: NEXTAUTH_URL=https://yourapp.com # no trailing slash NEXTAUTH_SECRET=your-secret-here # required in production
Error: invalid_grant from provider
# Auth.js exchanges the code immediately — but if NEXTAUTH_URL is wrong, # the redirect_uri in the exchange does not match the registered URI # Debug: check what redirect_uri Auth.js is sending # It uses: NEXTAUTH_URL + "/api/auth/callback/[provider]" # This must EXACTLY match what you registered in the provider dashboard # Example for Google: # Registered: https://yourapp.com/api/auth/callback/google # NEXTAUTH_URL: https://yourapp.com → callback = https://yourapp.com/api/auth/callback/google ✅ # Wrong: # NEXTAUTH_URL: https://yourapp.com/ → callback = https://yourapp.com//api/auth/callback/google ❌
Error: CORS on /api/auth endpoints
# Auth.js API routes are Next.js API routes — they handle their own CORS # You should NOT add CORS headers to /api/auth/* routes # If you see CORS errors on /api/auth, the issue is usually: # 1. Calling Auth.js endpoints from a different domain (wrong architecture) # 2. A middleware intercepting and stripping headers # Correct: Auth.js endpoints are called from the same domain as your frontend # Wrong: Calling /api/auth from a separate frontend app on a different domain
Auth.js v5 (Next.js App Router) setup
// auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), ],
});
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
Required environment variables
# .env.local (development) NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=your-development-secret # Production (Vercel environment variables) NEXTAUTH_URL=https://yourapp.com NEXTAUTH_SECRET=your-production-secret # generate with: openssl rand -base64 32 # Provider credentials GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret
Provider callback URLs to register
| Provider | Callback URL to register |
|---|---|
| https://yourapp.com/api/auth/callback/google | |
| GitHub | https://yourapp.com/api/auth/callback/github |
| Discord | https://yourapp.com/api/auth/callback/discord |
| Auth0 | https://yourapp.com/api/auth/callback/auth0 |
| Okta | https://yourapp.com/api/auth/callback/okta |
Debug mode
// auth.ts — enable debug logging
export const { handlers, auth } = NextAuth({ debug: process.env.NODE_ENV === "development", providers: [...],
});
Debug mode logs the full OAuth flow including the redirect URIs being used. Run locally with your production environment variables to reproduce production-only errors.
Debug your OAuth error → OAuthFixer