Repository: serverless/serverless (JavaScript)
Date: 2019-04-05 05:38:18+00:00
Task type: Feature

Issue:
Support authorization scopes for COGNITO_USER_POOLS authorizer
# This is a Feature Proposal

## Description

When using client credentials flow with Cognito, API Gateway provides the `authorizationScopes` property on the API Gateway Method to match against scopes in the access token.

This is currently only supported by the API Gateway API, and not yet by CloudFormation, which I'm guessing is why it is not yet supported by Serverless. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html

Probably best to wait for CloudFormation to catch up on this so I'm just registering as a future task.

Additional config may take shape in the form of a `authorizationScopes` list property underneath `authorizer`:

```yml
functions:
  myFunction:
    events:
      - http:
          authorizer:
            arn: arn:aws:cognito-idp:ap-southeast-2:XXXXXXXXXXXX:userpool/ap-southeast-2_XXXXXXXXX
            authorizationScopes:
              - myScope1
              - myScope2
```


Issue discussion:
+1
+1
+1
+1
Do it finally!!!!
I hate to do this and spam but +1 for me as well, this is super important and a missing piece.

Thanks,
Dan
Any news on this?
Has anyone posted on the AWS forums requesting this feature be implemented in CloudFormation? I haven't found anything
It looks like this is possible via API calls. Specifically [`updateMethod`](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#updateMethod-property) in the JS SDK. According to the [API Docs](https://docs.aws.amazon.com/apigateway/api-reference/link-relation/method-update/), it would have to be 2 calls: the first to remove the existing `authorizationScopes` and the second to set the new value. `op:replace` is not supported for this key.

Some enterprising individual could probably write a serverless plugin that hooks into the post-deploy event, as many other plugins do, to make this happen.
Here is a plugin that handles it:
https://github.com/birdcatcher/serverless-oauth-scopes
> Here is a plugin that handles it:
> https://github.com/birdcatcher/serverless-oauth-scopes

Works perfectly, thanks @mdanku !! This should really just be brought into the main project as it really helps!
> 
> 
> Here is a plugin that handles it:
> https://github.com/birdcatcher/serverless-oauth-scopes

doesn't work for me :( 

AWSOAuthScope: /api/something/{input+} : GET : api/something
 
  Type Error ---------------------------------------------
 
  Cannot read property 'id' of undefined





@gwosty this is what I tested and managed to get it working if it's at all helpful :S
```
plugins:
  - serverless-oauth-scopes

service:
  name: Cognito-Authentication-Test

custom:
  stage: dev

provider:
  name: aws
  region: eu-west-1
  runtime: nodejs8.10

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
          integration: lambda
          scopes: [aws.cognito.signin.user.admin]
          authorizer:
            name: authorizer
            arn: <redacted>
            identitySource: method.request.header.Authorization
```

> > Here is a plugin that handles it:
> > https://github.com/birdcatcher/serverless-oauth-scopes
> 
> doesn't work for me :(
> 
> AWSOAuthScope: /api/something/{input+} : GET : api/something
> 
> Type Error ---------------------------------------------
> 
> Cannot read property 'id' of undefined


Looks like CloudFormation supports this now with `AuthorizationScopes` property on the AWS::ApiGateway::Method type (see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html)

I was able to add scopes by manually overriding the method resource in the `resources` section, replacing ` {ServerlessResourceMethodName}` from the below snippet with the serverless generated name for the api gateway method (using `serverless package` to look at the generated cloudformation template was useful in discovering the correct resource name for the method)

```
resources:
  Resources:
      {ServerlessResourceMethodName}:
        Type: "AWS::ApiGateway::Method"
        Properties:
          AuthorizationScopes:
            - "myscope"
```
@mattbryce93 thanks for the example. It looks like I forgot the array brackets. It still doesn't work though. Could it be that it's because I have a proxy resource? 

```
AWSOAuthScope: /co2/xml/{chassis+} : GET : api/co2
 
  Type Error ---------------------------------------------
 
  Cannot read property 'id' of undefined
 
  Stack Trace --------------------------------------------
 
TypeError: Cannot read property 'id' of undefined
    at setOAuthScopes (backend/co2/node_modules/serverless-oauth-scopes/index.js:37:86)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:228:7)
```

@mdanku do you have any idea why this fails? 
@gwosty It looks like it isn't able to find a resource item with path matching the defined path in your serverless config.  Try adding some logging to your local copy of serverless-oauth-scopes/index.js to see if that helps.  Log the value of "path" after line 29 and the value of "resp.items" after line 36.
@mdanku i did what you suggested. It looks like it fails because in index.js we retrieve stage name as
`const stage = serverless.service.provider.stage;`

This only works if the stage is set inside serverless.yml. It doesn't seam to work if you pass the stage name as a CLI option. I'm looking for a fix now, do you know where the options are saved?
@gwosty You should be able to read stage name from the CLI like this:
```
provider:
  stage: ${opt:stage, 'dev'}
```
as per @mikemckibben comment the AuthorizationScopes property of "AWS::ApiGateway::Method" is now supported by cloudformation.

Time to bring this into apigateway http event natively?

Until this is implemented, we have decided to use an environment variable as a workaround:
```
...
authorizer:
    handler: ...
    environment:
      SCOPES: [your,scopes,here]
```
The plugin did not work for me, only updated the first function :(
I ended up using @mikemckibben solution with resources. To get the right method name check the .serverless/serverless-state.json file. The method names should start with ApiGatewayMethod

@mikemckibben methods with Resource overrides worked for me, Thank you!!!
Implementing the `authorizationScopes` in the function definition would be really handy though.
Just like @agouz I found that `serverless-oauth-scopes` is not working.

What is the status on this feature?

It would be really useful, rather than the alternatives of having to manually alter each method on aws ui or having to create a script to add the scopes after serverless deploys.

Solution patch:
diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md
index b32ca8b88e1..dd3e984398a 100644
--- a/docs/providers/aws/events/apigateway.md
+++ b/docs/providers/aws/events/apigateway.md
@@ -279,7 +279,7 @@ functions:
             maxAge: 86400
 ```
 
-If you are using CloudFront or another CDN for your API Gateway, you may want to setup a `Cache-Control` header to allow for OPTIONS request to be cached to avoid the additional hop.  
+If you are using CloudFront or another CDN for your API Gateway, you may want to setup a `Cache-Control` header to allow for OPTIONS request to be cached to avoid the additional hop.
 
 To enable the `Cache-Control` header on preflight response, set the `cacheControl` property in the `cors` object:
 
@@ -446,7 +446,7 @@ functions:
 ```
 
 You can also configure an existing Cognito User Pool as the authorizer, as shown
-in the following example:
+in the following example with optional access token allowed scopes:
 
 ```yml
 functions:
@@ -458,6 +458,8 @@ functions:
           method: post
           authorizer:
             arn: arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ
+            scopes:
+              - my-app/read
 ```
 
 If you are using the default `lambda-proxy` integration, your attributes will be
@@ -1242,7 +1244,7 @@ functions:
     events:
       - http:
           path: /users
-          ...     
+          ...
           authorizer:
             # Provide both type and authorizerId
             type: COGNITO_USER_POOLS # TOKEN or REQUEST or COGNITO_USER_POOLS, same as AWS Cloudformation documentation
@@ -1254,7 +1256,7 @@ functions:
     events:
       - http:
           path: /users/{userId}
-          ...     
+          ...
           # Provide both type and authorizerId
           type: COGNITO_USER_POOLS # TOKEN or REQUEST or COGNITO_USER_POOLS, same as AWS Cloudformation documentation
           authorizerId:
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 c9314397b7a..03957b8e8ee 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
@@ -26,15 +26,25 @@ module.exports = {
       const authorizerLogicalId = this.provider.naming
         .getAuthorizerLogicalId(http.authorizer.name);
 
-      let authorizationType;
       const authorizerArn = http.authorizer.arn;
+
+      let authorizationType;
       if (typeof authorizerArn === 'string'
         && awsArnRegExs.cognitoIdpArnExpr.test(authorizerArn)) {
         authorizationType = 'COGNITO_USER_POOLS';
-      } else {
-        authorizationType = 'CUSTOM';
+        const cognitoReturn = {
+          Properties: {
+            AuthorizationType: authorizationType,
+            AuthorizerId: { Ref: authorizerLogicalId },
+          },
+          DependsOn: authorizerLogicalId,
+        };
+        if (http.authorizer.scopes) {
+          cognitoReturn.Properties.AuthorizationScopes = http.authorizer.scopes;
+        }
+        return cognitoReturn;
       }
-
+      authorizationType = 'CUSTOM';
       return {
         Properties: {
           AuthorizationType: authorizationType,
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 f0b724b6829..fd957ddc3d4 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js
@@ -220,6 +220,7 @@ module.exports = {
     let identityValidationExpression;
     let claims;
     let authorizerId;
+    let scopes;
 
     if (typeof authorizer === 'string') {
       if (authorizer.toUpperCase() === 'AWS_IAM') {
@@ -258,6 +259,7 @@ module.exports = {
       resultTtlInSeconds = Number.parseInt(authorizer.resultTtlInSeconds, 10);
       resultTtlInSeconds = Number.isNaN(resultTtlInSeconds) ? 300 : resultTtlInSeconds;
       claims = authorizer.claims || [];
+      scopes = authorizer.scopes;
 
       identitySource = authorizer.identitySource;
       identityValidationExpression = authorizer.identityValidationExpression;
@@ -295,6 +297,7 @@ module.exports = {
       identitySource,
       identityValidationExpression,
       claims,
+      scopes,
     };
   },
 


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 705e477ce50..0c457203dab 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
@@ -502,6 +502,7 @@ describe('#compileMethods()', () => {
           authorizer: {
             name: 'authorizer',
             arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ',
+            scopes: ['myapp/read', 'myapp/write'],
           },
           integration: 'AWS',
           path: 'users/create',
@@ -516,6 +517,11 @@ describe('#compileMethods()', () => {
           .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