DEV Community

Cover image for Securing API Gateway with AWS Cognito Authentication using OAuth 2.0 and Custom Domains
4

Securing API Gateway with AWS Cognito Authentication using OAuth 2.0 and Custom Domains

Introduction

In our previous post, we explored securing API Gateway using a Lambda Authorizer with JWT tokens via Terraform.

Lambda Authorizer with JWT Token provides fine-grained access control by enabling custom logic, supports multiple identity providers, and allows additional validation before granting access. However, it introduces increased request latency due to execution time, requires additional infrastructure and maintenance, and can lead to higher AWS costs from Lambda invocation fees.

In this post, we will implement the same API Gateway but with Cognito authentication using OAuth 2.0. Additionally, we will set up custom authentication and API domains using Route 53 and ACM. The implementation will be done using Terraform.

What is an AWS Cognito User Pool?

AWS Cognito User Pool is a user directory service that enables authentication and authorization using industry-standard protocols such as OAuth 2.0, OpenID Connect (OIDC), and SAML.

Using a Cognito User Pool for OAuth token authentication allows API Gateway to validate access tokens without the need for a custom Lambda Authorizer, reducing complexity and improving performance.

What is OAuth 2.0 and Its Grant Types?

OAuth 2.0 is an authorization framework that allows applications to securely access user resources without exposing credentials.

OAuth 2.0 Grant Types:

  1. Authorization Code Grant: Used for server-side applications where tokens are retrieved via a separate authorization server.
  2. Implicit Grant: Suitable for single-page applications (SPA) where tokens are obtained directly from the authorization server.
  3. Client Credentials Grant: Used for machine-to-machine authentication where no user interaction is required.
  4. Resource Owner Password Grant: Deprecated but allows users to provide credentials directly to the application.

We will implement Client Credentials grant type of OAuth authorization.

Architecture

Follwing is the serverless architecture we will be dealing with.

Architecture

Step 1: Create API Gateway, DynamoDB Table, and Lambda Function

Refer to our previous post for setting up API Gateway, DynamoDB, and Lambda using Terraform.

Step 2: Create ACM Certificate and Validate Against Custom Domains

To enable HTTPS for API Gateway and Cognito authentication endpoints, we need an SSL certificate from AWS ACM.

Create ACM Certificate: Request a new certificate for auth.chinmayto.com and api.chinmayto.com in ACM.

################################################################################
# Create an ACM certificate for the domain
################################################################################
resource "aws_acm_certificate" "my_api_cert" {
  domain_name               = var.domain_name
  subject_alternative_names = ["auth.${var.domain_name}", "api.${var.domain_name}"]
  validation_method         = "DNS"
}
Enter fullscreen mode Exit fullscreen mode

Add Cert Validation CNAME Records in Route 53: AWS provides a CNAME record that must be added to Route 53 for validation.

