diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md index c5a6f0dac..c6b8f8041 100644 --- a/docs/providers/aws/events/apigateway.md +++ b/docs/providers/aws/events/apigateway.md @@ -224,6 +224,21 @@ functions: Configuring the `cors` property sets [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin), [Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers), [Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods),[Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) headers in the CORS preflight response. +To enable the `Access-Control-Max-Age` preflight response header, set the `maxAge` property in the `cors` object: + +```yml +functions: + hello: + handler: handler.hello + events: + - http: + path: hello + method: get + cors: + origin: '*' + maxAge: 86400 +``` + If you want to use CORS with the lambda-proxy integration, remember to include the `Access-Control-Allow-*` headers in your headers object, like this: ```javascript diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js index 167d113fd..8b4dc4747 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.js @@ -25,6 +25,16 @@ module.exports = { 'Access-Control-Allow-Credentials': `'${config.allowCredentials}'`, }; + // Enable CORS Max Age usage if set + if (_.has(config, 'maxAge')) { + if (_.isInteger(config.maxAge) && config.maxAge > 0) { + preflightHeaders['Access-Control-Max-Age'] = `'${config.maxAge}'`; + } else { + const errorMessage = 'maxAge should be an integer over 0'; + throw new this.serverless.classes.Error(errorMessage); + } + } + if (_.includes(config.methods, 'ANY')) { preflightHeaders['Access-Control-Allow-Methods'] = preflightHeaders['Access-Control-Allow-Methods'] diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js index db2d06c87..64f65624b 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/cors.test.js @@ -67,18 +67,21 @@ describe('#compileCors()', () => { headers: ['*'], methods: ['OPTIONS', 'PUT'], allowCredentials: false, + maxAge: 86400, }, 'users/create': { origins: ['*', 'http://example.com'], headers: ['*'], methods: ['OPTIONS', 'POST'], allowCredentials: true, + maxAge: 86400, }, 'users/delete': { origins: ['*'], headers: ['CustomHeaderA', 'CustomHeaderB'], methods: ['OPTIONS', 'DELETE'], allowCredentials: false, + maxAge: 86400, }, 'users/any': { origins: ['http://example.com'], @@ -117,6 +120,13 @@ describe('#compileCors()', () => { .ResponseParameters['method.response.header.Access-Control-Allow-Credentials'] ).to.equal('\'true\''); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersCreateOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Max-Age'] + ).to.equal('\'86400\''); + // users/update expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -139,6 +149,13 @@ describe('#compileCors()', () => { .ResponseParameters['method.response.header.Access-Control-Allow-Credentials'] ).to.equal('\'false\''); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersUpdateOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Max-Age'] + ).to.equal('\'86400\''); + // users/delete expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -168,6 +185,13 @@ describe('#compileCors()', () => { .ResponseParameters['method.response.header.Access-Control-Allow-Credentials'] ).to.equal('\'false\''); + expect( + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ApiGatewayMethodUsersDeleteOptions + .Properties.Integration.IntegrationResponses[0] + .ResponseParameters['method.response.header.Access-Control-Max-Age'] + ).to.equal('\'86400\''); + // users/any expect( awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate @@ -198,4 +222,34 @@ describe('#compileCors()', () => { ).to.equal('\'false\''); }); }); + + it('should throw error if maxAge is not an integer greater than 0', () => { + awsCompileApigEvents.validated.corsPreflight = { + 'users/update': { + origin: 'http://example.com', + headers: ['*'], + methods: ['OPTIONS', 'PUT'], + allowCredentials: false, + maxAge: -1, + }, + }; + + expect(() => awsCompileApigEvents.compileCors()) + .to.throw(Error, 'maxAge should be an integer over 0'); + }); + + it('should throw error if maxAge is not an integer', () => { + awsCompileApigEvents.validated.corsPreflight = { + 'users/update': { + origin: 'http://example.com', + headers: ['*'], + methods: ['OPTIONS', 'PUT'], + allowCredentials: false, + maxAge: 'five', + }, + }; + + expect(() => awsCompileApigEvents.compileCors()) + .to.throw(Error, 'maxAge should be an integer over 0'); + }); }); diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js index 936931cdb..6d1b82947 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js @@ -61,6 +61,11 @@ module.exports = { cors.origin = http.cors.origin || '*'; cors.allowCredentials = cors.allowCredentials || http.cors.allowCredentials; + // when merging, last one defined wins + if (_.has(http.cors, 'maxAge')) { + cors.maxAge = http.cors.maxAge; + } + corsPreflight[http.path] = cors; } @@ -332,10 +337,15 @@ module.exports = { if (cors.methods.indexOf(http.method.toUpperCase()) === NOT_FOUND) { cors.methods.push(http.method.toUpperCase()); } + if (_.has(cors, 'maxAge')) { + if (!_.isInteger(cors.maxAge) || cors.maxAge < 1) { + const errorMessage = 'maxAge should be an integer over 0'; + throw new this.serverless.classes.Error(errorMessage); + } + } } else { cors.methods.push(http.method.toUpperCase()); } - return cors; }, diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js index 4e481c9d8..6cbbfaac5 100644 --- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js +++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.test.js @@ -643,6 +643,7 @@ describe('#validate()', () => { headers: ['X-Foo-Bar'], origins: ['acme.com'], methods: ['POST', 'OPTIONS'], + maxAge: 86400, }, }, }, @@ -657,10 +658,11 @@ describe('#validate()', () => { methods: ['POST', 'OPTIONS'], origins: ['acme.com'], allowCredentials: false, + maxAge: 86400, }); }); - it('should merge all preflight origins, method, headers and allowCredentials for a path', () => { + it('should merge all preflight cors options for a path', () => { awsCompileApigEvents.serverless.service.functions = { first: { events: [ @@ -673,6 +675,7 @@ describe('#validate()', () => { 'http://example.com', ], allowCredentials: true, + maxAge: 10000, }, }, }, { @@ -683,6 +686,7 @@ describe('#validate()', () => { origins: [ 'http://example2.com', ], + maxAge: 86400, }, }, }, { @@ -717,12 +721,35 @@ describe('#validate()', () => { .to.deep.equal(['http://example2.com', 'http://example.com']); expect(validated.corsPreflight['users/{id}'].headers) .to.deep.equal(['TestHeader2', 'TestHeader']); + expect(validated.corsPreflight.users.maxAge) + .to.equal(86400); expect(validated.corsPreflight.users.allowCredentials) .to.equal(true); expect(validated.corsPreflight['users/{id}'].allowCredentials) .to.equal(false); }); + it('should throw an error if the maxAge is not a positive integer', () => { + awsCompileApigEvents.serverless.service.functions = { + first: { + events: [ + { + http: { + method: 'POST', + path: '/foo/bar', + cors: { + origin: '*', + maxAge: -1, + }, + }, + }, + ], + }, + }; + + expect(() => awsCompileApigEvents.validate()).to.throw(Error); + }); + it('should add default statusCode to custom statusCodes', () => { awsCompileApigEvents.serverless.service.functions = { first: {