Files
terraform-foundation/terraform/05-workloads/_template/cognito-auth/main.tf
Greg Hendrickson 6136cde9bb feat: Terraform Foundation - AWS Landing Zone
Enterprise-grade multi-tenant AWS cloud foundation.

Modules:
- GitHub OIDC for keyless CI/CD authentication
- IAM account settings and security baseline
- AWS Config Rules for compliance
- ABAC (Attribute-Based Access Control)
- SCPs (Service Control Policies)

Features:
- Multi-account architecture
- Cost optimization patterns
- Security best practices
- Comprehensive documentation

Tech: Terraform, AWS Organizations, IAM Identity Center
2026-02-02 02:57:23 +00:00

502 lines
16 KiB
HCL

################################################################################
# Workload: Cognito User Pool
#
# User authentication infrastructure:
# - User Pool with customizable password policy
# - App clients (web, mobile, machine-to-machine)
# - Identity Pool for AWS credential federation
# - Social/SAML/OIDC identity providers
# - Custom domain
# - Lambda triggers (pre/post auth, migration)
#
# Use cases: Web/mobile auth, B2C apps, admin portals
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-auth/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
pool_name = "${local.tenant}-${local.name}-${local.env}"
# Email configuration
email_sending_account = "COGNITO_DEFAULT" # COGNITO_DEFAULT or DEVELOPER
ses_email_from = null # Required if DEVELOPER
# Password policy
password_minimum_length = 12
password_require_lowercase = true
password_require_numbers = true
password_require_symbols = true
password_require_uppercase = true
temporary_password_validity_days = 7
# MFA
mfa_configuration = "OPTIONAL" # OFF, ON, OPTIONAL
mfa_methods = ["SOFTWARE_TOKEN_MFA"] # SOFTWARE_TOKEN_MFA, SMS_MFA
# Account recovery
recovery_mechanisms = [
{ name = "verified_email", priority = 1 },
{ name = "verified_phone_number", priority = 2 }
]
# User attributes
auto_verified_attributes = ["email"]
username_attributes = ["email"] # email, phone_number
alias_attributes = [] # email, phone_number, preferred_username
# Custom attributes
custom_attributes = {
# "tenant_id" = {
# type = "String"
# mutable = false
# min_length = 1
# max_length = 50
# }
}
# App clients
app_clients = {
web = {
generate_secret = false
explicit_auth_flows = ["ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"]
supported_identity_providers = ["COGNITO"]
callback_urls = ["https://example.com/callback"]
logout_urls = ["https://example.com/logout"]
allowed_oauth_flows = ["code"]
allowed_oauth_scopes = ["email", "openid", "profile"]
allowed_oauth_flows_user_pool_client = true
access_token_validity = 60 # minutes
id_token_validity = 60
refresh_token_validity = 30 # days
}
mobile = {
generate_secret = false
explicit_auth_flows = ["ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"]
supported_identity_providers = ["COGNITO"]
callback_urls = ["myapp://callback"]
logout_urls = ["myapp://logout"]
allowed_oauth_flows = ["code"]
allowed_oauth_scopes = ["email", "openid", "profile"]
allowed_oauth_flows_user_pool_client = true
access_token_validity = 60
id_token_validity = 60
refresh_token_validity = 30
}
# m2m = {
# generate_secret = true
# explicit_auth_flows = ["ALLOW_ADMIN_USER_PASSWORD_AUTH"]
# supported_identity_providers = ["COGNITO"]
# allowed_oauth_flows = ["client_credentials"]
# allowed_oauth_scopes = ["api/read", "api/write"]
# allowed_oauth_flows_user_pool_client = true
# }
}
# Custom domain (requires ACM cert in us-east-1 for CloudFront)
custom_domain = null # e.g., "auth.example.com"
custom_domain_cert = null # ACM certificate ARN
hosted_zone_id = null
# Identity Pool (for AWS credential federation)
enable_identity_pool = false
# Lambda triggers
lambda_triggers = {
# pre_sign_up = "arn:aws:lambda:..."
# post_confirmation = "arn:aws:lambda:..."
# pre_authentication = "arn:aws:lambda:..."
# post_authentication = "arn:aws:lambda:..."
# pre_token_generation = "arn:aws:lambda:..."
# user_migration = "arn:aws:lambda:..."
# custom_message = "arn:aws:lambda:..."
}
# Social identity providers
social_providers = {
# google = {
# client_id = "..."
# client_secret = "..."
# scopes = ["email", "profile", "openid"]
# }
# facebook = {
# client_id = "..."
# client_secret = "..."
# scopes = ["email", "public_profile"]
# }
}
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# Cognito User Pool
################################################################################
resource "aws_cognito_user_pool" "main" {
name = local.pool_name
# Username configuration
username_attributes = local.username_attributes
alias_attributes = length(local.alias_attributes) > 0 ? local.alias_attributes : null
auto_verified_attributes = local.auto_verified_attributes
# Password policy
password_policy {
minimum_length = local.password_minimum_length
require_lowercase = local.password_require_lowercase
require_numbers = local.password_require_numbers
require_symbols = local.password_require_symbols
require_uppercase = local.password_require_uppercase
temporary_password_validity_days = local.temporary_password_validity_days
}
# MFA
mfa_configuration = local.mfa_configuration
dynamic "software_token_mfa_configuration" {
for_each = contains(local.mfa_methods, "SOFTWARE_TOKEN_MFA") && local.mfa_configuration != "OFF" ? [1] : []
content {
enabled = true
}
}
# Account recovery
account_recovery_setting {
dynamic "recovery_mechanism" {
for_each = local.recovery_mechanisms
content {
name = recovery_mechanism.value.name
priority = recovery_mechanism.value.priority
}
}
}
# Email configuration
email_configuration {
email_sending_account = local.email_sending_account
source_arn = local.email_sending_account == "DEVELOPER" ? local.ses_email_from : null
}
# User attribute verification
user_attribute_update_settings {
attributes_require_verification_before_update = ["email"]
}
# Admin create user config
admin_create_user_config {
allow_admin_create_user_only = false
invite_message_template {
email_subject = "Your ${local.pool_name} account"
email_message = "Your username is {username} and temporary password is {####}"
sms_message = "Your username is {username} and temporary password is {####}"
}
}
# Verification message
verification_message_template {
default_email_option = "CONFIRM_WITH_CODE"
email_subject = "Verify your email for ${local.pool_name}"
email_message = "Your verification code is {####}"
}
# Schema (custom attributes)
dynamic "schema" {
for_each = local.custom_attributes
content {
name = schema.key
attribute_data_type = schema.value.type
mutable = schema.value.mutable
required = false
developer_only_attribute = false
dynamic "string_attribute_constraints" {
for_each = schema.value.type == "String" ? [1] : []
content {
min_length = lookup(schema.value, "min_length", 0)
max_length = lookup(schema.value, "max_length", 2048)
}
}
dynamic "number_attribute_constraints" {
for_each = schema.value.type == "Number" ? [1] : []
content {
min_value = lookup(schema.value, "min_value", null)
max_value = lookup(schema.value, "max_value", null)
}
}
}
}
# Lambda triggers
lambda_config {
pre_sign_up = lookup(local.lambda_triggers, "pre_sign_up", null)
post_confirmation = lookup(local.lambda_triggers, "post_confirmation", null)
pre_authentication = lookup(local.lambda_triggers, "pre_authentication", null)
post_authentication = lookup(local.lambda_triggers, "post_authentication", null)
pre_token_generation = lookup(local.lambda_triggers, "pre_token_generation", null)
user_migration = lookup(local.lambda_triggers, "user_migration", null)
custom_message = lookup(local.lambda_triggers, "custom_message", null)
}
tags = { Name = local.pool_name }
}
################################################################################
# User Pool Domain
################################################################################
resource "aws_cognito_user_pool_domain" "main" {
domain = local.custom_domain != null ? local.custom_domain : local.pool_name
user_pool_id = aws_cognito_user_pool.main.id
certificate_arn = local.custom_domain_cert
}
# Route53 record for custom domain
resource "aws_route53_record" "cognito" {
count = local.custom_domain != null ? 1 : 0
zone_id = local.hosted_zone_id
name = local.custom_domain
type = "A"
alias {
name = aws_cognito_user_pool_domain.main.cloudfront_distribution_arn
zone_id = "Z2FDTNDATAQYW2" # CloudFront zone ID
evaluate_target_health = false
}
}
################################################################################
# App Clients
################################################################################
resource "aws_cognito_user_pool_client" "clients" {
for_each = local.app_clients
name = "${local.pool_name}-${each.key}"
user_pool_id = aws_cognito_user_pool.main.id
generate_secret = each.value.generate_secret
explicit_auth_flows = each.value.explicit_auth_flows
supported_identity_providers = each.value.supported_identity_providers
callback_urls = lookup(each.value, "callback_urls", null)
logout_urls = lookup(each.value, "logout_urls", null)
allowed_oauth_flows = lookup(each.value, "allowed_oauth_flows", null)
allowed_oauth_scopes = lookup(each.value, "allowed_oauth_scopes", null)
allowed_oauth_flows_user_pool_client = lookup(each.value, "allowed_oauth_flows_user_pool_client", false)
access_token_validity = lookup(each.value, "access_token_validity", 60)
id_token_validity = lookup(each.value, "id_token_validity", 60)
refresh_token_validity = lookup(each.value, "refresh_token_validity", 30)
token_validity_units {
access_token = "minutes"
id_token = "minutes"
refresh_token = "days"
}
prevent_user_existence_errors = "ENABLED"
enable_token_revocation = true
}
################################################################################
# Social Identity Providers
################################################################################
resource "aws_cognito_identity_provider" "google" {
count = contains(keys(local.social_providers), "google") ? 1 : 0
user_pool_id = aws_cognito_user_pool.main.id
provider_name = "Google"
provider_type = "Google"
provider_details = {
client_id = local.social_providers.google.client_id
client_secret = local.social_providers.google.client_secret
authorize_scopes = join(" ", local.social_providers.google.scopes)
}
attribute_mapping = {
email = "email"
username = "sub"
}
}
resource "aws_cognito_identity_provider" "facebook" {
count = contains(keys(local.social_providers), "facebook") ? 1 : 0
user_pool_id = aws_cognito_user_pool.main.id
provider_name = "Facebook"
provider_type = "Facebook"
provider_details = {
client_id = local.social_providers.facebook.client_id
client_secret = local.social_providers.facebook.client_secret
authorize_scopes = join(",", local.social_providers.facebook.scopes)
}
attribute_mapping = {
email = "email"
username = "id"
}
}
################################################################################
# Identity Pool (Optional)
################################################################################
resource "aws_cognito_identity_pool" "main" {
count = local.enable_identity_pool ? 1 : 0
identity_pool_name = replace(local.pool_name, "-", "_")
allow_unauthenticated_identities = false
cognito_identity_providers {
client_id = aws_cognito_user_pool_client.clients["web"].id
provider_name = aws_cognito_user_pool.main.endpoint
server_side_token_check = true
}
tags = { Name = local.pool_name }
}
resource "aws_iam_role" "authenticated" {
count = local.enable_identity_pool ? 1 : 0
name = "${local.pool_name}-authenticated"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "cognito-identity.amazonaws.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"cognito-identity.amazonaws.com:aud" = aws_cognito_identity_pool.main[0].id
}
"ForAnyValue:StringLike" = {
"cognito-identity.amazonaws.com:amr" = "authenticated"
}
}
}]
})
tags = { Name = "${local.pool_name}-authenticated" }
}
resource "aws_iam_role_policy" "authenticated" {
count = local.enable_identity_pool ? 1 : 0
name = "authenticated-policy"
role = aws_iam_role.authenticated[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"mobileanalytics:PutEvents",
"cognito-sync:*",
"cognito-identity:*"
]
Resource = "*"
}]
})
}
resource "aws_cognito_identity_pool_roles_attachment" "main" {
count = local.enable_identity_pool ? 1 : 0
identity_pool_id = aws_cognito_identity_pool.main[0].id
roles = {
authenticated = aws_iam_role.authenticated[0].arn
}
}
################################################################################
# Outputs
################################################################################
output "user_pool_id" {
value = aws_cognito_user_pool.main.id
}
output "user_pool_arn" {
value = aws_cognito_user_pool.main.arn
}
output "user_pool_endpoint" {
value = aws_cognito_user_pool.main.endpoint
}
output "user_pool_domain" {
value = local.custom_domain != null ? "https://${local.custom_domain}" : "https://${aws_cognito_user_pool_domain.main.domain}.auth.${data.aws_region.current.name}.amazoncognito.com"
}
output "client_ids" {
value = { for k, v in aws_cognito_user_pool_client.clients : k => v.id }
}
output "identity_pool_id" {
value = local.enable_identity_pool ? aws_cognito_identity_pool.main[0].id : null
}
output "hosted_ui_url" {
value = "${local.custom_domain != null ? "https://${local.custom_domain}" : "https://${aws_cognito_user_pool_domain.main.domain}.auth.${data.aws_region.current.name}.amazoncognito.com"}/login?client_id=${aws_cognito_user_pool_client.clients["web"].id}&response_type=code&redirect_uri=${urlencode(local.app_clients.web.callback_urls[0])}"
}