Repository: serverless/serverless (JavaScript)
Date: 2020-04-13 18:11:49+00:00
Task type: Feature

Issue:
Pass CORS allowCredentials directive to method and integration responses
# Feature Proposal

## Description

When using `lambda` integration with CORS enabled and setting the `allowCredentials` to `true`, the `Access-Control-Allow-Credentials` header is present in the OPTION request response but not in the subsequent request response, which blocks the browser from reading it.

<img width="380" alt="Serverless.yml" src="https://user-images.githubusercontent.com/38014240/79142514-eb676680-7dbb-11ea-8466-04aabb975a11.png">

<img width="942" alt="API Gateway" src="https://user-images.githubusercontent.com/38014240/79142533-f15d4780-7dbb-11ea-9a64-3d9c41b47c0a.png">

<img width="459" alt="browser network error" src="https://user-images.githubusercontent.com/38014240/79142606-0d60e900-7dbc-11ea-84e9-1fcdb5eccd2e.png">


<img width="483" alt="browser console error" src="https://user-images.githubusercontent.com/38014240/79142547-f621fb80-7dbb-11ea-83b8-167e559a8ec4.png">

This forces me to additionally set the `Access-Control-Allow-Credentials` to `true` in the response headers of my lambda.

Is it possible to pass down the original allowCredential directive to the method and integration responses ?

Current configuration:
```
functions:
  refresh:
    handler: /my/handler
    events:
      - http:
          method: get
          path: my/path
          integration: lambda
          cors:
            origin: http://localhost:3000/
            allowCredentials: true
          response:
            headers:
              Access-Control-Allow-Credentials: "'true'"
```

Desired configuration:
```
functions:
  refresh:
    handler: /my/handler
    events:
      - http:
          method: get
          path: my/path
          integration: lambda
          cors:
            origin: http://localhost:3000/
            allowCredentials: true
```

Many thanks,

Thomas


Issue discussion:

Solution patch:
diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md
index 6fb8754769b..25564bf79c2 100644
--- a/docs/providers/aws/events/apigateway.md
+++ b/docs/providers/aws/events/apigateway.md
@@ -285,6 +285,8 @@ Please note that since you can't send multiple values for [Access-Control-Allow-
 
 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 use the lambda integration, the Access-Control-Allow-Origin and Access-Control-Allow-Credentials will also be provided to the method and integration responses.
+
 Please note that the [Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials)-Header is omitted when not explicitly set to `true`.
 
 To enable the `Access-Control-Max-Age` preflight response header, set the `maxAge` property in the `cors` object:
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js
index 9aa96592d74..16333163368 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/integration.js
@@ -129,8 +129,9 @@ module.exports = {
       const integrationResponseHeaders = [];
 
       if (http.cors) {
-        // TODO remove once "origins" config is deprecated
         let origin = http.cors.origin;
+
+        // TODO remove once "origins" config is deprecated
         if (http.cors.origins && http.cors.origins.length) {
           origin = http.cors.origins.join(',');
         }
@@ -138,6 +139,11 @@ module.exports = {
         _.merge(integrationResponseHeaders, {
           'Access-Control-Allow-Origin': `'${origin}'`,
         });
+
+        // Only set Access-Control-Allow-Credentials when explicitly allowed (omit if false)
+        if (http.cors.allowCredentials) {
+          integrationResponseHeaders['Access-Control-Allow-Credentials'] = 'true';
+        }
       }
 
       if (http.response.headers) {
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/responses.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/responses.js
index f44bc6ce3a1..4066ed5dbef 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/responses.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/responses.js
@@ -11,8 +11,9 @@ module.exports = {
         const methodResponseHeaders = [];
 
         if (http.cors) {
-          // TODO remove once "origins" config is deprecated
           let origin = http.cors.origin;
+
+          // TODO remove once "origins" config is deprecated
           if (http.cors.origins && http.cors.origins.length) {
             origin = http.cors.origins.join(',');
           }
@@ -20,6 +21,11 @@ module.exports = {
           _.merge(methodResponseHeaders, {
             'Access-Control-Allow-Origin': `'${origin}'`,
           });
+
+          // Only set Access-Control-Allow-Credentials when explicitly allowed (omit if false)
+          if (http.cors.allowCredentials) {
+            methodResponseHeaders['Access-Control-Allow-Credentials'] = 'true';
+          }
         }
 
         if (http.response.headers) {


Test patch:
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js
index c5cf31d0c54..b26f9476881 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/index.test.js
@@ -1063,6 +1063,62 @@ describe('#compileMethods()', () => {
     });
   });
 
+  it('should set CORS allowCredentials to method only when specified', () => {
+    awsCompileApigEvents.validated.events = [
+      {
+        functionName: 'First',
+        http: {
+          path: 'users/create',
+          method: 'post',
+          integration: 'AWS',
+          cors: {
+            origin: 'http://example.com',
+            allowCredentials: true,
+          },
+          response: {
+            statusCodes: {
+              200: {
+                pattern: '',
+              },
+            },
+          },
+        },
+      },
+      {
+        functionName: 'Second',
+        http: {
+          method: 'get',
+          path: 'users/list',
+          integration: 'AWS',
+          cors: {
+            origin: 'http://example.com',
+          },
+          response: {
+            statusCodes: {
+              200: {
+                pattern: '',
+              },
+            },
+          },
+        },
+      },
+    ];
+    return awsCompileApigEvents.compileMethods().then(() => {
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .ApiGatewayMethodUsersCreatePost.Properties.Integration.IntegrationResponses[0]
+          .ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
+      ).to.equal('true');
+
+      // allowCredentials not enabled
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+          .ResponseParameters['method.response.header.Access-Control-Allow-Credentials']
+      ).to.not.equal('true');
+    });
+  });
+
   describe('when dealing with request configuration', () => {
     it('should setup a default "application/json" template', () => {
       awsCompileApigEvents.validated.events = [