Repository: serverless/serverless (JavaScript)
Date: 2016-11-17 19:04:05+00:00
Task type: Feature

Issue:
Add Access-Control-Allow-Credentials for CORS settings
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->
# This is a Feature Proposal
## Description

Right now I am developing a cross origin resource sharing (CORS) application which requires sharing cookies. The server is required to return the Access-Control-Allow-Credentials header set to true to allow this. While there is way to set headers, methods and origins with serverless, there is no easy way of setting the allow-credentials header. Will have to go to aws console to enable this which is a headache for maintenance. 
## Similar or dependent issues:
## Additional Data
- **_Serverless Framework Version you're using**_:
- **_Operating System**_:
- **_Stack Trace**_:
- **_Provider Error messages**_:

Add Access-Control-Allow-Credentials for CORS settings
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->
# This is a Feature Proposal
## Description

Right now I am developing a cross origin resource sharing (CORS) application which requires sharing cookies. The server is required to return the Access-Control-Allow-Credentials header set to true to allow this. While there is way to set headers, methods and origins with serverless, there is no easy way of setting the allow-credentials header. Will have to go to aws console to enable this which is a headache for maintenance. 
## Similar or dependent issues:
## Additional Data
- **_Serverless Framework Version you're using**_:
- **_Operating System**_:
- **_Stack Trace**_:
- **_Provider Error messages**_:



Issue discussion:
+

+1, is there a fix for this anywhere?

I needed that as well and now have an experimental version working for me.
I'll polish it a bit, write the tests and start the PR

ha, I also have a rough and ready version too. need to polish it a lot, so you might beat me to it. Will be good to compare approaches

Ready for review
Works on my machine! ;-)

+

+1, is there a fix for this anywhere?

I needed that as well and now have an experimental version working for me.
I'll polish it a bit, write the tests and start the PR

ha, I also have a rough and ready version too. need to polish it a lot, so you might beat me to it. Will be good to compare approaches

Ready for review
Works on my machine! ;-)


Solution patch:
diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md
index da0b225110c..12651e12e8e 100644
--- a/docs/providers/aws/events/apigateway.md
+++ b/docs/providers/aws/events/apigateway.md
@@ -99,7 +99,31 @@ functions:
           cors: true
 ```
 
-If you want to use CORS with the lambda-proxy integration, remember to include `Access-Control-Allow-Origin` in your returned headers object, like this:
+Setting `cors` to `true` assumes a default configuration which is equivalent to:
+
+```yml
+functions:
+  hello:
+    handler: handler.hello
+    events:
+      - http:
+          path: hello
+          method: get
+          cors:
+            origins:
+              - '*'
+            headers:
+              - Content-Type
+              - X-Amz-Date
+              - Authorization
+              - X-Api-Key
+              - X-Amz-Security-Token
+            allowCredentials: false  
+```
+
+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.
+
+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
 // handler.js
@@ -111,7 +135,8 @@ module.exports.hello = function(event, context, callback) {
     const response = {
       statusCode: 200,
       headers: {
-        "Access-Control-Allow-Origin" : "*" // Required for CORS support to work
+        "Access-Control-Allow-Origin" : "*", // Required for CORS support to work
+        "Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS 
       },
       body: JSON.stringify({ "message": "Hello World!" })
     };
@@ -545,58 +570,6 @@ functions:
                       Content-Type: "'application/json+hal'"
 ```
 
-## Enabling CORS with the Lambda Integration Method
-
-```yml
-functions:
-  hello:
-    handler: handler.hello
-    events:
-      - http:
-          path: user/create
-          method: get
-          integration: lambda
-          cors: true
-```
-
-You can equally set your own attributes:
-
-```yml
-functions:
-  hello:
-    handler: handler.hello
-    events:
-      - http:
-          path: user/create
-          method: get
-          integration: lambda
-          cors:
-            origins:
-              - '*'
-            headers:
-              - Content-Type
-              - X-Amz-Date
-              - Authorization
-              - X-Api-Key
-              - X-Amz-Security-Token
-```
-
-This example is the default setting and is exactly the same as the previous example. The `Access-Control-Allow-Methods` header is set automatically, based on the endpoints specified in your service configuration with CORS enabled.
-
-**Note:** If you are using the default lambda proxy integration, remember to include `Access-Control-Allow-Origin` in your returned headers object otherwise CORS will fail.
-
-```
-module.exports.hello = (event, context, callback) => {
-  return callback(null, {
-    statusCode: 200,
-    headers: {
-      'Access-Control-Allow-Origin': '*'
-    },
-    body: 'Hello World!'
-  });
-}
-```
-
 ## Setting an HTTP Proxy on API Gateway
 
 To set up an HTTP proxy, you'll need two CloudFormation templates, one for the endpoint (known as resource in CF), and
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js
index 465cfcda46e..8f4616f950b 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.js
@@ -16,6 +16,7 @@ module.exports = {
         'Access-Control-Allow-Origin': `'${config.origins.join(',')}'`,
         'Access-Control-Allow-Headers': `'${config.headers.join(',')}'`,
         'Access-Control-Allow-Methods': `'${config.methods.join(',')}'`,
+        'Access-Control-Allow-Credentials': `'${config.allowCredentials}'`,
       };
 
       _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js