################################################################################
# Create a certificate validation record for the domain
################################################################################
resource "aws_route53_record" "cert_validation_record" {
  for_each = {
    for dvo in aws_acm_certificate.my_api_cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.my_domain.zone_id
}
Enter fullscreen mode Exit fullscreen mode

DNS Validation of ACM Certificate: AWS will validate the domain automatically once the correct CNAME records are added.

################################################################################
# Validate the certificate
################################################################################
resource "aws_acm_certificate_validation" "cert_validation" {
  timeouts {
    create = "5m"
  }
  certificate_arn         = aws_acm_certificate.my_api_cert.arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation_record : record.fqdn]
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Domain Name for API Gateway Endpoint

Next, we configure API Gateway with a custom domain api.chinmayto.com.

Create API Gateway Domain Name: Define a custom domain api.chinmayto.com for API Gateway.

################################################################################
# Create a domain name for the API Gateway endpoint
################################################################################
resource "aws_api_gateway_domain_name" "custom_domain" {

  depends_on = [aws_acm_certificate_validation.cert_validation]

  domain_name              = "api.${var.domain_name}"
  regional_certificate_arn = aws_acm_certificate.my_api_cert.arn

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Create API Gateway Base Path Mapping: Associate the custom domain with API Gateway stages and paths.

################################################################################
# Create a base path mapping for the domain
################################################################################
resource "aws_api_gateway_base_path_mapping" "custom_domain_mapping" {
  domain_name = aws_api_gateway_domain_name.custom_domain.domain_name
  api_id      = aws_api_gateway_rest_api.API-gateway.id
  stage_name  = aws_api_gateway_stage.my-prod-stage.stage_name
}
Enter fullscreen mode Exit fullscreen mode

Create CNAME Record in Route 53: Map api.chinmayto.com to the API Gateway distribution.

################################################################################
# Create a CNAME record for the API Gateway endpoint
################################################################################
resource "aws_route53_record" "custom_domain_record" {
  name = "api" # The subdomain (api.chinmayto.com)
  type = "CNAME"
  ttl  = "300" # TTL in seconds

  records = ["${aws_api_gateway_rest_api.API-gateway.id}.execute-api.us-east-1.amazonaws.com"]

  zone_id = data.aws_route53_zone.my_domain.zone_id
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Create API Gateway authorizer as Cognito User Pools

We next create API gateway authorizer and use COGNITO_USER_POOLS. (Cognito pool created in furher steps!)

################################################################################
# Cognito Authorizer
################################################################################
resource "aws_api_gateway_authorizer" "my_authorizer" {
  name                             = "my_authorizer"
  rest_api_id                      = aws_api_gateway_rest_api.API-gateway.id
  type                             = "COGNITO_USER_POOLS"
  provider_arns                    = [aws_cognito_user_pool.my_cognito_user_pool.arn]
  authorizer_result_ttl_in_seconds = 0
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Update API Gateway Methods to Use Cognito Authentication

Modify API Gateway methods to enforce authentication via Cognito User Pools.

Update Authorization to COGNITO_USER_POOLS and update Authorization Scope as myapi/all. Example API gateway method definition below:

################################################################################
## GET /book/{bookId}
################################################################################

resource "aws_api_gateway_method" "GET_one_method" {
  rest_api_id          = aws_api_gateway_rest_api.API-gateway.id
  resource_id          = aws_api_gateway_resource.API-resource-book.id
  http_method          = "GET"
  authorization        = "COGNITO_USER_POOLS"
  authorizer_id        = aws_api_gateway_authorizer.my_authorizer.id
  authorization_scopes = ["${var.authorization_scopes}"]
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Create Cognito User Pool

Set up AWS Cognito for user authentication and token generation.

Create Cognito User Pool: Define a user pool

################################################################################
# AWS Cognito User Pool
################################################################################
resource "aws_cognito_user_pool" "my_cognito_user_pool" {
  name = "my_cognito_user_pool"
}
Enter fullscreen mode Exit fullscreen mode

Create User Pool Domain: Configure Cognito to use auth.chinmayto.com as the authentication domain.

################################################################################
# Create a domain for the user pool
################################################################################
resource "aws_cognito_user_pool_domain" "user_pool_domain" {
  domain          = "auth.${var.domain_name}"
  certificate_arn = aws_acm_certificate.my_api_cert.arn
  user_pool_id    = aws_cognito_user_pool.my_cognito_user_pool.id

  depends_on = [aws_acm_certificate_validation.cert_validation]
}
Enter fullscreen mode Exit fullscreen mode

Create User Pool Client for Client Credentials Grant: Register an OAuth 2.0 client with the user pool.

################################################################################
# Create a user pool client
################################################################################
resource "aws_cognito_user_pool_client" "user_pool_client" {
  name                                 = "cognito_client"
  user_pool_id                         = aws_cognito_user_pool.my_cognito_user_pool.id
  generate_secret                      = true
  allowed_oauth_flows                  = ["client_credentials"]
  supported_identity_providers         = ["COGNITO"]
  allowed_oauth_flows_user_pool_client = true
  allowed_oauth_scopes                 = [aws_cognito_resource_server.resource_server.scope_identifiers[0]]

  depends_on = [aws_cognito_user_pool.my_cognito_user_pool, aws_cognito_resource_server.resource_server]
}
Enter fullscreen mode Exit fullscreen mode

Create Cognito Resource Server: Define a resource server with scopes to match API Gateway requirements.

################################################################################
# Create a resource server
################################################################################
resource "aws_cognito_resource_server" "resource_server" {
  name         = "cognito_resource_server"
  identifier   = "myapi"
  user_pool_id = aws_cognito_user_pool.my_cognito_user_pool.id

  scope {
    scope_name        = "all"
    scope_description = "Get access to all API Gateway endpoints."
  }
}
Enter fullscreen mode Exit fullscreen mode

Update Route 53 Records: Add A records for the custom authentication domain.

################################################################################
# Create a record for the domain name
# Required for Cognito Custom Domain validation
################################################################################
resource "aws_route53_record" "root_record" {
  name    = var.domain_name
  type    = "A"
  zone_id = data.aws_route53_zone.my_domain.id

  records         = ["8.8.8.8"] #This can be any dummy IP address
  allow_overwrite = true
}

resource "aws_route53_record" "auth-cognito-A" {
  name    = "auth.${var.domain_name}"
  type    = "A"
  zone_id = data.aws_route53_zone.my_domain.zone_id
  alias {
    evaluate_target_health = false

    name    = aws_cognito_user_pool_domain.user_pool_domain.cloudfront_distribution_arn
    zone_id = "Z2FDTNDATAQYW2" # CloudFront Zone ID
  }
}
Enter fullscreen mode Exit fullscreen mode

Steps to Run Terraform

Follow these steps to execute the Terraform configuration:

terraform init
terraform plan 
terraform apply -auto-approve
Enter fullscreen mode Exit fullscreen mode

Upon successful completion, Terraform will provide relevant outputs.

Apply complete! Resources: 56 added, 0 changed, 0 destroyed.
Enter fullscreen mode Exit fullscreen mode

Testing

API Authoriser as Cognito User Pool:

Cognito Authorizer for API Gateway

API Gateway Custom Domain with stage mapping:

API Gateway Custom Domain

Cognito User Pool:

Cognito User Pool

Cogntio Custom Auth Domain with resource servers having same scope defined:

Cogntio Custom Auth Domain

Cognito App Client having defined authentication flows:

Cognito App Client

Cognito App Client login pages with OAuth 2.0 client_credentials grant:

Cognito OAuth2 Grant

Route53 CNAME and A records (required for DNS validation, API endpoint custom domain and Cognito auth custom domain):

Route53 CNAME records

Route53 A records

Get token using client_id and client_secret from Cognito Auth Endpoint on postman:

Get token using Postman

Get Token Success

Use the token to call API Gateway using custom domain endpoint:

Token use to call API

Token encoded in jwt.io

Token Details

Cleanup

Remember to stop AWS components to avoid large bills.

terraform destroy -auto-approve
Enter fullscreen mode Exit fullscreen mode

Conclusion

Using AWS Cognito with API Gateway provides a scalable and managed way to secure APIs with OAuth 2.0 authentication. Unlike Lambda Authorizers, Cognito improves performance, reduces cost, and integrates seamlessly with other AWS services. By implementing custom domains, we enhance security and branding for authentication and API endpoints.

References

  1. GitHub Repo: https://github.com/chinmayto/terraform-aws-api-gateway-cognito-authorizer
  2. AWS Blog: https://aws.amazon.com/blogs/security/how-to-use-oauth-2-0-in-amazon-cognito-learn-about-the-different-oauth-2-0-grants/
  3. OAuth 2.0 Grants: https://docs.aws.amazon.com/cognito/latest/developerguide/federation-endpoints-oauth-grants.html
  4. YouTube blog by Ravi: https://www.youtube.com/@raaviblog105

Image of Quadratic

Python + AI + Spreadsheet

Chat with your data and get insights in seconds with the all-in-one spreadsheet that connects to your data, supports code natively, and has built-in AI.

Try Quadratic free

Top comments (0)

Best Practices for Running  Container WordPress on AWS (ECS, EFS, RDS, ELB) using CDK cover image

Best Practices for Running Container WordPress on AWS (ECS, EFS, RDS, ELB) using CDK

This post discusses the process of migrating a growing WordPress eShop business to AWS using AWS CDK for an easily scalable, high availability architecture. The detailed structure encompasses several pillars: Compute, Storage, Database, Cache, CDN, DNS, Security, and Backup.

Read full post

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay