CORS in API Gateway

Patrick Krisko
4 min readMay 2, 2020

--

Setting up a Cross-Origin Resource Sharing (CORS) policy correctly is something that can be tricky to get right, especially for new developers. Its actually such a common issue that there’s a chrome plugin to unblock CORS requests with over 200,000 downloads.

In a nutshell, CORS is a policy enforced by browsers to control requests from one domain (origin) to another. The browser first makes an OPTIONS request to verify that the request is “allowed” before it makes the actual “real” request (“GET”, “POST”). There is a set of headers that support and enforce this mechanism. I’m oversimplifying here, but the Mozilla docs for this are great.

For my use case, I wanted to have different stages of my API, i.e. dev/prod, support different origins. For example, I wanted my dev environment to support localhost:3000, but my prod API to only allow requests from my deployed site. One of the nuances though of the headerAccess-Control-Allow-Origin is that it does not accept more than one domain as a value. The only accepted values for this header are the wildcard “*”, meaning all domains, or one specific domain, like “https://www.medium.com”.

One strategy for handling this is parsing the origin from the request headers, then checking if that value is in some list of allowed domains and returning that origin in the Access-Control-Allow-Origin header. This solution feels a bit contrived though. Instead, I used stage variables set through API Gateway.

We’ll start by creating the lambda function. I’m using Python 3.8 for this example. The key parts here are parsing the allowed_origin from the event object, and passing the CORS headers in the request body.

If you’re looking to follow along (or just copy and paste), you can find all the code from this example here https://github.com/pkrisko/cors-example , including a sample test event.

After a quick test to make sure everything is compiling, next step is integration with API Gateway. Click “+ Add trigger” at the top of the page.

Here are the configurations I am using. The important bits are creating a new API, and using the REST API configuration.

Navigate to the API Gateway for the resource you just created. There should be an “ANY” method created by default. However, I’m actually going to delete that and create a “GET” myself. Click “Actions”, → “Create Method”.

Point to the lambda function you just created, and be sure to check the box for “Use Lambda Proxy Integration”. This will pass the stage variables in the event object for you, so you don’t have to set that up in the mapping templates manually.

Next, let’s enable CORS for this resource. “Actions” → “Enable CORS”. The default configurations are fine! We’ll be overriding the Access-Control-Allow-Origin value later. After doing this, it will add an OPTIONS method and the appropriate CORS headers.

Navigate to the “Integration Response” on the “OPTIONS” method. You’ll see the CORS headers here. Originally, I was thinking I could do something like this to get it to work:

NOTE: Will not work!

However, it will not get parsed.

The secret here is to use the Mapping Templates. For this OPTIONS integration response, select application/json, and we’ll use this snippet, which sets the Access-Control-Allow-Origin header’s value to the current stage’s stageVariable named allowedOrigin.

#set($context.responseOverride.header[“Access-Control-Allow-Origin”] = $stageVariables.allowedOrigin)

Should look something like this:

Next step is to deploy this API, with “Actions” → “Deploy API”. Create a new stage or use an existing one.

Next, on the left column select “Stages” and navigate to the stage you just created. Navigate to the “Stage Variables” tab, and create one called allowedOrigin. You may have to deploy again.

Repeat this for as many stages as you need! For my use case I created a dev and prod stage.

You can test to make sure everything is working with fetch.html, serving it via a simple http server. Instructions in the README.

Boom!

I didn’t see anything online related to configuring allowed origins or manipulating the request object headers with mapping templates in this way. Hope this was helpful!

--

--