aws-amplify / amplify-category-api

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development. This plugin provides functionality for the API category, allowing for the creation and management of GraphQL and REST based backends for your amplify project.
https://docs.amplify.aws/
Apache License 2.0
82 stars 71 forks source link

[Feature Request] REST API with Cognito User Pool authorization #345

Open lawmicha opened 4 years ago

lawmicha commented 4 years ago

Is your feature request related to a problem? Please describe. Amplify CLI provides a way to create a REST api (API Gateway + lambda) with IAM authorization. in the JS docs, there are manual steps to add cognito user pool as the authorizator for the requests to API gateway: https://aws-amplify.github.io/docs/js/api#cognito-user-pools-authorization (however, this could be improved, see step 11 below). Amplify iOS (https://github.com/aws-amplify/amplify-ios/pull/312) and Android also supports this API with user pools as the auth mechanism.

The pain point is here is that Amplify CLI doesn't support creating API Gateway + Cognito User Pool authorizator. I'm opening this issue here to provide more details around what needs to done to enable API Gateway with Cognito.

The following steps show how to set up an API endpoint with APIGateway and Lambda source. The auth configured will be Cognito User Pool.

  1. Initialize an amplify project. amplify init

  2. Create an API Gateway which proxies requests to an AWS Lambda with no authorization needed. amplify add api.

? Please select from one of the below mentioned services: `REST`
? Provide a friendly name for your resource to be used as a label for this category in the project: `restAPI`
? Provide a path (e.g., /items) `/items`
? Choose a Lambda source `Create a new Lambda function`
? Provide a friendly name for your resource to be used as a label for this category in the project: `restwithuserpoolinte22de6072`
? Provide the AWS Lambda function name: `restwithuserpoolinte22de6072`
? Choose the function template that you want to use: `Serverless express function (Integration with Amazon API Gateway)`
? Do you want to access other resources created in this project from your Lambda function? `No`
? Do you want to edit the local lambda function now? `No`
Succesfully added the Lambda function locally
? Restrict API access `No`
? Do you want to add another path? `No`
Successfully added resource apid7c040db locally
  1. Create Cognito User Pool. Run amplify add auth

    Using service: Cognito, provided by: awscloudformation
    
    The current configured provider is Amazon Cognito. 
    
    Do you want to use the default authentication and security configuration? Default configuration
    Warning: you will not be able to edit these selections. 
    How do you want users to be able to sign in? Email
    Do you want to configure advanced settings? No, I am done.
    Successfully added resource restwithuserpoolintea05fdd00 locally
  2. Provision the resources. Run amplify push to provision the API Gateway, Lambda, and the Cognito User Pool.

  3. In amplifyconfiguration.json. update authorizationType to AMAZON_COGNITO_USER_POOLS like so

    {
    "UserAgent": "aws-amplify-cli/2.0",
    "Version": "1.0",
    "api": {
        "plugins": {
            "awsAPIPlugin": {
                "apid7c040db": {
                    "endpointType": "REST",
                    "endpoint": "https://endpoint.execute-api.us-west-2.amazonaws.com/devo",
                    "region": "us-west-2",
                    "authorizationType": "AMAZON_COGNITO_USER_POOLS"
                }
            }
        }
    }
    }
  4. Find your API name. Run amplify console to open the AWS Console. The latest deployment activty logs will indicate the API Gateway that is provisioned. There will be a Resource ID that looks like <api name> (api). Navigate to API Gateway console, select your API.

  5. Find your Cognito User Pool name by click on the Authentication tab in the AWS Console.

  6. Add Cognito User Pool as an authorization mechanism. Select Authorizers, click on "+ Create New Authorizer",

    • type in a Name
    • select Cognito as the type
    • Select the Cognito UserPool
    • For Token Source, enter Authorization
    • Once completed, refresh the page.
  7. Enable requests to the API with the Cognito User Pool Authorizer as the authorization mechanism.

    • Select Resources on the left, Under Resources, and each individual resource path, select Any. You will see a Test section, Method Request, Method Response, Integration Request, etc
    • Click on Method Request, under Settings, Authorization, click on edit. In the drop down, select the User Pool authorizer, then click on the check mark to save it.
    • Repeat this for each of the resource paths
    • Click on Actions, deploy API, and select the deployment stage, and click Deploy.

There are some additional gotcha's that need to be noted here:

Describe the solution you'd like A clear and concise description of what you want to happen. amplify add api

Restrict API Access? Yes Cognito User Pool or IAM? Cognito User Pool Auth access? [create|update|delete] Unauth access? [create|update|delete]

Alternatives JS docs could use some improvements with step number 11, for example it doesn't guide the develop to re-deploy the API. iOS and Android Amplify docs will be written simiarly.

