diff --git a/terraform/cognito/README.md b/terraform/cognito/README.md new file mode 100644 index 0000000..0c317e7 --- /dev/null +++ b/terraform/cognito/README.md @@ -0,0 +1,64 @@ +# AWS Cognito + +Production-ready Terraform module for AWS Cognito User Pools and Identity Pools with comprehensive authentication and authorization features. + +## Features + +- User Pools User directory and authentication service +- Identity Pools Provide AWS credentials to authenticated users +- Hosted UI Pre-built authentication UI with OAuth 2.0 support +- MFA Support SMS and TOTP-based multi-factor authentication +- Advanced Security Adaptive authentication and compromised credentials detection +- Custom Attributes Extend user profiles with custom data +- Lambda Triggers Customize authentication flows with Lambda functions +- Comprehensive Outputs User pool IDs, client IDs, and authentication examples + +## Quick Start + +```hcl +module "cognito" { + source = "github.com/llamandcoco/infra-modules//terraform/cognito?ref=" + + user_pool_name = "my-app-users" + username_attributes = ["email"] + auto_verified_attributes = ["email"] + mfa_configuration = "OPTIONAL" + + user_pool_clients = [ + { + name = "web-client" + } + ] +} +``` + +## Examples + +Complete, tested configurations in [`tests/`](tests/): + +| Example | Directory | +|---------|----------| +| Basic | [`tests/basic/main.tf`](tests/basic/main.tf) | +| Advanced | [`tests/advanced/main.tf`](tests/advanced/main.tf) | + +**Usage:** +```bash +# View example +cat tests/basic/ + +# Copy and adapt +cp -r tests/basic/ my-project/ +``` + +## Testing + +```bash +cd tests/basic && terraform init && terraform plan +``` + +
+Terraform Documentation + + + +
diff --git a/terraform/cognito/main.tf b/terraform/cognito/main.tf new file mode 100644 index 0000000..7460795 --- /dev/null +++ b/terraform/cognito/main.tf @@ -0,0 +1,450 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +# ----------------------------------------------------------------------------- +# Local Variables +# ----------------------------------------------------------------------------- + +locals { + # Common tags to apply to all resources + common_tags = merge( + var.tags, + { + ManagedBy = "Terraform" + Module = "cognito" + } + ) + + # Determine if we should create identity pool + create_identity_pool = var.create_identity_pool + + # Determine if we should create user pool domain + create_domain = var.user_pool_domain != null +} + + +# ----------------------------------------------------------------------------- +# Cognito User Pool +# Manages user directory and authentication +# ----------------------------------------------------------------------------- +resource "aws_cognito_user_pool" "this" { + name = var.user_pool_name + + # Username configuration + username_attributes = var.username_attributes + auto_verified_attributes = var.auto_verified_attributes + + # Username case sensitivity + username_configuration { + case_sensitive = var.username_case_sensitive + } + + # Alias attributes (alternative login methods) + dynamic "alias_attributes" { + for_each = length(var.alias_attributes) > 0 ? [1] : [] + content { + alias_attributes = var.alias_attributes + } + } + + # Password policy + password_policy { + minimum_length = var.password_minimum_length + require_lowercase = var.password_require_lowercase + require_uppercase = var.password_require_uppercase + require_numbers = var.password_require_numbers + require_symbols = var.password_require_symbols + temporary_password_validity_days = var.temporary_password_validity_days + } + + # MFA configuration + mfa_configuration = var.mfa_configuration + + # Software token MFA (TOTP) + dynamic "software_token_mfa_configuration" { + for_each = var.mfa_configuration != "OFF" ? [1] : [] + + content { + enabled = true + } + } + + # SMS MFA configuration + dynamic "sms_configuration" { + for_each = var.mfa_configuration == "ON" || var.mfa_configuration == "OPTIONAL" ? [1] : [] + + content { + external_id = var.sms_configuration_external_id + sns_caller_arn = var.sms_configuration_sns_caller_arn != null ? var.sms_configuration_sns_caller_arn : aws_iam_role.sms[0].arn + sns_region = var.sms_configuration_sns_region != null ? var.sms_configuration_sns_region : data.aws_region.current.name + } + } + + # Account recovery settings + dynamic "account_recovery_setting" { + for_each = length(var.account_recovery_mechanisms) > 0 ? [1] : [] + + content { + dynamic "recovery_mechanism" { + for_each = var.account_recovery_mechanisms + + content { + name = recovery_mechanism.value.name + priority = recovery_mechanism.value.priority + } + } + } + } + + # Email configuration + dynamic "email_configuration" { + for_each = var.email_configuration != null ? [var.email_configuration] : [] + + content { + email_sending_account = email_configuration.value.email_sending_account + from_email_address = email_configuration.value.from_email_address + reply_to_email_address = email_configuration.value.reply_to_email_address + source_arn = email_configuration.value.source_arn + configuration_set = email_configuration.value.configuration_set + } + } + + # User attribute schema (custom attributes) + dynamic "schema" { + for_each = var.schema_attributes + + content { + name = schema.value.name + attribute_data_type = schema.value.attribute_data_type + developer_only_attribute = lookup(schema.value, "developer_only_attribute", false) + mutable = lookup(schema.value, "mutable", true) + required = lookup(schema.value, "required", false) + + dynamic "string_attribute_constraints" { + for_each = schema.value.attribute_data_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.attribute_data_type == "Number" ? [1] : [] + + content { + min_value = lookup(schema.value, "min_value", 0) + max_value = lookup(schema.value, "max_value", 2048) + } + } + } + } + + # Lambda triggers + dynamic "lambda_config" { + for_each = length(var.lambda_config) > 0 ? [var.lambda_config] : [] + + content { + pre_sign_up = lookup(lambda_config.value, "pre_sign_up", null) + post_confirmation = lookup(lambda_config.value, "post_confirmation", null) + pre_authentication = lookup(lambda_config.value, "pre_authentication", null) + post_authentication = lookup(lambda_config.value, "post_authentication", null) + pre_token_generation = lookup(lambda_config.value, "pre_token_generation", null) + user_migration = lookup(lambda_config.value, "user_migration", null) + custom_message = lookup(lambda_config.value, "custom_message", null) + define_auth_challenge = lookup(lambda_config.value, "define_auth_challenge", null) + create_auth_challenge = lookup(lambda_config.value, "create_auth_challenge", null) + verify_auth_challenge_response = lookup(lambda_config.value, "verify_auth_challenge_response", null) + } + } + + # User pool add-ons + dynamic "user_pool_add_ons" { + for_each = var.enable_advanced_security ? [1] : [] + + content { + advanced_security_mode = var.advanced_security_mode + } + } + + # Device tracking + dynamic "device_configuration" { + for_each = var.device_tracking != null ? [1] : [] + + content { + challenge_required_on_new_device = var.device_tracking.challenge_required_on_new_device + device_only_remembered_on_user_prompt = var.device_tracking.device_only_remembered_on_user_prompt + } + } + + # Deletion protection + deletion_protection = var.deletion_protection + + tags = merge( + local.common_tags, + { + Name = var.user_pool_name + } + ) +} + +# ----------------------------------------------------------------------------- +# IAM Role for SMS MFA +# Allows Cognito to send SMS via SNS +# ----------------------------------------------------------------------------- +resource "aws_iam_role" "sms" { + count = (var.mfa_configuration == "ON" || var.mfa_configuration == "OPTIONAL") && var.sms_configuration_sns_caller_arn == null ? 1 : 0 + + name = "${var.user_pool_name}-sms-role" + description = "IAM role for Cognito User Pool to send SMS messages" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "cognito-idp.amazonaws.com" + } + Action = "sts:AssumeRole" + Condition = { + StringEquals = { + "sts:ExternalId" = var.sms_configuration_external_id + } + } + } + ] + }) + + tags = merge( + local.common_tags, + { + Name = "${var.user_pool_name}-sms-role" + } + ) +} + +resource "aws_iam_role_policy" "sms" { + count = (var.mfa_configuration == "ON" || var.mfa_configuration == "OPTIONAL") && var.sms_configuration_sns_caller_arn == null ? 1 : 0 + + name = "cognito-sms-policy" + role = aws_iam_role.sms[0].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "sns:Publish" + ] + Resource = "*" + } + ] + }) +} + +# ----------------------------------------------------------------------------- +# User Pool Clients +# Applications that can authenticate users +# ----------------------------------------------------------------------------- +resource "aws_cognito_user_pool_client" "this" { + for_each = { for client in var.user_pool_clients : client.name => client } + + name = each.value.name + user_pool_id = aws_cognito_user_pool.this.id + + # OAuth configuration + allowed_oauth_flows = lookup(each.value, "allowed_oauth_flows", []) + allowed_oauth_scopes = lookup(each.value, "allowed_oauth_scopes", []) + allowed_oauth_flows_user_pool_client = lookup(each.value, "allowed_oauth_flows_user_pool_client", false) + callback_urls = lookup(each.value, "callback_urls", []) + logout_urls = lookup(each.value, "logout_urls", []) + supported_identity_providers = lookup(each.value, "supported_identity_providers", []) + + # Token validity + 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 = lookup(each.value, "access_token_validity_unit", "minutes") + id_token = lookup(each.value, "id_token_validity_unit", "minutes") + refresh_token = lookup(each.value, "refresh_token_validity_unit", "days") + } + + # Client secret + generate_secret = lookup(each.value, "generate_secret", false) + + # Prevent user existence errors + prevent_user_existence_errors = lookup(each.value, "prevent_user_existence_errors", "ENABLED") + + # Read and write attributes + read_attributes = lookup(each.value, "read_attributes", []) + write_attributes = lookup(each.value, "write_attributes", []) + + # Enable token revocation + enable_token_revocation = lookup(each.value, "enable_token_revocation", true) + + # Explicit auth flows + explicit_auth_flows = lookup(each.value, "explicit_auth_flows", [ + "ALLOW_REFRESH_TOKEN_AUTH", + "ALLOW_USER_SRP_AUTH" + ]) +} + +# ----------------------------------------------------------------------------- +# User Pool Domain +# Hosted UI domain for authentication +# ----------------------------------------------------------------------------- +resource "aws_cognito_user_pool_domain" "this" { + count = local.create_domain ? 1 : 0 + + domain = var.user_pool_domain + user_pool_id = aws_cognito_user_pool.this.id + certificate_arn = var.user_pool_domain_certificate_arn +} + +# ----------------------------------------------------------------------------- +# Identity Pool +# Provides AWS credentials to authenticated users +# ----------------------------------------------------------------------------- +resource "aws_cognito_identity_pool" "this" { + count = local.create_identity_pool ? 1 : 0 + + identity_pool_name = var.identity_pool_name != null ? var.identity_pool_name : "${var.user_pool_name}-identity-pool" + allow_unauthenticated_identities = var.allow_unauthenticated_identities + allow_classic_flow = var.allow_classic_flow + + # Cognito Identity Providers (User Pools) + dynamic "cognito_identity_providers" { + for_each = var.user_pool_clients + + content { + client_id = aws_cognito_user_pool_client.this[cognito_identity_providers.value.name].id + provider_name = aws_cognito_user_pool.this.endpoint + server_side_token_check = lookup(cognito_identity_providers.value, "server_side_token_check", false) + } + } + + tags = merge( + local.common_tags, + { + Name = var.identity_pool_name != null ? var.identity_pool_name : "${var.user_pool_name}-identity-pool" + } + ) +} + +# ----------------------------------------------------------------------------- +# IAM Roles for Identity Pool +# ----------------------------------------------------------------------------- + +# Authenticated role +resource "aws_iam_role" "authenticated" { + count = local.create_identity_pool ? 1 : 0 + + name = "${var.user_pool_name}-authenticated-role" + description = "IAM role for authenticated Cognito users" + + 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.this[0].id + } + "ForAnyValue:StringLike" = { + "cognito-identity.amazonaws.com:amr" = "authenticated" + } + } + } + ] + }) + + tags = merge( + local.common_tags, + { + Name = "${var.user_pool_name}-authenticated-role" + } + ) +} + +resource "aws_iam_role_policy_attachment" "authenticated" { + for_each = local.create_identity_pool ? toset(var.authenticated_role_policy_arns) : [] + + role = aws_iam_role.authenticated[0].name + policy_arn = each.value +} + +# Unauthenticated role (if allowed) +resource "aws_iam_role" "unauthenticated" { + count = local.create_identity_pool && var.allow_unauthenticated_identities ? 1 : 0 + + name = "${var.user_pool_name}-unauthenticated-role" + description = "IAM role for unauthenticated Cognito users" + + 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.this[0].id + } + "ForAnyValue:StringLike" = { + "cognito-identity.amazonaws.com:amr" = "unauthenticated" + } + } + } + ] + }) + + tags = merge( + local.common_tags, + { + Name = "${var.user_pool_name}-unauthenticated-role" + } + ) +} + +resource "aws_iam_role_policy_attachment" "unauthenticated" { + for_each = local.create_identity_pool && var.allow_unauthenticated_identities ? toset(var.unauthenticated_role_policy_arns) : [] + + role = aws_iam_role.unauthenticated[0].name + policy_arn = each.value +} + +# Attach roles to identity pool +resource "aws_cognito_identity_pool_roles_attachment" "this" { + count = local.create_identity_pool ? 1 : 0 + + identity_pool_id = aws_cognito_identity_pool.this[0].id + + roles = merge( + { + authenticated = aws_iam_role.authenticated[0].arn + }, + var.allow_unauthenticated_identities ? { + unauthenticated = aws_iam_role.unauthenticated[0].arn + } : {} + ) +} diff --git a/terraform/cognito/outputs.tf b/terraform/cognito/outputs.tf new file mode 100644 index 0000000..d63784f --- /dev/null +++ b/terraform/cognito/outputs.tf @@ -0,0 +1,232 @@ +# ----------------------------------------------------------------------------- +# User Pool Outputs +# ----------------------------------------------------------------------------- + +output "user_pool_id" { + description = "The ID of the Cognito User Pool. Use this for SDK calls and API Gateway authorizers." + value = aws_cognito_user_pool.this.id +} + +output "user_pool_arn" { + description = "The ARN of the Cognito User Pool. Use this for IAM policies and resource permissions." + value = aws_cognito_user_pool.this.arn +} + +output "user_pool_name" { + description = "The name of the Cognito User Pool." + value = aws_cognito_user_pool.this.name +} + +output "user_pool_endpoint" { + description = "The endpoint URL of the Cognito User Pool." + value = aws_cognito_user_pool.this.endpoint +} + +# ----------------------------------------------------------------------------- +# User Pool Client Outputs +# ----------------------------------------------------------------------------- + +output "user_pool_client_ids" { + description = "Map of client names to their IDs. Use these for authentication in your applications." + value = { + for name, client in aws_cognito_user_pool_client.this : + name => client.id + } +} + +output "user_pool_client_secrets" { + description = "Map of client names to their secrets. Only populated for clients with generate_secret = true." + value = { + for name, client in aws_cognito_user_pool_client.this : + name => client.client_secret if client.client_secret != null + } + sensitive = true +} + +# ----------------------------------------------------------------------------- +# User Pool Domain Outputs +# ----------------------------------------------------------------------------- + +output "user_pool_domain" { + description = "The Cognito User Pool domain." + value = local.create_domain ? aws_cognito_user_pool_domain.this[0].domain : null +} + +output "user_pool_domain_cloudfront_distribution" { + description = "The CloudFront distribution ARN for the domain." + value = local.create_domain ? aws_cognito_user_pool_domain.this[0].cloudfront_distribution : null +} + +output "hosted_ui_url" { + description = "The URL for the hosted UI login page." + value = local.create_domain ? "https://${aws_cognito_user_pool_domain.this[0].domain}.auth.${provider::aws::region}.amazoncognito.com" : null +} + +# ----------------------------------------------------------------------------- +# Identity Pool Outputs +# ----------------------------------------------------------------------------- + +output "identity_pool_id" { + description = "The ID of the Cognito Identity Pool. Use this to get AWS credentials for authenticated users." + value = local.create_identity_pool ? aws_cognito_identity_pool.this[0].id : null +} + +output "identity_pool_arn" { + description = "The ARN of the Cognito Identity Pool." + value = local.create_identity_pool ? aws_cognito_identity_pool.this[0].arn : null +} + +output "identity_pool_name" { + description = "The name of the Cognito Identity Pool." + value = local.create_identity_pool ? aws_cognito_identity_pool.this[0].identity_pool_name : null +} + +# ----------------------------------------------------------------------------- +# IAM Role Outputs +# ----------------------------------------------------------------------------- + +output "authenticated_role_arn" { + description = "The ARN of the IAM role for authenticated users." + value = local.create_identity_pool ? aws_iam_role.authenticated[0].arn : null +} + +output "authenticated_role_name" { + description = "The name of the IAM role for authenticated users." + value = local.create_identity_pool ? aws_iam_role.authenticated[0].name : null +} + +output "unauthenticated_role_arn" { + description = "The ARN of the IAM role for unauthenticated users." + value = local.create_identity_pool && var.allow_unauthenticated_identities ? aws_iam_role.unauthenticated[0].arn : null +} + +output "unauthenticated_role_name" { + description = "The name of the IAM role for unauthenticated users." + value = local.create_identity_pool && var.allow_unauthenticated_identities ? aws_iam_role.unauthenticated[0].name : null +} + +# ----------------------------------------------------------------------------- +# Configuration Outputs +# ----------------------------------------------------------------------------- + +output "mfa_configuration" { + description = "The MFA configuration for the user pool." + value = aws_cognito_user_pool.this.mfa_configuration +} + +output "region" { + description = "The AWS region where Cognito resources are deployed." + value = provider::aws::region +} + +output "account_id" { + description = "The AWS account ID where Cognito resources are deployed." + value = "*" +} + +# ----------------------------------------------------------------------------- +# SDK Configuration Examples +# ----------------------------------------------------------------------------- + +output "aws_cli_login_example" { + description = "AWS CLI example for user authentication." + value = length(var.user_pool_clients) > 0 ? <<-EOT + aws cognito-idp initiate-auth \ + --auth-flow USER_SRP_AUTH \ + --client-id ${aws_cognito_user_pool_client.this[var.user_pool_clients[0].name].id} \ + --auth-parameters USERNAME=user@example.com,SRP_A= + EOT + : "No user pool clients configured" +} + +output "boto3_authentication_example" { + description = "Python boto3 example for user authentication." + value = length(var.user_pool_clients) > 0 ? <<-EOT + import boto3 + from warrant import Cognito + + # Using python-jose-cryptodome library + cognito = Cognito( + user_pool_id='${aws_cognito_user_pool.this.id}', + client_id='${aws_cognito_user_pool_client.this[var.user_pool_clients[0].name].id}', + user_pool_region='${provider::aws::region}' + ) + + # Authenticate user + cognito.authenticate(password='user_password') + + # Get ID token + id_token = cognito.id_token + + # Get access token + access_token = cognito.access_token + EOT + : "No user pool clients configured" +} + +output "javascript_authentication_example" { + description = "JavaScript example for user authentication with AWS Amplify." + value = length(var.user_pool_clients) > 0 ? <<-EOT + import { Amplify, Auth } from 'aws-amplify'; + + Amplify.configure({ + Auth: { + region: '${provider::aws::region}', + userPoolId: '${aws_cognito_user_pool.this.id}', + userPoolWebClientId: '${aws_cognito_user_pool_client.this[var.user_pool_clients[0].name].id}' + } + }); + + // Sign in + const user = await Auth.signIn('username', 'password'); + + // Get current session + const session = await Auth.currentSession(); + const idToken = session.getIdToken().getJwtToken(); + const accessToken = session.getAccessToken().getJwtToken(); + EOT + : "No user pool clients configured" +} + +output "identity_pool_credentials_example" { + description = "Example for getting AWS credentials from Identity Pool." + value = local.create_identity_pool && length(var.user_pool_clients) > 0 ? <<-EOT + import boto3 + from warrant import Cognito + + # Step 1: Authenticate with User Pool + cognito = Cognito( + user_pool_id='${aws_cognito_user_pool.this.id}', + client_id='${aws_cognito_user_pool_client.this[var.user_pool_clients[0].name].id}', + user_pool_region='${provider::aws::region}' + ) + cognito.authenticate(password='user_password') + id_token = cognito.id_token + + # Step 2: Get credentials from Identity Pool + cognito_identity = boto3.client('cognito-identity') + + # Get identity ID + identity_response = cognito_identity.get_id( + IdentityPoolId='${aws_cognito_identity_pool.this[0].id}', + Logins={ + '${aws_cognito_user_pool.this.endpoint}': id_token + } + ) + + # Get credentials + credentials_response = cognito_identity.get_credentials_for_identity( + IdentityId=identity_response['IdentityId'], + Logins={ + '${aws_cognito_user_pool.this.endpoint}': id_token + } + ) + + # Use AWS credentials + credentials = credentials_response['Credentials'] + access_key = credentials['AccessKeyId'] + secret_key = credentials['SecretKey'] + session_token = credentials['SessionToken'] + EOT + : "No identity pool or clients configured" +} diff --git a/terraform/cognito/tests/advanced/main.tf b/terraform/cognito/tests/advanced/main.tf new file mode 100644 index 0000000..5b98c32 --- /dev/null +++ b/terraform/cognito/tests/advanced/main.tf @@ -0,0 +1,289 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +# ----------------------------------------------------------------------------- +# Advanced Cognito Test +# Tests comprehensive Cognito configuration with: +# - User pool with custom attributes +# - Identity pool for AWS credentials +# - Multiple user pool clients (web, mobile) +# - Hosted UI domain +# - Advanced security features +# - OAuth 2.0 flows +# ----------------------------------------------------------------------------- + +provider "aws" { + region = "us-west-2" + + # Mock configuration for testing - no real AWS credentials needed for plan + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + + endpoints { + cognitoidentity = "http://localhost:4566" + cognitoidp = "http://localhost:4566" + iam = "http://localhost:4566" + sts = "http://localhost:4566" + } +} + +# Create advanced Cognito user pool +module "cognito" { + source = "../.." + + # User pool configuration + user_pool_name = "advanced-user-pool" + + # Email-based sign-in + username_attributes = ["email"] + auto_verified_attributes = ["email"] + username_case_sensitive = false + + # Strong password policy + password_minimum_length = 12 + password_require_lowercase = true + password_require_uppercase = true + password_require_numbers = true + password_require_symbols = true + temporary_password_validity_days = 3 + + # Required MFA + mfa_configuration = "ON" + + # Account recovery mechanisms + account_recovery_mechanisms = [ + { + name = "verified_email" + priority = 1 + }, + { + name = "verified_phone_number" + priority = 2 + } + ] + + # Advanced security features + enable_advanced_security = true + advanced_security_mode = "ENFORCED" + + # Device tracking + device_tracking = { + challenge_required_on_new_device = true + device_only_remembered_on_user_prompt = true + } + + # Custom attributes + schema_attributes = [ + { + name = "tenant_id" + attribute_data_type = "String" + mutable = false + required = false + min_length = 1 + max_length = 256 + }, + { + name = "subscription_tier" + attribute_data_type = "String" + mutable = true + required = false + min_length = 1 + max_length = 50 + }, + { + name = "onboarding_completed" + attribute_data_type = "Boolean" + mutable = true + required = false + } + ] + + # User pool clients + user_pool_clients = [ + { + name = "web-client" + + # OAuth configuration for hosted UI + allowed_oauth_flows = [ + "code", + "implicit" + ] + allowed_oauth_scopes = [ + "email", + "openid", + "profile" + ] + allowed_oauth_flows_user_pool_client = true + + callback_urls = [ + "https://app.example.com/callback", + "http://localhost:3000/callback" + ] + logout_urls = [ + "https://app.example.com/logout", + "http://localhost:3000/logout" + ] + + supported_identity_providers = ["COGNITO"] + + # Token validity + access_token_validity = 60 + id_token_validity = 60 + refresh_token_validity = 30 + access_token_validity_unit = "minutes" + id_token_validity_unit = "minutes" + refresh_token_validity_unit = "days" + + # Auth flows + explicit_auth_flows = [ + "ALLOW_USER_SRP_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" + ] + + prevent_user_existence_errors = "ENABLED" + enable_token_revocation = true + server_side_token_check = true + }, + { + name = "mobile-client" + + # Mobile-specific configuration + generate_secret = false + + # Token validity - longer refresh for mobile + access_token_validity = 60 + id_token_validity = 60 + refresh_token_validity = 90 + access_token_validity_unit = "minutes" + id_token_validity_unit = "minutes" + refresh_token_validity_unit = "days" + + # Auth flows for mobile + explicit_auth_flows = [ + "ALLOW_USER_SRP_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH", + "ALLOW_CUSTOM_AUTH" + ] + + enable_token_revocation = true + server_side_token_check = false + }, + { + name = "backend-service" + + # Service-to-service authentication + generate_secret = true + + # Token validity + access_token_validity = 60 + id_token_validity = 60 + refresh_token_validity = 30 + access_token_validity_unit = "minutes" + id_token_validity_unit = "minutes" + refresh_token_validity_unit = "days" + + # Auth flows + explicit_auth_flows = [ + "ALLOW_USER_PASSWORD_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" + ] + + server_side_token_check = true + } + ] + + # Hosted UI domain + user_pool_domain = "advanced-user-pool-${data.aws_caller_identity.current.account_id}" + + # Identity pool for AWS credentials + create_identity_pool = true + identity_pool_name = "advanced-identity-pool" + allow_unauthenticated_identities = false + allow_classic_flow = false + + # IAM policies for authenticated users + authenticated_role_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" + ] + + # Deletion protection + deletion_protection = "ACTIVE" + + tags = { + Environment = "production" + Purpose = "advanced-cognito-test" + Team = "platform" + CostCenter = "engineering" + } +} + +# Data source for account ID +data "aws_caller_identity" "current" {} + +# ----------------------------------------------------------------------------- +# Outputs +# ----------------------------------------------------------------------------- + +output "user_pool_id" { + description = "ID of the user pool" + value = module.cognito.user_pool_id +} + +output "user_pool_arn" { + description = "ARN of the user pool" + value = module.cognito.user_pool_arn +} + +output "user_pool_endpoint" { + description = "Endpoint of the user pool" + value = module.cognito.user_pool_endpoint +} + +output "client_ids" { + description = "User pool client IDs" + value = module.cognito.user_pool_client_ids +} + +output "hosted_ui_url" { + description = "Hosted UI URL" + value = module.cognito.hosted_ui_url +} + +output "identity_pool_id" { + description = "Identity pool ID" + value = module.cognito.identity_pool_id +} + +output "authenticated_role_arn" { + description = "Authenticated role ARN" + value = module.cognito.authenticated_role_arn +} + +output "region" { + description = "AWS region" + value = module.cognito.region +} + +output "authentication_example" { + description = "Example authentication code" + value = module.cognito.boto3_authentication_example +} + +output "identity_pool_credentials_example" { + description = "Example code for getting AWS credentials" + value = module.cognito.identity_pool_credentials_example +} + +output "javascript_auth_example" { + description = "JavaScript authentication example" + value = module.cognito.javascript_authentication_example +} diff --git a/terraform/cognito/tests/basic/main.tf b/terraform/cognito/tests/basic/main.tf new file mode 100644 index 0000000..6a79a7d --- /dev/null +++ b/terraform/cognito/tests/basic/main.tf @@ -0,0 +1,115 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +# ----------------------------------------------------------------------------- +# Basic Cognito Test +# Tests minimal Cognito configuration with: +# - User pool with email sign-in +# - Basic password policy +# - One user pool client +# - Optional MFA +# ----------------------------------------------------------------------------- + +provider "aws" { + region = "us-east-1" + + # Mock configuration for testing - no real AWS credentials needed for plan + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + + endpoints { + cognitoidentity = "http://localhost:4566" + cognitoidp = "http://localhost:4566" + iam = "http://localhost:4566" + sts = "http://localhost:4566" + } +} + +# Create basic Cognito user pool +module "cognito" { + source = "../.." + + # User pool configuration + user_pool_name = "basic-user-pool" + + # Email-based sign-in + username_attributes = ["email"] + auto_verified_attributes = ["email"] + + # Basic password policy (use defaults) + # - minimum_length: 8 + # - require_lowercase: true + # - require_uppercase: true + # - require_numbers: true + # - require_symbols: true + + # Optional MFA + mfa_configuration = "OPTIONAL" + + # Account recovery via email + account_recovery_mechanisms = [ + { + name = "verified_email" + priority = 1 + } + ] + + # User pool client for web application + user_pool_clients = [ + { + name = "web-client" + explicit_auth_flows = [ + "ALLOW_USER_SRP_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" + ] + } + ] + + tags = { + Environment = "test" + Purpose = "basic-cognito-test" + } +} + +# ----------------------------------------------------------------------------- +# Outputs +# ----------------------------------------------------------------------------- + +output "user_pool_id" { + description = "ID of the user pool" + value = module.cognito.user_pool_id +} + +output "user_pool_arn" { + description = "ARN of the user pool" + value = module.cognito.user_pool_arn +} + +output "user_pool_endpoint" { + description = "Endpoint of the user pool" + value = module.cognito.user_pool_endpoint +} + +output "client_ids" { + description = "User pool client IDs" + value = module.cognito.user_pool_client_ids +} + +output "region" { + description = "AWS region" + value = module.cognito.region +} + +output "authentication_example" { + description = "Example authentication code" + value = module.cognito.boto3_authentication_example +} diff --git a/terraform/cognito/variables.tf b/terraform/cognito/variables.tf new file mode 100644 index 0000000..6996471 --- /dev/null +++ b/terraform/cognito/variables.tf @@ -0,0 +1,376 @@ +# ----------------------------------------------------------------------------- +# Required Variables +# ----------------------------------------------------------------------------- + +variable "user_pool_name" { + description = "Name of the Cognito User Pool. This will be displayed in the AWS console." + type = string + + validation { + condition = length(var.user_pool_name) > 0 && length(var.user_pool_name) <= 128 + error_message = "User pool name must be between 1 and 128 characters." + } +} + +# ----------------------------------------------------------------------------- +# User Pool Configuration +# ----------------------------------------------------------------------------- + +variable "username_attributes" { + description = "Whether email addresses or phone numbers can be used as usernames. Valid values: 'email', 'phone_number'. Cannot be changed after creation." + type = list(string) + default = [] + + validation { + condition = alltrue([ + for attr in var.username_attributes : contains(["email", "phone_number"], attr) + ]) + error_message = "Username attributes must be 'email' or 'phone_number'." + } +} + +variable "auto_verified_attributes" { + description = "Attributes to auto-verify. Valid values: 'email', 'phone_number'." + type = list(string) + default = [] + + validation { + condition = alltrue([ + for attr in var.auto_verified_attributes : contains(["email", "phone_number"], attr) + ]) + error_message = "Auto verified attributes must be 'email' or 'phone_number'." + } +} + +variable "username_case_sensitive" { + description = "Whether username is case sensitive. Cannot be changed after creation." + type = bool + default = false +} + +variable "alias_attributes" { + description = "Attributes that can be used as aliases for user pool sign-in. Valid values: 'email', 'phone_number', 'preferred_username'." + type = list(string) + default = [] + + validation { + condition = alltrue([ + for attr in var.alias_attributes : contains(["email", "phone_number", "preferred_username"], attr) + ]) + error_message = "Alias attributes must be 'email', 'phone_number', or 'preferred_username'." + } +} + +# ----------------------------------------------------------------------------- +# Password Policy +# ----------------------------------------------------------------------------- + +variable "password_minimum_length" { + description = "Minimum password length. Valid range: 6-99." + type = number + default = 8 + + validation { + condition = var.password_minimum_length >= 6 && var.password_minimum_length <= 99 + error_message = "Password minimum length must be between 6 and 99." + } +} + +variable "password_require_lowercase" { + description = "Whether password must contain at least one lowercase letter." + type = bool + default = true +} + +variable "password_require_uppercase" { + description = "Whether password must contain at least one uppercase letter." + type = bool + default = true +} + +variable "password_require_numbers" { + description = "Whether password must contain at least one number." + type = bool + default = true +} + +variable "password_require_symbols" { + description = "Whether password must contain at least one special character." + type = bool + default = true +} + +variable "temporary_password_validity_days" { + description = "Number of days a temporary password is valid. Valid range: 1-365." + type = number + default = 7 + + validation { + condition = var.temporary_password_validity_days >= 1 && var.temporary_password_validity_days <= 365 + error_message = "Temporary password validity must be between 1 and 365 days." + } +} + +# ----------------------------------------------------------------------------- +# MFA Configuration +# ----------------------------------------------------------------------------- + +variable "mfa_configuration" { + description = "Multi-factor authentication configuration. Valid values: 'OFF', 'ON', 'OPTIONAL'." + type = string + default = "OPTIONAL" + + validation { + condition = contains(["OFF", "ON", "OPTIONAL"], var.mfa_configuration) + error_message = "MFA configuration must be 'OFF', 'ON', or 'OPTIONAL'." + } +} + +variable "sms_configuration_external_id" { + description = "External ID for SMS configuration. Used when assuming IAM role for SNS." + type = string + default = "cognito-sms" +} + +variable "sms_configuration_sns_caller_arn" { + description = "ARN of the IAM role for SNS SMS sending. If not specified, a role will be created." + type = string + default = null +} + +variable "sms_configuration_sns_region" { + description = "AWS region for SNS. If not specified, uses current region." + type = string + default = null +} + +# ----------------------------------------------------------------------------- +# Account Recovery +# ----------------------------------------------------------------------------- + +variable "account_recovery_mechanisms" { + description = "List of account recovery mechanisms with priorities. Valid names: 'verified_email', 'verified_phone_number', 'admin_only'." + type = list(object({ + name = string + priority = number + })) + default = [ + { + name = "verified_email" + priority = 1 + } + ] + + validation { + condition = alltrue([ + for mechanism in var.account_recovery_mechanisms : contains(["verified_email", "verified_phone_number", "admin_only"], mechanism.name) + ]) + error_message = "Recovery mechanism names must be 'verified_email', 'verified_phone_number', or 'admin_only'." + } +} + +# ----------------------------------------------------------------------------- +# Email Configuration +# ----------------------------------------------------------------------------- + +variable "email_configuration" { + description = "Email configuration for the user pool. Use SES for production." + type = object({ + email_sending_account = string + from_email_address = optional(string) + reply_to_email_address = optional(string) + source_arn = optional(string) + configuration_set = optional(string) + }) + default = null + + validation { + condition = var.email_configuration == null || ( + contains(["COGNITO_DEFAULT", "DEVELOPER"], var.email_configuration.email_sending_account) + ) + error_message = "email_sending_account must be 'COGNITO_DEFAULT' or 'DEVELOPER'." + } +} + +# ----------------------------------------------------------------------------- +# Custom Attributes Schema +# ----------------------------------------------------------------------------- + +variable "schema_attributes" { + description = "List of custom schema attributes for the user pool." + type = list(object({ + name = string + attribute_data_type = string + developer_only_attribute = optional(bool, false) + mutable = optional(bool, true) + required = optional(bool, false) + min_length = optional(number) + max_length = optional(number) + min_value = optional(number) + max_value = optional(number) + })) + default = [] + + validation { + condition = alltrue([ + for attr in var.schema_attributes : contains(["String", "Number", "DateTime", "Boolean"], attr.attribute_data_type) + ]) + error_message = "Attribute data type must be 'String', 'Number', 'DateTime', or 'Boolean'." + } +} + +# ----------------------------------------------------------------------------- +# Lambda Triggers +# ----------------------------------------------------------------------------- + +variable "lambda_config" { + description = "Lambda trigger configuration for user pool events." + type = map(string) + default = {} +} + +# ----------------------------------------------------------------------------- +# Advanced Security +# ----------------------------------------------------------------------------- + +variable "enable_advanced_security" { + description = "Enable advanced security features (adaptive authentication, compromised credentials detection)." + type = bool + default = false +} + +variable "advanced_security_mode" { + description = "Advanced security mode. Valid values: 'OFF', 'AUDIT', 'ENFORCED'. Requires enable_advanced_security = true." + type = string + default = "AUDIT" + + validation { + condition = contains(["OFF", "AUDIT", "ENFORCED"], var.advanced_security_mode) + error_message = "Advanced security mode must be 'OFF', 'AUDIT', or 'ENFORCED'." + } +} + +# ----------------------------------------------------------------------------- +# Device Tracking +# ----------------------------------------------------------------------------- + +variable "device_tracking" { + description = "Device tracking configuration." + type = object({ + challenge_required_on_new_device = bool + device_only_remembered_on_user_prompt = bool + }) + default = null +} + +# ----------------------------------------------------------------------------- +# Deletion Protection +# ----------------------------------------------------------------------------- + +variable "deletion_protection" { + description = "Enable deletion protection for the user pool. Valid values: 'ACTIVE', 'INACTIVE'." + type = string + default = "INACTIVE" + + validation { + condition = contains(["ACTIVE", "INACTIVE"], var.deletion_protection) + error_message = "Deletion protection must be 'ACTIVE' or 'INACTIVE'." + } +} + +# ----------------------------------------------------------------------------- +# User Pool Clients +# ----------------------------------------------------------------------------- + +variable "user_pool_clients" { + description = "List of user pool client configurations." + type = list(object({ + name = string + allowed_oauth_flows = optional(list(string), []) + allowed_oauth_scopes = optional(list(string), []) + allowed_oauth_flows_user_pool_client = optional(bool, false) + callback_urls = optional(list(string), []) + logout_urls = optional(list(string), []) + supported_identity_providers = optional(list(string), []) + access_token_validity = optional(number, 60) + id_token_validity = optional(number, 60) + refresh_token_validity = optional(number, 30) + access_token_validity_unit = optional(string, "minutes") + id_token_validity_unit = optional(string, "minutes") + refresh_token_validity_unit = optional(string, "days") + generate_secret = optional(bool, false) + prevent_user_existence_errors = optional(string, "ENABLED") + read_attributes = optional(list(string), []) + write_attributes = optional(list(string), []) + enable_token_revocation = optional(bool, true) + explicit_auth_flows = optional(list(string), ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_USER_SRP_AUTH"]) + server_side_token_check = optional(bool, false) + })) + default = [] +} + +# ----------------------------------------------------------------------------- +# User Pool Domain +# ----------------------------------------------------------------------------- + +variable "user_pool_domain" { + description = "Domain name for the hosted UI. If specified, a Cognito domain will be created." + type = string + default = null +} + +variable "user_pool_domain_certificate_arn" { + description = "ARN of ACM certificate for custom domain. Required for custom domains." + type = string + default = null +} + +# ----------------------------------------------------------------------------- +# Identity Pool Configuration +# ----------------------------------------------------------------------------- + +variable "create_identity_pool" { + description = "Whether to create a Cognito Identity Pool for AWS credentials." + type = bool + default = false +} + +variable "identity_pool_name" { + description = "Name of the Cognito Identity Pool. If not specified, defaults to '-identity-pool'." + type = string + default = null +} + +variable "allow_unauthenticated_identities" { + description = "Whether to allow unauthenticated identities in the identity pool." + type = bool + default = false +} + +variable "allow_classic_flow" { + description = "Enable classic (basic) authentication flow for identity pool." + type = bool + default = false +} + +variable "authenticated_role_policy_arns" { + description = "List of IAM policy ARNs to attach to the authenticated role." + type = list(string) + default = [] +} + +variable "unauthenticated_role_policy_arns" { + description = "List of IAM policy ARNs to attach to the unauthenticated role." + type = list(string) + default = [] +} + +# ----------------------------------------------------------------------------- +# General Variables +# ----------------------------------------------------------------------------- + +variable "tags" { + description = "A map of tags to add to all resources. Use this for cost allocation, resource organization, and governance." + type = map(string) + default = {} +}