Fix Django Authorization Header Missing Behind Apache, Nginx, or a Proxy
Fix Django REST Framework returning authentication credentials were not provided because the Authorization header never reaches Django: enable WSGIPassAuthorization in mod_wsgi, check Nginx and load balancer header forwarding, and test with curl at each hop.
Problem statement
Token or JWT authentication works with runserver, but in production every API call returns:
{"detail": "Authentication credentials were not provided."}
The client definitely sends Authorization: Bearer eyJ.... Somewhere between the client and Django, the header is removed. The most common culprit is Apache with mod_wsgi, which does not pass Authorization to the application unless told to. Others are proxies that strip it, CORS preflights that the browser never completes, and redirects that drop it.
- Log what Django receives: check
request.META.get("HTTP_AUTHORIZATION")in a debug view. - Apache + mod_wsgi: add
WSGIPassAuthorization Onto the virtual host. - Nginx: headers pass through by default; check you did not override them, and avoid redirects.
- CORS: allow the
authorizationheader inCORS_ALLOW_HEADERS(django-cors-headers includes it by default). - Avoid redirects such as missing trailing slashes or HTTP to HTTPS; clients drop
Authorizationon cross-origin redirects.
Step-by-step solution
1. See what reaches Django
Add a temporary view, protected or removed after use:
from django.http import JsonResponse
def header_debug(request):
return JsonResponse({
"has_authorization": "HTTP_AUTHORIZATION" in request.META,
"server": request.META.get("SERVER_SOFTWARE"),
})
Call it with curl from outside and from the server itself:
curl -s -H "Authorization: Bearer test" https://api.example.com/debug/headers/
curl -s -H "Authorization: Bearer test" http://127.0.0.1:8000/debug/headers/
If the local call to Gunicorn shows the header and the public one does not, a proxy layer removes it.
2. Apache with mod_wsgi
mod_wsgi withholds Authorization by default, so applications cannot see credentials meant for Apache's own auth modules. Turn that off:
<VirtualHost *:443>
ServerName api.example.com
WSGIDaemonProcess api python-home=/srv/api/.venv python-path=/srv/api
WSGIProcessGroup api
WSGIScriptAlias / /srv/api/config/wsgi.py
WSGIPassAuthorization On
</VirtualHost>
Reload Apache. This is the single most common cause on Apache, cPanel, and many shared hosts.
3. Nginx and other proxies
Nginx forwards client headers to the upstream unless you change them. Check for lines that clear it, such as proxy_set_header Authorization ""; left over from a basic-auth setup, or an auth_basic block on the same location, which consumes the header for Nginx's own authentication. Basic auth on a staging site and Bearer tokens on the API cannot share one location.
Cloud load balancers forward Authorization, but some add their own authentication (ALB OIDC, IAP, Cloudflare Access) and replace or require it. Check those settings if they are enabled.
4. Avoid redirects on API calls
Browsers and many HTTP clients remove Authorization when following a redirect to a different origin, and some do it for any redirect. Common accidental redirects:
APPEND_SLASHturning/api/ordersinto/api/orders/.SECURE_SSL_REDIRECTsendinghttp://calls tohttps://.wwwto apex domain redirects.
Call the final URL directly, and check with curl -I that the API endpoint returns 200 or 401, not 301.
5. Allow the header in CORS
For browser clients on another origin, the preflight OPTIONS request must allow authorization:
from corsheaders.defaults import default_headers
CORS_ALLOW_HEADERS = (*default_headers,) # includes "authorization"
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
If you replaced CORS_ALLOW_HEADERS with a custom list, make sure authorization is in it.
Explanation
Django exposes request headers in request.META with an HTTP_ prefix, and DRF's authentication classes read HTTP_AUTHORIZATION. Under WSGI, the server decides which headers become environment variables. mod_wsgi deliberately leaves out Authorization because Apache traditionally handled authentication itself and passing credentials to applications was considered a risk. Other servers pass it through. Redirect handling and CORS add client-side reasons for the header to vanish, which is why testing with curl at each hop is the fastest diagnosis.
Edge cases / notes
- Custom headers. Headers such as
X-Api-Keypass more easily, but Nginx drops header names with underscores by default; see Fix Nginx Headers With Underscores Dropped. - CGI and FastCGI. Some shared hosts run Python through CGI, where
Authorizationmust be passed with a rewrite rule into an environment variable. - Logging. Never log full Authorization values; log only whether the header was present.
Next steps
- Configure Apache properly with Deploy Django With Apache and mod_wsgi.
- Harden the API with Deploy Django REST Framework API in Production.
- For browser cross-origin issues, see Fix Django CORS Errors in Production.
FAQ
Why does it work with runserver?
The development server passes every header to Django. Apache with mod_wsgi does not pass Authorization unless configured.
What does WSGIPassAuthorization do?
It tells mod_wsgi to include the Authorization header in the WSGI environment so Django can read it.
Does Nginx strip the Authorization header?
Not by default. It does if you override it with proxy_set_header or use auth_basic on the same location.
Can I send the token in a different header instead?
You can, with a custom DRF authentication class, but fixing the proxy keeps standard clients and tools working.