Setup CORS in Serverless AWS
You thoroughly test your Lambda, deploy it on AWS, invoke it using your favorite client, and everything is going well... until your start invoking it from your web app.
Suddenly, nothing worked anymore. And you couldn't even tell why because your browser's console just told you that 'the preflight request has failed' or that 'origin https://whatever.com is not allowed'. What on earth just happened?
The experienced developpers among us will have guessed it immediately. CORS. CORS happened. What is CORS? Cross-origin Resource Sharing. A security feature enforced by browsers, and setup server-side.
And sadly, being serverless doesn't exempt us from handling it too. Rest assured, it is quite simple. You'll find the demo project here.
Basic CORS in Serverless
First things first, you only need to setup CORS for your Lambda if it is invoked by an API Gateway HTTP event. If your Lambda function is invoked via Kinesis, an S3, SNS or SQS, setting up CORS would be... pointless.
Ok, still here? Good. Next thing, this post will assume you use the Serverless framework. My personal favorite. But you are free to use what suits you best, in which case some settings will not work the same way.
Next, the Serverless framework offers some prebuilt default CORS configuration. Check out their documentation on the matter if you're interested.
Setup CORS for you Lambda in the Serverless project
You can setup CORS for one specific host, or all. You can't however setup CORS for multiple different hosts, because for some reason browsers will freak out if you return an array (go figure).
To setup the CORS host, update your lambda function's http event, with either *
(wildcard for all hosts) or your chosen host ( https://localhost:8000
, http://example.com
,...)
demoApi:
handler: src/get.handler
events:
- http:
path: /
method: get
cors:
origins:
- "http://localhost:3000"
Next, we define the headers we choose to allow. If you choose to use to authorize your Lambda through IAM, your requests will be signed using AWS Signature V4. This requires a few mandatory headers to validate the signature. A few of them are absolutely necessary if you choose to use an IAM authorizer: X-Amz-Date
, x-amz-date
, Authorization
, X-Api-Key
, X-Amz-Security-Token
, Content-Type
. A few nice to haves will be Accept
, Origin
, DNT
, User-agent
, Referer
, Content-Type
. Here's a list of headers if you want to learn more about headers.
I am trying to keep this short, so in order to avoid going into setting up IAM roles, etc, we'll not be using an IAM authorizer. Here's what such a CORS headers setup would look like:
demoApi:
handler: src/get.handler
events:
- http:
path: /
method: get
cors:
origins:
- "http://localhost:3000"
headers:
- Accept
- Origin
- DNT
- User-Agent
- Referer
- Content-Type
- X-Amz-Date
- x-amz-date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
CORS in AWS Lambda (response headers)
Setting up the cors headers in API Gateway isn't enough. In the default 'lambda-proxy' configuration used by the Serverless framework, you need to set the headers in your response, and therefore add the Access-Control-Allow-*
headers in your responses.
It's a simple thing to do. Simply update the demo function's response like that:
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin" : "your_origin", // (* or a specific host)
"Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS
},
body: "Hello! Go Serverless!"
};
The one thing no one told you about
When things go south and your lambda executions fail altogether, API Gateway will sometimes take over your CORS settings, and... not return Access-Control-Allow-*
headers.
Meaning that when you need your response the most, instead of giving you some useful information, your browser will keep it from you and merely, once more, tell you that 'the preflight request has failed' or 'origin https://whatever.com is not allowed'. Frustrating and annoying.
How do we avoid that? By overriding API Gateway's default responses for 4xx and 5xx responses.
Go to the AWS Console, pick your service's region in the top-right drop-down menu, and open the API Gateway Console. There, go to Gateway Responses.
Here, we'll have to set the Default Gateway Responses' headers for both 4xx and 5xx responses. Pick 'Default 4xx'.
Finally, set your default access-control-allow-origin
header. If it's a string (such as *
or localhost:8000
), and not a mapped value, keep in mind the value will have to be between simple quotes (such as '*'
). Like so.
Save, and next do the same for the default 5xx response.
It might randomly display an error on "Save" in the UI. Now? Redeploy your service! These settings are only applied on API Gateway deployment... Your API Gateway gateway only exists once the microservice is deployed. So... yes you'll have to deploy your service twice! Yes, for real! AWS' quirks.
Fun fact of the week / PSA:
Removing then redeploying your serverless service will give a new api gateway. With a new arn, and... new settings! So yes, if you do that, you will have to set your API Gateway default responses once more, and might need to remember to update any IAM policy containing an ExecuteAPI rule giving access to this service (such as cognito user pools' user policies).
Ah... these cute little awkward AWS quirks... What would we do without them?
Have fun, and I wish you all a great day!