Vinícius A dos Santos

Vinícius A dos Santos

About MeEmail Me
LinkedInGitHubEmail

AWS API Gateway + Terraform + Serverless Framework - Part 3

javascript
nodejs
serverless
terraform
lambda
aws
aws api gateway
rest api

Hi, everyone! For this "Hands on!" we're building a REST API with AWS API Gateway, provisioned with Terraform and backed by AWS Lambda built with Serverless Framework.
The REST API will allow us to send SMS Messages using AWS SNS. Sounds like a lot of things, but it's not that lot of work.
For this part 3, we'll secure the API with OAuth using AWS Cognito, and for parts 1 and 2:

Part 1: provisioning an AWS API Gateway with Terraform
Part 2: coding the backend with Serverless Framework

About the tech stack

  • AWS: Most popular Cloud provider. You need an account to follow this article properly;
  • AWS API Gateway: AWS managed API Gateway that will expose our rest endpoints;
  • AWS Lambda: serverless functions on AWS working as our backend;
  • AWS SNS: AWS Simple Notification Service that, among other types of notifications, allows us to send SMS for a phone number;
  • Terraform: IaC (Infrastructure as Code) tool that allows us to provision cloud resources supporting several cloud providers;
  • Serverless Framework: a Framework for support building and deploying serverless functions grouped as a Serverless Service, allowing also the provisioning of resources needed for these functions;
  • NodeJS: JS runtime where our JavaScript lambda functions going to be running;
  • JavaScript: Of course, the programming language we'll write our lambda.

AWS Cognito

Cognito is an AWS resource that provides several patterns of authentication and authorization.
We are going to choose OAuth, in a very basic way, with the only purpose of seeing how to provision it with Terraform a set it to secure our API. For a production purpose, there are other details you should care about.

Setting project

For secure and API through a combination of client and secret keys, we need to provision a Cognito User Pool, set a Domain, Resource Server, and App Client

Back to terraform files, create cognito.tf:

# cognito.tf
resource "aws_cognito_user_pool" "pool" {
  name = "my-api-user-pool"
}

resource "aws_cognito_user_pool_domain" "domain" {
  domain       = "my-api-serverless"
  user_pool_id = aws_cognito_user_pool.pool.id
}

resource "aws_cognito_resource_server" "resource_server" {
  identifier   = "my-api"
  name         = "resource-server-my-api"
  user_pool_id = aws_cognito_user_pool.pool.id
  scope {
    scope_name        = "sms"
    scope_description = "All resources"
  }
}