index e722ba871de..60c3f1eb80e 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js
@@ -58,6 +58,7 @@ module.exports = {
             cors.headers = _.union(http.cors.headers, cors.headers);
             cors.methods = _.union(http.cors.methods, cors.methods);
             cors.origins = _.union(http.cors.origins, cors.origins);
+            cors.allowCredentials = cors.allowCredentials || http.cors.allowCredentials;
 
             corsPreflight[http.path] = cors;
           }
@@ -261,11 +262,14 @@ module.exports = {
       origins: ['*'],
       methods: ['OPTIONS'],
       headers,
+      allowCredentials: false,
     };
 
     if (typeof http.cors === 'object') {
       cors = http.cors;
       cors.methods = cors.methods || [];
+      cors.allowCredentials = Boolean(cors.allowCredentials);
+
       if (cors.headers) {
         if (!Array.isArray(cors.headers)) {
           const errorMessage = [


Test patch:
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.test.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.test.js
index 584a1b1e876..be451a6dba9 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.test.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/cors.test.js
@@ -55,16 +55,19 @@ describe('#compileCors()', () => {
         origins: ['*'],
         headers: ['*'],
         methods: ['OPTIONS', 'PUT'],
+        allowCredentials: false,
       },
       'users/create': {
         origins: ['*'],
         headers: ['*'],
         methods: ['OPTIONS', 'POST'],
+        allowCredentials: true,
       },
       'users/delete': {
         origins: ['*'],
         headers: ['CustomHeaderA', 'CustomHeaderB'],
         methods: ['OPTIONS', 'DELETE'],
+        allowCredentials: false,
       },
     };
     return awsCompileApigEvents.compileCors().then(() => {
@@ -89,6 +92,13 @@ describe('#compileCors()', () => {
           .ResponseParameters['method.response.header.Access-Control-Allow-Methods']
       ).to.equal('\'OPTIONS,POST\'');
 
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersCreateOptions
+          .Properties.Integration.IntegrationResponses[0]
+          .ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
+      ).to.equal('\'true\'');
+
       expect(
         awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
           .Resources.ApiGatewayMethodUsersUpdateOptions
@@ -103,6 +113,13 @@ describe('#compileCors()', () => {
           .ResponseParameters['method.response.header.Access-Control-Allow-Methods']
       ).to.equal('\'OPTIONS,PUT\'');
 
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersUpdateOptions
+          .Properties.Integration.IntegrationResponses[0]
+          .ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
+      ).to.equal('\'false\'');
+
       expect(
         awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
           .Resources.ApiGatewayMethodUsersDeleteOptions
@@ -123,6 +140,13 @@ describe('#compileCors()', () => {
           .Properties.Integration.IntegrationResponses[0]
           .ResponseParameters['method.response.header.Access-Control-Allow-Methods']
       ).to.equal('\'OPTIONS,DELETE\'');
+
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersDeleteOptions
+          .Properties.Integration.IntegrationResponses[0]
+          .ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
+      ).to.equal('\'false\'');
     });
   });
 });
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.test.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.test.js
index 6b64dc34941..536bfb30132 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.test.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.test.js
@@ -401,6 +401,7 @@ describe('#validate()', () => {
       headers: ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', 'X-Amz-Security-Token'],
       methods: ['OPTIONS', 'POST'],
       origins: ['*'],
+      allowCredentials: false,
     });
   });
 
@@ -550,10 +551,11 @@ describe('#validate()', () => {
       headers: ['X-Foo-Bar'],
       methods: ['POST', 'OPTIONS'],
       origins: ['acme.com'],
+      allowCredentials: false,
     });
   });
 
-  it('should merge all preflight origins, method, and headers for a path', () => {
+  it('should merge all preflight origins, method, headers and allowCredentials for a path', () => {
     awsCompileApigEvents.serverless.service.functions = {
       first: {
         events: [
@@ -565,6 +567,7 @@ describe('#validate()', () => {
                 origins: [
                   'http://example.com',
                 ],
+                allowCredentials: true,
               },
             },
           }, {
@@ -609,6 +612,10 @@ 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.allowCredentials)
+      .to.equal(true);
+    expect(validated.corsPreflight['users/{id}'].allowCredentials)
+      .to.equal(false);
   });
 
   it('should add default statusCode to custom statusCodes', () => {