How to make python on Heroku https only?

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?


Solution 1:

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)

Solution 2:

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

Solution 3:

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.

Solution 4:

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)

Solution 5:

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:

  1. Do "pip install flask-sslify"

(github is here: https://github.com/kennethreitz/flask-sslify)

  1. 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)