From ce091977458d6eec5733eda08c4e14b6d6009ae9 Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:00:15 +0000 Subject: [PATCH] feat: add initial working module and example --- README.md | 33 +++++-- examples/complete/README.md | 25 ----- examples/complete/main.tf | 50 ++++++++++ examples/complete/provider.tf | 4 - examples/complete/version.tf | 3 - main.tf | 180 ++++++++++++++++++++++++++++++++++ outputs.tf | 10 ++ variables.tf | 140 ++++++++++++++++++++++++++ 8 files changed, 407 insertions(+), 38 deletions(-) delete mode 100644 examples/complete/README.md diff --git a/README.md b/README.md index 1eff105..430cb64 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Terraform Module: AWS Cognito Userpool Clients -_This repository is under development. See `dev` branch for latest progress._ +This module creates and manages AWS Cognito User Pool Clients, allowing +fine-grained control over client configurations, UI customization, and secrets +management. ## Usage @@ -9,7 +11,18 @@ module "cognito_userpool_clients" { source = "cruxstack/cognito-userpool-clients/aws" version = "x.x.x" - # TBD + userpool_id = "" + + client_defaults = { + callback_urls = ["https://example.com/auth/"] + } + + clients = { + internal_web_app = {} + public_web_app = {} + mobile_app = {} + foobar_service = {} + } } ``` @@ -20,13 +33,21 @@ resources. As such, it also includes a `context.tf` file with additional optional variables you can set. Refer to the [`cloudposse/label` documentation](https://registry.terraform.io/modules/cloudposse/label/null/latest) for more details on these variables. -| Name | Description | Type | Default | Required | -|---------------|-------------|--------|---------|----------| -| `placeholder` | N/A | string | `""` | No | +| Name | Description | Type | Default | Required | +|--------------------|-----------------------------------------------------------------------|----------------------|---------|----------| +| `userpool_id` | Cognito user pool ID. | `string` | `null` | No | +| `client_defaults` | Default configurations for each client. | `object({...})` | `{}` | No | +| `clients` | Map of client-specific configurations. The key is the client name. | `map(object({...}))` | `{}` | No | +| `secrets_enabled` | Toggle to create SecretsManager secrets for each client. | `bool` | `true` | No | +| `aws_kv_namespace` | The namespace or prefix for AWS SSM parameters and similar resources. | `string` | `""` | No | ## Outputs -_This module does not currently provide any outputs._ +| Name | Description | +|-----------------------|------------------------------------------------------------------------------------------------------| +| `clients` | Map of Cognito user pool clients created by the module. | +| `client_secret_names` | Map of the names of Secrets Manager secrets for the Cognito user pool clients, keyed by client name. | + ## Contributing diff --git a/examples/complete/README.md b/examples/complete/README.md deleted file mode 100644 index ffefb04..0000000 --- a/examples/complete/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Terraform Module Example - -## Example: Deploying a Static Website to S3 - -This example demonstrates how to use the `terraform-aws-cognito-userpool-clients` module -to deploy a static website to an S3 bucket. - -The Terraform configuration uses a Dockerfile to build a static website, which -is then packaged into a ZIP file. The `artifact_builder` module handles the -Docker build and the packaging of the build artifacts. The output is a ZIP file -saved to a local directory. - -The `terraform-aws-cognito-userpool-clients` module is then used to upload this ZIP file -to an AWS S3 bucket and extract its contents into the root of the bucket. This -is done using an AWS Lambda function, which is created and managed by the -module. - -The S3 bucket is configured to serve a static website, with `index.html` as the -index document. After running this Terraform configuration, you should be able -to access the website by navigating to the S3 bucket's website endpoint in your -web browser. - -This example clearly shows the usefulness of the `terraform-aws-cognito-userpool-clients` -module in real-world scenarios, demonstrating how it can be used to automate the -process of deploying static websites to S3. diff --git a/examples/complete/main.tf b/examples/complete/main.tf index e69de29..5553456 100755 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -0,0 +1,50 @@ +locals { + name = "tf-example-complete-${random_string.example_random_suffix.result}" + tags = { tf_module = "cruxstack/cognito-userpool/aws", tf_module_example = "complete" } +} + +# ================================================================== example === + +module "congito_userpool_clients" { + source = "../../" + + userpool_id = module.congito_userpool.id + + client_defaults = { + callback_urls = ["https://example.com/auth/"] + } + + clients = { + internal_web_app = {} + public_web_app = {} + mobile_app = {} + foobar_service = {} + } + + context = module.example_label.context # not required +} + +# ===================================================== supporting-resources === + +module "congito_userpool" { + source = "cruxstack/cognito-userpool/aws" + version = "0.1.1" + + context = module.example_label.context +} + +# ------------------------------------------------------------------- labels --- + +module "example_label" { + source = "cloudposse/label/null" + version = "0.25.0" + + name = local.name + tags = local.tags +} + +resource "random_string" "example_random_suffix" { + length = 6 + special = false + upper = false +} diff --git a/examples/complete/provider.tf b/examples/complete/provider.tf index 09e8b04..c125940 100755 --- a/examples/complete/provider.tf +++ b/examples/complete/provider.tf @@ -1,7 +1,3 @@ provider "aws" { region = "us-east-1" } - -provider "docker" { - host = "unix:///var/run/docker.sock" -} diff --git a/examples/complete/version.tf b/examples/complete/version.tf index 758f8f6..f2702bf 100644 --- a/examples/complete/version.tf +++ b/examples/complete/version.tf @@ -3,8 +3,5 @@ terraform { aws = { source = "hashicorp/aws" } - docker = { - source = "kreuzwerker/docker" - } } } diff --git a/main.tf b/main.tf index e69de29..ab8c20d 100755 --- a/main.tf +++ b/main.tf @@ -0,0 +1,180 @@ +locals { + enabled = module.this.enabled + userpool_id = var.userpool_id + + aws_kv_namespace = trim(coalesce(var.aws_kv_namespace, "cognito-userpool-clients/${local.userpool_id}"), "/") + + defaults = merge(var.client_defaults, { userpool_id = var.userpool_id }) + + clients = { + for client_name in(local.enabled ? keys(var.clients) : []) : client_name => merge(local.defaults, { + name = var.clients[client_name].name + userpool_id = local.defaults.userpool_id + identity_providers = coalesce(var.clients[client_name].identity_providers, local.defaults.identity_providers) + generate_secret = coalesce(var.clients[client_name].generate_secret, local.defaults.generate_secret) + token_revocation_enabled = coalesce(var.clients[client_name].token_revocation_enabled, local.defaults.token_revocation_enabled) + prevent_user_existence_errors = coalesce(var.clients[client_name].prevent_user_existence_errors, local.defaults.prevent_user_existence_errors) + + cognito_auths = { + admin_user_password_auth = coalesce(var.clients[client_name].cognito_auths.admin_user_password_auth, local.defaults.cognito_auths.admin_user_password_auth) + custom_auth = coalesce(var.clients[client_name].cognito_auths.custom_auth, local.defaults.cognito_auths.custom_auth) + refresh_token_auth = coalesce(var.clients[client_name].cognito_auths.refresh_token_auth, local.defaults.cognito_auths.refresh_token_auth) + user_password_auth = coalesce(var.clients[client_name].cognito_auths.user_password_auth, local.defaults.cognito_auths.user_password_auth) + user_srp_auth = coalesce(var.clients[client_name].cognito_auths.user_srp_auth, local.defaults.cognito_auths.user_srp_auth) + } + + read_attributes = distinct(compact(flatten([ + coalesce(var.clients[client_name].read_attributes, local.defaults.read_attributes), + coalesce(var.clients[client_name].read_attributes_builtin_included, local.defaults.read_attributes_builtin_included) == true ? local.builtin_read_attrs : [] + ]))) + + write_attributes = distinct(compact(flatten([ + coalesce(var.clients[client_name].write_attributes, local.defaults.write_attributes), + coalesce(var.clients[client_name].write_attributes_builtin_included, local.defaults.write_attributes_builtin_included) == true ? local.builtin_write_attrs : [] + ]))) + + allowed_oauth_flows = coalesce(var.clients[client_name].allowed_oauth_flows, local.defaults.allowed_oauth_flows) + allowed_oauth_flows_user_pool_client = coalesce(var.clients[client_name].allowed_oauth_flows_user_pool_client, local.defaults.allowed_oauth_flows_user_pool_client) + allowed_oauth_scopes = coalesce(var.clients[client_name].allowed_oauth_scopes, local.defaults.allowed_oauth_scopes) + + callback_urls = try(coalescelist(var.clients[client_name].callback_urls, local.defaults.callback_urls), []) + logout_urls = try(coalescelist(var.clients[client_name].logout_urls, local.defaults.logout_urls), []) + + token_validity = { + access_token = coalesce(var.clients[client_name].token_validity.access_token, local.defaults.token_validity.access_token) + id_token = coalesce(var.clients[client_name].token_validity.id_token, local.defaults.token_validity.id_token) + refresh_token = coalesce(var.clients[client_name].token_validity.refresh_token, local.defaults.token_validity.refresh_token) + } + + token_validity_units = { + access_token = coalesce(var.clients[client_name].token_validity_units.access_token, local.defaults.token_validity_units.access_token) + id_token = coalesce(var.clients[client_name].token_validity_units.id_token, local.defaults.token_validity_units.id_token) + refresh_token = coalesce(var.clients[client_name].token_validity_units.refresh_token, local.defaults.token_validity_units.refresh_token) + } + + ui_customization = { + enabled = coalesce(var.clients[client_name].ui_customization.enabled, local.defaults.ui_customization.enabled) + css_path = try(coalesce(var.clients[client_name].ui_customization.css_path, local.defaults.ui_customization.css_path), "") + image_path = try(coalesce(var.clients[client_name].ui_customization.image_path, local.defaults.ui_customization.image_path), "") + } + }) if var.clients[client_name].enabled + } + + ui_customizations = { + for client_name, client_opts in local.clients : client_name => merge({}, + client_opts.ui_customization + ) if client_opts.ui_customization.enabled && (client_opts.ui_customization.css_path != "" || client_opts.ui_customization.image_path != "") + } + + builtin_read_attrs = [ + "address", + "birthdate", + "email", + "email_verified", + "family_name", + "gender", + "given_name", + "locale", + "middle_name", + "name", + "nickname", + "phone_number", + "phone_number_verified", + "picture", + "preferred_username", + "profile", + "updated_at", + "website", + "zoneinfo" + ] + + builtin_write_attrs = [ + for x in local.builtin_read_attrs : x if !contains(["email_verified", "phone_number_verified"], x) + ] +} + +# ================================================================== clients === + +module "client_label" { + source = "cloudposse/label/null" + version = "0.25.0" + for_each = local.clients + + attributes = [each.key] + context = module.this.context +} + +resource "aws_cognito_user_pool_client" "this" { + for_each = local.clients + + name = coalesce(each.value.name, module.client_label[each.key].id) + user_pool_id = each.value.userpool_id + + explicit_auth_flows = compact([ + each.value.cognito_auths.admin_user_password_auth ? "ALLOW_ADMIN_USER_PASSWORD_AUTH" : "", + each.value.cognito_auths.custom_auth ? "ALLOW_CUSTOM_AUTH" : "", + each.value.cognito_auths.refresh_token_auth ? "ALLOW_REFRESH_TOKEN_AUTH" : "", + each.value.cognito_auths.user_password_auth ? "ALLOW_USER_PASSWORD_AUTH" : "", + each.value.cognito_auths.user_srp_auth ? "ALLOW_USER_SRP_AUTH" : "", + ]) + + enable_token_revocation = each.value.token_revocation_enabled + generate_secret = each.value.generate_secret + prevent_user_existence_errors = each.value.prevent_user_existence_errors + + read_attributes = each.value.read_attributes + write_attributes = each.value.write_attributes + + allowed_oauth_flows = each.value.allowed_oauth_flows + allowed_oauth_flows_user_pool_client = each.value.allowed_oauth_flows_user_pool_client + allowed_oauth_scopes = each.value.allowed_oauth_scopes + + callback_urls = each.value.callback_urls + logout_urls = each.value.logout_urls + supported_identity_providers = each.value.identity_providers + + access_token_validity = each.value.token_validity.access_token + id_token_validity = each.value.token_validity.id_token + refresh_token_validity = each.value.token_validity.refresh_token + + token_validity_units { + access_token = each.value.token_validity_units.access_token + id_token = each.value.token_validity_units.id_token + refresh_token = each.value.token_validity_units.refresh_token + } +} + +resource "aws_cognito_user_pool_ui_customization" "this" { + for_each = local.ui_customizations + + user_pool_id = aws_cognito_user_pool_client.this[each.key].user_pool_id + client_id = aws_cognito_user_pool_client.this[each.key].id + css = each.value.css_path != "" ? file(each.value.css_path) : null + image_file = each.value.image_path != "" ? filebase64(each.value.image_path) : null +} + +# ------------------------------------------------------------------ secrets --- + +resource "aws_secretsmanager_secret" "clients" { + for_each = var.secrets_enabled ? aws_cognito_user_pool_client.this : {} + + name = "${local.aws_kv_namespace}/clients/${each.key}" + description = "client secrets" + kms_key_id = "" + recovery_window_in_days = 0 # not needed bc it is not a secret + tags = module.this.tags +} + +resource "aws_secretsmanager_secret_version" "clients" { + for_each = var.secrets_enabled ? aws_cognito_user_pool_client.this : {} + + secret_id = aws_secretsmanager_secret.clients[each.key].id + secret_string = jsonencode({ + user_pool_id = local.userpool_id + client_id = each.value.id + client_secret = each.value.client_secret + scopes = each.value.allowed_oauth_scopes + callback_urls = each.value.callback_urls + logout_urls = each.value.logout_urls + }) +} diff --git a/outputs.tf b/outputs.tf index 8b13789..1434b54 100755 --- a/outputs.tf +++ b/outputs.tf @@ -1 +1,11 @@ +output "clients" { + description = "Map of Cognito user pool clients created by the module." + value = aws_cognito_user_pool_client.this +} +output "client_secret_names" { + description = "Map of the names of Secrets Manager secrets for the Cognito user pool clients, keyed by client name." + value = { + for client_name, client_secret in aws_secretsmanager_secret.clients : client_name => client_secret.name + } +} diff --git a/variables.tf b/variables.tf index b7cb0da..e3e2d08 100755 --- a/variables.tf +++ b/variables.tf @@ -1 +1,141 @@ # ================================================================== general === + +variable "userpool_id" { + type = string + description = "Cognito user pool ID." + default = null +} + +variable "client_defaults" { + type = object({ + identity_providers = optional(list(string), ["COGNITO"]) + generate_secret = optional(bool, true) + token_revocation_enabled = optional(bool, true) + prevent_user_existence_errors = optional(string, "ENABLED") + + cognito_auths = optional(object({ + admin_user_password_auth = optional(bool, false) + custom_auth = optional(bool, false) + refresh_token_auth = optional(bool, true) + user_password_auth = optional(bool, false) + user_srp_auth = optional(bool, true) + }), { + admin_user_password_auth = false + custom_auth = false + refresh_token_auth = true + user_password_auth = false + user_srp_auth = true + }) + + read_attributes = optional(list(string), []) + read_attributes_builtin_included = optional(bool, true) + write_attributes = optional(list(string), []) + write_attributes_builtin_included = optional(bool, true) + + allowed_oauth_flows = optional(list(string), ["code"]) + allowed_oauth_flows_user_pool_client = optional(bool, true) + allowed_oauth_scopes = optional(list(string), ["email", "openid", "phone", "profile"]) + + callback_urls = optional(list(string), []) + logout_urls = optional(list(string), []) + + token_validity = optional(object({ + access_token = optional(number, 10) + id_token = optional(number, 60) + refresh_token = optional(number, 4320) + }), { + access_token = 10 + id_token = 60 + refresh_token = 4320 + }) + + token_validity_units = optional(object({ + access_token = optional(string, "minutes") + id_token = optional(string, "minutes") + refresh_token = optional(string, "minutes") + }), { + access_token = "minutes" + id_token = "minutes" + refresh_token = "minutes" + }) + + ui_customization = optional(object({ + enabled = optional(bool, false) + css_path = optional(string, "") + image_path = optional(string, "") + }), { + enabled = false + css_path = "" + image_path = "" + }) + }) + description = "Default configurations for each client." + default = {} +} + +variable "clients" { + type = map(object({ + enabled = optional(bool, true) + + name = optional(string) + pool_id = optional(string) + identity_providers = optional(list(string)) + generate_secret = optional(bool) + token_revocation_enabled = optional(bool) + prevent_user_existence_errors = optional(string) + + cognito_auths = optional(object({ + admin_user_password_auth = optional(bool) + custom_auth = optional(bool) + refresh_token_auth = optional(bool) + user_password_auth = optional(bool) + user_srp_auth = optional(bool) + }), {}) + + read_attributes = optional(list(string)) + read_attributes_builtin_included = optional(bool) + write_attributes = optional(list(string)) + write_attributes_builtin_included = optional(bool) + + allowed_oauth_flows = optional(list(string)) + allowed_oauth_flows_user_pool_client = optional(bool) + allowed_oauth_scopes = optional(list(string)) + + callback_urls = optional(list(string), []) + logout_urls = optional(list(string), []) + + token_validity = optional(object({ + access_token = optional(number) + id_token = optional(number) + refresh_token = optional(number) + }), {}) + + token_validity_units = optional(object({ + access_token = optional(string) + id_token = optional(string) + refresh_token = optional(string) + }), {}) + + ui_customization = optional(object({ + enabled = optional(bool) + css_path = optional(string) + image_path = optional(string) + }), {}) + })) + description = "Map of client-specific configurations. The key is the client name." + default = {} +} + +variable "secrets_enabled" { + type = bool + description = "Toggle to create SecretsManger secrets for each client." + default = true +} + +# ------------------------------------------------------------------ context --- + +variable "aws_kv_namespace" { + type = string + description = "The namespace or prefix for AWS SSM parameters and similar resources." + default = "" +}