resource "aws_cognito_user_pool_client" "client" {
  name                                 = "my-api-client"
  user_pool_id                         = aws_cognito_user_pool.pool.id
  generate_secret                      = true
  explicit_auth_flows                  = ["ALLOW_CUSTOM_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"]
  allowed_oauth_flows_user_pool_client = true
  allowed_oauth_flows                  = ["client_credentials"]
  allowed_oauth_scopes                 = ["my-api/sms"]
  supported_identity_providers         = ["COGNITO"]
  depends_on                           = [aws_cognito_resource_server.resource_server]
}
  • aws_cognito_user_pool: creates a pool;
  • aws_cognito_user_pool_domain: defines the URL that the client application calls to authenticate. The URL has the form https://<domain>.auth.<aws region>.amazoncognito.com/oauth2/token. Obviously, the domain should be unique between all Amazon Cognito domains in this region;
  • aws_cognito_resource_server: defines a server that protects the resources, allowing only requests with valid tokens to access them. The scope is the level of access. For example, you can create a scope of reading and other for writing to resources;
  • aws_cognito_user_pool_client: here we configure a client for the pool, specifying allowed OAuth flows and scopes. Note the depends_on telling Terraform explicitly that it depends on another resource (since resource_server is not referenced in any attribute in this block, Terraform doesn't know that).
    We also have to set an authorizer for the API provisioned in Part 1. Add this in api-gateway.tf file:
resource "aws_api_gateway_authorizer" "authorizer" {
  name          = "cognito"
  rest_api_id   = aws_api_gateway_rest_api.api_gateway.id
  type          = "COGNITO_USER_POOLS"
  provider_arns = [aws_cognito_user_pool.pool.arn]
}

With this, we going to have an authorizer associated with our API which can be set as the authorizer of any endpoint of that. We will reference the ID of the authorizer in the HTTP event of the serverless function later: $ terraform apply

On the Authorizers on AWS Console's Amazon API Gateway, we should see the authorizer created. We need its ID:

Back to the Serverless Framework project, in the functions attribute of serverless.yml, we set the authorizer like this:

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          method: POST
          path: /sms
          authorizer:
            type: COGNITO_USER_POOLS
            authorizerId: 2n2p57
            scopes:
              - my-api/sms

Note the scope attribute, the same as allowed_oauth_scopes on aws_cognito_user_pool_client in cognito.tf.

Run $ sls deploy and let's test our API with Postman:

Oops! We got 401 Unauthorized. Let's get a token now.
On AWS Console, go to Cognito -> Manage User Pools -> my-api-user-pool. On the left panel, click on App Clients and look for "client id" and "client secret". Convert "<client id>:<client secret>" to base64 and use it as a Basic header Authorizer.
The curl of the request should be like this:

curl --location --request POST 'https://my-api-serverless.auth.us-east-1.amazoncognito.com/oauth2/token' \
--header 'Authorization: Basic slkfjdsalfsdkfjhskjfhalkfnasjkdnsakjdnaskfnakfjsndkfjsndkfjsdnfkjd==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials'

The response should be a JSON with an access token:

{
  "access_token": "eyJraWQiOiJFZHpCcFo1YWZ6NXVcLzBuZ3JBRUh2WDZWTVE2V0k2Z3JKMUtxclNMRTNHVT0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2YXYzZ245bWk5YmFjc2loNG1jaG1qcTE3bSIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoibWluaGEtYXBpXC9zbXMiLCJhdXRoX3RpbWUiOjE1ODkyMDU5NDUsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy1lYXN0LTEuYW1hem9uYXdzLmNvbVwvdXMtZWFzdC0xX2ZEa2dXdW9GUSIsImV4cCI6MTU4OTIwOTU0NSwiaWF0IjoxNTg5MjA1OTQ1LCJ2ZXJzaW9uIjoyLCJqdGkiOiI2NzRiZmM2ZS1iZWU2LTQ5MjUtYTUwNy1iODk4MDEwNDY3ODIiLCJjbGllbnRfaWQiOiI2YXYzZ245bWk5YmFjc2loNG1jaG1qcTE3bSJ9.nnmaGMapSCRtY4b4bHZac8_AD-UeM-MRQcf6Ug02kCHWurfZH_SuNtyr8hqXME-23wUOKj8PQdwIzL0EnBcUpjih6XzAG-AEKzCxwJCS2CPaNVkIX7ScMBhIf_J7OFrPNCXCu_hFifLMD-LQ_9E_5fRhxLitKOkesQSwFvsJKB7uwVfDZftwK-lHYBfTNDL6F_F8aF1cc2xMqAxv1xBLndO1pTCySDBMXR7NGaNQGSU8OrrSs2rLbAb5Vd95zgs_XA-FGQoFd1btYQCZgcVmQs_hpKv6bWsFoU8aKDwpDmN-Vi7A1pVpN3fBHqPhy61ms6IDxTgxFNai7Ujtvv2qJA",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Setting the access token on the Authorization header of the request for the SMS API, it works fine again:


That's it! We finished our API provisioned on AWS with Terraform, backed by AWS Lambda built with Serverless Framework, and secured with Amazon Cognito.

Full code here viniciusvasti

  • Some details are different because I implemented this in Portuguese before. So "my api" is "minha api" in the images, sorry for that.
← Back to home