Skip to main content

CORS

What is CORS and when do you need it?

AWS provides great documentation that explains CORS.

A cross-origin HTTP requestis simple if all of the following conditions are true:

  • It is issued against an API resource that allows only GET, HEAD, and POST requests.
  • If it is a POST method request, it must include an Origin header.
  • The request payload content type is text/plain, multipart/form-data, or application/x-www-form-urlencoded.
  • The request does not contain custom headers.
  • Any additional requirements that are listed in the Mozilla CORS documentation for simple requests.

If your request contains an Authorization header or any other custom-header, it is non-simple.

Important points: For simple cross-origin POST method requests, the response from your resource needs to include the header Access-Control-Allow-Origin, where the value of the header key is set to '*'(any origin) or is set to the origins allowed to access that resource.

All other cross-origin HTTP requests are non-simple requests. If your API's resources receive non-simple requests, you need to enable CORS support.

Enable CORS for non-simple requests

To enable CORS support for non-simple requets,

  • You must set up an OPTIONS method to handle preflight requests to support CORS.
  • To do this in serverless.yml, you MUST include cors:true in serverless.yml so that API Gateway can respond to the OPTION preflight request.
    • cors: true in the serverless file sets up API gateway to respond to the OPTION preflight request. However it’s only needed if the browser makes one. AWS COTS docs for API Gateway.
    • Lambda still needs to send response headers for CORS (i.e. Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods).

What is a preflight request?

A preflight request is one that is required by non-simple HTTP requests, and is sent from the browser to the server using the OPTIONS method. A preflight request"

  • includes an Origin header
  • uses the OPTIONS method
  • includes the following headers
    • Access-Control-Request-Method
    • Access-Control-Request-Headers

CORS setup for Lambda Proxy

Read detailed documentation here

Lambda Proxy integration in API Gateway

The proxy integration is an easy way to configure the API Gateway. It starts with the gateway forwarding all parts of an HTTP request to the Lambda function. Next, the Lambda function returns all details of an HTTP response. No complicated configuration and data mapping needed on the API Gateway.

Three steps are necessary to enable CORS for the backend when using the Lambda proxy integration along with CloudFront

  1. Add static response for OPTIONS requests
  2. Implement adding CORS headers with the Lambda function
  3. Add CORS headers to server-side errors
  4. Set proper header forwarding in CloudFront

1. CORS Preflight request

Add static response for OPTIONS requests for API Gateway

The browser sends a so called CORS preflight requests (HTTP method OPTIONS) to each API resource to check for CORS configuration. Therefore, you need to make sure that the API Gateway answers OPTIONS requests for all resources that should be accessed by the SPA. To simplify the configuration, creating a proxy resource allows you to add an OPTIONS method to all resources at once.

To handle preflight requests, you have to add cors: true to each HTTP endpoint in your serverless.yml:

# serverless.yml

functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
cors: true

Setting cors to true assumes a default configuration which is equivalent to:

functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
cors:
origin: '*'
headers:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
allowCredentials: false

You can add custom headers if you face any custom headers CORS erros as such:

functions:
MyFunction:
handler: src/handlers/userHandler.profileSetup
events:
- http:
path: my/path
method: post
cors:
origin: '*' # <-- Specify allowed origin
headers: # <-- Specify allowed headers.
- Content-Type
- Authorization
- Access-Token # <-- Custom header
- Token-ID # <-- Custom header
allowCredentials: false # this can't be true when origin: '*'

Remember, you are only configuring the HTTP end-point for OPTION, but Lambda must return the RESPONSE

2. CORS Response

Implement adding CORS headers with the Lambda function

When configuring the proxy integration on the API Gateway, the Lambda function needs to return a response in a specific format. The following code snippet shows how to add the necessary CORS header Access-Control-Allow-Origin.

exports.handler = async event => {
let data = {}
let res = {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*' // replace with hostname of frontend (CloudFront),
"X-Requested-With": '*',
"Access-Control-Allow-Headers": 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,x-requested-with',
"Access-Control-Allow-Methods": 'POST,GET,OPTIONS'
},
body: JSON.stringify(data)
}
return res
}

Remember to add the Access-Control-Allow-Origin to all Lambda functions.

If you are using cookies, you have to:

    'Access-Control-Allow-Credentials': true // Required for cookies, authorization headers with HTTPS

But note that you CAN'T set a wildcard () for 'Access-Control-Allow-Origin': ''

Custom authorizer

If you're using a custom authorizer, you'll need to add the following CloudFormation in your resources block of serverless.yml:

Custom authorizers allow you to protect your Lambda endpoints with a function that is responsible for handling authorization.

If the authorization is successful, it will forward the request onto the Lambda handler. If it's unsuccessful, it will reject the request and return to the user.

The CORS difficulty lies in the second scenario—if you reject an authorization request, you don't have the ability to specify the CORS headers in your response. This can make it difficult for the client browser to understand the response.

To handle this, you'll need to add a custom GatewayResponse to your API Gateway. You'll add this in the resources block of your serverless.yml:

# serverless.yml
---
resources:
Resources:
# ApiGatewayAuthorizer
ApiGatewayAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
Name: Cognito_Auth
Type: COGNITO_USER_POOLS
IdentitySource: method.request.header.Authorization
RestApiId:
Ref: ApiGatewayRestApi
ProviderARNs:
- ${self:custom.cognitoUserPoolArn}
# Error responses for 400 and 500 errors added with Response Parameters to satisy CORS preflight requests on failed responses from Authorizer
GatewayResponseDefault4XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_4XX
RestApiId:
Ref: 'ApiGatewayRestApi'
GatewayResponseDefault5XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_5XX
RestApiId:
Ref: 'ApiGatewayRestApi'

3. Add CORS headers to server-side errors

Finally, you need to make sure that the API Gateway is also adding the CORS headers in case of server-side errors. Why is that important? For example, when the API Gateway fails to invoke your Lambda function, it will return a server-side error. In that case, the Lambda function cannot add the necessary Access-Control-Allow-Origin header to the response. Therefore, you need to configure the API Gateway to add the CORS header for server-side errors, as illustrated in the following Terraform configuration snippet.

In our BaseController implementation, we are doing this with the following code:

  public static jsonResponse(statusCode: number, message: string, data?: any): any {
return {
statusCode: statusCode,
// IMPORTANT CORS note: If 'Access-Control-Allow-Credentials': true, we cannot do a wildcard('*') on Access-Control-Allow-Origin.
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,API-Key,Authorization,Access-Token',
'Access-Control-Allow-Methods': 'OPTIONS,POST,GET,PUT'
},
body: JSON.stringify({
message: message,
data: data
})
}
}

public success(data: any, message?: string) {
return BaseController.jsonResponse(200, message ? message : 'Success', data)
}

public unauthorized(message?: string) {
return BaseController.jsonResponse(401, message ? message : 'Unauthorized')
}

public notFound(message?: string) {
return BaseController.jsonResponse(404, message ? message : 'Not found')
}

4. CloudFront Configuration

If you have enabled cross-origin resource sharing (CORS) on an Amazon S3 bucket or a custom origin, you must choose specific headers to forward, to respect the CORS settings. The headers that you must forward differ depending on the origin (Amazon S3 or custom) and whether you want to cache OPTIONS responses.

Custom origins – Forward the Origin header along with any other headers required by your origin.

You configure CloudFront to forward headers by using a cache policy or an origin request policy.

Here is the setup you need to do in a CloudFront Behavior if you want to enable CORS and require a custom Authorization header:

dependency rule

Viewer

  • Here we are Redirecting HTTP to HTTPS if any client tries to make a HTTP request
  • OPTIONS headers must be set for Preflight CORS.
  • DO NOT cache OPTIONS headers. CloudFront removes the Authorization header field before forwarding the request to your origin if you configure CloudFront to cache responses to OPTIONS requests. CloudFront forwards the Authorization header field to your origin if you do not configure CloudFront to cache responses to OPTIONS requests.
  • In either the Cache/origin request policy or in Lehacy cache settings, you must forward Origin for CORS to be configured on CloudFront. Here we are also passing Authorization headers as it is required for our backend. Add any custom headers if you are using others
  • Finally, object caching is set to 0 for all TTL. This means that for this end-point, no objects are cached by CloudFront. If you need caching, you can set TTL times here.

If the all points 1-4 are set, then you should be good to go.

If you are still getting errors, try to invalidate the cache in case it is still being used in invalidations inside of CloudFront.

Two important links that explains CloudFront Cache Behavior:

A note on Header Convention

Follow this convention for headers:

Access-Token <-- correct
accessToken <-- incorrect
access_token <-- incorrect

Note, if you use a custom header like accessToken, the camelcase will be ignored and you will need to add "accesstoken". But this should be avoided and we should use the above mentioned convention

Casing abbreviations

Abbreviations should be uppercase.

Token-ID  # <-- Good
Token-ID # <-- Bad

API-Key # <-- Good
Api-Key # <-- Bad

Using Headers in Typescript

Making Requests

const headers = {
'Token-ID': tokenId,
'Api-Key': apiKey
}

Receiving Requests

const {'Token-ID': tokenId, 'Api-Key': apiKey} = request.headers