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.
- Read the exact
redirect_urifrom the error page or the authorization URL's query string. - Register that exact production URL in the provider console, with
httpsand the right host. - If Django sends
http, setSECURE_PROXY_SSL_HEADERand forwardX-Forwarded-Proto; for allauth also setACCOUNT_DEFAULT_HTTP_PROTOCOL = "https". - If the host is wrong, forward
Hostfrom Nginx and check the Sites framework domain. - 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.
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.
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/andhttps://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
wwwand apex; canonicalise the host.
Next steps
- Make Django scheme-aware with Fix build_absolute_uri Returning http Instead of https.
- If the callback host is internal, see Fix Django Redirecting to localhost or the Wrong Host.
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.