Dedupped to https://github.com/aws-amplify/amplify-cli/issues/3058 aws-amplify/amplify-category-api#438 (Just realized this one is for API + API Key, not user pool)

attilah commented 4 years ago

@lawmicha thanks for opening the issue, I see the customer need here and this is something the CLI could support out of the box. I take this to the team for discussion and get back to you.

alexladerman commented 4 years ago

I agree this is really important. API+Lambda+Cognito did not work out of the box for me!

hi120ki commented 4 years ago

I need this feature. Cognito User Pool Authorization provides us to powerful and useful permission management.

harakiro commented 4 years ago

I spent hours banging my head on this and went through multiple tutorials and digging through code. Thank you for such a great walk-thru and this needs to be added!

xitanggg commented 4 years ago

Great walk through. Thanks a lot. Hope this automation is added soon. for such an important feature. It is essentially the same as AdminQueries API. It should be easy for the Amplify team to copy what has done over.

smakinson commented 4 years ago

@lawmicha I wondered if something may have changed or if perhaps a step is missing. I have tried a couple times, at first with the auth I already had set up and the removed all the api, functions & auth and tried again and it doesn't seem to generate amplifyconfiguration.json. Where do you see that file generated? I've also tried searching for plugins and authorizationType I apologize if I am just missing it some how.

I had previously tried manually adding the Cognito Authorizer in another project and realized when I ran push again for some changes the Authorizer got trashed, so I'm hoping this will solve that. Thanks for any info you can provide.

lawmicha commented 4 years ago

@lawmicha I wondered if something may have changed or if perhaps a step is missing. I have tried a couple times, at first with the auth I already had set up and the removed all the api, functions & auth and tried again and it doesn't seem to generate amplifyconfiguration.json. Where do you see that file generated? I've also tried searching for plugins and authorizationType I apologize if I am just missing it some how.

Hi @smakinson, amplifyconfiguration.json is used in iOS and Android projects. Sorry I should have clarified. Are you using Amplify CLI with your JS app? (amplify init -> which type of project). I believe JS should be aws_exports.json or something

I had previously tried manually adding the Cognito Authorizer in another project and realized when I ran push again for some changes the Authorizer got trashed, so I'm hoping this will solve that. Thanks for any info you can provide.

This sounds like expected behavior but also a problem if amplify push reverts any manual changes done through AWS Console. If confirmed, then is it supported to modify the cloud formation templates ourselves after using Amplify CLI that will not get overwritten after another amplify push?

smakinson commented 4 years ago

@lawmicha Thanks for your response, you are correct, it is a javascript project. Hoping to find a way around it losing the Authorizer within Amplify.

aws-exports.js shows:

// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

so it seems that is not the file I want. I'll keep digging.

lawmicha commented 4 years ago

@smakinson aws-exports.js will be reverted after amplify push. So maybe the APIGateway still has the authorizer set up but the configuration file in the app is reverted. this will be same issue with amplifyconfiguration.json. You can also programmatically configure Amplify.API without using aws-exports.js, although that will mean you need to transfer all the values over to code, and configure Amplify.Auth programmtically and any other categories you have as well

I just want to note that this is a feature request, and not necessarily steps for developers to follow and get working, they are steps provided to assist in turning this into an Amplify CLI feature. Following these steps are not supported by Amplify CLI. As we've seen the issue with the configuration file being overwritten.

smakinson commented 4 years ago

@lawmicha Incase this is helpful for others I thought I'd post up here what I am looking at doing with the cloudformation template, and if you have a moment I have 2 questions, one general and the other (without taking too much of your time) if you might know the fix for an error:

"myApi": {
  "service": "API Gateway",
    "providerPlugin": "awscloudformation",
    "dependsOn": [
    {
      "category": "function",
      "resourceName": "myFunc",
      "attributes": [
        "Name",
        "Arn"
      ]
    },
    {
      "category": "auth",
      "resourceName": "myAuthResource",     // from amplify status
      "attributes": [
        "UserPoolId"
      ]
    }
  ]
}

in the Parameters section of the myApi-cloudformation-template.json added:

"authmyAuthResourceUserPoolId": { // The format out here is `<category><resource-name><attribute-name>
  "Type": "String"
}

the methods have the authorizer attached by adding the following under x-amazon-apigateway-any-method

"security": [
  {
    "CognitoAuthorizer": []
  }
],

and further down in the file I'm trying to reference it like:

