CORS
For security reasons, modern web browsers enforce a same origin policy,
restricting the ability of sites from a given origin
to access resources at a different origin. Http4s provides Middleware, named CORS
, for adding the appropriate headers
to responses to allow limited exceptions to this via cross origin resource sharing.
⚠️ Warning ⚠️
This guide assumes you are already familiar with CORS and its attendant security risks.
By enabling CORS you are bypassing an important protection against malicious third-party
websites - before doing so for any potentially sensitive resource, make sure you understand
what you are doing and why.
Usage
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 cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
Let’s start by making a simple service.
val service = HttpRoutes.of[IO] {
case _ =>
Ok()
}
// service: HttpRoutes[IO] = Kleisli(
// org.http4s.HttpRoutes$$$Lambda$8265/1868696275@4351abb
// )
val request = Request[IO](Method.GET, uri"/")
// request: Request[IO] = (
// Method("GET"),
// Uri(None, None, "/", , None),
// HttpVersion(1, 1),
// Headers(),
// Stream(..),
// io.chrisdavenport.vault.Vault@68351c32
// )
service.orNotFound(request).unsafeRunSync()
// res0: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0),
// Stream(..),
// io.chrisdavenport.vault.Vault@7d921033
// )
Now we can wrap the service in the CORS
middleware.
import org.http4s.server.middleware._
val corsService = CORS.policy.withAllowOriginAll(service)
// corsService: Http[cats.data.OptionT[IO, A], IO] = Kleisli(
// org.http4s.server.middleware.CORSPolicy$$Lambda$8998/713668082@dcdae66
// )
corsService.orNotFound(request).unsafeRunSync()
// res1: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0),
// Stream(..),
// io.chrisdavenport.vault.Vault@21019da6
// )
So far, there was no change. That’s because an Origin
header is required
in the requests and it must include a scheme. This, of course, is the responsibility of the caller.
val originHeader = Header("Origin", "https://somewhere.com")
// originHeader: Header.Raw = Raw(Origin, "https://somewhere.com")
val corsRequest = request.putHeaders(originHeader)
// corsRequest: request.Self = (
// Method("GET"),
// Uri(None, None, "/", , None),
// HttpVersion(1, 1),
// Headers(Origin: https://somewhere.com),
// Stream(..),
// io.chrisdavenport.vault.Vault@68351c32
// )
corsService.orNotFound(corsRequest).unsafeRunSync()
// res2: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, Access-Control-Allow-Origin: *),
// Stream(..),
// io.chrisdavenport.vault.Vault@6195d138
// )
Notice how the response has the CORS headers added. How easy was
that? And, as described in Middleware, services and middleware can be
composed such that only some of your endpoints are CORS enabled.
Configuration
The example above showed one basic configuration for CORS, which adds the
headers to any successful response, regardless of origin or HTTP method. There
are configuration options to modify that.
First, we’ll create some requests to use in our example. We want these requests
have a variety of origins and methods.
val googleGet = Request[IO](Method.GET, uri"/", headers = Headers.of(Header("Origin", "https://google.com")))
// googleGet: Request[IO] = (
// Method("GET"),
// Uri(None, None, "/", , None),
// HttpVersion(1, 1),
// Headers(Origin: https://google.com),
// Stream(..),
// io.chrisdavenport.vault.Vault@c6b56b1
// )
val yahooPut = Request[IO](Method.PUT, uri"/", headers = Headers.of(Header("Origin", "https://yahoo.com")))
// yahooPut: Request[IO] = (
// Method("PUT"),
// Uri(None, None, "/", , None),
// HttpVersion(1, 1),
// Headers(Origin: https://yahoo.com),
// Stream(..),
// io.chrisdavenport.vault.Vault@6c3577c4
// )
val duckPost = Request[IO](Method.POST, uri"/", headers = Headers.of(Header("Origin", "https://duckduckgo.com")))
// duckPost: Request[IO] = (
// Method("POST"),
// Uri(None, None, "/", , None),
// HttpVersion(1, 1),
// Headers(Origin: https://duckduckgo.com),
// Stream(..),
// io.chrisdavenport.vault.Vault@41742b49
// )
Now, we’ll create a configuration that limits the allowed methods to GET
and POST
, pass that to the CORS
middleware, and try it out on our requests.
import scala.concurrent.duration._
val corsMethodSvc = CORS.policy
.withAllowOriginAll
.withAllowMethodsIn(Set(Method.GET, Method.POST))
.withAllowCredentials(false)
.withMaxAge(1.day)
.apply(service)
// corsMethodSvc: Http[cats.data.OptionT[IO, A], IO] = Kleisli(
// org.http4s.server.middleware.CORSPolicy$$Lambda$8998/713668082@e272e4e
// )
corsMethodSvc.orNotFound(googleGet).unsafeRunSync()
// res3: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, Access-Control-Allow-Origin: *),
// Stream(..),
// io.chrisdavenport.vault.Vault@58af2fe5
// )
corsMethodSvc.orNotFound(yahooPut).unsafeRunSync()
// res4: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, Access-Control-Allow-Origin: *),
// Stream(..),
// io.chrisdavenport.vault.Vault@1f63f863
// )
corsMethodSvc.orNotFound(duckPost).unsafeRunSync()
// res5: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, Access-Control-Allow-Origin: *),
// Stream(..),
// io.chrisdavenport.vault.Vault@7fadb6a5
// )
As you can see, the CORS headers were only added to the GET
and POST
requests.
Next, we’ll create a configuration that limits the origins to “yahoo.com” and
“duckduckgo.com”. withAllowOriginHost
accepts an Origin.Host => Boolean
.
If you’re simply enumerating allowed hosts, a Set
is convenient:
import org.http4s.headers.Origin
val corsOriginSvc = CORS.policy
.withAllowOriginHost(Set(
Origin.Host(Uri.Scheme.https, Uri.RegName("yahoo.com"), None),
Origin.Host(Uri.Scheme.https, Uri.RegName("duckduckgo.com"), None)
))
.withAllowCredentials(false)
.withMaxAge(1.day)
.apply(service)
// corsOriginSvc: Http[cats.data.OptionT[IO, A], IO] = Kleisli(
// org.http4s.server.middleware.CORSPolicy$$Lambda$8998/713668082@48cfa240
// )
corsOriginSvc.orNotFound(googleGet).unsafeRunSync()
// res6: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, Vary: Origin),
// Stream(..),
// io.chrisdavenport.vault.Vault@693e6cbb
// )
corsOriginSvc.orNotFound(yahooPut).unsafeRunSync()
// res7: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, Access-Control-Allow-Origin: https://yahoo.com, Vary: Origin),
// Stream(..),
// io.chrisdavenport.vault.Vault@699e311f
// )
corsOriginSvc.orNotFound(duckPost).unsafeRunSync()
// res8: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, Access-Control-Allow-Origin: https://duckduckgo.com, Vary: Origin),
// Stream(..),
// io.chrisdavenport.vault.Vault@7b14aa17
// )
Again, the results are as expected. You can, of course, create a configuration that
combines limits on HTTP method, origin, and headers.
As described in Middleware, services and middleware can be composed such
that only some of your endpoints are CORS enabled.