CloudFront reverse proxy API Gateway to prevent CORS

In this blog we will do a quick recap of CORS and reverse proxies. Then we will show how a reverse proxy can eliminate CORS, specifically in the context of a SPA hosted on CloudFront with an API Gateway backend. The sample code focuses on public, authenticated routes (Authorization header) and IAM signed request all being reverse proxied through CloudFront. Everything is done with the AWS CDK and can be found here => https://github.com/rehanvdm/cloudfront-reverse-proxy-apigw

I wonder if it is safe to assume that every developer that has worked with an API, knows what CORS is. I bet you are here because like many others you have lost countless hours against the battle to properly implement CORS.

Pre-requisites and assumptions:

  • You have basic knowledge of AWS services like CloudFront, S3, Lambda, API GW and IAM.
  • If you are following the code samples, that you may already have the AWS SDK and CDK installed.

Quick CORS intro

CORS stands for Cross-Origin Resource Sharing, it restricts a web application running on one origin (protocol & domain & port) from accessing resources on a different origin.

The browser will first have to do what is known as a preflight request. It sends an OPTIONS request to the target, asking if it is allowed and willing to accept the actual request. This way the target domain can decide who and what the trusted sources are.

CORS logic, credits Wikipedia

This results in two separate requests for every request that you make, which really makes for a terrible user experience especially for users that are not close to your origin. To give you a better idea, here in South Africa the round trip to us-east-1 is anything from 400 to 800ms on a decent connection. With the latency that CORS adds, you can expect a basic request in our region, as observed by the user to be between 800 and 1200ms. Eliminating CORS would almost yield a 50% increase in API latency. 


CORS flow showing latency, credits Mozilla

The only way to eliminate CORS and prevent the preflight requests is to have both the frontend and the backend on the same origin.  This problem basically does not exist in a traditional application where both the frontend and the backend are on the same server and thus origin.

It is only when your frontend and backend are on different origins like in our case the frontend is running as a SPA on CloudFront and the backend is done using API Gateway. A reverse proxy solves this by allowing the frontend to call a path on its origin that forwards the request to API Gateway.

What does a Reverse Proxy Solve?

A reverse proxy requests resources on behalf of the current origin from one or more of the target origins. These resources are returned to the client appearing as if they originated from the current origin.

This is better understood visually, in the image below, CORS will be present as the frontend code makes requests to API Gateway directly to interact with the backend.

The solution here is to set CloudFront up as a reverse proxy on let’s say path /backend-api/* so that whenever data is sent to /backend-api/*, it is sent to the API Gateway. The frontend code then needs to make requests to itself (the origin it uses) at path /backend-api instead of using the different origin that is API Gateway.

CloudFront acts as both a CDN and a reverse proxy. The benefits that we gain from having this specific CloudFront setup includes:

  • No CORS preflight request is needed, both frontend and backend API are on the same origin. Thus an approximate 50% decrease in API request latency.
  • More consistent (and usually faster) API request routing. From a user perspective, the API requests will hit the closest CloudFront Point-of-Presence(POP) and then traverse to API Gateway on the AWS backbone network as opposed to traversing the public internet to the API Gateway.

We can also leverage other functionality, for example:

  • Terminate HTTPS at CloudFront and send data using HTTP to the backend, saving backend resources from doing the computationally expensive SSL dance.
  • Response compression.
  • Independent request caching available that can be set using the backend.

I am not going into further details about why/what and how CORS & Reverse Proxies work, this should be sufficient information for the rest of the post. I linked to additional resources at the end of the post if you want to do further reading.

Show me the codes!

The project has a stock standard CDK layout and is written in TypeScript, while the backend Lambdas are written JavaScript. The frontend is basic HTML and JavaScript, there is both a /src and a /dist folder for the frontend as one of the libraries needed is bundled using browsify. The complete project can be found here => https://github.com/rehanvdm/cloudfront-reverse-proxy-apigw

Our CloudFront has a specific behavior to forward all requests at path /cf-apigw to our API Gateway domain, it is very important that we use the API Gateway stage as the origin path. This configuration eliminates CORS as the frontend no longer has to call the API Gateway directly but just a path on the same frontend domain.

The majority of the time you don’t want to integrate directly to an API Gateway domain so that it can be treated as ephemeral when you are doing Infrastructure as Code (IaC). Most scenarios front API Gateway with an API Gateway Custom domain that you own which then forwards to the AWS API Gateway domain and stage.

I have also included this as a different path on the same CloudFront. This path being /cf-cust-domain which will forward all requests to the custom domain which in return forwards it to the actual API Gateway. Keep in mind that the API Gateway Custom domain service is a “specially” designed CloudFront that AWS controls for you.

The main takeaway from these diagrams is to show the configuration for the CloudFronts and what path it will result in when it hits your API Gateway. If you haven’t figured it out already, CloudFront prepends the behavior path pattern to the path that it is forwarding. This means that we need the whole API to be beneath a resource with the same name as the path pattern. This logic holds true for anything that you reverse proxy through CloudFront.

Below are a few screenshots of what the AWS console looks like for these resources configured with the CDK in the above setup. I am only showing the API Gateway Custom domain setup as it is the trickier of the two to get right.

CloudFront Origin for behavior

CloudFront Behavior

Be sure to allow ALL methods for your API as you would need the full range when implementing REST. Since we authenticated paths, you must whitelist the Authorization header. Never whitelist the Host header, the second CloudFront(Custom Domain) will just refuse the request.

I am effectively disabling the caching for this behavior by setting all the TTL values to 0. The backend processing, in our case Lambda, can respond with the appropriate caching headers and CloudFront will apply them.

API Gateway custom domain
API Gateway prod stage

This is the important part, especially if you are not proxying all requests to the same Lambda. All of your API resources now need to live under the same path as the CloudFront behavior path, in our case, it is /cf-cust-domain. All of these methods hit the same Lambda function that just echoes back the event object as received by the Lambda.

I want to focus on 3 different requests as made from CloudFront:

<CloudFront domain>/cf-cust-domain/no-auth

This route will not specifically match anything that we defined in our API Gateway stage above, thus it will fall under the /{proxy+} path under the root. I specifically set up this “catch undefined routes” resource to debug and get my head around the problem. This is also how I am testing unauthenticated routes.

By inspecting the browser web console, we can see the responses for this request, observe the path that the Lambda saw and what resource was hit on API Gateway:

<CloudFront domain>/cf-cust-domain/auth

This route does have a resource and method defined which is set up to use a Lambda Token Authorizer with the Token Source set to the Authorization header. We can see from the response that this route resource was hit.

We can also inspect and see the authorizer that is attached values that have been added by the stock standard AWS example of the Lambda Token Authorizer that I implemented. The request included an Authorization header with the value of allow as per example, this allowed the authorizer to pass.

<CloudFront domain>/cf-cust-domain/auth-iam

This is the last and most complicated route, the method on API Gateway has Auth set to AWS_IAM. This requires you to first sign the request with your current IAM profile/role before making the request and then adding the signing headers when you make the request, you can read more about this here and here. I used the aws4 npm package to do the signing process for me as per browser example.

The function that we have been using so far looks like this:

The one that includes the signed headers that we must use for IAM auth looks like this:

You still require the Custom domain & path mapping OR the API Gateway domain & stage for signing the request. You can then make the request as per usual. Below are all the requests made to both CloudFront paths, more info can be found under /src/frontend/src/index.js

Result

Navigating to the CloudFront domain that was created by the CDK stack will greet you with this very basic html page to capture the details required to make the requests above.

I left the default values for example only, please change these if you are following along in code. Running this on my localhost means I have a different domain than the CloudFront one, the browser will thus send OPTIONS requests as per CORS specification. I left CORS enabled on the API Gateway with very permissive values of all (or *) so that we can compare the result while running on our localhost during development.

Requests made from localhost origin

You can see the impact CORS has on latency by inspecting the Waterfall, the GET request is made and is then blocked until the OPTIONS request informs the browser to continue with the GET request. The same requests on the CloudFront site results in this network output:

Requests made from CloudFront origin

We see that there are no more OPTIONS request being made, success!

For those who might be interested in the full project code, it can be found here => https://github.com/rehanvdm/cloudfront-reverse-proxy-apigw. Find below for those curious in the 200 line CDK stack that produced the 1500 lines CloudFormation template for this example.

Click to enlarge

Conclusion (TL;DR)

We showed you can use CloudFront to reverse proxy to your backend on API Gateway. This eliminates CORS which can hugely decrease request latency up to 50%. You have no excuse to not prevent CORS, if you control both the backend and frontend, every technology and framework has some concept of a reverse proxy.

Final note, you can use Lambda@Edge to remove the path that CloudFront prepends when it forwards the request. This adds extra latency to the request, but it will be much less than a round trip. My recommendation is to create a completely new API Gateway and API Custom domain to be used by CloudFront that is the exact same copy as used by the original Custom domain. This is becomes really easy if you are using an IaC tool.


Additional Resources:

CORS:

Reverse Proxy: