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.

Where the header disappearsClient sendsAuthorization: BearerProxy or Apachemay strip itDjango sees noneHTTP_AUTHORIZATIONDRF answersnot providedEach hop must forward the header; mod_wsgi drops it by default.
  1. Log what Django receives: check request.META.get("HTTP_AUTHORIZATION") in a debug view.
  2. Apache + mod_wsgi: add WSGIPassAuthorization On to the virtual host.
  3. Nginx: headers pass through by default; check you did not override them, and avoid redirects.
  4. CORS: allow the authorization header in CORS_ALLOW_HEADERS (django-cors-headers includes it by default).
  5. Avoid redirects such as missing trailing slashes or HTTP to HTTPS; clients drop Authorization on 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.

default behavior per layerDefaultFix if missingApache mod_wsgi!drops itWSGIPassAuthorization OnNginx proxy_passforwardsremove header overridesGunicorn, Uvicornforwardsnothing neededLoad balancersforwardscheck auth or WAF rulesmod_wsgi is the only common layer that drops it by defaultdjango-deployment.com
Test each hop with curl to find the one that removes it.

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_SLASH turning /api/orders into /api/orders/.
  • SECURE_SSL_REDIRECT sending http:// calls to https://.
  • www to 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.

symptom and causeSymptomCause and fixApache or cPanel hostmod_wsgi drops it;WSGIPassAuthorization Oncurl -I shows 301redirect drops header;call the final URLbrowser only, preflight failsCORS headers list; allow authorizationstaging with basic authauth_basic consumesit; separate locationFind the layer that removes the header, then fix that layerdjango-deployment.com
runserver forwards everything, which is why the bug appears only in production.

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-Key pass 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 Authorization must 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

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.

Keep going