Repository: serverless/serverless (JavaScript)
Date: 2019-05-17 06:23:28+00:00
Task type: Bug Fix

Issue:
API Gateway cognito authorizer scopes don't work with a authorizerId
<!--
1. If you have a question and not a bug report please ask first at http://forum.serverless.com
2. Please check if an issue already exists. This bug may have already been documented
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 Bug Report

## Description

* What went wrong?
OAuth Scopes were not added to the API Gateway method when using an authorizerId rather than an arn.
* What did you expect should have happened?
The OAuth Scopes should have been added to the API Gateway method.
* What was the config you used?
```yaml
service: cognito-scope-bug
provider:
  name: aws
  runtime: nodejs8.10
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'ap-southeast-2'}
  endpointType: REGIONAL

functions:
  cognito-authorizer:
    handler: handler.handler
    events:
      - http:
          path: test
          method: get
          authorizer:
            type: COGNITO_USER_POOLS
            authorizerId:
              Ref: CognitoAuthorizer
            #Using arn instead of authorizerId works
            # arn: COGNITO_ARN_GOES_HERE
            scopes:
              - app-name/scope.read

resources:
  Resources:
    CognitoAuthorizer:
      Type: AWS::ApiGateway::Authorizer
      Properties:
        Name: CognitoAuthorizer
        Type: COGNITO_USER_POOLS
        IdentitySource: method.request.header.Authorization
        ProviderARNs:
          - COGNITO_ARN_GOES_HERE
        RestApiId:
          Ref: ApiGatewayRestApi
```
* What stacktrace or error message from your provider did you see?
None. It deployed fine but the scopes were not added.

Similar or dependent issues:
* OAuth scope functionality was added with #6000

## Additional Data

* ***Serverless Framework Version you're using***:
1.42.2
* ***Operating System***:
MacOS Mojave 10.14.4
* ***Stack Trace***:
None.
* ***Provider Error messages***:
None.


Issue discussion:

Solution patch:
diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md
index ad8ff969de9..3e60adfe8f2 100644
--- a/docs/providers/aws/events/apigateway.md
+++ b/docs/providers/aws/events/apigateway.md
@@ -499,9 +499,9 @@ functions:
               - nickname
 ```
 
-### Using asyncronous integration
+### Using asynchronous integration
 
-Use `async: true` when integrating a lambda function using [event invocation](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#SSS-Invoke-request-InvocationType). This lets API Gateway to return immediately with a 200 status code while the lambda continues running. If not othewise speficied integration type will be `AWS`.
+Use `async: true` when integrating a lambda function using [event invocation](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#SSS-Invoke-request-InvocationType). This lets API Gateway to return immediately with a 200 status code while the lambda continues running. If not otherwise specified integration type will be `AWS`.
 
 ```yml
 functions:
@@ -868,7 +868,7 @@ If you want to spread a string into multiple lines, you can use the `>` or `|` s
 #### Pass Through Behavior
 API Gateway provides multiple ways to handle requests where the Content-Type header does not match any of the specified mapping templates.  When this happens, the request payload will either be passed through the integration request *without transformation* or rejected with a `415 - Unsupported Media Type`, depending on the configuration.
 
-You can define this behavior as follows (if not specified, a value of **NEVER** will be used):
+You can define this behaviour as follows (if not specified, a value of **NEVER** will be used):
 
 ```yml
 functions:
@@ -1355,6 +1355,8 @@ functions:
             type: COGNITO_USER_POOLS # TOKEN or REQUEST or COGNITO_USER_POOLS, same as AWS Cloudformation documentation
             authorizerId:
               Ref: ApiGatewayAuthorizer  # or hard-code Authorizer ID
+              scopes: # Optional - List of Oauth2 scopes when type is COGNITO_USER_POOLS
+                - myapp/myscope
 
   deleteUser:
      ...
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js
index 03957b8e8ee..21ae109d068 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/method/authorization.js
@@ -15,12 +15,18 @@ module.exports = {
 
     if (http.authorizer) {
       if (http.authorizer.type && http.authorizer.authorizerId) {
-        return {
+        const authReturn = {
           Properties: {
             AuthorizationType: http.authorizer.type,
             AuthorizerId: http.authorizer.authorizerId,
           },
         };
+        if (http.authorizer.type === 'COGNITO_USER_POOLS'
+          && http.authorizer.scopes
+          && http.authorizer.scopes.length) {
+          authReturn.Properties.AuthorizationScopes = http.authorizer.scopes;
+        }
+        return authReturn;
       }
 
       const authorizerLogicalId = this.provider.naming
@@ -39,7 +45,7 @@ module.exports = {
           },
           DependsOn: authorizerLogicalId,
         };
-        if (http.authorizer.scopes) {
+        if (http.authorizer.scopes && http.authorizer.scopes.length) {
           cognitoReturn.Properties.AuthorizationScopes = http.authorizer.scopes;
         }
         return cognitoReturn;


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 2ac2b295760..e6871ab9dc6 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
@@ -489,7 +489,7 @@ describe('#compileMethods()', () => {
     });
   });
 
-  it('should set custom authorizer config with authorizeId', () => {
+  it('should set custom authorizer config with authorizerId', () => {
     awsCompileApigEvents.validated.events = [
       {
         functionName: 'First',
@@ -542,7 +542,7 @@ describe('#compileMethods()', () => {
     });
   });
 
-  it('should set authorizer config for a cognito user pool', () => {
+  it('should set authorizer config for a cognito user pool when given authorizer arn', () => {
     awsCompileApigEvents.validated.events = [
       {
         functionName: 'First',
@@ -583,6 +583,75 @@ describe('#compileMethods()', () => {
     });
   });
 
+  it('should set authorizer config for a cognito user pool when given authorizerId Ref', () => {
+    awsCompileApigEvents.validated.events = [
+      {
+        functionName: 'First',
+        http: {
+          authorizer: {
+            name: 'authorizer',
+            type: 'COGNITO_USER_POOLS',
+            authorizerId: { Ref: 'CognitoAuthorizer' },
+            scopes: ['myapp/read', 'myapp/write'],
+          },
+          integration: 'AWS',
+          path: 'users/create',
+          method: 'post',
+        },
+      },
+    ];
+
+    return awsCompileApigEvents.compileMethods().then(() => {
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizationType
+      ).to.equal('COGNITO_USER_POOLS');
+
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizationScopes
+      ).to.contain('myapp/read');
+
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersCreatePost.Properties.AuthorizerId.Ref
+      ).to.equal('CognitoAuthorizer');
+
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersCreatePost.Properties
+          .Integration.RequestTemplates['application/json']
+      ).to.not.match(/undefined/);
+    });
+  });
+
+  it('should not scopes for a cognito user pool when given empty scopes array', () => {
+    awsCompileApigEvents.validated.events = [
+      {
+        functionName: 'First',
+        http: {
+          authorizer: {
+            name: 'authorizer',
+            type: 'COGNITO_USER_POOLS',
+            authorizerId: { Ref: 'CognitoAuthorizer' },
+            scopes: [],
+          },
+          integration: 'AWS',
+          path: 'users/create',
+          method: 'post',
+        },
+      },
+    ];
+
+    return awsCompileApigEvents.compileMethods().then(() => {
+      expect(
+        awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+          .Resources.ApiGatewayMethodUsersCreatePost.Properties
+      ).to.not.have.property('AuthorizationScopes');
+    });
+  });
+
+
   it('should set claims for a cognito user pool', () => {
     awsCompileApigEvents.validated.events = [
       {