How to make python on Heroku https only?
Question:
I have python/django app on Heroku (Cedar stack) and would like to make it accessible over https only. I have enabled the “ssl piggyback”-option, and can connect to it via https.
But what is the best way to disable http access, or redirect to https?
Answers:
What framework are you using for your application? If you’re using Django you could simple use some middleware similar to:
import re
from django.conf import settings
from django.core import urlresolvers
from django.http import HttpResponse, HttpResponseRedirect
class SSLMiddleware(object):
def process_request(self, request):
if not any([settings.DEBUG, request.is_secure()]):
url = request.build_absolute_uri(request.get_full_path())
secure_url = url.replace("http://", "https://")
return HttpResponseRedirect(secure_url)
Not sure if @CraigKerstiens’s answer takes into account that request.is_secure()
always returns False
if behind Heroku’s reverse proxy and not “fixed”. If I remember correctly, this will cause a HTTP redirect loop.
If you are running Django with gunicorn, another way to do it is to add the following to gunicorn’s config
secure_scheme_headers = {
'X-FORWARDED-PROTO': 'https'
}
Run with some like this in your Procfile
web: python manage.py run_gunicorn -b 0.0.0.0:$PORT -c config/gunicorn.conf
By setting gunicorn’s secure-scheme-header
, request.is_secure()
will properly return True
on https requests. See Gunicorn Config.
Now @CraigKerstiens’s middleware will work properly, including any calls to request.is_secure()
in your app.
Note: Django also has the same config setting call SECURE_PROXY_SSL_HEADER
, buts in the dev version.
Combining the answer from @CraigKerstiens and @allanlei into something I have tested, and verified to work. Heroku sets the HTTP_X_FORWARDED_PROTO to https when request is ssl, and we can use this to check:
from django.conf import settings
from django.http import HttpResponseRedirect
class SSLMiddleware(object):
def process_request(self, request):
if not any([settings.DEBUG, request.is_secure(), request.META.get("HTTP_X_FORWARDED_PROTO", "") == 'https']):
url = request.build_absolute_uri(request.get_full_path())
secure_url = url.replace("http://", "https://")
return HttpResponseRedirect(secure_url)
2020 update:
If you are using Flask, I would recommend the following:
@app.before_request
def before_request():
if 'DYNO' in os.environ:
if request.url.startswith('http://'):
url = request.url.replace('http://', 'https://', 1)
code = 301
return redirect(url, code=code)
The above works excellent on Heroku and allows you to use http
in local development with heroku local
.
Flask-SSLify is no longer maintained and no longer officially supported by the Flask community.
2014 original answer:
If you’re using Flask, this works quite well:
- Do "pip install flask-sslify"
(github is here: https://github.com/kennethreitz/flask-sslify)
- Include the following lines:
from flask_sslify import SSLify
if 'DYNO' in os.environ: # only trigger SSLify if the app is running
on Heroku
sslify = SSLify(app)
Django 1.8 will have core support for non-HTTPS redirect (integrated from django-secure):
SECURE_SSL_REDIRECT = True # [1]
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
In order for SECURE_SSL_REDIRECT
to be handled you have to use the SecurityMiddleware
:
MIDDLEWARE = [
...
'django.middleware.security.SecurityMiddleware',
]
[1] https://docs.djangoproject.com/en/1.8/ref/settings/#secure-ssl-redirect
For Flask use Talisman. Flask, Heroku and SSLify documentations favor the use of Talisman over SSLify because the later is no longer maintained.
From SSLify:
The extension is no longer maintained, prefer using Flask-Talisman as
it is encouraged by the Flask Security Guide.
Install via pip:
$ pip install flask-talisman
Instatiate the extension (example):
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
if 'DYNO' in os.environ:
Talisman(app)
Talisman enables CSP (Content Security Policy) by default only allowing resources from the same domain to be loaded. If you want to disable it and deal with the implications:
Talisman(app, content_security_policy=None)
If you don’t want to disable it you have to set the content_security_policy
argument to allow resources from external domains, like CDNs, for instance. For that refer to the documentation.
I have python/django app on Heroku (Cedar stack) and would like to make it accessible over https only. I have enabled the “ssl piggyback”-option, and can connect to it via https.
But what is the best way to disable http access, or redirect to https?
What framework are you using for your application? If you’re using Django you could simple use some middleware similar to:
import re
from django.conf import settings
from django.core import urlresolvers
from django.http import HttpResponse, HttpResponseRedirect
class SSLMiddleware(object):
def process_request(self, request):
if not any([settings.DEBUG, request.is_secure()]):
url = request.build_absolute_uri(request.get_full_path())
secure_url = url.replace("http://", "https://")
return HttpResponseRedirect(secure_url)
Not sure if @CraigKerstiens’s answer takes into account that request.is_secure()
always returns False
if behind Heroku’s reverse proxy and not “fixed”. If I remember correctly, this will cause a HTTP redirect loop.
If you are running Django with gunicorn, another way to do it is to add the following to gunicorn’s config
secure_scheme_headers = {
'X-FORWARDED-PROTO': 'https'
}
Run with some like this in your Procfile
web: python manage.py run_gunicorn -b 0.0.0.0:$PORT -c config/gunicorn.conf
By setting gunicorn’s secure-scheme-header
, request.is_secure()
will properly return True
on https requests. See Gunicorn Config.
Now @CraigKerstiens’s middleware will work properly, including any calls to request.is_secure()
in your app.
Note: Django also has the same config setting call SECURE_PROXY_SSL_HEADER
, buts in the dev version.
Combining the answer from @CraigKerstiens and @allanlei into something I have tested, and verified to work. Heroku sets the HTTP_X_FORWARDED_PROTO to https when request is ssl, and we can use this to check:
from django.conf import settings
from django.http import HttpResponseRedirect
class SSLMiddleware(object):
def process_request(self, request):
if not any([settings.DEBUG, request.is_secure(), request.META.get("HTTP_X_FORWARDED_PROTO", "") == 'https']):
url = request.build_absolute_uri(request.get_full_path())
secure_url = url.replace("http://", "https://")
return HttpResponseRedirect(secure_url)
2020 update:
If you are using Flask, I would recommend the following:
@app.before_request
def before_request():
if 'DYNO' in os.environ:
if request.url.startswith('http://'):
url = request.url.replace('http://', 'https://', 1)
code = 301
return redirect(url, code=code)
The above works excellent on Heroku and allows you to use http
in local development with heroku local
.
Flask-SSLify is no longer maintained and no longer officially supported by the Flask community.
2014 original answer:
If you’re using Flask, this works quite well:
- Do "pip install flask-sslify"
(github is here: https://github.com/kennethreitz/flask-sslify)
- Include the following lines:
from flask_sslify import SSLify
if 'DYNO' in os.environ: # only trigger SSLify if the app is running
on Heroku
sslify = SSLify(app)
Django 1.8 will have core support for non-HTTPS redirect (integrated from django-secure):
SECURE_SSL_REDIRECT = True # [1]
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
In order for SECURE_SSL_REDIRECT
to be handled you have to use the SecurityMiddleware
:
MIDDLEWARE = [
...
'django.middleware.security.SecurityMiddleware',
]
[1] https://docs.djangoproject.com/en/1.8/ref/settings/#secure-ssl-redirect
For Flask use Talisman. Flask, Heroku and SSLify documentations favor the use of Talisman over SSLify because the later is no longer maintained.
From SSLify:
The extension is no longer maintained, prefer using Flask-Talisman as
it is encouraged by the Flask Security Guide.
Install via pip:
$ pip install flask-talisman
Instatiate the extension (example):
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
if 'DYNO' in os.environ:
Talisman(app)
Talisman enables CSP (Content Security Policy) by default only allowing resources from the same domain to be loaded. If you want to disable it and deal with the implications:
Talisman(app, content_security_policy=None)
If you don’t want to disable it you have to set the content_security_policy
argument to allow resources from external domains, like CDNs, for instance. For that refer to the documentation.