DevOps Automation: Secure API Gateway with Cognito and a Custom Domain in Route53 Using Terraform

Joel Wembo
Towards AWS
Published in
14 min readApr 8, 2024

--

As an alternative to using IAM roles and policies or Lambda authorizers (formerly known as custom authorizers), you can use an Amazon Cognito user pool to control who can access your API in Amazon API Gateway.

AWS Cognito Architecture

Abstract

The web application client-server pattern is widely adopted. The access control allows only authorized clients to access the backend server resources by authenticating the client and providing granular-level access based on who the client is.

This Article is about how to implement devops automation to to set up a secure API Gateway with Cognito for authentication and a custom domain using Route 53 with SSL Certificate with Terraform for infrastructure as code , Terraform Cloud for state management and Github Actions for CI/CD pipelines automation.

API Gateway can support both REST and HTTP API. API Gateway has integration with Amazon Cognito, whereas it can also have control access to HTTP APIs with a JSON Web Token (JWT) authorizer, which interacts with Amazon Cognito. The lambda function can be integrated with API Gateway. The client is responsible for authenticating with Amazon Cognito to obtain the access token.

  1. The client starts authentication with Amazon Cognito to obtain the access token.
  2. The client sends REST API or HTTP API request with a header that contains the access token.
  3. The API Gateway is configured to have:
  • Amazon Cognito user pool as the authorizer to validate the access token in REST API request, or
  • A JWT authorizer, which interacts with the Amazon Cognito user pool to validate the access token in HTTP API request.

Table of Contents

· Abstract
·
Table of Contents
· Prerequisites
· What is Cognito ?
· Why Github Actions ?
· 1. Create AWS Access Keys
·
2. Terraform Cloud Configuration
·
3. CI/CD Workflows Setup with Github Actions
·
4. backend.tf
·
5. acm_certificate.tf
·
6. provider.tf
·
7. lambda function and path
·
8. Cognito
·
9. Set Up Custom Domain
·
10. Create Route53 Hosted Zone
·
11. Validate and Test your solution
·
12. Check result in app.terraform.io
·
13. Check result in your AWS Management Console
· Summary
· References

Prerequisites

Before we get into the good stuffs, first we need to make sure we have the required services on our local machine or dev server, which are:

  1. Basic knowledge of Terraform.
  2. AWS Account
  3. Github Account
  4. AWS CLI installed and configured.
  5. Docker installed locally.
  6. Typescript installed
  7. NPM
  8. NodeJS
  9. Terraform
  10. A Domain name Hosted from any domain name provider ( Ex: AWS Route 53 )
  11. Any Browser for testing

What is Cognito ?

Amazon Cognito is an identity platform for web and mobile apps. It’s a user directory, an authentication server, and an authorization service for OAuth 2.0 access tokens and AWS credentials.

It offers several benefits for developers and organizations looking to add authentication, authorization, and user management features to their applications:

  1. User Authentication: Cognito provides built-in support for various authentication methods, including username/password, social identity providers (such as Facebook, Google, and Amazon), and SAML-based identity providers. This allows developers to easily implement user authentication without having to build and maintain their own authentication systems.
  2. Scalability: Cognito is a fully managed service, meaning AWS handles the underlying infrastructure and scaling requirements. It can scale to support millions of users and handle sudden spikes in authentication requests seamlessly.
  3. Security: Cognito integrates with AWS Identity and Access Management (IAM) to control access to AWS resources. It also supports multi-factor authentication (MFA) and adaptive authentication, providing additional layers of security to protect user accounts.
  4. User Management: With Cognito, developers can easily manage user accounts, including user registration, account verification, password reset, and account deletion. It also supports user profile management, allowing users to update their profile information within the application.
  5. Customizable Workflows: Cognito allows developers to customize authentication and authorization workflows to fit their specific use cases. This includes customizing email and SMS templates, defining custom authentication challenges, and integrating with external identity providers.
  6. Integration with AWS Services: Cognito integrates seamlessly with other AWS services, such as API Gateway, Lambda, S3, and DynamoDB, allowing developers to build secure and scalable serverless applications. It also supports seamless integration with Amazon CloudFront for content delivery and Amazon Pinpoint for user engagement and analytics.
  7. Mobile and Web Support: Cognito provides SDKs and libraries for popular programming languages and platforms, including iOS, Android, JavaScript, and React Native, making it easy to integrate authentication and user management features into mobile and web applications.
  8. Compliance and Privacy: Cognito is designed to comply with various industry standards and regulations, including GDPR, HIPAA, and SOC. It provides features such as data encryption, data residency controls, and audit logs to help organizations meet their compliance requirements.