"securityDefinitions": {
  "CognitoAuthorizer": {
    "type": "apiKey",
      "name": "Authorization",
      "in": "header",
      "x-amazon-apigateway-authtype": "cognito_user_pools",
      "x-amazon-apigateway-authorizer": {
      "providerARNs": [
        {
          "authmyAuthResourceUserPoolId": {   // The format out here is `<category><resource-name><attribute-name>

            "Fn::GetAtt": [
              "authmyAuthResource",   // I've tried this with and without the auth category prefix
              "Outputs.UserPoolId"
            ]
          }
        }
      ],
        "type": "cognito_user_pools"
    }
  }
}

Next I ran amplify env checkout CURRENT_ENV and then amplify push

The error in question is the one I see when I run amplify-push:

Template error: instance of Fn::GetAtt references undefined resource authmyAuthResource

Maybe I'm just mistyping something, hopefully this can be helpful for anyone in a javascript amplify project until the cli supports it. Thanks for any insight.

lawmicha commented 4 years ago

Thanks @smakinson for posting your details. I'm not very familar with those files you are asking about in your questions. + @SwaySway et al to help answer

SwaySway commented 4 years ago

Hello @smakinson, Your API specific changes in backend-config.js and myApi-cloudformation-template.json are changed when you run amplify update API.

I have it working if I hardcode my POOL ARN in the last bit of this and I'm trying to change now to get it based on the current environment based on https://aws-amplify.github.io/docs/cli-toolchain/quickstart#custom-cloudformation-stacks

The docs here specify a way to get the user pool id into another stack. What you are looking for is the UserPoolArn. For that you'll need to import will be different, I've detailed this below.

You'll need to use a different ref for the ARN such as "Ref": "authMyAuthResourceNameUserPoolArn" and add that as a parameter in the template.

        "authMyAuthResourceNameUserPoolArn": {
          "Type": "String"
        }

In parameters.json you'll need to point to the auth stack to reference the UserPoolArn

    "authMyAuthResourceNameUserPoolArn": {
        "Fn::GetAtt": [
            "MyAuthResourceName",
            "Outputs.UserPoolArn",
            "Arn"
        ]
    }

Currently arn is not included in the outputs of the auth stack, this is something we can consider as a part of this feature request. For this you'll want to export the Arn as well from the auth stack.

  UserPoolArn:
    Value: !GetAtt UserPool.Arn
    Description: Arn for cognito userpool 

The last thing here to edit would be to change the backend-config.json to denote that the api resource depends on the auth stack.

    "api": {
        "demorest": {
            "service": "API Gateway",
            "providerPlugin": "awscloudformation",
            "dependsOn": [
                {
                    "category": "function",
                    "resourceName": "demoRestFN",
                    "attributes": [
                        "Name",
                        "Arn"
                    ]
                },
                {
                    "category": "auth",
                    "resourceName": "MyAuthResourceName",
                    "attributes": [
                        "UserPoolArn"
                    ]
                }
            ]
        }
    }

All that's left here is to run amplify env checkout <current-env-name> and amplify push.

smakinson commented 4 years ago

@SwaySway Thank you for your help. I currently have manually setup an api for this, do you think its worth (risky?) going this route or would it be better to wait on the feature request and change it then?

nibab commented 4 years ago

This is a great workaround for now for creating Cognito Authorizers. Perhaps in a similiar vein, Is it safe to edit lambdaFunction-cloudformation-template.json? When does it get overwritten ? If I needed to add custom resources (ie a lambda policy), is the recommended approach to modify this cfn template ? Im looking for an analogy to the CustomResources.json template in the api/stacks folder.

jwoehrle commented 4 years ago

I'm trying to accomplish the same thing (calling API GW with Cognito Auth) and made it work with above method.

However: my call only succeeds if I use an ID Token to authenticate:

let init = {
            headers: { Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}` }
        }
API.get('xxx', '/items', myInit).then( result => {
      console.log(result);
    })

It's not working with an access token:

let init = {
            headers: { Authorization: `Bearer ${(await Auth.currentSession()).getAccessToken().getJwtToken()}` }
        }
API.get('xxx', '/items', myInit).then( result => {
      console.log(result);
    })

Fails with 401 and body { "message" : "unauthorized"}

I'd expect an access token to work instead of an id token. Is there anything I'm doing wrong?

ahansson89 commented 3 years ago

What's the latest status on this?

Been scratching my head for hours trying to figure out why this was not working out of the box. The latest CLI makes you go through steps to set it all up, but does not build a CloudFormation template that actually works.

Again, it is blowing my mind how often I have to fix this stuff myself in vs having the Amplify CLI do what it is supposed to do. Does the Amplify product team not have any QA engineers?

jfhidakatsu commented 3 years ago

I got this working using @lawmicha 's steps and @jwoehrle 's code. To my understanding, I'm going to have to follow these steps every time we deploy (though I haven't tested this part yet). Is there any other workaround that will work automatically?

Edit: It is working well, but does require this configuration to be set every time the API is updated from the amplify CLI.

AtomicSimon commented 3 years ago

Any news on getting this implemented? Overriding the amplify config manually is a time-consuming process for a feature that should already be there.

sapher commented 3 years ago

I'm trying to accomplish the same thing (calling API GW with Cognito Auth) and made it work with above method.

However: my call only succeeds if I use an ID Token to authenticate:

let init = {
            headers: { Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}` }
        }
