SSL

It is best practice, to ensure communication between the user and the app's server be encrypted. It is essential however when the communication consists of sensitive information such as the user's password. As it is quite easy to enable this encryption I'd recommend only using encrypted communication.

To do so we can utilise HTTPS using SSL (or TLS) which is widely supported and easy to use. To do so we need to get a certificate that browsers will recognise. Fortunately Let's Encrypt will issue us a certificate for free. Let's Encrypt is usable with Terraform via the acme provider, which is activated by adding the following to infrastructure/main.tf,

terraform {
  required_providers {
    ...
    acme = {
      source  = "vancluever/acme"
      version = "~> 2.0"
    }
  }
}

and running terraform init to initialise it.

To acquire a certificate for a domain name we'll need to prove to Let's Encrypt that we control the domain name. Fortunately this is doable via the acme provider via the following added to infrastructure/certs.tf,

provider "acme" {
  server_url = "https://acme-v02.api.letsencrypt.org/directory"
}

resource "tls_private_key" "private_key" {
  algorithm = "RSA"
}

resource "acme_registration" "me" {
  account_key_pem = tls_private_key.private_key.private_key_pem
  email_address   = var.heroku_username
}

resource "acme_certificate" "tozo_dev" {
  account_key_pem = acme_registration.me.account_key_pem
  common_name     = "tozo.dev"

  dns_challenge {
    provider = "gandiv5"

    config = {
      GANDIV5_API_KEY = var.gandi_api_key
    }
  }
}

Now we have a certificate we can ask Heroku to use it when serving our app, via the following additions to infrastructure/heroku.tf,

resource "heroku_domain" "tozo" {
  ...
  sni_endpoint_id = heroku_ssl.tozo_dev.id
}

resource "heroku_ssl" "tozo_dev" {
  app_id            = heroku_app.tozo.uuid
  certificate_chain = "${acme_certificate.tozo_dev.certificate_pem}${acme_certificate.tozo_dev.issuer_pem}"
  private_key       = acme_certificate.tozo_dev.private_key_pem

  depends_on = [heroku_formation.tozo-web]
}

Finally we need to ensure that any visitor to our app uses HTTPS, even if they initially visit using HTTP. This is achieved by a responding with a redirect for any HTTP visitors by adding the following to backend/src/backend/run.py,

from typing import Optional

from quart import redirect, request, Response


def create_app() -> Quart:
    ...

    @app.before_request
    async def redirect_to_https() -> Optional[Response]:
        if request.headers.get("X-Forwarded-Proto", "https") != "https":
            return redirect(request.url.replace("http://", "https://", 1), 301)
        return None