60

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?

Kristian
  • 6,357
  • 4
  • 36
  • 37

6 Answers6

67

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)
Symmetric
  • 4,495
  • 5
  • 32
  • 50
Kristian
  • 6,357
  • 4
  • 36
  • 37
  • 1
    Upvote for putting on github... Thanks! Just what I was looking for today. – David S May 22 '12 at 13:46
  • 4
    As a side note, this doesn't work if you have DEBUG set to True. Spent an hour figuring that one out, so hopefully this saves someone some time. – Femi Jul 08 '12 at 18:13
  • 4
    In this case, remember to add this to settings to let django know requests are secure: SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') – Bob Spryn Aug 19 '12 at 23:44
  • 1
    It appears that you cannot serve static files with Django using that middleware. I still don't know why since I'm accessing it through *https* – Gustavo Torres Mar 01 '13 at 14:51
  • 1
    request.is_secure() already takes care of the HTTP_X_FORWARDED_PROTO header, you should not check for it again, see https://github.com/return1/django-sslify-admin/issues/1 . _Currently, HTTP_X_FORWARDED_PROTO is always inspected. However; this header can be faked. As noted by the devs of django, you should be very explicit with such options: https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header._ – return1.at Jun 28 '13 at 17:06
  • I had to manually set SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') in my settings for is_secure to recognize heroku-style https – B Robster Jan 27 '14 at 16:56
  • this doesn't solve the problem of cookies being sent in cleartext on the first, non-SSL "GET" request. but then I don't suppose there *is* a fix for that. – jcomeau_ictx Feb 12 '15 at 04:42
  • Where do I put this? In what file? – Kovy Jacob Apr 02 '21 at 15:22
54

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

Thismatters
  • 507
  • 4
  • 14
shangxiao
  • 1,206
  • 11
  • 10
  • Does this mean the pip package sslify is obsolete as of Django 1.8? – dfrankow Aug 02 '15 at 17:52
  • @dfrankow django-sslify sounds similar to django-secure, but you'll have to confirm that with the package author – shangxiao Aug 03 '15 at 06:39
  • @dfrankow No, you still still need sslify with Django 1.8, if you want to automatically redirect users from http to https. – Ed J Dec 21 '15 at 08:15
  • 11
    sslify's author confirms [here](https://github.com/rdegges/django-sslify/issues/31) that @dfrankow is correct, sslify is obsolete for Django >= 1.8 – grrrrrr Apr 22 '16 at 19:29
  • Set SECURE_SSL_REDIRECT=False for local server and True for production. This can be done by setting environment variable. os.environ.get("SECURE_SSL_REDIRECT") – Aseem Apr 01 '19 at 06:09
  • Where do I put this? In what file? – Kovy Jacob Apr 02 '21 at 15:22
  • @KovyJacob You probably figured this out by now, but the answer is that all these go in the settings.py ... – rschwieb Jun 04 '22 at 01:05
13

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.

Allan Lei
  • 328
  • 4
  • 4
6

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)
Ryan Shea
  • 4,252
  • 4
  • 32
  • 32
  • If we do this.... do we still need to do the Heroku stuff? sorry bit new to this stuff – John May 27 '15 at 19:45
  • Though see the issue re "flip-flopping" at https://github.com/kennethreitz/flask-sslify/issues/3 – wodow May 19 '16 at 17:47
6

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)
CraigKerstiens
  • 5,906
  • 1
  • 25
  • 28
  • Yes, I am using django. Thanks for the answer: I will give it a try unless something simpler (like a hidden heroku option) appears.. – Kristian Dec 08 '11 at 19:51
  • I had to make a small tweak to you answer, but the moderators rejected my edit. I have created my own answer which fixes the problem with never-ending redirects in your current answer. Thanks anyway, would never have thought of a middleware-solution without your contribution. – Kristian Feb 09 '12 at 08:52
0

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.

Maicon Mauricio
  • 2,052
  • 1
  • 13
  • 29