Overall, AWS Cognito simplifies the process of adding authentication and user management features to applications, allowing developers to focus on building core functionality while leveraging AWS’s secure and scalable infrastructure.

Why Github Actions ?

GitHub Actions is a feature provided by GitHub that allows you to automate various tasks within your software development workflows directly from your GitHub repository. It enables you to build, test, and deploy your code directly within GitHub’s ecosystem. Some benefits of using GitHub Actions are native integration with GitHub, flexibility, large ecosystem of Actions, Custom Actions, Workflow Visualization and much more.

1. Create AWS Access Keys

AWS access keys are credentials used to access Amazon Web Services (AWS) programmatically. They consist of an access key ID and a secret access key. These keys are used to authenticate requests made to AWS services via APIs, SDKs, command-line tools, and other means.

Steps to Create Access Keys

  1. Go to the AWS management console, click on your Profile name, and then click on My Security Credentials. …
  2. Go to Access Keys and select Create New Access Key. …
  3. Click on Show Access Key and save/download the access key and secret access key.

2. Terraform Cloud Configuration

HashiCorp provides GitHub Actions that integrate with the Terraform Cloud API. These actions let you create your own custom CI/CD workflows to meet the needs of your organization.

Step 1: Create your project and workplace in terraform cloud

Create project in terraform Cloud

Step 2: Define Variables set to allow terraform cloud for state management

Step 3 : Change the default execution Mode to remote

Step 4 : Create API tokens for Github actions to interact with Terraform Cloud

Terraform cloud API tokens
API Token for your project
Generated Token
Organization token ( Optional )

3. CI/CD Workflows Setup with Github Actions

GitHub Actions add continuous integration to GitHub repositories to automate your software builds, tests, and deployments. Automating Terraform with CI/CD enforces configuration best practices, promotes collaboration, and automates the Terraform workflow.

Next, setup aws environment in Github for github actions to access aws resources for terraform.

To Setup a CI/CD workflows Using Github Actions:

  1. On GitHub.com, navigate to the main page of the repository.
  2. Under your repository name, click Settings. …
  3. In the “Security” section of the sidebar, select Secrets and variables, then click Actions.
  4. Click the Secrets tab.
  5. Click New repository secret.
name: "Terraform Pipeline AWS API Gateway , Cognito and Route53"

on:
push:
branches: [qa]
permissions:
contents: write
env:
TF_LOG: INFO
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
TF_CLOUD_ORGANIZATION: "prodxcloud"
TF_API_TOKEN: ${{ secrets.TF_API_TOKEN}}
TF_WORKSPACE: "prodxcloud"
CONFIG_DIRECTORY: "./terraform/aws/terraform-aws-api-gateway-cognito-route53/"

jobs:
terraform:
name: "Auth & API Using Cognito and Custom Domain"
runs-on: ubuntu-latest
environment: production
defaults:
run:
shell: bash
# We keep Terraform files in the terraform directory.
working-directory: terraform/aws/terraform-aws-api-gateway-cognito-route53

steps:
- name: Checkout the repository to the runner
uses: actions/checkout@v2

- name: Setup Terraform with specified version on the runner
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.3.0

- name: Terraform init
id: init
run: terraform init -reconfigure -lock=false

- name: Terraform format
id: fmt
run: terraform fmt

- name: Terraform validate
id: validate
run: terraform validate

- name: Terraform plan
id: plan
# if: github.event_name == 'pull_request'
run: terraform plan -no-color -input=false
continue-on-error: true

- uses: actions/github-script@v6
# if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

<details><summary>Show Plan</summary>

