Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add identity provider modules #650

Merged
merged 28 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9016261
Add identity provider modules
rocketnova Jun 19, 2024
9bbc5de
Add identity provider option to service layer
rocketnova Jun 19, 2024
321e873
Add support for auto deleting the identity provider
rocketnova Jun 19, 2024
33550ee
Use is_temporary
rocketnova Jun 24, 2024
1441550
Update ci-infra-service test
rocketnova Jun 24, 2024
dbd45a9
Format
rocketnova Jun 24, 2024
78cb519
Replace old all caps var with lowercase
rocketnova Jun 24, 2024
b2137e5
Escape double quotes in go
rocketnova Jun 24, 2024
1d72080
Address PR feedback
rocketnova Jun 25, 2024
9c6b3a2
Fix locals output structure
rocketnova Jun 25, 2024
61fc8b6
Move config to right level
rocketnova Jun 25, 2024
546ca8e
Merge branch 'main' into rocket/idp
rocketnova Jun 26, 2024
c36bbf1
Remove extra _auth_ in var name
rocketnova Jun 27, 2024
54b8fba
Sort variables.tf and outputs.tf alphabetically
rocketnova Jun 27, 2024
c8bf7d2
Use active voice
rocketnova Jun 27, 2024
96c771b
Use consistent naming convention
rocketnova Jun 27, 2024
dcf394b
Use better variable description
rocketnova Jun 27, 2024
c24f582
Use clearer comment language
rocketnova Jun 27, 2024
7560036
Support callback/logout paths
rocketnova Jun 27, 2024
bc414e5
Fix typo
rocketnova Jun 27, 2024
d1ab6af
Break out email configuration into separate config
rocketnova Jun 27, 2024
fb4e88e
Handle case if sender_display_name is null
rocketnova Jul 27, 2024
f551243
Merge branch 'main' into rocket/idp
rocketnova Jul 27, 2024
34728aa
Alphabetize env config vars
rocketnova Jul 27, 2024
0957f2c
Merge branch 'main' into rocket/idp
rocketnova Aug 1, 2024
08008e4
Fix bad locals path
rocketnova Aug 1, 2024
906b3c3
Add missing variable
rocketnova Aug 1, 2024
9928371
Add links to aws docs
rocketnova Aug 1, 2024
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
7 changes: 7 additions & 0 deletions infra/app/app-config/dev.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ module "dev_config" {
has_database = local.has_database
has_incident_management_service = local.has_incident_management_service
Comment on lines 10 to 11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tangential thought: i've been thinking of renaming these at some point to:

  • enable_database
  • enable_incident_management_service_integration


# Enable and configure identity provider.
enable_identity_provider = local.enable_identity_provider

# Support local development against the dev instance.
extra_identity_provider_callback_urls = ["http://localhost"]
extra_identity_provider_logout_urls = ["http://localhost"]

# Enables ECS Exec access for debugging or jump access.
# See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
# Defaults to `false`. Uncomment the next line to enable.
Expand Down
42 changes: 42 additions & 0 deletions infra/app/app-config/env-config/identity-provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Identity provider configuration.
# If the notification service is configured, the identity provider will use the
# SES-verified email to send notifications.
locals {
# If your application should redirect users, after successful authentication, to a
# page other than the homepage, specify the path fragment here.
# Example: "profile"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Example: "profile"
# Example: "profile"
# docs: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html

callback_url_path = ""

# If your application should redirect users, after signing out, to a page other than
# the homepage, specify the path fragment here.
# Example: "logout"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Example: "logout"
# Example: "logout"
# docs: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html

logout_url_path = ""

identity_provider_config = var.enable_identity_provider ? {
identity_provider_name = "${local.prefix}${var.app_name}-${var.environment}"

password_policy = {
password_minimum_length = 12
temporary_password_validity_days = 7
}

# Optionally configure email template for resetting a password.
# Set any attribute to a non-null value to override AWS Cognito defaults.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Set any attribute to a non-null value to override AWS Cognito defaults.
# Set any attribute to a non-null value to override AWS Cognito defaults.
# docs: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-customizations.html

verification_email = {
verification_email_message = null
verification_email_subject = null
}

# Do not modify this block directly.
client = {
callback_urls = concat(
var.domain_name != null ? ["https://${var.domain_name}/${local.callback_url_path}"] : [],
var.extra_identity_provider_callback_urls
)
logout_urls = concat(
var.domain_name != null ? ["https://${var.domain_name}/${local.logout_url_path}"] : [],
var.extra_identity_provider_logout_urls
)
}
} : null
}
15 changes: 15 additions & 0 deletions infra/app/app-config/env-config/notifications.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Notifications configuration
locals {
notifications_config = var.enable_notifications ? {
# Set to an SES-verified email address to be used when sending emails.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Set to an SES-verified email address to be used when sending emails.
# Set to an SES-verified email address to be used when sending emails.
# docs: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html

sender_email = null

# Configure the name that users see in the "From" section of their inbox, so that it's
# clearer who the email is from.
sender_display_name = null

# Configure the REPLY-TO email address if it should be different from the sender.
# Note: Only used by the identity-provider service.
reply_to_email = null
} : null
}
8 changes: 8 additions & 0 deletions infra/app/app-config/env-config/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ output "service_config" {
}
}

