-
Couldn't load subscription status.
- Fork 2.4k
Added API Gateway multi-origin CORS example #468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| Properties: | ||
| Path: '/test' | ||
| Method: 'options' | ||
| PostTest: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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", | |||
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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 = [ | |||
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Also updated old test to reflect desired behavior
| * @return {Object} an object containing allowed header and its value | ||
| */ | ||
| exports.makeOriginHeader = (origin, allowedOrigins) => { | ||
| if (origin === undefined || origin === null) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 + "*"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't python ;)
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
|
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) |
|
Improved the routing |
There was a problem hiding this 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work. Thanks!
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.