setting up cors on API Gateway with cloudformation

Yesterday I finally (re)-launched ngnr.club and as expected, things didn’t really go as planned. When trying to pull information about a public profile, I was met with CORS errors.link

It was a classic case of “but it worked locally !”. The reason it did work in my dev environemnt is because I was using vite.js and its proxy feature, which allowed me to bypass CORS checks. Except the proxy only works when running the dev server, not when building the bundle.

So I had to go back to the drawing board and figure out how to setup CORS on my APIs.

Editing the Method resources

The first step to setup cors on api gateway with cloudformation is to update the method resource with the correct integration/method response information It looks like this:

GetPublicProfileMethod:
Properties:
  HttpMethod: GET
  ResourceId: !Ref SomeResource
  RestApiId: !Ref RestAPI
  AuthorizationType: NONE
  Integration:
    ConnectionType: INTERNET
    IntegrationHttpMethod: POST
    Type: AWS_PROXY
    Uri: <arn of the lambda function>
    IntegrationResponses:
      - StatusCode: "200"
        ResponseParameters:
          method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'"
          method.response.header.Access-Control-Allow-Origin: "'*'"
          method.response.header.Access-Control-Allow-Methods: "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'"
  MethodResponses:
    - StatusCode: "200"
      ResponseParameters:
        method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'"
        method.response.header.Access-Control-Allow-Origin: "'*'"
        method.response.header.Access-Control-Allow-Methods: "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'"
Type: AWS::ApiGateway::Method

This will set the correct headers on the response. But we’re not done. We have to update our code to return the same headers with our response. This is an example from my codebase in go:

return events.APIGatewayProxyResponse{
  Body: string(body),
  Headers: map[string]string{
    "content-type":                 "application/json",
    "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,auth,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent",
    "Access-Control-Allow-Origin":  "*",
    "Access-Control-Allow-Methods": "OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD",
  },
  StatusCode: r.StatusCode,
}

If all you have is unauthenticated endpoints then you’re good to go !

However, if you have authenticated endpoints, it’s going to involve a few more steps. On top of updating our method with the integration/method response, we have to add our custom authorization header

method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,my-auth-header,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'"

For this example, I called it my-auth-header. This must match the cognito authorizer header that you’ve set on creation. Don’t forget to update your code to include your header. If you try to make authenticated requests like this, you will still get CORS errors on the browser.

To resolve the issue, we have to create what’s called GatewayResponses.

Here’s an example:

GatewayResponseDefault4XX:
  Type: AWS::ApiGateway::GatewayResponse
  Properties:
    ResponseParameters:
      gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
      gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
      gatewayresponse.header.Access-Control-Allow-Methods: "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'"
    ResponseType: DEFAULT_4XX
    RestApiId: !Ref RestAPI
    StatusCode: "200"

GatewayResponseServerError:
  Type: AWS::ApiGateway::GatewayResponse
  Properties:
    ResponseParameters:
      gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
      gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
      gatewayresponse.header.Access-Control-Allow-Methods: "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'"
    ResponseType: DEFAULT_5XX
    RestApiId: !Ref RestAPI
    StatusCode: "200"

Notice how the status code says 200 ? That’s because if you set it to anything else, the browser will complain that the returned status code isn’t 200.

You can now re-deploy your infrastructure and enjoy an error free experience in production. Don’t forget to re-deploy the API on the API Gateway console for the changes to take effect.

Note: you might want to change the Access-Control-Allow-Origin header’s value to restrict requests coming from your domain and your domain only.