AWS has a great blog post about enabling CORS on API Gateway. I followed the AWS blog and CORS works well for most requests. The one thing it didn’t work for was secure request made from JavaScript.
JavaScript GET request
This code worked, but would not send cookies that were set for the example.com domain.
xhr.open('GET', 'https://subdomain.example.com/ab.json’, true);
xhr.withCredentials = true;
xhr.onreadystatechange= function() {
if (this.readyState!==4) return;
if (this.status!==200) return; // or whatever error handling you want
document.getElementById('ab').innerHTML= this.responseText;
};
xhr.send(null);
Chrome Browser Error
XMLHttpRequest cannot load https://subdomain.example.com/ab.json. The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. Origin 'https://example.com' is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
The method in the AWS blog works well but sets Access-Control-Allow-Origin to ‘*’ which is not compatible with secure requests. In the case that a cross origin request is made for an HTTPS resource the browser will throw an error if the Access-Control-Allow-Origin is not EXACTLY the same as the Origin, this includes requests from subdomains.
The solution is to pass the request Origin to Access-Control-Allow-Origin. Since I am using AWS Lambda functions with my API Gateway, I use the Lambda function to read the Origin, filter it and return it to API Gateway.
First I setup API Gateway to respond with the required headers.
Create the OPTIONS Method
Under Method Response add a 200 response.
Add the CORS headers to the 200 response.
Access-Control-Allow-Headers
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Under Integration Response add the values to each of the CORS headers
Access-Control-Allow-Headers 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,x-requested-with'
Access-Control-Allow-Origin '*'
Access-Control-Allow-Credentials 'true'
Access-Control-Allow-Methods 'GET'
Create or modify the GET Method
The GET method may already be present if you are adding CORS to an existing API
Under Method Response add a 200 response.
Add the CORS headers to the 200 response.
Access-Control-Allow-Headers
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Under Integration Response add the values to each of the CORS headers
Access-Control-Allow-Headers 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,x-requested-with'
Access-Control-Allow-Origin integration.response.body.Origin
Access-Control-Allow-Credentials 'true'
Access-Control-Allow-Methods 'GET'
Then in the Lambda Function associated with the GET method I send back the Origin. I do some simple filtering on the Origin and only pass it through if it is one of my subdomains.
index.js
‘use strict’
exports.handler = function (event, context, callback) {
...
const origin = filter(event.params.header.Origin);
const someHtml = “<h1>Title</h1>”;
…
callback(null, { html: someHtml, Origin:origin });
}
function filter(origin) {
var result = origin ? origin : '*';
if( result === “https://subdomain.example.com” ) {
// just a simple filter example, not to be used in production
return “https://subdomain.example.com”;
}
return '*';
}
Now when I visit example.com and make secure requests to subdomain.example.com cookies are sent without error or warning.