\`\`\`\n
${process.env.PLAN}
\`\`\`

</details>
*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})

- name: Terraform Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1

- name: Terraform Apply
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false -lock=false

# - name: Terraform Apply
# # if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# run: terraform destroy -auto-approve -input=false -lock=false

4. backend.tf

Remote state is simply storing that state file remotely, rather than on your local filesystem. With a single state file stored remotely

Terraform Cloud provides a number of remote network services for use with Terraform, and Terraform Enterprise allows hosting those services inside your own infrastructure. For example, these systems offer both remote operations and a private module registry.

When interacting with Terraform-specific network services, Terraform expects to find API tokens in CLI configuration files in credentials blocks:

In your terraform solution directory create a file .terraformrc and update the token with your TF_API_TOKEN that we created earlier.

credentials "app.terraform.io" {
token = "xxxxxx.atlasv1.zzzzzzzzzzzzz"
}

backend.tf

terraform {
backend "remote" {
hostname="app.terraform.io"
token = ""
organization = "prodxcloud"
workspaces {
prefix = "prodxcloud-"
}
}
}
Workspace Configuration in terraform

5. acm_certificate.tf

An ACM (AWS Certificate Manager) certificate is a service provided by Amazon Web Services (AWS) that allows you to provision, manage, and deploy SSL/TLS certificates for use with AWS services and your internal resources. SSL/TLS certificates are used to encrypt traffic between clients and servers, ensuring secure communication over the internet.

module "aws_acm_certificate" {
source = "terraform-aws-modules/acm/aws"
version = "~> 4.0"
domain_name = "socialcloudsync.com"
zone_id = ""
wait_for_validation = false
}


resource "aws_acm_certificate" "cert" {
domain_name = "socialcloudsync.com"
validation_method = "EMAIL"

tags = {
Environment = "test"
}

lifecycle {
create_before_destroy = true
}
}

6. provider.tf

A Terraform provider is a plugin responsible for understanding API interactions with a particular infrastructure service. Providers can manage resources, execute operations, and handle authentication and communication with the underlying infrastructure.

provider "aws" {
region = "us-east-1"
}

7. lambda function and path

A Lambda function, often referred to simply as “Lambda,” is a serverless compute service provided by Amazon Web Services (AWS). It allows you to run code without provisioning or managing servers. You can write your code in languages such as Python, Node.js, Java, Go, or Ruby, and Lambda automatically scales and manages the infrastructure required to run your code in response to triggers such as HTTP requests, database events, file uploads, or custom events.

Simple lambda function ( as lambda.py ) using python :

import stripe
import boto3
import jsonclient = boto3.client('secretsmanager')
keys = json.loads(client.get_secret_value(
SecretId = '',
)['SecretString'])
public_key = keys['stripe-public']
secret_key = keys['stripe-secret']
stripe.api_key = secret_keydef signup(event, context):
card_data = event.get('cardData')
email = event.get('email')
attributes = event.get('attributes')
create_stripe_customer(email, attributes, card_data)
return eventdef create_stripe_customer(email, user_data, payment_info):
customer_id = stripe.Customer.create(email = email, metadata = user_data)['id']
payment_method_id = create_payment_method(payment_info)
stripe.PaymentMethod.attach(
payment_method_id,
customer = customer_id
)
return {
"customer_id": customer_id,
"plan": create_stripe_plan(customer_id)
}def create_payment_method(payment_info):
return stripe.PaymentMethod.create(
type = "card",
card = {
"number": payment_info.get('cardNumber'),
"exp_month": payment_info.get('expirationMonth'),
"exp_year": payment_info.get('expirationYear'),
"cvc": payment_info.get('ccv'),
}).get('id')def create_stripe_plan(customer_id):
return stripe.Subscription.create(
customer = customer_id,
items = [{
"plan": "plan_idxxxxxxx"
}]
).get("id")
resource "aws_lambda_permission" "apigw_lambda" {
action = "lambda:InvokeFunction"
function_name = "${aws_lambda_function.lambda.function_name}"
principal = "apigateway.amazonaws.com"
source_arn = "arn:aws:execute-api:us-east-1:604020082473:${aws_api_gateway_rest_api.api.id}/*/${aws_api_gateway_method.random_api.http_method}${aws_api_gateway_resource.random_api.path}"
statement_id = "AllowExecutionFromAPIGateway"
}

resource "aws_lambda_function" "lambda" {
filename = "${path.module}/lambda.zip"
function_name = "random_api"
role = "${aws_iam_role.role_lambda.arn}"
handler = "lambda.lambda_handler"
runtime = "python3.9"
source_code_hash = "${filebase64sha256("${path.module}/lambda.zip")}"
}

resource "aws_iam_role" "role_lambda" {
name = "LambdaRole"

assume_role_policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
POLICY
}

Lambda policies are typically defined as IAM policies, which are JSON documents that specify a set of permissions. These policies can be attached to IAM roles, which are then associated with Lambda functions. Lambda functions assume the permissions of the IAM role they are configured to use, allowing them to perform actions according to the permissions granted by the attached policies.

Lambda policies should follow the principle of least privilege, granting only the permissions necessary for the Lambda function to perform its intended tasks. This helps improve security by minimizing the potential impact of security breaches or misconfigurations.

8. Cognito

Amazon Cognito identity pools provide temporary AWS credentials for users who are guests (unauthenticated) and for users who have been authenticated and received a token. An identity pool is a store of user identity data specific to your account.

Amazon Cognito identity pools support both authenticated and unauthenticated identities. Authenticated identities belong to users who are authenticated by any supported identity provider. Unauthenticated identities typically belong to guest users.

resource "aws_cognito_user_pool" "user_pool" {
name = "livestore"
}

resource "aws_cognito_resource_server" "resource_server" {
name = "livestore"
identifier = "https://api.socialcloudsync.com"
user_pool_id = "${aws_cognito_user_pool.user_pool.id}"

scope {
scope_name = "all"
scope_description = "Get access to all API Gateway endpoints."
}
}

resource "aws_cognito_user_pool_domain" "domain" {
domain = "api.socialcloudsync.com"
certificate_arn = aws_acm_certificate.cert.arn
user_pool_id = "${aws_cognito_user_pool.user_pool.id}"
}

resource "aws_cognito_user_pool_client" "client" {
name = "livestore"
user_pool_id = "${aws_cognito_user_pool.user_pool.id}"
generate_secret = true
allowed_oauth_flows = ["code", "implicit"]
supported_identity_providers = ["COGNITO"]
callback_urls = ["https://socialcloudsync.com"]
allowed_oauth_flows_user_pool_client = true
# allowed_oauth_flows = ["code", "implicit"]
allowed_oauth_scopes = ["phone", "email", "openid", "profile"]

depends_on = [
aws_cognito_user_pool.user_pool,
aws_cognito_resource_server.resource_server,
]
}

9. Set Up Custom Domain

To configure a custom domain with CloudFront, you need to create a CloudFront SSL certificate in AWS Certificate Manager (ACM) and associate it with your CloudFront distribution. Then, you can configure Route 53 or your DNS provider to point your custom domain to the CloudFront distribution.

To set up a custom domain using Route 53 with your CloudFront distribution, you’ll need to follow these steps:

Register a Domain: If you haven’t already, register a domain name through Route 53 or another registrar.

Create a Hosted Zone in Route 53: This is where you’ll manage DNS records for your domain.

Create an Alias Record: Alias records are used to map your domain to your CloudFront distribution.

How to register a domain name in Route 53
Route 53 Domain pricing and validation

10. Create Route53 Hosted Zone

A Hosted Zone, in the context of Amazon Web Services (AWS) Route 53, is a container for records that define how you want to route traffic for a specific domain, such as example.com or subdomain.example.com. Route 53 is a scalable Domain Name System (DNS) web service designed to provide reliable and cost-effective domain name resolution.

Hosted Zone creation

11. Validate and Test your solution

Project folder structure

Run Terraform

To init Terraform:

terraform init
terraform fmt
terraform plan

Create the Resources:

terraform apply -auto-approve
Terraform plan
Terraform Operation Progress

12. Check result in app.terraform.io

Github Action
Github Action Progress
Terraform init
Terraform Cloud View

If you make a new changes, Remember to run terraform init, terraform plan, and terraform apply commands in your CI/CD pipeline to apply changes or simply push your changes to your repo !

13. Check result in your AWS Management Console

ACM Approval
ACM Approval
Route53 Records managed by terraform for your custom domain A Type
Records Created Using terraform
Created Cognito User Tool via terraform
Cognito Custom Server identifier
Secure AWS API Gateway

Summary

This is a basic outline of how to setup a CI/CD Using Terraform and Github Actions to deploy a Secure API Gateway with Cognito and Custom Domain in Route53 , and you may need to customize it based on your specific requirements and infrastructure setup. Additionally, ensure that you’re following security best practices and optimizing configurations for performance and cost efficiency.

“Simply put, things always had to be in a production-ready state: if you wrote it, you darn well had to be there to get it running!” — Mike Miller

You can also find the codes on Github here.

Thank you for Reading !! 🙌🏻, see you in the next article.🤘

Fore more information about the author ( Joel O. Wembo ) visit https://www.linkedin.com/in/joelotepawembo/

References

--

--

I am a Cloud Solutions Architect, I provide IT solutions using AWS, AWS CDK, Kubernetes, and Terraform. https://www.linkedin.com/in/joelotepawembo