-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1a485b4
Added API Gateway multi-origin CORS example
dc7235e
Updated docs and improved naming of endpoint
0926287
Created new test for compileURLWildcards
c40408b
Fixed compileURLWildcards
d4a6db8
Refactored to avoid passing event into cors-util; improved clarity
ca85e79
Improve routing, use async syntax for handlers
7fe3ae0
Moved multi-origin cors example to examples/apps
1cb6bb0
Move multiple origin CORS example to more uniformly-named directory
293027f
Refactor multiple-origin-cors example
f71d07f
Automatically compile wildcards for multiple-origin-cors
65ec9eb
makeHeaders replaced with createPreflightResponse
c8585ac
Route using API Gateway, refactor
2376325
improve routing
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # cors-multiple-origin | ||
|
|
||
| *Example of Multiple-Origin CORS using API Gateway and Lambda* | ||
|
|
||
| [Cross-Origin Resource Sharing (CORS) - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) | ||
|
|
||
| ### Local development | ||
|
|
||
| First, [set up the SAM CLI](https://github.com/awslabs/aws-sam-cli#installation). | ||
|
|
||
| Now, test the application locally using: | ||
|
|
||
| `sam local start-api` | ||
|
|
||
| Note that there was an [issue](https://github.com/awslabs/aws-sam-cli/issues/400) that prevented OPTIONS requests from being handled when running with the SAM CLI version 0.3.0. This does not occur when the application is deployed. | ||
|
|
||
| Run the tests: | ||
|
|
||
| `npm install` | ||
|
|
||
| `npm test` | ||
|
|
||
| ### Deploying | ||
|
|
||
| ```bash | ||
| sam package \ | ||
| --template-file template.yaml \ | ||
| --output-template-file packaged.yaml \ | ||
| --s3-bucket $YOUR_BUCKET_NAME | ||
| ``` | ||
|
|
||
| ```bash | ||
| sam deploy \ | ||
| --template-file packaged.yaml \ | ||
| --stack-name cors-multiple-origin \ | ||
| --capabilities CAPABILITY_IAM | ||
| ``` | ||
|
|
||
| ### Getting the URL of the deployed instance | ||
|
|
||
| ```bash | ||
| aws cloudformation describe-stacks \ | ||
| --stack-name cors-multiple-origin \ | ||
| --query 'Stacks[].Outputs' | ||
| ``` |
7 changes: 7 additions & 0 deletions
7
examples/apps/api-gateway-multiple-origin-cors/cors-config.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "allowedOrigins": [ | ||
| "http://127.0.0.1", | ||
| "https://*.example.com", | ||
| "https://*.amazon.com" | ||
| ] | ||
| } |
81 changes: 81 additions & 0 deletions
81
examples/apps/api-gateway-multiple-origin-cors/cors-util.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // this is the list of headers allowed by default by the API Gateway console | ||
| // see: https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html | ||
| // and: https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonRequestHeaders.html | ||
| const DEFAULT_ALLOWED_HEADERS = [ | ||
| "Content-Type", // indicates the media type of the resource | ||
| "X-Amz-Date", // the current date and time according to the requester (must be present for authorization) | ||
| "Authorization", // information required for request authentication | ||
| "X-Api-Key", // an AWS API key | ||
| "X-Amz-Security-Token" // see link above | ||
| ]; | ||
| exports.DEFAULT_ALLOWED_HEADERS = DEFAULT_ALLOWED_HEADERS; | ||
|
|
||
| /** | ||
| * Extract the Origin header from a Lambda event | ||
| * @param event Lambda event | ||
| */ | ||
| exports.getOriginFromEvent = event => event.headers.Origin || event.headers.origin; | ||
|
|
||
| /** | ||
| * Return an object that contains an Access-Control-Allow-Origin header | ||
| * if the request origin matches a pattern for an allowed origin. | ||
| * Otherwise, return an empty object. | ||
| * @param {String} origin the origin to test against the allowed list | ||
| * @param {Array} allowedOrigins A list of strings or regexes representing allowed origin URLs | ||
| * @return {Object} an object containing allowed header and its value | ||
| */ | ||
| exports.createOriginHeader = (origin, allowedOrigins) => { | ||
| if (!origin) | ||
| return {}; // no CORS headers necessary; browser will load resource | ||
|
|
||
| // look for origin in list of allowed origins | ||
| const allowedPatterns = allowedOrigins.map(exports.compileURLWildcards); | ||
| const isAllowed = allowedPatterns.some(pattern => origin.match(pattern)); | ||
| if (isAllowed) | ||
| return {"Access-Control-Allow-Origin": origin}; | ||
|
|
||
| // the origin does not match any allowed origins | ||
| return {}; // return no CORS headers; browser will not load resource | ||
| // we do not return a "null" origin because this is exploitable | ||
| }; | ||
|
|
||
| /** | ||
| * Return an object that contains a preflight response to be returned | ||
| * from a Lambda function. | ||
| * @param {String} origin the origin to test against the allowed list | ||
| * @param {Array} allowedOrigins A list of strings or regexes representing allowed origin URLs | ||
| * @param {Array} allowedMethods a list of strings representing allowed HTTP methods | ||
| * @param {Array} allowedHeaders (optional) a list of strings representing allowed headers | ||
| * @param {Number} maxAge (optional) time in seconds until preflight response expires | ||
| * @return {Object} an object containing several header => value mappings | ||
| */ | ||
| exports.createPreflightResponse = (origin, allowedOrigins, allowedMethods, allowedHeaders = DEFAULT_ALLOWED_HEADERS, maxAge) => { | ||
| let headers = Object.assign(exports.createOriginHeader(origin, allowedOrigins), { | ||
| "Access-Control-Allow-Headers": allowedHeaders.join(","), | ||
| "Access-Control-Allow-Methods": allowedMethods.join(",") | ||
| }); | ||
| if (maxAge !== undefined) | ||
| headers["Access-Control-Max-Age"] = maxAge; | ||
| return {headers, statusCode: 204}; | ||
| }; | ||
|
|
||
| /** | ||
| * Compiles a URL containing wildcards into a regular expression. | ||
| * | ||
| * Builds a regular expression that matches exactly the input URL, but allows | ||
| * any number of URL characters in place of each wildcard (*) character. | ||
| * http://*.example.com matches http://abc.xyz.example.com but not http://example.com | ||
| * http://*.example.com does not match http://example.org/.example.com | ||
| * @param {String} url the url to compile | ||
| * @return {RegExp} compiled regular expression | ||
| */ | ||
| exports.compileURLWildcards = (url) => { | ||
| // unreserved characters as per https://tools.ietf.org/html/rfc3986#section-2.3 | ||
| const urlUnreservedPattern = "[A-Za-z0-9\-._~]"; | ||
| const wildcardPattern = urlUnreservedPattern + "*"; | ||
|
|
||
| const parts = url.split("*"); | ||
| const escapeRegex = str => str.replace(/([.?*+^$(){}|[\-\]\\])/g, "\\$1"); | ||
| const escaped = parts.map(escapeRegex); | ||
| return new RegExp("^" + escaped.join(wildcardPattern) + "$"); | ||
| }; | ||
84 changes: 84 additions & 0 deletions
84
examples/apps/api-gateway-multiple-origin-cors/cors-util.test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| const cors = require("./cors-util"); | ||
|
|
||
| // createOriginHeader | ||
| test("use createOriginHeader to make a header for no origin", () => { | ||
| const result = cors.createOriginHeader(undefined, []); | ||
| expect(result).toEqual({}); | ||
| }); | ||
|
|
||
| test("use createOriginHeader to make a header for a single origin", () => { | ||
| const origin = "https://amazon.com"; | ||
| const allowedOrigins = [origin]; | ||
| const result = cors.createOriginHeader(origin, allowedOrigins); | ||
| expect(result).toEqual({"Access-Control-Allow-Origin": origin}); | ||
| }); | ||
|
|
||
| test("use createOriginHeader to make a header for one of several origins", () => { | ||
| const origin = "https://amazon.com"; | ||
| const allowedOrigins = ["https://example.com", origin, "http://amazon.com"]; | ||
| const result = cors.createOriginHeader(origin, allowedOrigins); | ||
| expect(result).toEqual({"Access-Control-Allow-Origin": origin}); | ||
| }); | ||
|
|
||
| test("use createOriginHeader to make a header for a disallowed origin", () => { | ||
| const origin = "https://not-amazon.com"; | ||
| const allowedOrigins = []; | ||
| const result = cors.createOriginHeader(origin, allowedOrigins); | ||
| expect(result).toEqual({}); | ||
| }); | ||
|
|
||
| test("use createOriginHeader to make a header for a disallowed origin", () => { | ||
| const origin = "https://not-amazon.com"; | ||
| const allowedOrigins = ["https://example.com", "https://amazon.com", "http://amazon.com"]; | ||
| const result = cors.createOriginHeader(origin, allowedOrigins); | ||
| expect(result).toEqual({}); | ||
| }); | ||
|
|
||
| // createPreflightResponse | ||
| test("use createPreflightResponse to make CORS preflight headers", () => { | ||
| const origin = "https://amazon.com"; | ||
| const allowedOrigins = [origin]; | ||
| const allowedMethods = ["CREATE", "OPTIONS"]; | ||
| const allowedHeaders = ["Authorization"]; | ||
| const maxAge = 8400; | ||
| const result = cors.createPreflightResponse(origin, allowedOrigins, allowedMethods, allowedHeaders, maxAge); | ||
| expect(result).toEqual({ | ||
| headers: { | ||
| "Access-Control-Allow-Origin": origin, | ||
| "Access-Control-Allow-Methods": "CREATE,OPTIONS", | ||
| "Access-Control-Allow-Headers": "Authorization", | ||
| "Access-Control-Max-Age": 8400 | ||
| }, | ||
| statusCode: 204 | ||
| }); | ||
| }); | ||
|
|
||
| // compileURLWildcards | ||
| test("compile pattern with no wildcards", () => { | ||
| const pattern = "https://amazon.com"; | ||
| const regex = cors.compileURLWildcards(pattern); | ||
| expect(pattern).toMatch(regex); | ||
| expect("https://example.com").not.toMatch(regex); | ||
| }); | ||
|
|
||
| test("test pattern with wildcard", () => { | ||
| const pattern = "https://*"; | ||
| const regex = cors.compileURLWildcards(pattern); | ||
| expect("https://example.com").toMatch(regex); | ||
| }); | ||
|
|
||
| test("test pattern with subdomain wildcard", () => { | ||
| const pattern = "https://*.amazon.com"; | ||
| const regex = cors.compileURLWildcards(pattern); | ||
| expect("https://restaurants.amazon.com").toMatch(regex); | ||
| expect("https://amazon.com").not.toMatch(regex); | ||
| expect("https://x.y.z.amazon.com").toMatch(regex); | ||
| expect("https://restaurants.example.com").not.toMatch(regex); | ||
| }); | ||
|
|
||
| test("test pattern with subdomain wildcard against malicious input", () => { | ||
| const pattern = "https://*.amazon.com"; | ||
| const regex = cors.compileURLWildcards(pattern); | ||
| expect("https://restaurants.amazon.com").toMatch(regex); | ||
| expect("https://my.website/restaurants.amazon.com").not.toMatch(regex); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.