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)` |
[
"acr",
"amr",
"email",
"idassurancelevel",
"nhsid_nrbac_roles",
"bcss_username",
"sid",
"uid"
]
| 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. |
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.
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. | `any` |
{
"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: - -- - -```java -The following values are needed by the BS-Select application to connect to this Cognito instance: -|Value|Default Profile Value|Description| -|-----|---------------------|-----------| -|spring.security.oauth2.client.registration.nhs-identity.scope|email, openid, profile, aws.cognito.signin.user.admin|The scope used by OAuth2 for the users.| -|spring.security.oauth2.client.registration.nhs-identity.client-id|COGNITO_CLIENT_ID_TO_BE_REPLACED|The client ID for the Cognito user client instance.| -|spring.security.oauth2.client.registration.nhs-identity.client-secret|COGNITO_CLIENT_SECRET_TO_BE_REPLACED|The client secret for the Cognito user client instance.| -|spring.security.oauth2.client.registration.nhs-identity.redirect-uri|https:///bss/login/oauth2/code/nhs-identity|The redirect once authentication has been completed.| -|spring.security.oauth2.client.provider.nhs-identity.issuer-uri||The issuer-uri, the full URL is required but main value required is the ID of the Cognito user pool| -| spring.security.oauth2.client.provider.nhs-identity.cognito-domain ||The domain to direct to for login.| -``` - -## Creating users - -Users for this Cognito client are managed via the users.csv file. The following values need to be -specified: - -| Column | Value | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| UUID | The UUID associated with the user in the BS-Select database. If the user is not in the BS-Select database for the environment, login will fail. | -| bss_username | The BS-Select username associated with the user and the value used for the Username value in Cognito. | -| rbac_role | This replicates the roles CIS2 would provide by using a subset of the data provided. Use the following as the default value for a valid user: `"[{activities=[BS-Select], activity_codes=[B1808]}]"` | -| id_assurance_level | This replicates the assurance level that CIS2 would provide for the user. | - -When running the nonprod-shared infrastructure pipeline, all the users listed in the CSV file will be created (or modified if a change is made) and -will be automatically marked as being valid. All users are created with the same default password specified in the variables.tf file. - - - - -## Requirements - -| Name | Version | -| ---- | ------- | -| [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.47.0 | -| [random](#requirement\_random) | >= 3.9.0 | - -## Providers - -| Name | Version | -| ---- | ------- | -| [aws](#provider\_aws) | 6.50.0 | -| [random](#provider\_random) | 3.9.0 | - -## Modules - -No modules. - -## Resources - -| Name | Type | -| ---- | ---- | -| [aws_cognito_user.cognito_user_creation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user) | resource | -| [aws_cognito_user_pool.cognito_user_pool](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool) | resource | -| [aws_cognito_user_pool_domain.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool_domain) | resource | -| [aws_secretsmanager_secret.password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | -| [aws_secretsmanager_secret_version.password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | -| [random_password.password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | -| [aws_ssm_parameter.cognito_users](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | - -## Inputs - -| Name | Description | Type | Default | Required | -| ---- | ----------- | ---- | ------- | :------: | -| [acr](#input\_acr) | Default ACR value for user attributes | `string` | `"AAL1_USERPASS"` | no | -| [amr](#input\_amr) | Default AMR value for user attributes | `string` | `"USERPASS"` | no | -| [attribute\_names](#input\_attribute\_names) | Cognito custom attributes to create | `list(string)` |
[
"acr",
"amr",
"email",
"idassurancelevel",
"nhsid_nrbac_roles",
"bss_username",
"sid",
"uid"
]
| 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" } } }