API.get('xxx', '/items', myInit).then( result => {
      console.log(result);
    })

It's not working with an access token:

let init = {
            headers: { Authorization: `Bearer ${(await Auth.currentSession()).getAccessToken().getJwtToken()}` }
        }
API.get('xxx', '/items', myInit).then( result => {
      console.log(result);
    })

Fails with 401 and body { "message" : "unauthorized"}

I'd expect an access token to work instead of an id token. Is there anything I'm doing wrong?

Here's from the documentation.

With the COGNITO_USER_POOLS authorizer, if the OAuth Scopes option isn't specified, API Gateway treats the supplied token as an identity token and verifies the claimed identity against the one from the user pool. Otherwise, API Gateway treats the supplied token as an access token and verifies the access scopes that are claimed in the token against the authorization scopes declared on the method.

My last comment got deleted, pretty good Team.

mansdahlstrom1 commented 3 years ago

Is this being worked on? This seems like a very important feature and i was surprised when it did not work out of the box. Have been trying to solve an issue for some hours now and stumbled upon this now

FlaegelEmbed commented 3 years ago

This would be a very useful feature! For now, we have to set the authorizer for all resources after every "amplify update api", because amplify overwrites the template file : (

patrizz commented 3 years ago

I love Amplify, and this is a feature that I’d very much like to see implemented, like the many others in this thread :) could you please share any status updates, dear Amplify Team?

mattfysh commented 3 years ago

I've also just come across this issue. My first thought was to simply update the generated cloudformation file, however... the template used by the CLI to generate the CloudFormation config is located here:

https://github.com/aws-amplify/amplify-cli/blob/master/packages/amplify-category-api/resources/awscloudformation/cloudformation-templates/apigw-cloudformation-template-default.json.ejs

In your generated API folder, e.g. amplify/backend/api/myapi there'll be another file called api-params.json which I believe is used to control the template generation process. e.g. running amplify update api will update the params and then re-generate the CloudFormation file. This explains why any update commands will re-generate the cloudformation file and overwrite any changes you've made.

My second thought is to somehow define cloudformation customizations in a separate file, and then somehow hook into the amplify push process to merge the customizations into the generated template. If anyone is keen to help me look, let me know!

caiconkhicon commented 3 years ago

Hi, is there any roadmap for this feature request? I also face this issue. IMO, it is a fundamental feature (API-Gateway authentication using Cognito User Pool) and it should be supported. What is the point of creating API-Gateway and Cognito, both by Amplify CLI, but cannot use them together?

Thanks.

brymartinez commented 3 years ago

Here's what I did to add the Custom Authorizer to the API's CloudFormation template.

It's worth noting that AdminQueries has COGNITO_USER_POOLS Authorizer, so I copy-pasted parts of the template there.

Add securityDefinitions to your API name properties ( Resources > <API name> > Properties )

Example:

          "securityDefinitions": {
            "Cognito": {
              "type": "apiKey",
              "name": "Authorization",
              "in": "header",
              "x-amazon-apigateway-authtype": "cognito_user_pools",
              "x-amazon-apigateway-authorizer": {
                "providerARNs": [
                  {
                    "Fn::Join": [
                      "",
                      [
                        "arn:aws:cognito-idp:",
                        {
                          "Ref": "AWS::Region"
                        },
                        ":",
                        {
                          "Ref": "AWS::AccountId"
                        },
                        ":userpool/",
                        {
                          "Ref": "<your_user_pool>"
                        }
                      ]
                    ]
                  }
                ],
                "type": "cognito_user_pools"
              }
            }
          },

This defines the Custom Authorizer called "Cognito" that's based on your user pool. <your_user_pool> should then be added to the CF's template parameters: (Top level > Parameters, just below env and authRoleName/unauthRoleName)

    "<your_user_pool>": {
      "Type": "String",
      "Default": "<your_user_pool>"
    },

This validates the Ref created through securityDefinitions. Finally, add this to the paths > x-amazon-apigateway-any-method (for ANY) and for all paths:

parameters:

                  {
                    "name": "Authorization",
                    "in": "header",
                    "required": false,
                    "type": "string"
                  }

