Fix OAuth redirect_uri_mismatch After Deploying Django

Fix Google, GitHub, and Microsoft OAuth redirect_uri_mismatch errors after deploying Django with django-allauth or social-auth: register the exact production callback URL, force https behind a proxy, and fix host and trailing-slash differences.

Problem statement

Social login works locally and breaks in production. Clicking "Sign in with Google" ends on Google's page:

Error 400: redirect_uri_mismatch
The redirect URI in the request, http://example.com/accounts/google/login/callback/,
does not match the ones authorized for the OAuth client.

GitHub shows "The redirect_uri MUST match the registered callback URL for this application", and Microsoft shows AADSTS50011. The provider compares the callback URL your Django app sends, character for character, with the list registered in the provider's console. In production the URL Django builds usually differs from what you registered: http instead of https, an internal host, a missing www, or a different path.

Why the provider rejects the loginDjango buildscallback URLSends to providerredirect_uri paramProvider comparesexact string matchMismatch400 error pageThe provider rejects any difference in scheme, host, port, path, or trailing slash.
  1. Read the exact redirect_uri from the error page or the authorization URL's query string.
  2. Register that exact production URL in the provider console, with https and the right host.
  3. If Django sends http, set SECURE_PROXY_SSL_HEADER and forward X-Forwarded-Proto; for allauth also set ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https".
  4. If the host is wrong, forward Host from Nginx and check the Sites framework domain.
  5. Keep separate OAuth clients or callback entries for local, staging, and production.

Step-by-step solution

1. See exactly what Django sends

Click the login button and copy the provider URL from the address bar before it errors, or open dev tools. Decode the redirect_uri parameter:

https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=http%3A%2F%2Fexample.com%2Faccounts%2Fgoogle%2Flogin%2Fcallback%2F&...

Decoded: http://example.com/accounts/google/login/callback/. Compare it with the registered list character by character.

2. Register the production callback

For django-allauth the callback paths are:

  • Google: https://example.com/accounts/google/login/callback/
  • GitHub: https://example.com/accounts/github/login/callback/
  • Microsoft: https://example.com/accounts/microsoft/login/callback/

For social-auth-app-django they look like https://example.com/complete/google-oauth2/. Add the exact URL, including the trailing slash, to the provider console. Google allows several redirect URIs per client; GitHub OAuth apps allow one callback URL (with subpaths), so use a separate app per environment.

which part of the URL differsPart that differsCause and fixhttp vs httpsproxy terminates TLS;SECURE_PROXY_SSL_HEADERinternal host or portHost not forwarded;proxy_set_header Hostwww vs apexregister both, or redirectto one before logintrailing slash or pathregister the exact allauthor social-auth pathlocalhost registered onlyadd the productionURL or a prod clientProviders compare the full string; every character countsdjango-deployment.com
Decode the redirect_uri parameter first; it tells you which row applies.

3. Fix http callbacks behind a proxy

If the decoded URL starts with http:// while the site is HTTPS, Django does not know the original scheme. Configure the proxy header and tell allauth to build https URLs:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
proxy_set_header X-Forwarded-Proto $scheme;

See Fix build_absolute_uri Returning http Instead of https.

4. Fix the host

If the callback contains 127.0.0.1:8000, a container name, or the load balancer hostname, Nginx is not forwarding the public Host. Add proxy_set_header Host $host;. Also check the Sites framework: allauth stores SocialApp entries per site, and SITE_ID must point to a Site whose domain is the production domain, not example.com.

5. Canonicalise www and apex

If users can reach both example.com and www.example.com, the callback follows whichever host they started on. Either register both callback URLs, or redirect all traffic to one canonical host in Nginx before login begins, which also avoids cookie and CSRF issues across hosts.

one callback per environmentCallback URLOAuth clientlocalhttp://localhost:8000/accounts/...dev clientstaginghttps://staging.example.com/...staging clientproductionโœ“https://example.com/accounts/...โœ“prod client, own secretSeparate clients keep production secrets out of developmentdjango-deployment.com
Store each client ID and secret in that environment's secrets, not in the database dump you share.

Verification check: log in with each provider on production in a private window; the provider should redirect back to https://example.com/accounts/<provider>/login/callback/ and you should land logged in. Check the decoded redirect_uri once more to confirm it matches.

Explanation

OAuth 2.0 requires providers to validate the redirect URI exactly against pre-registered values, because an open redirect would let an attacker receive authorization codes meant for your app. Django libraries build the redirect URI from the current request with build_absolute_uri, so any proxy misconfiguration that changes the scheme or host as seen by Django changes the URI sent to the provider. Fixing the proxy headers makes Django's view of the URL match the public one; registering that URL completes the match.

Edge cases / notes

  • Ports. Some providers treat https://example.com:443/ and https://example.com/ differently; never include default ports.
  • Propagation. Changes in the Google console can take a few minutes to apply.
  • Apple Sign In and Microsoft require HTTPS callbacks even for testing, except for specific localhost allowances.
  • Callback CSRF errors after fixing the URL usually come from cookies not being sent across www and apex; canonicalise the host.

Next steps

FAQ

Why does social login work locally but not in production?

The callback URL Django sends in production differs from the registered one, usually in scheme or host.

Does the trailing slash matter?

Yes. Providers compare the full string, including the trailing slash.

Can one OAuth client serve local and production?

Some providers allow several redirect URIs per client, but separate clients per environment are safer.

Why does Django send http when the site is https?

TLS ends at the proxy. Set SECURE_PROXY_SSL_HEADER and, for allauth, ACCOUNT_DEFAULT_HTTP_PROTOCOL.

Keep going