output "identity_provider_config" {
value = local.identity_provider_config
}

output "notifications_config" {
value = local.notifications_config
}

output "storage_config" {
value = {
# Include project name in bucket name since buckets need to be globally unique across AWS
Expand Down
24 changes: 24 additions & 0 deletions infra/app/app-config/env-config/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,35 @@ variable "enable_https" {
default = false
}

variable "enable_identity_provider" {
type = bool
description = "Enables identity provider"
default = false
}

variable "enable_notifications" {
type = bool
description = "Enables notifications"
default = false
}

variable "environment" {
description = "name of the application environment (e.g. dev, staging, prod)"
type = string
}

variable "extra_identity_provider_callback_urls" {
type = list(string)
description = "List of additional URLs that the identity provider will redirect the user to after a successful sign-in. Used for local development."
default = []
}

variable "extra_identity_provider_logout_urls" {
type = list(string)
description = "List of additional URLs that the identity provider will redirect the user to after signing out. Used for local development."
default = []
}

variable "has_database" {
type = bool
}
Expand Down
12 changes: 12 additions & 0 deletions infra/app/app-config/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ locals {

has_incident_management_service = false

# Whether or not the application should deploy an identity provider
# If enabled:
# 1. Creates a Cognito user pool
# 2. Creates a Cognito user pool app client
# 3. Adds environment variables for the app client to the service
enable_identity_provider = false

# Whether or not the application should deploy a notification service
# Note: This is not yet ready for use.
# TODO(https://github.com/navapbc/template-infra/issues/567)
enable_notifications = false

environment_configs = {
dev = module.dev_config
staging = module.staging_config
Expand Down
4 changes: 4 additions & 0 deletions infra/app/app-config/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ output "has_incident_management_service" {
value = local.has_incident_management_service
}

output "enable_identity_provider" {
value = local.enable_identity_provider
}

output "image_repository_name" {
value = local.image_repository_name
}
Expand Down
1 change: 1 addition & 0 deletions infra/app/app-config/prod.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module "prod_config" {
enable_https = false
has_database = local.has_database
has_incident_management_service = local.has_incident_management_service
enable_identity_provider = local.enable_identity_provider

# These numbers are a starting point based on this article
# Update the desired instance size and counts based on the project's specific needs
Expand Down
1 change: 1 addition & 0 deletions infra/app/app-config/staging.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module "staging_config" {
enable_https = false
has_database = local.has_database
has_incident_management_service = local.has_incident_management_service
enable_identity_provider = local.enable_identity_provider

# Enables ECS Exec access for debugging or jump access.
# See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
Expand Down
72 changes: 58 additions & 14 deletions infra/app/service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ locals {
database_config = local.environment_config.database_config
storage_config = local.environment_config.storage_config
incident_management_service_integration_config = local.environment_config.incident_management_service_integration
identity_provider_config = local.environment_config.identity_provider_config
notifications_config = local.environment_config.notifications_config

network_config = module.project_config.network_configs[local.environment_config.network_name]
}
Expand Down Expand Up @@ -150,22 +152,38 @@ module "service" {
}
} : null

extra_environment_variables = merge({
FEATURE_FLAGS_PROJECT = module.feature_flags.evidently_project_name
BUCKET_NAME = local.storage_config.bucket_name
}, local.service_config.extra_environment_variables)

secrets = [
for secret_name in keys(local.service_config.secrets) : {
extra_environment_variables = merge(
{
FEATURE_FLAGS_PROJECT = module.feature_flags.evidently_project_name
BUCKET_NAME = local.storage_config.bucket_name
},
module.app_config.enable_identity_provider ? {
COGNITO_USER_POOL_ID = module.identity_provider[0].user_pool_id
COGNITO_CLIENT_ID = module.identity_provider_client[0].client_id
lorenyu marked this conversation as resolved.
Show resolved Hide resolved
} : {},
local.service_config.extra_environment_variables
)

secrets = concat(
[for secret_name in keys(local.service_config.secrets) : {
name = secret_name
valueFrom = module.secrets[secret_name].secret_arn
}
]

extra_policies = {
feature_flags_access = module.feature_flags.access_policy_arn,
storage_access = module.storage.access_policy_arn
}
}],
module.app_config.enable_identity_provider ? [{
name = "COGNITO_CLIENT_SECRET"
valueFrom = module.identity_provider_client[0].client_secret_arn
}] : []
)

extra_policies = merge(
{
feature_flags_access = module.feature_flags.access_policy_arn,
storage_access = module.storage.access_policy_arn
},
module.app_config.enable_identity_provider ? {
identity_provider_access = module.identity_provider_client[0].access_policy_arn,
} : {}
)

is_temporary = local.is_temporary
}
Expand All @@ -192,3 +210,29 @@ module "storage" {
name = local.storage_config.bucket_name
is_temporary = local.is_temporary
}

module "identity_provider" {
count = module.app_config.enable_identity_provider ? 1 : 0
source = "../../modules/identity-provider"
is_temporary = local.is_temporary

name = local.identity_provider_config.identity_provider_name
password_minimum_length = local.identity_provider_config.password_policy.password_minimum_length
temporary_password_validity_days = local.identity_provider_config.password_policy.temporary_password_validity_days
verification_email_message = local.identity_provider_config.verification_email.verification_email_message
verification_email_subject = local.identity_provider_config.verification_email.verification_email_subject

sender_email = local.notifications_config == null ? null : local.notifications_config.sender_email
sender_display_name = local.notifications_config == null ? null : local.notifications_config.sender_display_name
reply_to_email = local.notifications_config == null ? null : local.notifications_config.reply_to_email
}

module "identity_provider_client" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(non-blocking question) what's the reasoning behind splitting this up into two terraform modules?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! This work is adapted from a another project where they were already two modules. Not a great reason on its own, but it was the starting point and in combination with the following reasons, I felt it was the right choice here:

  • They are configuring slightly different things: the user pool vs the app client that talks to the user pool. This PR doesn't include the following functionality, but a project could have a single user pool with multiple app clients or a project could have just a user pool without an app client. I wanted to keep the foundation flexible to accommodate that additional configuration.
  • The two modules have largely distinct rather than overlapping variables. Combining them makes a larger variables file than necessary.
  • If we want to combine them, it's easier to do so than to pull them apart.

Thoughts?

count = module.app_config.enable_identity_provider ? 1 : 0
source = "../../modules/identity-provider-client"

name = local.identity_provider_config.identity_provider_name
cognito_user_pool_id = module.identity_provider[0].user_pool_id
callback_urls = local.identity_provider_config.client.callback_urls
logout_urls = local.identity_provider_config.client.logout_urls
}
15 changes: 15 additions & 0 deletions infra/modules/identity-provider-client/access-control.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

resource "aws_iam_policy" "cognito_access" {
name = "${var.name}-cognito-access"
policy = data.aws_iam_policy_document.cognito_access.json
}

data "aws_iam_policy_document" "cognito_access" {
statement {
actions = ["cognito-idp:*"]
effect = "Allow"
resources = ["arn:aws:cognito-idp:${data.aws_region.current.name}:${data.aws_caller_identity.current.id}:userpool/${var.cognito_user_pool_id}"]
}
}
37 changes: 37 additions & 0 deletions infra/modules/identity-provider-client/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
resource "aws_cognito_user_pool_client" "client" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
resource "aws_cognito_user_pool_client" "client" {
# docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool_client
resource "aws_cognito_user_pool_client" "client" {

This is something I like to do, especially for complex terraform resources. I still remember years ago being a terraform newbie and not knowing how to connect the configuration to the documentation

name = var.name
user_pool_id = var.cognito_user_pool_id

callback_urls = var.callback_urls
logout_urls = var.logout_urls
supported_identity_providers = ["COGNITO"]
refresh_token_validity = 1
access_token_validity = 60
id_token_validity = 60
token_validity_units {
refresh_token = "days"
access_token = "minutes"
id_token = "minutes"
}
Comment on lines +8 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(non-blocking question) I'm assuming you just guessed at these numbers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brought these values over from a previous project that established these as reasonable defaults. Do you think they should be something else?


generate_secret = true
allowed_oauth_flows_user_pool_client = true
allowed_oauth_flows = ["code"]
allowed_oauth_scopes = ["phone", "email", "openid", "profile"]
explicit_auth_flows = ["ALLOW_ADMIN_USER_PASSWORD_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"]

# Avoid security issue where error messages indicate when a user doesn't exist
prevent_user_existence_errors = "ENABLED"

enable_token_revocation = true
enable_propagate_additional_user_context_data = false

read_attributes = ["email", "email_verified", "phone_number", "phone_number_verified", "updated_at"]
write_attributes = ["email", "updated_at", "phone_number"]
Comment on lines +17 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole section could use some inline documentation, but I don't wanna block on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably establish some guidelines on how much we want to inline document modules, which we are currently anticipating most projects should not modify, vs the /infra/app dirs, which is where we are currently instructing template adopters to put configuration.

}

resource "aws_ssm_parameter" "client_secret" {
name = "/${var.name}/identity-provider/client-secret"
type = "SecureString"
value = aws_cognito_user_pool_client.client.client_secret
}
13 changes: 13 additions & 0 deletions infra/modules/identity-provider-client/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
output "access_policy_arn" {
value = aws_iam_policy.cognito_access.arn
}

output "client_id" {
description = "The ID of the user pool client"
value = aws_cognito_user_pool_client.client.id
}

output "client_secret_arn" {
description = "The arn for the SSM parameter storing the user pool client secret"
value = aws_ssm_parameter.client_secret.arn
}
21 changes: 21 additions & 0 deletions infra/modules/identity-provider-client/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
variable "callback_urls" {
type = list(string)
description = "The URL(s) that the identity provider will redirect to after a successful login"
default = []
}

variable "cognito_user_pool_id" {
type = string
description = "The ID of the user pool that the client will be associated with"
}

variable "logout_urls" {
type = list(string)
description = "The URL that the identity provider will redirect to after a successful logout"
default = []
}

variable "name" {
type = string
description = "Name of the application or service that will act as a client to the identity provider"
}
Loading
Loading