Top level (inside paths):

                "security": [
                  {
                    "Cognito": []
                  }
                ],

Note that Cognito accepts an array of OAuth scopes, mine's empty. AdminQueries has aws.cognito.signin.user.admin as an OAuth scope.

I'm guessing this follows the same logic as https://github.com/aws-amplify/amplify-cli/issues/4351 , a manual adding of permissions to the policy, which can be pushed to a project. I tried using amplify pull on a separate branch and the CF edits stuck. I haven't tried amplify pull on a separate project though that shares the amplify configuration.

johnEthicalTechnology commented 2 years ago

Setting this up was a bit unclear for me, but eventually I got it through a combination of @brymartinez and @SwaySway post's. Thanks for that!

This consolidates those posts into one to make it easier for future dwellers to implement this functionality.

The following will apply to the JavaScript Amplify library.

STEP 1

Set up the appropriate parameters coming into your REST API's CloudFormation (CF) template. This is found in: ./amplify/backend/api/<the name of your api>/parameters.json.

From my guess this file acts as a way for the backend resources to pass values to each other just as long as they have been set in the resources CF template Outputs key.

Add the following key/value after whatever key/values are there already or within the empty object. For me this was an empty file, but for you it could have preexisting values. You'll find name for auth in ./amplify/backend/backend-config.json under the "auth" key.

You'll have to check the ./amplify/backend/auth/<the name of your auth>/<name of your auth>-cloudformation-template.yml to see if UserPoolId is being outputted. I'd be surprised if it wasn't.

AuthCognitoUserPoolId is a descriptive name I gave it. This will be used in the next step as reference to the UserPoolId.

  "AuthCognitoUserPoolId": {
    "Fn::GetAtt": ["auth<name for auth>", "Outputs.UserPoolId"]
  }

STEP 2

Set up the appropriate Parameters, securityDefinitions, security, and parameter values in your REST API's CF template. This is found in ./amplify/backend/api/<the name of your api>/<the name of your api>-cloudformation-template.json.

In the "Parameters" key add the following key/value:

"AuthCognitoUserPoolId": {
      "Type": "String",
      "Description": "The id of an existing User Pool to connect. If this is changed, a user pool will not be created for you.",
      "Default": "NONE"
    },

In "Resources" > "<your api name>" > "Properties" > "Body" add the following key/value. This is creating the authorizer and the AuthCognitoUserPoolId parameter is being used her to reference the Cognito user pool you want to use.

I originally thought AuthCognitoUserPoolId was the literal user pool id value, but I got an error when I added that as the key for the parameter.

The "Cognito" key is a custom name and you can name it anything.

"securityDefinitions": {
            "Cognito": {
              "type": "apiKey",
              "name": "Authorization",
              "in": "header",
              "x-amazon-apigateway-authtype": "cognito_user_pools",
              "x-amazon-apigateway-authorizer": {
                "type": "cognito_user_pools",
                "providerARNs": [
                  {
                    "Fn::Join": [
                      "",
                      [
                        "arn:aws:cognito-idp:",
                        {
                          "Ref": "AWS::Region"
                        },
                        ":",
                        {
                          "Ref": "AWS::AccountId"
                        },
                        ":userpool/",
                        {
                          "Ref": "AuthCognitoUserPoolId"
                        }
                      ]
                    ]
                  }
                ]
              }
            }
          },

In "Resources" > "<your api name>" > "Properties" > "Body" > "paths" > "<your path/s name>" > "x-amazon-apigateway-any-method" > "parameters" add the following key/value to the array. This is defining the Authorization header as a parameter that has the token.

This has to be added to all of the methods to which you want to apply this authorization. It will be under the appropriate http method key such as "get", "post", etc. I didn't apply it to "options" because it's not needed.

{
                    "name": "Authorization",
                    "in": "header",
                    "required": false,
                    "type": "string"
                  }

In "Resources" > "<your api name>" > "Properties" > "Body" > "paths" > "<your path/s name>" > "x-amazon-apigateway-any-method" > "security" add the following key/value. Like the parameter above it needs to be added to any methods you want this authorization to apply. This has to be the same as the name you gave the key in the "securityDefinitions" object.

This will select this authorizor for the method. If it's not added when you go into the API Gateway console and select the method you'll see NONE in the Auth setting even though the Cognito authorizer has been setup and is an option in the dropdown menu.

For anything other than OAuth2 type security scheme this must be empty.

"security": [
                  {
                    "Cognito": []
                  }
                ],

That's it for the implementation of the infrastructure. Run amplify push to set it all up.

From what I can tell the configuration setup doesn't get overridden with updates. But I've not thoroughly tested it.

