diff --git a/infrastructure/modules/cognito/.terraform.lock.hcl b/infrastructure/modules/cognito/.terraform.lock.hcl index ef2e44ed..74487bfd 100644 --- a/infrastructure/modules/cognito/.terraform.lock.hcl +++ b/infrastructure/modules/cognito/.terraform.lock.hcl @@ -3,7 +3,7 @@ provider "registry.terraform.io/hashicorp/aws" { version = "6.50.0" - constraints = ">= 6.47.0" + constraints = ">= 6.0.0, >= 6.14.0, >= 6.42.0" hashes = [ "h1:8y10QFtGLHl3pF/R1/hO7VCPHTexm1whc0BfuG4uruw=", "h1:D8uNiOpl3UkAX4zI5T47ALMiRFXTa1XfdQC+TBu3RmE=", @@ -29,27 +29,28 @@ provider "registry.terraform.io/hashicorp/aws" { ] } -provider "registry.terraform.io/hashicorp/random" { - version = "3.9.0" - constraints = ">= 3.9.0" +provider "registry.terraform.io/hashicorp/awscc" { + version = "1.89.0" hashes = [ - "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=", - "h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=", - "h1:lVDv+0AjDjrLfpmaJbWqUmIw/k3/AHXLc3N4m55SNdo=", - "h1:o0s5Mk9NXMP60nlheO1r0LsDGGratFb3oL0t7bD2QnM=", - "h1:q/uaUTBdKgAmZESrwsoeDQff9uUA/cI/N5ZKNgVwa9c=", - "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", - "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", - "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", - "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", - "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", - "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", - "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", - "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", - "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", - "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", - "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", + "h1:DyIiErDhoQCJi54zEUf7E4GRm5FzAEHJFW9GTu+SNkc=", + "h1:F672dO9zM66TDfIHttDmaENXwd9ZyKi2SiFNA7aOjfo=", + "h1:QZYfne5HgtW+iL0SCZXNIspeDQUIBty+5clq754wXWg=", + "h1:VoA2As0xZ05sC1lFPKActbHEAB4+3WHj/9WuxWMX0ps=", + "h1:ov0Eyeineeq629FvErPttIVKgBV1d0QZJT2zEbn1Ay8=", + "zh:2589d15db6a9a180ac1b021341c1225b9cae59a7d7003c05b787b91ccade504f", + "zh:446d9b1ae47dcf26b8ef49ea2ad3013a406adc33b24c7460d33c779021161f70", + "zh:44927f518b28a5a454a479e4c0eb8021709217cc51c4b1f8b51203cf8f8fa1c4", + "zh:521b1a2d757cf9d41ee3d412b6026b4483c905b92724b98372357bcb22ab43a3", + "zh:719b4f9608d394bc40e8e23d057e9693be2035dca48ad0bbc65e9025bd31c209", + "zh:7ea429e5541af0cd45e87ad9a6d5a56dd91e593413f24ef4935fa6a16930a1ed", + "zh:b6103766e0094296fcd63ac414597cff3d1e1f2491b7de7e8dfd8fc9ad915fd2", + "zh:c2f0ea3039ce2a2433d9839ce04cfdb98652f76c6ee3a69d389f4036785417f1", + "zh:d0da4cfccc05ec2a9e961be468e2e98cea12a331262d3e524a402150f4884db2", + "zh:d17d19cb0337d5e3677e969f09bf0d67dfe286d8e1f34d150430e0d076fda9e1", + "zh:d2972f77c2254759ed0d44fac72b48bfe5701d6b9327f49ce26ce4551cd42884", + "zh:dcec5e6970dbf90624dfe648df7e8b3fa2963d3c5cbd1e0748dbad9a2eea097e", + "zh:dfffb14a9818224ae7004dda8c259439afdc94aabe208793fe24d2c9760b2b14", + "zh:f03ee3c782533a2e04bccbdc2291e1922b6bfabcd03a5a1be4f8a97059bef4d1", + "zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32", ] } diff --git a/infrastructure/modules/cognito/README.md b/infrastructure/modules/cognito/README.md new file mode 100644 index 00000000..5f93898a --- /dev/null +++ b/infrastructure/modules/cognito/README.md @@ -0,0 +1,171 @@ +# Cognito + +NHS Screening wrapper around the community +[`lgallard/cognito-user-pool/aws`](https://registry.terraform.io/modules/lgallard/cognito-user-pool/aws/4.0.2) +module that enforces the platform's baseline controls and consumes +the shared `context.tf` for naming and tagging. + +It keeps the default pool/domain/schema behaviour from the existing bespoke +module where practical, but deliberately avoids carrying forward the old +Secrets Manager password flow. + +## Design choices + +* Uses the upstream `lgallard/cognito-user-pool/aws` module pinned to `4.0.2` +* Derives the user pool name from `user_pool_name`, then `name_prefix`, then the + shared context-derived module ID +* Creates a Cognito domain by default, following the prior module behaviour +* Enables `ignore_schema_changes = true` by default because this is recommended + for new Cognito deployments with custom schemas +* Keeps a small compatibility layer for legacy inputs such as `name_prefix` and + `attribute_names` +* Narrows application client configuration to an `app_clients` interface instead + of exposing the upstream generic `clients`, `resource_servers`, `user_groups`, + and `identity_providers` inputs directly +* Supports optional bootstrap user creation because the current BCSS Cognito + stacks still provision initial users during stack deployment + +## Usage + +```hcl +module "cognito" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/cognito?ref=main" + + name = "cognito" + project = "shared" + environment = "dev" + + app_clients = [ + { + callback_urls = ["https://example.internal/login/oauth2/code/nhs-identity"] + logout_urls = ["https://example.internal/logout"] + default_redirect_uri = "https://example.internal/login/oauth2/code/nhs-identity" + + allowed_oauth_flows_user_pool_client = true + allowed_oauth_flows = ["code"] + allowed_oauth_scopes = [ + "email", + "openid", + "profile", + "aws.cognito.signin.user.admin", + ] + supported_identity_providers = ["COGNITO"] + generate_secret = true + } + ] + + bootstrap_users = [ + { + uuid = "11111111-1111-1111-1111-111111111111" + bcss_username = "test.user" + id_assurance_level = "3" + rbac_role = "[{activities=[BS-Select], activity_codes=[B1808]}]" + } + ] +} +``` + +## Current defaults inherited from the old module + +* `auto_verified_attributes = ["email"]` +* `mfa_configuration = "OFF"` +* `admin_create_user_config.allow_admin_create_user_only = false` +* `email_configuration.email_sending_account = "COGNITO_DEFAULT"` +* `verification_message_template.default_email_option = "CONFIRM_WITH_CODE"` +* `username_configuration.case_sensitive = false` +* `attribute_names` produces the same default custom string attributes as the old module +* `bootstrap_users` can be used to preserve the current stack behavior where Cognito users are created during deployment + +## What this module does NOT do + +* It does not create or replicate a Secrets Manager password secret +* It does not create the KMS keys or SSM parameters used by the older external + and training stacks; those remain stack-level concerns and can consume this + module's outputs instead + +The wrapper does not directly expose the upstream generic inputs for: + +* identity providers +* resource servers +* user groups +* arbitrary client definitions beyond the curated `app_clients` OAuth client shape + +If those become required later, they can be added back with an explicit shared-resources use case. + + + + +## Requirements + +| Name | Version | +| ---- | ------- | +| [terraform](#requirement\_terraform) | >= 1.13 | +| [aws](#requirement\_aws) | >= 6.42 | +| [awscc](#requirement\_awscc) | >= 1.89 | + +## Providers + +| Name | Version | +| ---- | ------- | +| [aws](#provider\_aws) | 6.50.0 | + +## Modules + +| Name | Source | Version | +| ---- | ------ | ------- | +| [cognito](#module\_cognito) | lgallard/cognito-user-pool/aws | 4.0.2 | +| [this](#module\_this) | git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags | v2.6.0 | + +## Resources + +| Name | Type | +| ---- | ---- | +| [aws_cognito_user.bootstrap_users](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| ---- | ----------- | ---- | ------- | :------: | +| [acr](#input\_acr) | ACR attribute applied to bootstrap Cognito users. | `string` | `"AAL1_USERPASS"` | no | +| [amr](#input\_amr) | AMR attribute applied to bootstrap Cognito users. | `string` | `"USERPASS"` | no | +| [app\_clients](#input\_app\_clients) | List of Cognito application clients to create. This wrapper intentionally supports the shared-resources OAuth client pattern rather than the full upstream clients surface. |
list(object({
name = optional(string)
callback_urls = list(string)
logout_urls = optional(list(string), [])
default_redirect_uri = optional(string)
generate_secret = optional(bool, true)
auth_session_validity = optional(number, 3)
enable_propagate_additional_user_context_data = optional(bool, false)
id_token_validity = optional(number)
refresh_token_validity = optional(number)
prevent_user_existence_errors = optional(string)
enable_token_revocation = optional(bool, true)
})) | `[]` | no |
+| [attribute\_names](#input\_attribute\_names) | Compatibility list of simple string schema attributes. Used to derive string\_schemas when string\_schemas is empty. | `list(string)` | [| no | +| [aws\_region](#input\_aws\_region) | AWS region used for derived Cognito hosted UI outputs. | `string` | `"eu-west-2"` | no | +| [bootstrap\_users](#input\_bootstrap\_users) | Optional list of bootstrap Cognito users to create. This covers the current BCSS stack pattern where initial training or shared users are provisioned during stack deployment. |
"acr",
"amr",
"email",
"idassurancelevel",
"nhsid_nrbac_roles",
"bcss_username",
"sid",
"uid"
]
list(object({
uuid = string
bcss_username = string
id_assurance_level = string
rbac_role = string
user_password = optional(string)
})) | `[]` | no |
+| [context](#input\_context) | Single object for setting entire context at once.{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
} | no |
+| [create](#input\_create) | Determines whether Cognito resources will be created. | `bool` | `true` | no |
+| [deletion\_protection](#input\_deletion\_protection) | Deletion protection setting for the user pool. Valid values are ACTIVE and INACTIVE. | `string` | `"INACTIVE"` | no |
+| [domain](#input\_domain) | Optional Cognito user pool domain prefix. Defaults to name\_prefix or the resolved user pool name. | `string` | `null` | no |
+| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources. | `bool` | `null` | no |
+| [environment](#input\_environment) | Environment identifier used by the shared tags module. | `string` | `null` | no |
+| [message\_action](#input\_message\_action) | Message action for bootstrap Cognito user creation. Defaults to SUPPRESS to match the current BCSS stacks. | `string` | `"SUPPRESS"` | no |
+| [mfa\_configuration](#input\_mfa\_configuration) | MFA setting for the user pool. Valid values are ON, OFF, or OPTIONAL. | `string` | `"OFF"` | no |
+| [name](#input\_name) | Name identifier used by the shared tags module. | `string` | `null` | no |
+| [name\_prefix](#input\_name\_prefix) | Compatibility alias for older callers. Used as the default user pool and domain prefix when user\_pool\_name or domain are unset. | `string` | `null` | no |
+| [project](#input\_project) | Project identifier used by the shared tags module. | `string` | `null` | no |
+| [service](#input\_service) | Service identifier used by the shared tags module. | `string` | `null` | no |
+| [tags](#input\_tags) | Additional tags merged with any tags supplied through the context object. | `map(string)` | `{}` | no |
+| [terraform\_source](#input\_terraform\_source) | Source location to record in the Terraform\_source tag. Defaults to the caller module path when not set. | `string` | `null` | no |
+| [user\_email](#input\_user\_email) | Email attribute applied to bootstrap Cognito users. | `string` | `"nhsdigital.axe@nhs.net"` | no |
+| [user\_password](#input\_user\_password) | Fallback password for bootstrap Cognito users when an individual bootstrap\_users entry does not provide user\_password. | `string` | `"changeme"` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [app\_client\_ids](#output\_app\_client\_ids) | Map of shared-resources app client names to client IDs. |
+| [app\_client\_secrets](#output\_app\_client\_secrets) | Map of shared-resources app client names to client secrets. |
+| [client\_ids](#output\_client\_ids) | IDs of any Cognito user pool clients created by this module. |
+| [client\_ids\_map](#output\_client\_ids\_map) | Map of Cognito client names to client IDs. |
+| [client\_secrets](#output\_client\_secrets) | Secrets of any Cognito user pool clients created by this module. |
+| [client\_secrets\_map](#output\_client\_secrets\_map) | Map of Cognito client names to client secrets. |
+| [secrets\_manager\_random\_passsword\_arn](#output\_secrets\_manager\_random\_passsword\_arn) | Deprecated compatibility output from the bespoke BS-Select bootstrap-user flow. This wrapper does not create a bootstrap user secret. |
+| [user\_pool\_arn](#output\_user\_pool\_arn) | ARN of the Cognito user pool. |
+| [user\_pool\_domain\_prefix](#output\_user\_pool\_domain\_prefix) | Configured Cognito domain value. |
+| [user\_pool\_endpoint](#output\_user\_pool\_endpoint) | Cognito user pool endpoint. |
+| [user\_pool\_hosted\_ui\_url](#output\_user\_pool\_hosted\_ui\_url) | Hosted UI URL for the Cognito domain when a default domain prefix is configured. |
+| [user\_pool\_id](#output\_user\_pool\_id) | ID of the Cognito user pool. |
+| [user\_pool\_name](#output\_user\_pool\_name) | Name of the Cognito user pool. |
+
+
+
diff --git a/infrastructure/modules/cognito/context.tf b/infrastructure/modules/cognito/context.tf
new file mode 100644
index 00000000..0e99c0b4
--- /dev/null
+++ b/infrastructure/modules/cognito/context.tf
@@ -0,0 +1,145 @@
+# tflint-ignore-file: terraform_standard_module_structure, terraform_unused_declarations
+#
+# ONLY EDIT THIS FILE IN github.com/NHSDigital/screening-terraform-modules-aws/infrastructure/modules/tags
+# All other instances of this file should be a copy of that one
+#
+#
+# Copy this file from https://github.com/NHSDigital/screening-terraform-modules-aws/blob/master/infrastructure/modules/tags/exports/context.tf
+# and then place it in your Terraform module to automatically get
+# tag module standard configuration inputs suitable for passing
+# to other modules.
+#
+# curl -sL https://raw.githubusercontent.com/NHSDigital/screening-terraform-modules-aws/master/infrastructure/modules/tags/exports/context.tf -o context.tf
+#
+# Modules should access the whole context as `module.this.context`
+# to get the input variables with nulls for defaults,
+# for example `context = module.this.context`,
+# and access individual variables as `module.this.`,
+# with final values filled in.
+#
+# For example, when using defaults, `module.this.context.delimiter`
+# will be null, and `module.this.delimiter` will be `-` (hyphen).
+#
+
+module "this" {
+ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=v2.6.0"
+
+ enabled = coalesce(var.enabled, lookup(var.context, "enabled", true))
+ service = coalesce(var.service, lookup(var.context, "service", null))
+ project = coalesce(var.project, lookup(var.context, "project", null))
+ region = lookup(var.context, "region", null)
+ environment = coalesce(var.environment, lookup(var.context, "environment", null))
+ stack = lookup(var.context, "stack", null)
+ workspace = lookup(var.context, "workspace", null)
+ name = coalesce(var.name, lookup(var.context, "name", null))
+ delimiter = lookup(var.context, "delimiter", null)
+ attributes = lookup(var.context, "attributes", [])
+ tags = merge(lookup(var.context, "tags", {}), var.tags)
+ additional_tag_map = lookup(var.context, "additional_tag_map", {})
+ label_order = lookup(var.context, "label_order", [])
+ regex_replace_chars = lookup(var.context, "regex_replace_chars", null)
+ id_length_limit = lookup(var.context, "id_length_limit", null)
+ label_key_case = lookup(var.context, "label_key_case", null)
+ label_value_case = lookup(var.context, "label_value_case", null)
+ terraform_source = coalesce(var.terraform_source, lookup(var.context, "terraform_source", null), path.module)
+ descriptor_formats = lookup(var.context, "descriptor_formats", {})
+ labels_as_tags = toset(lookup(var.context, "labels_as_tags", ["unset"]))
+
+ context = var.context
+}
+
+variable "aws_region" {
+ type = string
+ description = "AWS region used for derived Cognito hosted UI outputs."
+ default = "eu-west-2"
+
+ validation {
+ condition = contains(["eu-west-1", "eu-west-2", "us-east-1"], var.aws_region)
+ error_message = "AWS Region must be one of eu-west-1, eu-west-2, us-east-1"
+ }
+}
+
+variable "context" {
+ type = any
+ default = {
+ enabled = true
+ service = null
+ project = null
+ region = null
+ environment = null
+ stack = null
+ workspace = null
+ name = null
+ delimiter = null
+ attributes = []
+ tags = {}
+ additional_tag_map = {}
+ regex_replace_chars = null
+ label_order = []
+ id_length_limit = null
+ label_key_case = null
+ label_value_case = null
+ terraform_source = null
+ descriptor_formats = {}
+ labels_as_tags = ["unset"]
+ }
+ description = <<-EOT
+ Single object for setting entire context at once.
+ See description of individual variables for details.
+ Leave string and numeric variables as `null` to use default value.
+ Individual variable settings (non-null) override settings in context object,
+ except for attributes, tags, and additional_tag_map, which are merged.
+ EOT
+
+ validation {
+ condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"])
+ error_message = "Allowed values: `lower`, `title`, `upper`."
+ }
+
+ validation {
+ condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"])
+ error_message = "Allowed values: `lower`, `title`, `upper`, `none`."
+ }
+}
+
+variable "terraform_source" {
+ type = string
+ default = null
+ description = "Source location to record in the Terraform_source tag. Defaults to the caller module path when not set."
+}
+
+variable "enabled" {
+ type = bool
+ default = null
+ description = "Set to false to prevent the module from creating any resources."
+}
+
+variable "service" {
+ type = string
+ default = null
+ description = "Service identifier used by the shared tags module."
+}
+
+variable "project" {
+ type = string
+ default = null
+ description = "Project identifier used by the shared tags module."
+}
+
+variable "environment" {
+ type = string
+ default = null
+ description = "Environment identifier used by the shared tags module."
+}
+
+variable "name" {
+ type = string
+ default = null
+ description = "Name identifier used by the shared tags module."
+}
+
+variable "tags" {
+ type = map(string)
+ default = {}
+ description = "Additional tags merged with any tags supplied through the context object."
+}
diff --git a/infrastructure/modules/cognito/main.tf b/infrastructure/modules/cognito/main.tf
index c9fe18dc..0e8908cc 100644
--- a/infrastructure/modules/cognito/main.tf
+++ b/infrastructure/modules/cognito/main.tf
@@ -1,110 +1,114 @@
-data "aws_ssm_parameter" "cognito_users" {
- name = "/${var.name_prefix}/cognito/users"
-}
-
locals {
- userdata = jsondecode(nonsensitive(data.aws_ssm_parameter.cognito_users.value))
-}
-
-resource "random_password" "password" {
- length = 20
- special = true
- override_special = "!%^*-_+="
-}
+ user_pool_name = coalesce(var.name_prefix != null ? "${var.name_prefix}-users-pool" : null, module.this.id)
+ domain_name = coalesce(var.domain, var.name_prefix, local.user_pool_name)
+ default_app_client_name = coalesce(var.name_prefix != null ? "${var.name_prefix}-users-client" : null, "${local.user_pool_name}-client")
-resource "aws_secretsmanager_secret" "password" {
- name = "${var.name_prefix}-cognito-user"
- recovery_window_in_days = var.recovery_window
-
- dynamic "replica" {
- for_each = var.secret_replication_regions
- content {
- region = replica.value
- }
+ default_admin_create_user_config = {
+ allow_admin_create_user_only = false
}
-}
-
-resource "aws_secretsmanager_secret_version" "password" {
- secret_id = aws_secretsmanager_secret.password.id
- secret_string = jsonencode({
- password = random_password.password.result
- })
-}
-# Create user pool
-resource "aws_cognito_user_pool" "cognito_user_pool" {
- auto_verified_attributes = [
- "email",
- ]
+ default_email_configuration = {
+ email_sending_account = "COGNITO_DEFAULT"
+ }
- deletion_protection = var.deletion_protection
- mfa_configuration = var.mfa_configuration
- name = var.name_prefix
- username_attributes = []
+ default_password_policy = {
+ minimum_length = 8
+ require_lowercase = true
+ require_numbers = true
+ require_symbols = true
+ require_uppercase = true
+ temporary_password_validity_days = 7
+ password_history_size = 0 # https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-cognito-userpool-passwordpolicy.html#cfn-cognito-userpool-passwordpolicy-passwordhistorysize
+ }
- account_recovery_setting {
- recovery_mechanism {
+ default_recovery_mechanisms = [
+ {
name = "verified_email"
priority = 1
- }
- recovery_mechanism {
+ },
+ {
name = "verified_phone_number"
priority = 2
}
- }
+ ]
- admin_create_user_config {
- allow_admin_create_user_only = false
+ default_username_configuration = {
+ case_sensitive = false
}
- email_configuration {
- email_sending_account = "COGNITO_DEFAULT"
+ default_verification_message_template = {
+ default_email_option = "CONFIRM_WITH_CODE"
}
- password_policy {
- minimum_length = 8
- require_lowercase = false
- require_numbers = false
- require_symbols = false
- require_uppercase = false
- temporary_password_validity_days = 7
- }
- dynamic "schema" {
- for_each = var.attribute_names
- content {
+ default_string_schemas = [
+ for attribute_name in var.attribute_names : {
attribute_data_type = "String"
developer_only_attribute = false
mutable = true
- name = schema.value
+ name = attribute_name
required = false
-
- string_attribute_constraints {
- max_length = "256"
+ string_attribute_constraints = {
min_length = "1"
+ max_length = "256"
}
}
- }
- username_configuration {
- case_sensitive = false
- }
+ ]
- verification_message_template {
- default_email_option = "CONFIRM_WITH_CODE"
- }
+ cognito_clients = [
+ for client in var.app_clients : {
+ name = try(client.name, local.default_app_client_name)
+ callback_urls = client.callback_urls
+ logout_urls = client.logout_urls
+ default_redirect_uri = try(client.default_redirect_uri, null)
+ generate_secret = try(client.generate_secret, true)
+ auth_session_validity = try(client.auth_session_validity, 3)
+ allowed_oauth_flows_user_pool_client = true
+ allowed_oauth_flows = ["code"]
+ allowed_oauth_scopes = ["email", "openid", "profile", "aws.cognito.signin.user.admin"]
+ explicit_auth_flows = ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_USER_SRP_AUTH", "ALLOW_USER_PASSWORD_AUTH"]
+ supported_identity_providers = ["COGNITO"]
+ enable_propagate_additional_user_context_data = try(client.enable_propagate_additional_user_context_data, false)
+ access_token_validity = 60
+ id_token_validity = try(client.id_token_validity, null)
+ refresh_token_validity = try(client.refresh_token_validity, null)
+ token_validity_units = { access_token = "minutes", id_token = "minutes", refresh_token = "days" }
+ prevent_user_existence_errors = try(client.prevent_user_existence_errors, null)
+ enable_token_revocation = try(client.enable_token_revocation, true)
+ }
+ ]
}
-
-resource "aws_cognito_user_pool_domain" "main" {
- domain = var.name_prefix
- user_pool_id = aws_cognito_user_pool.cognito_user_pool.id
+module "cognito" {
+ source = "lgallard/cognito-user-pool/aws"
+ version = "4.0.2"
+
+ enabled = module.this.enabled
+
+ user_pool_name = local.user_pool_name
+ domain = local.domain_name
+ deletion_protection = var.deletion_protection
+ auto_verified_attributes = ["email"]
+ mfa_configuration = var.mfa_configuration
+
+ admin_create_user_config = local.default_admin_create_user_config
+ email_configuration = local.default_email_configuration
+ password_policy = local.default_password_policy
+ recovery_mechanisms = local.default_recovery_mechanisms
+ string_schemas = local.default_string_schemas
+ username_configuration = local.default_username_configuration
+ verification_message_template = local.default_verification_message_template
+ clients = local.cognito_clients
+ ignore_schema_changes = true
+
+ tags = module.this.tags
}
-resource "aws_cognito_user" "cognito_user_creation" {
- for_each = { for inst in local.userdata : inst.uuid => inst }
+resource "aws_cognito_user" "bootstrap_users" {
+ for_each = module.this.enabled && var.create ? { for user in var.bootstrap_users : user.uuid => user } : {}
- user_pool_id = aws_cognito_user_pool.cognito_user_pool.id
- username = each.value.bss_username
- password = random_password.password.result
+ user_pool_id = module.cognito.id
+ username = each.value.bcss_username
+ password = coalesce(try(each.value.user_password, null), var.user_password)
message_action = var.message_action
attributes = {
@@ -114,7 +118,7 @@ resource "aws_cognito_user" "cognito_user_creation" {
email_verified = true
idassurancelevel = each.value.id_assurance_level
nhsid_nrbac_roles = each.value.rbac_role
- bss_username = each.value.bss_username
+ bcss_username = each.value.bcss_username
sid = each.value.uuid
uid = each.value.uuid
}
diff --git a/infrastructure/modules/cognito/outputs.tf b/infrastructure/modules/cognito/outputs.tf
index c97cbac9..3cb1cfb9 100644
--- a/infrastructure/modules/cognito/outputs.tf
+++ b/infrastructure/modules/cognito/outputs.tf
@@ -1,14 +1,67 @@
output "user_pool_id" {
- description = "ID of the Cognito user pool"
- value = aws_cognito_user_pool.cognito_user_pool.id
+ description = "ID of the Cognito user pool."
+ value = module.cognito.id
}
-output "secrets_manager_random_passsword_arn" {
- description = "ARN of the Secrets Manager secret containing generated password"
- value = aws_secretsmanager_secret.password.arn
+output "user_pool_arn" {
+ description = "ARN of the Cognito user pool."
+ value = module.cognito.arn
+}
+
+output "user_pool_name" {
+ description = "Name of the Cognito user pool."
+ value = module.cognito.name
+}
+
+output "user_pool_endpoint" {
+ description = "Cognito user pool endpoint."
+ value = module.cognito.endpoint
}
output "user_pool_domain_prefix" {
- description = "Domain prefix configured for the Cognito user pool domain"
- value = aws_cognito_user_pool_domain.main.domain
+ description = "Configured Cognito domain value."
+ value = local.domain_name
+}
+
+output "user_pool_hosted_ui_url" {
+ description = "Hosted UI URL for the Cognito domain when a default domain prefix is configured."
+ value = local.domain_name != null ? "https://${local.domain_name}.auth.${var.aws_region}.amazoncognito.com" : null
+}
+
+output "client_ids" {
+ description = "IDs of any Cognito user pool clients created by this module."
+ value = module.cognito.client_ids
+}
+
+output "client_ids_map" {
+ description = "Map of Cognito client names to client IDs."
+ value = module.cognito.client_ids_map
+}
+
+output "app_client_ids" {
+ description = "Map of shared-resources app client names to client IDs."
+ value = module.cognito.client_ids_map
+}
+
+output "client_secrets" {
+ description = "Secrets of any Cognito user pool clients created by this module."
+ value = module.cognito.client_secrets
+ sensitive = true
+}
+
+output "client_secrets_map" {
+ description = "Map of Cognito client names to client secrets."
+ value = module.cognito.client_secrets_map
+ sensitive = true
+}
+
+output "app_client_secrets" {
+ description = "Map of shared-resources app client names to client secrets."
+ value = module.cognito.client_secrets_map
+ sensitive = true
+}
+
+output "secrets_manager_random_passsword_arn" {
+ description = "Deprecated compatibility output from the bespoke BS-Select bootstrap-user flow. This wrapper does not create a bootstrap user secret."
+ value = null
}
diff --git a/infrastructure/modules/cognito/readme.md b/infrastructure/modules/cognito/readme.md
deleted file mode 100644
index ad047a07..00000000
--- a/infrastructure/modules/cognito/readme.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Cognito
-
-## Summary
-
-This is a OAuth2 client that allows us to log into the BS-Select application using the
-same controls and security that CIS2 offers. We have the ability to control the configuration
-of the client, including the users available for logging in.
-
-## Useful Values
-
-The Cognito client when created will be accessible via the following url:
-
-- [| no | -| [deletion\_protection](#input\_deletion\_protection) | Whether user pool deletion protection is enabled | `string` | `"INACTIVE"` | no | -| [environment](#input\_environment) | The name of the Environment this is deployed into, for example CICD, NFT, UAT or PROD | `string` | n/a | yes | -| [message\_action](#input\_message\_action) | Message action used when creating users | `string` | `"SUPPRESS"` | no | -| [mfa\_configuration](#input\_mfa\_configuration) | MFA mode for the user pool | `string` | `"OFF"` | no | -| [name\_prefix](#input\_name\_prefix) | The account, environment etc | `string` | n/a | yes | -| [recovery\_window](#input\_recovery\_window) | The number of days that credentials should be retained for | `number` | n/a | yes | -| [secret\_replication\_regions](#input\_secret\_replication\_regions) | List of additional regions where created secrets should be replicated | `list(string)` | n/a | yes | -| [user\_email](#input\_user\_email) | Initial user email address | `string` | `"nhsdigital.axe@nhs.net"` | no | -| [user\_password](#input\_user\_password) | Initial user password placeholder | `string` | `"changeme"` | no | - -## Outputs - -| Name | Description | -| ---- | ----------- | -| [secrets\_manager\_random\_passsword\_arn](#output\_secrets\_manager\_random\_passsword\_arn) | ARN of the Secrets Manager secret containing generated password | -| [user\_pool\_domain\_prefix](#output\_user\_pool\_domain\_prefix) | Domain prefix configured for the Cognito user pool domain | -| [user\_pool\_id](#output\_user\_pool\_id) | ID of the Cognito user pool | - - - diff --git a/infrastructure/modules/cognito/variables.tf b/infrastructure/modules/cognito/variables.tf index fee1d823..582406da 100644 --- a/infrastructure/modules/cognito/variables.tf +++ b/infrastructure/modules/cognito/variables.tf @@ -1,76 +1,128 @@ -#################################################################################### -# BSS COMMON -#################################################################################### +variable "create" { + description = "Determines whether Cognito resources will be created." + type = bool + default = true +} variable "name_prefix" { - description = "The account, environment etc" + description = "Compatibility alias for older callers. Used as the default user pool and domain prefix when user_pool_name or domain are unset." type = string + default = null } -# tflint-ignore: terraform_unused_declarations -variable "environment" { - description = "The name of the Environment this is deployed into, for example CICD, NFT, UAT or PROD" +variable "domain" { + description = "Optional Cognito user pool domain prefix. Defaults to name_prefix or the resolved user pool name." type = string + default = null } -################################################################################## -# COGNITO -################################################################################## variable "deletion_protection" { - description = "Whether user pool deletion protection is enabled" + description = "Deletion protection setting for the user pool. Valid values are ACTIVE and INACTIVE." type = string default = "INACTIVE" + + validation { + condition = contains(["ACTIVE", "INACTIVE"], var.deletion_protection) + error_message = "Allowed values: `ACTIVE`, `INACTIVE`." + } } variable "mfa_configuration" { - description = "MFA mode for the user pool" + description = "MFA setting for the user pool. Valid values are ON, OFF, or OPTIONAL." type = string default = "OFF" + + validation { + condition = contains(["ON", "OFF", "OPTIONAL"], var.mfa_configuration) + error_message = "Allowed values: `ON`, `OFF`, `OPTIONAL`." + } } variable "attribute_names" { - description = "Cognito custom attributes to create" + description = "Compatibility list of simple string schema attributes. Used to derive string_schemas when string_schemas is empty." type = list(string) - default = ["acr", "amr", "email", "idassurancelevel", "nhsid_nrbac_roles", "bss_username", "sid", "uid"] + default = ["acr", "amr", "email", "idassurancelevel", "nhsid_nrbac_roles", "bcss_username", "sid", "uid"] +} + +variable "app_clients" { + description = "List of Cognito application clients to create. This wrapper intentionally supports the shared-resources OAuth client pattern rather than the full upstream clients surface." + type = list(object({ + name = optional(string) + callback_urls = list(string) + logout_urls = optional(list(string), []) + default_redirect_uri = optional(string) + generate_secret = optional(bool, true) + auth_session_validity = optional(number, 3) + enable_propagate_additional_user_context_data = optional(bool, false) + id_token_validity = optional(number) + refresh_token_validity = optional(number) + prevent_user_existence_errors = optional(string) + enable_token_revocation = optional(bool, true) + })) + default = [] + + validation { + condition = alltrue([ + for client in var.app_clients : + try(client.default_redirect_uri, null) == null || contains(client.callback_urls, client.default_redirect_uri) + ]) + error_message = "Each app_clients.default_redirect_uri must also appear in app_clients.callback_urls." + } +} + +variable "bootstrap_users" { + description = "Optional list of bootstrap Cognito users to create. This covers the current BCSS stack pattern where initial training or shared users are provisioned during stack deployment." + type = list(object({ + uuid = string + bcss_username = string + id_assurance_level = string + rbac_role = string + user_password = optional(string) + })) + default = [] + + validation { + condition = alltrue([ + for user in var.bootstrap_users : + user.uuid != "" && user.bcss_username != "" && user.id_assurance_level != "" && user.rbac_role != "" + ]) + error_message = "Each bootstrap user must include non-empty uuid, bcss_username, id_assurance_level, and rbac_role values." + } } variable "message_action" { - description = "Message action used when creating users" + description = "Message action for bootstrap Cognito user creation. Defaults to SUPPRESS to match the current BCSS stacks." type = string default = "SUPPRESS" + + validation { + condition = contains(["SUPPRESS", "RESEND"], var.message_action) + error_message = "Allowed values: `SUPPRESS`, `RESEND`." + } } variable "acr" { - description = "Default ACR value for user attributes" + description = "ACR attribute applied to bootstrap Cognito users." type = string default = "AAL1_USERPASS" } variable "amr" { - description = "Default AMR value for user attributes" + description = "AMR attribute applied to bootstrap Cognito users." type = string default = "USERPASS" } variable "user_email" { - description = "Initial user email address" + description = "Email attribute applied to bootstrap Cognito users." type = string default = "nhsdigital.axe@nhs.net" } # tflint-ignore: terraform_unused_declarations variable "user_password" { - description = "Initial user password placeholder" + description = "Fallback password for bootstrap Cognito users when an individual bootstrap_users entry does not provide user_password." type = string default = "changeme" -} - -variable "recovery_window" { - description = "The number of days that credentials should be retained for" - type = number -} - -variable "secret_replication_regions" { - description = "List of additional regions where created secrets should be replicated" - type = list(string) + sensitive = true } diff --git a/infrastructure/modules/cognito/versions.tf b/infrastructure/modules/cognito/versions.tf index 2af6cc14..9fc50fac 100644 --- a/infrastructure/modules/cognito/versions.tf +++ b/infrastructure/modules/cognito/versions.tf @@ -1,15 +1,18 @@ terraform { - required_version = ">= 1.5.7" + required_version = ">= 1.13" required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.47.0" + version = ">= 6.42" } - random = { - source = "hashicorp/random" - version = ">= 3.9.0" + awscc = { + # Not used directly in this module + # Used by `lgallard/cognito-user-pool/aws` for managed login branding + # Not clear what the minimum version should be + source = "hashicorp/awscc" + version = ">= 1.89" } } }
"acr",
"amr",
"email",
"idassurancelevel",
"nhsid_nrbac_roles",
"bss_username",
"sid",
"uid"
]