HTTP Strict Transport Security

Http4s provides a Middleware giving support for HTTP Strict Transport Security (HSTS). The middleware is called HSTS and simply adds a header to enable a HSTS security policy. Though it is not enforced, HSTS only makes sense for an https service.

Examples in this document have the following dependencies.

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-dsl" % http4sVersion,
  "org.http4s" %% "http4s-server" % http4sVersion
)

And we need some imports.

import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import cats.effect.IO

If you're in a REPL, we also need a runtime:

import cats.effect.unsafe.IORuntime
implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global

Let's make a simple service that will be exposed and wrapped with HSTS.

val service = HttpRoutes.of[IO] {
  case _ => Ok("ok")
}

val request = Request[IO](Method.GET, uri"/")
// Do not call 'unsafeRunSync' in your code
val responseOk = service.orNotFound(request).unsafeRunSync()
// responseOk: Response[[A]IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 2),
//    = Strict(
//     bytes = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtArray@322b71de,
//         offset = 0L,
//         size = 2L
//       )
//     )
//   ),
//    = org.typelevel.vault.Vault@73816d18
// )
responseOk.headers
// res0: Headers = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 2)

If we were to wrap this on the HSTS middleware.

import org.http4s.server.middleware._

val hstsService = HSTS(service)
// Do not call 'unsafeRunSync' in your code
val responseHSTS = hstsService.orNotFound(request).unsafeRunSync()
// responseHSTS: Response[[A]IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 2, Strict-Transport-Security: max-age=31536000; includeSubDomains),
//    = Strict(
//     bytes = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtArray@36c3eb22,
//         offset = 0L,
//         size = 2L
//       )
//     )
//   ),
//    = org.typelevel.vault.Vault@beaac51
// )
responseHSTS.headers
// res1: Headers = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 2, Strict-Transport-Security: max-age=31536000; includeSubDomains)

Now the response has the Strict-Transport-Security header which will mandate browsers supporting HSTS to always connect using https.

As described in Middleware, services and middleware can be composed though HSTS is something you may want enabled across all your routes.

Configuration

By default HSTS is configured to indicate that all requests during 1 year should be done over https and it will contain the includeSubDomains directive by default.

If you want to preload or change other default values you can pass a custom header, e.g.

import org.http4s.headers._
import scala.concurrent.duration._

val hstsHeader = `Strict-Transport-Security`
  .unsafeFromDuration(30.days, includeSubDomains = true, preload = true)
val hstsServiceCustom = HSTS(service, hstsHeader)
// Do not call 'unsafeRunSync' in your code
val responseCustom = hstsServiceCustom.orNotFound(request).unsafeRunSync()
// responseCustom: Response[[A]IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 2, Strict-Transport-Security: max-age=2592000; includeSubDomains; preload),
//    = Strict(
//     bytes = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtArray@6ee2ee6f,
//         offset = 0L,
//         size = 2L
//       )
//     )
//   ),
//    = org.typelevel.vault.Vault@30344fc6
// )
responseCustom.headers
// res2: Headers = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 2, Strict-Transport-Security: max-age=2592000; includeSubDomains; preload)

References