For usage, this is what I'm doing:

import { API, Auth } from 'aws-amplify'

....

 const apiName = '<your api name>'
      const path = '/<your api path>'
      const myInit = {
        body: {
          test: 'test'
        },
        headers: {
          Authorization: `Bearer ${(await Auth.currentSession())
            .getIdToken()
            .getJwtToken()}`
        },
        response: true,
        queryStringParameters: {}
      }
      const finaliseImportResp = await API.post(apiName, path, myInit)

For reference here's some AWS documentation:

RoniqueRicketts commented 2 years ago

From my experience working with Amplify when I run amplify api add and I go through and choose express backend```` I am unable to update the AGW's. Firstly, I can't make any changes toAccess-Control-Allow-Headers```. Secondly, I am unable to send my Access Token so I can crack it in the Express Backend function (which could be more control over what a user can/can't do). Additionally, if we follow the documentation's on sending access/id tokens we are unable to get that data pass the AGW even though it's suggested in the documentation. Having this handled from the cli would be easier for other devs.

ataravati commented 2 years ago

Setting this up was a bit unclear for me, but eventually I got it through a combination of @brymartinez and @SwaySway post's. Thanks for that!

This consolidates those posts into one to make it easier for future dwellers to implement this functionality.

The following will apply to the JavaScript Amplify library.

STEP 1

Set up the appropriate parameters coming into your REST API's CloudFormation (CF) template. This is found in: ./amplify/backend/api/<the name of your api>/parameters.json.

From my guess this file acts as a way for the backend resources to pass values to each other just as long as they have been set in the resources CF template Outputs key.

Add the following key/value after whatever key/values are there already or within the empty object. For me this was an empty file, but for you it could have preexisting values. You'll find name for auth in ./amplify/backend/backend-config.json under the "auth" key.

You'll have to check the ./amplify/backend/auth/<the name of your auth>/<name of your auth>-cloudformation-template.yml to see if UserPoolId is being outputted. I'd be surprised if it wasn't.

AuthCognitoUserPoolId is a descriptive name I gave it. This will be used in the next step as reference to the UserPoolId.

  "AuthCognitoUserPoolId": {
    "Fn::GetAtt": ["auth<name for auth>", "Outputs.UserPoolId"]
  }

STEP 2

Set up the appropriate Parameters, securityDefinitions, security, and parameter values in your REST API's CF template. This is found in ./amplify/backend/api/<the name of your api>/<the name of your api>-cloudformation-template.json.

In the "Parameters" key add the following key/value:

"AuthCognitoUserPoolId": {
      "Type": "String",
      "Description": "The id of an existing User Pool to connect. If this is changed, a user pool will not be created for you.",
      "Default": "NONE"
    },

In "Resources" > "<your api name>" > "Properties" > "Body" add the following key/value. This is creating the authorizer and the AuthCognitoUserPoolId parameter is being used her to reference the Cognito user pool you want to use.

I originally thought AuthCognitoUserPoolId was the literal user pool id value, but I got an error when I added that as the key for the parameter.

The "Cognito" key is a custom name and you can name it anything.

"securityDefinitions": {
            "Cognito": {
              "type": "apiKey",
              "name": "Authorization",
              "in": "header",
              "x-amazon-apigateway-authtype": "cognito_user_pools",
              "x-amazon-apigateway-authorizer": {
                "type": "cognito_user_pools",
                "providerARNs": [
                  {
                    "Fn::Join": [
                      "",
                      [
                        "arn:aws:cognito-idp:",
                        {
                          "Ref": "AWS::Region"
                        },
                        ":",
                        {
                          "Ref": "AWS::AccountId"
                        },
                        ":userpool/",
                        {
                          "Ref": "AuthCognitoUserPoolId"
                        }
                      ]
                    ]
                  }
                ]
              }
            }
          },

In "Resources" > "<your api name>" > "Properties" > "Body" > "paths" > "<your path/s name>" > "x-amazon-apigateway-any-method" > "parameters" add the following key/value to the array. This is defining the Authorization header as a parameter that has the token.

This has to be added to all of the methods to which you want to apply this authorization. It will be under the appropriate http method key such as "get", "post", etc. I didn't apply it to "options" because it's not needed.

{
                    "name": "Authorization",
                    "in": "header",
                    "required": false,
                    "type": "string"
                  }

In "Resources" > "<your api name>" > "Properties" > "Body" > "paths" > "<your path/s name>" > "x-amazon-apigateway-any-method" > "security" add the following key/value. Like the parameter above it needs to be added to any methods you want this authorization to apply. This has to be the same as the name you gave the key in the "securityDefinitions" object.

This will select this authorizor for the method. If it's not added when you go into the API Gateway console and select the method you'll see NONE in the Auth setting even though the Cognito authorizer has been setup and is an option in the dropdown menu.

For anything other than OAuth2 type security scheme this must be empty.

"security": [
                  {
                    "Cognito": []
                  }
                ],

That's it for the implementation of the infrastructure. Run amplify push to set it all up.

From what I can tell the configuration setup doesn't get overridden with updates. But I've not thoroughly tested it.

For usage, this is what I'm doing:

import { API, Auth } from 'aws-amplify'

....

 const apiName = '<your api name>'
      const path = '/<your api path>'
      const myInit = {
        body: {
          test: 'test'
        },
        headers: {
          Authorization: `Bearer ${(await Auth.currentSession())
            .getIdToken()
            .getJwtToken()}`
        },
        response: true,
        queryStringParameters: {}
      }
      const finaliseImportResp = await API.post(apiName, path, myInit)

For reference here's some AWS documentation:

@johnEthicalTechnology Thank you so much for this. Could you also explain how to restrict the api access?

evankirkiles commented 2 years ago

The above solution no longer works due to some changes in how cloud formation templates are generated / overridden in AWS Amplify (which is now done with CDK instead of directly modifying the cloud formation template). I threw together a gist for an override.ts file which should be put in your API folder instead as an update to the wonderful solution put together by @johnEthicalTechnology:

https://gist.github.com/evankirkiles/9de81f026a2e2c961a2b6a3d80d35519

Feel free to recommend changes!

blbigelow commented 2 years ago

The above solution no longer works due to some changes in how cloud formation templates are generated / overridden in AWS Amplify (which is now done with CDK instead of directly modifying the cloud formation template). I threw together a gist for an override.ts file which should be put in your API folder instead as an update to the wonderful solution put together by @johnEthicalTechnology:

https://gist.github.com/evankirkiles/9de81f026a2e2c961a2b6a3d80d35519

Feel free to recommend changes!

@evankirkiles Thanks for sharing! Your gist worked great for me.

sshvaiko commented 2 years ago

Hello, I've used this manual to configure Cognito User Pool authorizer, but during the deployment, I'm getting this error:

Resource Name: 1rfy2sfng2 (AWS::ApiGateway::RestApi)
Event Type: update
Reason: Errors found during import:
Unable to create authorizer 'Cognito': ProviderARNs need to be valid Cognito Userpools. Invalid ARNs-
arn:aws:cognito-idp:{AWS::Region}:{AWS::AccountId}:userpool/{AuthCognitoUserPoolId}
Unable to put method 'x-amazon-apigateway-any-method' on resource at path '/invite/verify': Invalid authorizer ID specified. Setting the authorization type to CUSTOM or COGNITO_USER_POOLS requires a valid authorizer.
Unable to put method 'x-amazon-apigateway-any-method' on resource at path '/invite/verify/{proxy+}': Invalid authorizer ID specified. Setting the authorization type to CUSTOM or COGNITO_USER_POOLS requires a valid authorizer. (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException; Request ID: 0ef6e1aa-3d3f-4d45-990b-e41b21ad64f8; Proxy: null)

I did not forget to replace your auth name here :) Any thoughts?

And one more question, we can make the function private from CLI. Then, there is information about the user in event.requestContext.identity. Why not use it?

shkomg commented 2 years ago

shit, every time I add new API path I need to recreate the Authorizer & redeploy the api

kinda a pain when quickly prototyping project

:(

rondondaniel commented 2 years ago

Hi @shkomg I'm experiencing exactly the same problem. If override I get the same errors @sshvaiko had.

UPDATE: ARNs- arn:aws:cognito-idp:{AWS::Region}:{AWS::AccountId}:userpool/{AuthCognitoUserPoolId} doesn't update variables. So you should manually modify the values: AWS::Region, AWS::AccountId; AuthCognitoUserPoolId. 😄 😄 😄

It effectively works using Amplify-CLI 9.1.0 but in my opinion could lead to security issues. Workarounds could be to modify the script or to add a env. var. for example.

shkomg commented 2 years ago

Hi @rondondaniel I definitely expect it working out of the box with default CLI prompts and waiting for Amplify team to resolve this. Meanwhile for me updating over the API Gateway once in while isn't perfect but Ok. It just simpler to remember VS changes in code, at least for me :)

rafhuys-klarrio commented 1 year ago

@sshvaiko @rondondaniel no need to manually update the values. That's the whole idea of the script, right :| I believe the documentation is just wrong and you should put $ before the {...} substitutions, like { "Fn::Sub": "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${AuthCognitoUserPoolId}" }

Duder-onomy commented 1 year ago

I had the same issue as @shkomg and @rondondaniel.
@rafhuys-klarrio fix (adding the $'s ) worked for me.

tzutalin commented 1 year ago

It is pretty bad that Amplify does not support this out of box

In a word, there are 3 steps to achieve this. Ref: https://docs.amplify.aws/cli/restapi/override/#add-a-cognito-user-pool-authorizer-to-your-rest-api

Step1: $ amplify override api

Step2: edit amplify/backend/api/yourProject/override.ts

export function override(resources: AmplifyApiRestResourceStackTemplate) {

    // Add a parameter to your Cloud Formation Template for the User Pool's ID. The name of resource is the name of the folder in amplify/backend/auth
    resources.addCfnParameter({
        type: "String",
        description: "The id of an existing User Pool to connect. If this is changed, a user pool will not be created for you.",
        default: "NONE",
      },
      "AuthCognitoUserPoolId",
      { "Fn::GetAtt": ["auth<YourAuthSetting>", "Outputs.UserPoolId"], }
    ); // YourAuthSetting can be found `ls amplify/backend/auth`

    // Create the authorizer using the AuthCognitoUserPoolId parameter defined above
    resources.restApi.addPropertyOverride("Body.securityDefinitions", {
      Cognito: {
        type: "apiKey",
        name: "Authorization",
        in: "header",
        "x-amazon-apigateway-authtype": "cognito_user_pools",
        "x-amazon-apigateway-authorizer": {
          type: "cognito_user_pools",
          providerARNs: [
            { 'Fn::Sub': 'arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${AuthCognitoUserPoolId}' },
          ],
        },
      },
    });

    // For every path in your REST API
    for (const path in resources.restApi.body.paths) {
      if (path.startsWith('/somepath')) {
         console.log('Skip the path not to use Authorization header', path);
         continue;
      }
       console.log('Make the path for restapi to have Authorization', path);
      // Add the Authorization header as a parameter to requests
      resources.restApi.addPropertyOverride(
        `Body.paths.${path}.x-amazon-apigateway-any-method.parameters`,
        [
          ...resources.restApi.body.paths[path]["x-amazon-apigateway-any-method"]
            .parameters,
          {
            name: "Authorization",
            in: "header",
            required: false,
            type: "string",
          },
        ]
      );
      // Use your new Cognito User Pool authorizer for security
      resources.restApi.addPropertyOverride(
        `Body.paths.${path}.x-amazon-apigateway-any-method.security`,
        [ { Cognito: [], }, ]
      );
    }
}

Step3: Update config applying the above CloudFormation

amplify push

orome commented 1 year ago

Again, it is blowing my mind how often I have to fix this stuff myself in vs having the Amplify CLI do what it is supposed to do. Does the Amplify product team not have any QA engineers?

I have the same experience and the same frustrations. I think it has less to do with the product team than what I expect is a disconnect between how executives have positioned Amplify, and what the team is actually resourced and directed to do. Mismanagement in other words.

mburakergenc commented 11 months ago

It has been 3 years since this feature was requested and no updates yet. It is very disappointing to see the Amplify team not taking any action on this.

orome commented 11 months ago

It has been 3 years since this feature was requested and no updates yet. It is very disappointing to see the Amplify team not taking any action on this.

As near as I can tell Amplify is dead. The whole thing is riddled with fatal bugs that eventually crop up and wreck you project. I regret ever using it.

Blackmesa-Canteen commented 5 months ago

Still not fixed.

mikeswain commented 4 months ago

OMG just came to add a simplay lambda with Cognito auth and see the hoops you've got to jump through. What a pile of junk

orome commented 4 months ago

OMG just came to add a simplay lambda with Cognito auth and see the hoops you've got to jump through. What a pile of junk

Nothing in my software experience has wasted more of my time than Amplify. Use Heroku.

bubai2000 commented 4 months ago

I can suggest an easier solution for NodeJS, you can use this library https://github.com/awslabs/aws-jwt-verify in the lambda function itself. Initializer: const verifier = CognitoJwtVerifier.create({ userPoolId: process.env.COGNITO_USER_POOL_ID, tokenUse: "id", clientId: process.env.COGNITO_CLIENT_ID, }); Verifier: try { await verifier.verify(event.headers.Authorization) } catch { return { statusCode: 401, body: "Unaothorized!", }; }

MyNameIsTakenOMG commented 4 months ago

Hi guys, for who are still looking for solutions of adding authorizers, here's the link I found in the amplify doc, which shows how to add cognito user pool authorizer or custom lambda authorizer : https://docs.amplify.aws/javascript/build-a-backend/restapi/override-api-gateway/