Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion infrastructure/modules/acm/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ module "acm" {
dns_ttl = var.dns_ttl

# ----------------------------------------------------------------
region = module.this.region
region = var.aws_region
tags = module.this.tags
}
119 changes: 100 additions & 19 deletions infrastructure/modules/cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ 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
* Derives the user pool name from `var.user_pool_name`, falling back to
`<module.this.id>-users-pool`
* 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
Expand All @@ -27,9 +25,11 @@ Secrets Manager password flow.

## Usage

### Minimal

```hcl
module "cognito" {
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/cognito?ref=main"
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/cognito?ref=vX.Y.Z"

name = "cognito"
project = "shared"
Expand All @@ -40,32 +40,113 @@ module "cognito" {
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"
generate_secret = true
},
]
}
```

### Common production-style

```hcl
module "cognito" {
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/cognito?ref=vX.Y.Z"

name = "bcss"
project = "screening"
environment = "prod"
context = module.this.context

deletion_protection = "ACTIVE"
mfa_configuration = "OPTIONAL"

app_clients = [
{
name = "bcss-web-client"
callback_urls = ["https://bcss.nhs.net/login/oauth2/code/nhs-identity"]
logout_urls = ["https://bcss.nhs.net/logout"]
default_redirect_uri = "https://bcss.nhs.net/login/oauth2/code/nhs-identity"
generate_secret = true
id_token_validity = 60
refresh_token_validity = 8
},
]
}
```

### Advanced — custom naming with bootstrap users

```hcl
module "cognito" {
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/cognito?ref=vX.Y.Z"

name = "bcss"
project = "screening"
environment = "training"
context = module.this.context

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
}
user_pool_name = "training-users-pool"
domain = "bcss-training"
app_client_name = "training-web-client"

app_clients = [
{
callback_urls = ["https://training.bcss.nhs.net/login/oauth2/code/nhs-identity"]
logout_urls = ["https://training.bcss.nhs.net/logout"]
default_redirect_uri = "https://training.bcss.nhs.net/login/oauth2/code/nhs-identity"
generate_secret = true
},
]

bootstrap_users = [
{
uuid = "11111111-1111-1111-1111-111111111111"
bcss_username = "test.user"
bcss_username = "training.user.1"
id_assurance_level = "3"
rbac_role = "[{activities=[BS-Select], activity_codes=[B1808]}]"
user_password = "InitialPass1!"
},
{
uuid = "22222222-2222-2222-2222-222222222222"
bcss_username = "training.user.2"
id_assurance_level = "3"
rbac_role = "[{activities=[BS-Select], activity_codes=[B1808]}]"
}
},
]
}
```

## Current defaults inherited from the old module
## Conventions

### Naming

All names derive from `module.this.id` (provided via `context.tf`) but can be
overridden individually:

|Resource|Default|Override variable|
|---|---|---|
|User pool name|`<module.this.id>-users-pool`|`var.user_pool_name`|
|Domain prefix|`<module.this.id>`|`var.domain`|
|App client name|`<module.this.id>-users-client`|`var.app_client_name`|

### Application clients

All clients receive the following hardcoded settings regardless of caller input:

- `allowed_oauth_flows = ["code"]`
- `allowed_oauth_flows_user_pool_client = true`
- `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"]`
- `access_token_validity = 60` (minutes)

### Schema changes

`ignore_schema_changes = true` is hardcoded. Cognito does not support removing
custom attributes after creation; enabling drift-detection would cause unnecessary
plan noise on every run.

## Enforced defaults

* `auto_verified_attributes = ["email"]`
* `mfa_configuration = "OFF"`
Expand Down
1 change: 1 addition & 0 deletions infrastructure/modules/cognito/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data "aws_region" "current" {}
59 changes: 59 additions & 0 deletions infrastructure/modules/cognito/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
locals {
user_pool_name = coalesce(var.user_pool_name, "${module.this.id}-users-pool")
domain_name = coalesce(var.domain, module.this.id)
default_app_client_name = coalesce(var.app_client_name, "${module.this.id}-users-client")

default_admin_create_user_config = {
allow_admin_create_user_only = false
}

default_email_configuration = {
email_sending_account = "COGNITO_DEFAULT"
}

default_username_configuration = {
case_sensitive = false
}

default_verification_message_template = {
default_email_option = "CONFIRM_WITH_CODE"
}

default_string_schemas = [
for attribute_name in var.attribute_names : {
attribute_data_type = "String"
developer_only_attribute = false
mutable = true
name = attribute_name
required = false
string_attribute_constraints = {
min_length = "1"
max_length = "256"
}
}
]

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 = 1 # 1 hour (Cognito default unit: hours)
id_token_validity = client.id_token_validity
refresh_token_validity = client.refresh_token_validity
# token_validity_units omitted — lgallard skips the block when absent;
# Cognito default units apply: hours for access/id tokens, days for refresh
prevent_user_existence_errors = try(client.prevent_user_existence_errors, null)
enable_token_revocation = try(client.enable_token_revocation, true)
}
]
}
101 changes: 20 additions & 81 deletions infrastructure/modules/cognito/main.tf
Original file line number Diff line number Diff line change
@@ -1,82 +1,20 @@
locals {
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")

default_admin_create_user_config = {
allow_admin_create_user_only = false
}

default_email_configuration = {
email_sending_account = "COGNITO_DEFAULT"
}

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
}

default_recovery_mechanisms = [
{
name = "verified_email"
priority = 1
},
{
name = "verified_phone_number"
priority = 2
}
]

default_username_configuration = {
case_sensitive = false
}

default_verification_message_template = {
default_email_option = "CONFIRM_WITH_CODE"
}

default_string_schemas = [
for attribute_name in var.attribute_names : {
attribute_data_type = "String"
developer_only_attribute = false
mutable = true
name = attribute_name
required = false
string_attribute_constraints = {
min_length = "1"
max_length = "256"
}
}
]

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)
}
]
}
################################################################
# Cognito User Pool
#
# Thin NHS wrapper around the lgallard/cognito-user-pool/aws
# community module that enforces the screening platform's
# baseline controls:
#
# * Password policy (minimum length, complexity, history)
# * Email-only auto-verification and account recovery
# * Token revocation enabled on all app clients by default
# * Schema changes ignored after initial deployment
# * Resources tagged via module.this.tags
# * Creation gated by module.this.enabled
#
# Naming and tagging are derived from context.tf via module.this.
# Derived locals (naming, client config) are defined in locals.tf.
################################################################

module "cognito" {
source = "lgallard/cognito-user-pool/aws"
Expand All @@ -89,11 +27,12 @@ module "cognito" {
deletion_protection = var.deletion_protection
auto_verified_attributes = ["email"]
mfa_configuration = var.mfa_configuration
user_pool_tier = var.user_pool_tier

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
password_policy = var.password_policy
recovery_mechanisms = var.recovery_mechanisms
string_schemas = local.default_string_schemas
username_configuration = local.default_username_configuration
verification_message_template = local.default_verification_message_template
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/modules/cognito/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ output "user_pool_domain_prefix" {

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
value = local.domain_name != null ? "https://${local.domain_name}.auth.${data.aws_region.current.region}.amazoncognito.com" : null
}

output "client_ids" {
Expand Down
Loading
Loading