I was trying to setup OpenID with the django-openid-auth plugin. Normally our sites don’t include absolute links (
https://example.com/hello-world) back to themselves, because relative URLs (
/hello-world) work perfectly well, so normally Django doesn’t need to know the domain name that it’s hosted it.
However, when authenticating with OpenID, our website needs to send the user off to
login.ubuntu.com with a callback url so that once they’re successfully authenticed they can be directed back to our site. This means that the
django-openid-auth needs to ask Django for an absolute URL to send off to the authenticator (e.g.
The problem with proxies
User <-> Apache <-> Gunicorn (Django)
(There’s actually an additional HAProxy load-balancer in between, which I thought was complicating matters, but it turns out HAProxy was just passing through requests absolutely untouched and so was irrelevant to the problem.)
Apache was setup as a reverse-proxy to Django, meaning that the user only ever talks to Apache, and Apache goes off to get the response from Django itself, with Django’s local network IP address – e.g.
It turns out this is the problem. Because Apache, and not the user directly, is making the request to Django, Django sees the request come in at
http://10.0.0.3/openid/login rather than
https://example.com/openid/login. This meant that
django-openid-auth was generating and sending the wrong callback URL of
How Django generates absolute URLs
HttpRequest.build_absolute_uri which in turn uses
HttpRequest.get_host to retrieve the domain.
get_host then normally uses the
HTTP_HOST header to generate the URL, or if it doesn’t exist, it uses the request URL (e.g.:
However, after inspecting the code for
get_host I discovered that if and only if
True then Django will look for the
X-Forwarded-Host header first to generate this URL. This is the key to the solution.
Solving the problem – Apache
In our Apache config, we were initially using mod_rewrite to forward requests to Django.
RewriteEngine On RewriteRule ^/?(.*)$ http://10.0.0.3/$1 [P,L]
However, when proxying with this method Apache2 doesn’t send the
X_Forwarded_Host header that we need. So we changed it to use
ProxyPass / http://10.0.0.3/ ProxyPassReverse / http://10.0.0.3/
This then means that Apache will send three headers to Django:
X-Forwarded-Server, which will contain the information for the original request.
In our case the Apache frontend used HTTPS protocol, whereas Django was only using so we had to pass that through as well by manually setting Apache to pass an
X-Forwarded-Proto to Django. Our eventual config changes looked like this:
<VirtualHost *:443> ... RequestHeader set X-Forwarded-Proto 'https' env=HTTPS ProxyPass / http://10.0.0.3/ ProxyPassReverse / http://10.0.0.3/ ... </VirtualHost>
This meant that Apache now passes through all the information Django needs to properly build absolute URLs, we just need to make Django parse them properly.
Solving the problem – Django
By default, Django ignores all
X-Forwarded headers. As mentioned earlier, you can set
get_host to read the
X-Forwarded-Host header by setting
USE_X_FORWARDED_HOST = True, but we also needed one more setting to get HTTPS to work. These are the settings we added to our Django
# Setup support for proxy headers USE_X_FORWARDED_HOST = True SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
After changing all these settings, we now have Apache passing all the relevant information (
X-Forwarded-Proto) so that Django is now able to successfully generate absolute URLs, and
django-openid-auth now works a charm.