Skip to content

Conversation

@loganzartman
Copy link
Contributor

Issue #, if available:

Description of changes:
Created an example of using Lambda to provide CORS headers on API Gateway endpoints given a list of allowed origins. The Lambda function checks the origin of incoming requests against a list, and only provides CORS headers if there is a match.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Properties:
Path: '/test'
Method: 'options'
PostTest:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see there is some special case for DELETE methods in the function. Is it useful to have a DELETE method in the example API? Up to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose DELETE arbitrarily to demonstrate how to handle preflight requests, since it is one of the methods that is guaranteed to trigger one. So I think it is more a specific example of a general case.
I do notice now that the event is still named "PostTest", whereas it should maybe be something like "DeleteTest".

* @param allowedOrigins A list of strings or regexes representing allowed origin URLs
* @return {Object} an object containing a header => value mapping, or nothing
*/
exports.makeOriginHeader = (event, allowedOrigins) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass in origin instead of event

@@ -0,0 +1,65 @@
const DEFAULT_ALLOWED_HEADERS = [
"Content-Type",
"X-Amz-Date",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments on what some of the X-Amz headers are would be useful


### Local development

`sam local start-api`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link to SAM CLI and mention these instructions assume it is set up

* Otherwise, return an empty object literal.
* @param event Lambda event event associated with a request
* @param allowedOrigins A list of strings or regexes representing allowed origin URLs
* @return {Object} an object containing a header => value mapping, or nothing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"containing allowed headers and their value" and remove ", or nothing"


// look for origin in list of allowed origins
const allowedPattern = allowedOrigins.find(pattern => origin.match(pattern));
if (typeof allowedPattern === "undefined")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (allowedPattern === undefined); same for origin check above

* @param {Array} allowedHeaders (optional) a list of strings representing allowed headers
* @return {Object} an object containing several header => value mappings
*/
exports.makeHeaders = (event, allowedOrigins, allowedMethods, allowedHeaders = DEFAULT_ALLOWED_HEADERS) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also just be origin

*/
exports.compileURLWildcards = (path) => {
const parts = path.split("*");
const escapeRegex = str => str.replace(/([.?*+^$(){}|[\-\]\\])/g, "\\$1");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment to make this clearer; maybe use an example.

return {"headers": {"origin": origin}};
};

// makeOriginHeader
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

* handle OPTIONS requests and provide the CORS headers.
* When the browser makes the DELETE request, we only need to provide the origin.
*/
const handleTest = (event, context, callback) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With nodejs8.10 you should be able to define handlers like

async () => {
  return { headers, statusCode }
}

@@ -0,0 +1,65 @@
const DEFAULT_ALLOWED_HEADERS = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a library that exists that might be better than rolling your own?

"http://127.0.0.1",
"https://*.example.com",
"https://*.amazon.com"
].map(cors.compileURLWildcards);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simpler interface would be cors.compileURLWildcards([]). Even better if you just do this for them behind the scenes (inside cors.makeHeaders)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My goal was to isolate the wildcard support from the rest of the implementation, in case a user wants to use some other library or provide their own regex for the origins.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can easily modify themselves. cors-utils is baked in; not a library.

* @return {Object} an object containing allowed header and its value
*/
exports.makeOriginHeader = (origin, allowedOrigins) => {
if (origin === undefined || origin === null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!origin)

*/
exports.makeOriginHeader = (origin, allowedOrigins) => {
if (origin === undefined || origin === null)
return {}; // no CORS headers necessary; browser will load resource
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"no CORS headers necessary" if no origin? Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firefox, for example, will not send an Origin header in a same-origin request, but will always send one in a cross-origin request. If there is no origin, I don't think there's anything else to be done here.

return {"Access-Control-Allow-Origin": origin};

// the origin does not match any allowed origins
return {}; // return no CORS headers; browser will not load resource
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. This is all much simpler.

exports.compileURLWildcards = (url) => {
// unreserved characters as per https://tools.ietf.org/html/rfc3986#section-2.3
const url_unreserved_pattern = "[A-Za-z0-9\-._~]";
const wildcard_pattern = url_unreserved_pattern + "*";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't python ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops

"http://127.0.0.1",
"https://*.example.com",
"https://*.amazon.com"
].map(cors.compileURLWildcards);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still want cors-util to do wildcard logic for me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change that

* We include all CORS headers in the GET response.
*/
exports.handleRoot = async (event, context) => {
const origin = event.headers.Origin || event.headers.origin;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this logic to a getOriginFromHeaders function

if (event.httpMethod === "OPTIONS") {
// return an empty response, with all CORS headers
return {
headers: cors.makeHeaders(origin, allowedOrigins, allowedMethods),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better name might be getPreflightRequestHeaders. Could also add a higher abstraction: return getPreflightResponse(origin, allowedOrigins, allowedMethods) since this is likely something customers will want to do a lot (every path/method that requires preflight)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A higher level of abstraction seems reasonable. I'm not sure if there would be a case where a user might want to add extra headers to the response. It might be sufficient to add a parameter for Access-Control-Max-Age, which seems to be the only other header relevant to a preflight response.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone ahead and done that

@brettstack
Copy link
Contributor

Looking good. The only other things (as communicated offline) is to split this into multiple Lambda Functions instead of implementing routing. Alternatively, if you prefer to use a single function, use aws-serverless-express (and in this case you could probably just use the cors middleware)

@loganzartman
Copy link
Contributor Author

Improved the routing

Copy link
Contributor

@brettstack brettstack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've split into additional Lambda functions which is great, but the handlers should be in separate files. Maybe something like routes/root.js and routes/test.js and move any common logic into a /service|helpers|utils.js file.

Also, just 2 Lambda functions here is fine; 1 per route; let routes/test Lambda function/handler also handle preflight. It's a common pattern to split on path/resource but perform different logic based on the method. This seems like a good middleground between being too aggressive with individual functions and jamming everything into a single function.

Path: '/test'
Method: 'delete'
OptionsTest:
ExampleTestPreflight:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go in the same Lambda function as delete.

Copy link
Contributor

@brettstack brettstack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work. Thanks!

@Simon2228 Simon2228 merged commit 21e9063 into aws:develop Jun 21, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants