@aws/nx-plugin 1.0.0-rc.76 → 1.0.0-rc.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/migrations.json +16 -1
  2. package/package.json +1 -1
  3. package/src/migrations/latest/dynamodb-configurable-encryption/__snapshots__/migration.spec.ts.snap +429 -0
  4. package/src/migrations/latest/dynamodb-configurable-encryption/metadata.json +3 -0
  5. package/src/migrations/latest/dynamodb-configurable-encryption/migration.d.ts +6 -0
  6. package/src/migrations/latest/dynamodb-configurable-encryption/migration.js +325 -0
  7. package/src/migrations/latest/dynamodb-configurable-encryption/migration.js.map +1 -0
  8. package/src/migrations/latest/static-website-configurable-waf-encryption/metadata.json +3 -0
  9. package/src/migrations/latest/static-website-configurable-waf-encryption/migration.d.ts +6 -0
  10. package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js +418 -0
  11. package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js.map +1 -0
  12. package/src/migrations/latest/user-identity-configurable-mfa/metadata.json +3 -0
  13. package/src/migrations/latest/user-identity-configurable-mfa/migration.d.ts +6 -0
  14. package/src/migrations/latest/user-identity-configurable-mfa/migration.js +362 -0
  15. package/src/migrations/latest/user-identity-configurable-mfa/migration.js.map +1 -0
  16. package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
  17. package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
  18. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +259 -48
  19. package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +37 -7
  20. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +26 -0
  21. package/src/utils/dynamodb-constructs/files/cdk/core/dynamodb.ts.template +23 -3
  22. package/src/utils/dynamodb-constructs/files/terraform/app/dynamodb/__nameKebabCase__/__nameKebabCase__.tf.template +27 -1
  23. package/src/utils/dynamodb-constructs/files/terraform/core/dynamodb/dynamodb.tf.template +39 -5
  24. package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +41 -7
  25. package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +48 -13
  26. package/src/utils/identity-constructs/files/terraform/core/user-identity/main.tf.template +26 -0
  27. package/src/utils/website-constructs/files/cdk/app/static-websites/__websiteNameKebabCase__.ts.template +11 -2
  28. package/src/utils/website-constructs/files/cdk/core/static-website.ts.template +47 -10
  29. package/src/utils/website-constructs/files/terraform/app/static-websites/__websiteNameKebabCase__/__websiteNameKebabCase__.tf.template +55 -0
  30. package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +57 -15
@@ -153,12 +153,38 @@ except Exception as e:
153
153
  default = true
154
154
  }
155
155
 
156
+ variable "mfa" {
157
+ description = "Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all"
158
+ type = string
159
+ default = "ON"
160
+
161
+ validation {
162
+ condition = contains(["OFF", "OPTIONAL", "ON"], var.mfa)
163
+ error_message = "mfa must be one of OFF, OPTIONAL, ON"
164
+ }
165
+ }
166
+
167
+ variable "mfa_second_factor_otp" {
168
+ description = "Whether users can use a time-based one time password (TOTP), generated by a hardware or software token, as an MFA method"
169
+ type = bool
170
+ default = true
171
+ }
172
+
173
+ variable "mfa_second_factor_sms" {
174
+ description = "Whether users can use SMS to their verified phone numbers as an MFA method. Due to a Terraform AWS provider limitation, disabling this also disables SMS-based phone number verification, unlike the equivalent CDK construct prop"
175
+ type = bool
176
+ default = true
177
+ }
178
+
156
179
  module "identity" {
157
180
  source = "./identity"
158
181
 
159
182
  user_pool_domain_prefix = "test"
160
183
  allow_signup = true
161
184
  enable_waf = var.enable_waf
185
+ mfa = var.mfa
186
+ mfa_second_factor_otp = var.mfa_second_factor_otp
187
+ mfa_second_factor_sms = var.mfa_second_factor_sms
162
188
  }
163
189
 
164
190
  # Outputs
@@ -8,7 +8,7 @@ import {
8
8
  } from 'aws-cdk-lib/aws-dynamodb';
9
9
  import { RemovalPolicy } from 'aws-cdk-lib';
10
10
  import { Grant, IGrantable } from 'aws-cdk-lib/aws-iam';
11
- import { Key } from 'aws-cdk-lib/aws-kms';
11
+ import { IKey, Key } from 'aws-cdk-lib/aws-kms';
12
12
  import { RuntimeConfig } from './runtime-config<% if (esm) { %>.js<% } %>';
13
13
 
14
14
  type _DynamoDBTableProps = Omit<TableProps, 'tableName' | 'partitionKey' | 'sortKey' | 'encryption' | 'encryptionKey'>;
@@ -25,8 +25,23 @@ export interface DynamoDBTableProps extends _DynamoDBTableProps {
25
25
  */
26
26
  readonly runtimeConfigKey: string;
27
27
 
28
+ /**
29
+ * Server-side encryption for the table.
30
+ *
31
+ * @default TableEncryption.CUSTOMER_MANAGED
32
+ */
33
+ readonly encryption?: TableEncryption;
34
+
35
+ /**
36
+ * KMS key used to encrypt the table. Only used when `encryption` is
37
+ * `TableEncryption.CUSTOMER_MANAGED`. When not provided, a new key is created.
38
+ */
39
+ readonly encryptionKey?: IKey;
40
+
28
41
  /**
29
42
  * Whether to enable automatic key rotation on the KMS key used to encrypt the table.
43
+ * Only applies when `encryption` is `TableEncryption.CUSTOMER_MANAGED` and no
44
+ * `encryptionKey` is supplied.
30
45
  *
31
46
  * @default true
32
47
  */
@@ -46,13 +61,18 @@ export abstract class DynamoDBTable extends Construct {
46
61
  pointInTimeRecoverySpecification = { pointInTimeRecoveryEnabled: true },
47
62
  deletionProtection = true,
48
63
  removalPolicy = RemovalPolicy.RETAIN,
64
+ encryption = TableEncryption.CUSTOMER_MANAGED,
65
+ encryptionKey,
49
66
  enableKeyRotation = true,
50
67
  ...rest
51
68
  }: DynamoDBTableProps,
52
69
  ) {
53
70
  super(scope, id);
54
71
 
55
- const key = new Key(this, 'EncryptionKey', { enableKeyRotation });
72
+ const key: IKey | undefined =
73
+ encryption === TableEncryption.CUSTOMER_MANAGED
74
+ ? (encryptionKey ?? new Key(this, 'EncryptionKey', { enableKeyRotation }))
75
+ : undefined;
56
76
 
57
77
  this.table = new Table(this, runtimeConfigKey, {
58
78
  ...rest,
@@ -63,7 +83,7 @@ export abstract class DynamoDBTable extends Construct {
63
83
  pointInTimeRecoverySpecification,
64
84
  deletionProtection,
65
85
  encryptionKey: key,
66
- encryption: TableEncryption.CUSTOMER_MANAGED,
86
+ encryption,
67
87
  removalPolicy,
68
88
  });
69
89
 
@@ -29,8 +29,31 @@ variable "deletion_protection_enabled" {
29
29
  default = true
30
30
  }
31
31
 
32
+ variable "encryption" {
33
+ description = "Server-side encryption for the table. One of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT (the AWS owned key, at no cost and with no key to manage). Changing this on an already-deployed table away from CUSTOMER_MANAGED requires a manual step first - see the ts#dynamodb generator's docs."
34
+ type = string
35
+ default = "CUSTOMER_MANAGED"
36
+
37
+ validation {
38
+ condition = contains(["CUSTOMER_MANAGED", "AWS_MANAGED", "DEFAULT"], var.encryption)
39
+ error_message = "encryption must be one of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT."
40
+ }
41
+ }
42
+
43
+ variable "kms_key_arn" {
44
+ description = "ARN of an existing KMS key used to encrypt the table when encryption is CUSTOMER_MANAGED. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the DynamoDB service the necessary permissions in its own key policy."
45
+ type = string
46
+ default = null
47
+ }
48
+
49
+ variable "create_kms_key" {
50
+ description = "Whether to create a KMS key for the table. Only applies when encryption is CUSTOMER_MANAGED. Set to false when supplying kms_key_arn."
51
+ type = bool
52
+ default = true
53
+ }
54
+
32
55
  variable "enable_key_rotation" {
33
- description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table."
56
+ description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table. Only applies when encryption is CUSTOMER_MANAGED and create_kms_key is true."
34
57
  type = bool
35
58
  default = true
36
59
  }
@@ -57,6 +80,9 @@ module "dynamodb_table" {
57
80
  billing_mode = var.billing_mode
58
81
  point_in_time_recovery_enabled = var.point_in_time_recovery_enabled
59
82
  deletion_protection_enabled = var.deletion_protection_enabled
83
+ encryption = var.encryption
84
+ kms_key_arn = var.kms_key_arn
85
+ create_kms_key = var.create_kms_key
60
86
  enable_key_rotation = var.enable_key_rotation
61
87
  global_secondary_indexes = local.global_secondary_indexes
62
88
  }
@@ -32,8 +32,31 @@ variable "deletion_protection_enabled" {
32
32
  default = true
33
33
  }
34
34
 
35
+ variable "encryption" {
36
+ description = "Server-side encryption for the table. One of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT (the AWS owned key, at no cost and with no key to manage). Changing this on an already-deployed table away from CUSTOMER_MANAGED requires a manual step first - see the ts#dynamodb generator's docs."
37
+ type = string
38
+ default = "CUSTOMER_MANAGED"
39
+
40
+ validation {
41
+ condition = contains(["CUSTOMER_MANAGED", "AWS_MANAGED", "DEFAULT"], var.encryption)
42
+ error_message = "encryption must be one of CUSTOMER_MANAGED, AWS_MANAGED or DEFAULT."
43
+ }
44
+ }
45
+
46
+ variable "kms_key_arn" {
47
+ description = "ARN of an existing KMS key used to encrypt the table when encryption is CUSTOMER_MANAGED. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the DynamoDB service the necessary permissions in its own key policy."
48
+ type = string
49
+ default = null
50
+ }
51
+
52
+ variable "create_kms_key" {
53
+ description = "Whether to create a KMS key for the table. Only applies when encryption is CUSTOMER_MANAGED. Set to false when supplying kms_key_arn."
54
+ type = bool
55
+ default = true
56
+ }
57
+
35
58
  variable "enable_key_rotation" {
36
- description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table."
59
+ description = "Whether to enable automatic key rotation on the KMS key used to encrypt the table. Only applies when encryption is CUSTOMER_MANAGED and create_kms_key is true."
37
60
  type = bool
38
61
  default = true
39
62
  }
@@ -53,12 +76,22 @@ locals {
53
76
  gsi_attribute_names = distinct(flatten([
54
77
  for gsi in var.global_secondary_indexes : [gsi.hash_key, gsi.range_key]
55
78
  ]))
79
+
80
+ create_table_key = var.encryption == "CUSTOMER_MANAGED" && var.create_kms_key
81
+ table_kms_key_arn = (
82
+ var.encryption == "CUSTOMER_MANAGED" ? (local.create_table_key ? aws_kms_key.table[0].arn : var.kms_key_arn) :
83
+ var.encryption == "AWS_MANAGED" ? "arn:${data.aws_partition.current.partition}:kms:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:alias/aws/dynamodb" :
84
+ null
85
+ )
56
86
  }
57
87
 
58
88
  data "aws_caller_identity" "current" {}
59
89
  data "aws_partition" "current" {}
90
+ data "aws_region" "current" {}
60
91
 
61
92
  resource "aws_kms_key" "table" {
93
+ count = local.create_table_key ? 1 : 0
94
+
62
95
  description = "KMS key for DynamoDB table ${var.name}"
63
96
  enable_key_rotation = var.enable_key_rotation
64
97
 
@@ -95,6 +128,7 @@ resource "aws_kms_key" "table" {
95
128
  }
96
129
 
97
130
  resource "aws_dynamodb_table" "table" {
131
+ #checkov:skip=CKV_AWS_119:Encryption is configurable via var.encryption; checkov cannot resolve the conditional server_side_encryption.enabled expression
98
132
  name = var.name
99
133
  billing_mode = var.billing_mode
100
134
  hash_key = "pk"
@@ -134,8 +168,8 @@ resource "aws_dynamodb_table" "table" {
134
168
  }
135
169
 
136
170
  server_side_encryption {
137
- enabled = true
138
- kms_key_arn = aws_kms_key.table.arn
171
+ enabled = var.encryption != "DEFAULT"
172
+ kms_key_arn = local.table_kms_key_arn
139
173
  }
140
174
  }
141
175
 
@@ -150,6 +184,6 @@ output "table_arn" {
150
184
  }
151
185
 
152
186
  output "kms_key_arn" {
153
- description = "ARN of the KMS key used to encrypt the DynamoDB table."
154
- value = aws_kms_key.table.arn
187
+ description = "ARN of the KMS key used to encrypt the DynamoDB table, or null when using the AWS owned key (encryption = DEFAULT)."
188
+ value = local.table_kms_key_arn
155
189
  }
@@ -16,6 +16,7 @@ import {
16
16
  FeaturePlan,
17
17
  ManagedLoginVersion,
18
18
  Mfa,
19
+ type MfaSecondFactor,
19
20
  OAuthScope,
20
21
  StandardThreatProtectionMode,
21
22
  UserPool,
@@ -48,6 +49,20 @@ export interface UserIdentityProps {
48
49
  * @default true
49
50
  */
50
51
  readonly enableWaf?: boolean;
52
+
53
+ /**
54
+ * Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all.
55
+ *
56
+ * @default Mfa.REQUIRED
57
+ */
58
+ readonly mfa?: Mfa;
59
+
60
+ /**
61
+ * The MFA methods available to users when MFA is not off.
62
+ *
63
+ * @default { sms: true, otp: true }
64
+ */
65
+ readonly mfaSecondFactor?: MfaSecondFactor;
51
66
  }
52
67
 
53
68
  /**
@@ -63,11 +78,25 @@ export class UserIdentity extends Construct {
63
78
  /** The WAFv2 Web ACL associated with the user pool, if WAF is enabled */
64
79
  public readonly webAcl?: CfnWebACL;
65
80
 
66
- constructor(scope: Construct, id: string, { enableWaf = true }: UserIdentityProps = {}) {
81
+ constructor(
82
+ scope: Construct,
83
+ id: string,
84
+ {
85
+ enableWaf = true,
86
+ mfa = Mfa.REQUIRED,
87
+ mfaSecondFactor = { sms: true, otp: true },
88
+ }: UserIdentityProps = {},
89
+ ) {
67
90
  super(scope, id);
68
91
 
92
+ if (mfa === Mfa.REQUIRED && !mfaSecondFactor.sms && !mfaSecondFactor.otp) {
93
+ throw new Error(
94
+ 'UserIdentity: mfa is REQUIRED but mfaSecondFactor has no methods enabled (sms and otp are both false)',
95
+ );
96
+ }
97
+
69
98
  this.region = Stack.of(this).region;
70
- this.userPool = this.createUserPool();
99
+ this.userPool = this.createUserPool(mfa, mfaSecondFactor);
71
100
 
72
101
  if (enableWaf) {
73
102
  this.webAcl = this.createWebAcl(
@@ -115,7 +144,12 @@ export class UserIdentity extends Construct {
115
144
  });
116
145
  }
117
146
 
118
- private createUserPool = () => {
147
+ private createUserPool = (mfa: Mfa, mfaSecondFactor: MfaSecondFactor) => {
148
+ // Cognito rejects SmsConfiguration (emitted whenever phone is auto-verified) unless SMS is
149
+ // also an enabled MFA method, for any non-OFF MfaConfiguration. So phone verification via SMS
150
+ // can only be offered when SMS is actually usable as a second factor.
151
+ const phoneVerificationEnabled = mfa === Mfa.OFF || mfaSecondFactor.sms;
152
+
119
153
  const userPool = new UserPool(this, 'UserPool', {
120
154
  deletionProtection: true,
121
155
  passwordPolicy: {
@@ -126,11 +160,11 @@ export class UserIdentity extends Construct {
126
160
  requireSymbols: true,
127
161
  tempPasswordValidity: Duration.days(3),
128
162
  },
129
- mfa: Mfa.REQUIRED,
163
+ mfa,
130
164
  featurePlan: FeaturePlan.PLUS,
131
165
  // Audit-only logs threat assessments without blocking sign-in. Switch to FULL_FUNCTION to enforce automatic responses.
132
166
  standardThreatProtectionMode: StandardThreatProtectionMode.AUDIT_ONLY,
133
- mfaSecondFactor: { sms: true, otp: true },
167
+ mfaSecondFactor,
134
168
  signInCaseSensitive: false,
135
169
  signInAliases: { username: true, email: true },
136
170
  accountRecovery: AccountRecovery.EMAIL_ONLY,
@@ -143,11 +177,11 @@ export class UserIdentity extends Construct {
143
177
  },
144
178
  autoVerify: {
145
179
  email: true,
146
- phone: true,
180
+ phone: phoneVerificationEnabled,
147
181
  },
148
182
  keepOriginal: {
149
183
  email: true,
150
- phone: true,
184
+ phone: phoneVerificationEnabled,
151
185
  },
152
186
  });
153
187
  // Retain the SMS role alongside the pool so the pool can still be updated and deleted manually
@@ -28,6 +28,29 @@ variable "enable_waf" {
28
28
  default = true
29
29
  }
30
30
 
31
+ variable "mfa" {
32
+ description = "Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all"
33
+ type = string
34
+ default = "ON"
35
+
36
+ validation {
37
+ condition = contains(["OFF", "OPTIONAL", "ON"], var.mfa)
38
+ error_message = "mfa must be one of OFF, OPTIONAL, ON"
39
+ }
40
+ }
41
+
42
+ variable "mfa_second_factor_otp" {
43
+ description = "Whether users can use a time-based one time password (TOTP), generated by a hardware or software token, as an MFA method"
44
+ type = bool
45
+ default = true
46
+ }
47
+
48
+ variable "mfa_second_factor_sms" {
49
+ description = "Whether users can use SMS to their verified phone numbers as an MFA method. Due to a Terraform AWS provider limitation, disabling this also disables SMS-based phone number verification, unlike the equivalent CDK construct prop"
50
+ type = bool
51
+ default = true
52
+ }
53
+
31
54
  variable "callback_urls" {
32
55
  description = "Additional callback URLs for the user pool client"
33
56
  type = list(string)
@@ -58,10 +81,14 @@ resource "random_id" "unique_suffix" {
58
81
  }
59
82
 
60
83
  # Generate a random external ID for SMS role security
61
- resource "random_uuid" "sms_external_id" {}
84
+ resource "random_uuid" "sms_external_id" {
85
+ count = var.mfa_second_factor_sms ? 1 : 0
86
+ }
62
87
 
63
88
  # IAM role for SMS MFA
64
89
  resource "aws_iam_role" "cognito_sms_role" {
90
+ count = var.mfa_second_factor_sms ? 1 : 0
91
+
65
92
  name = "cognito-sms-role-${random_id.unique_suffix.hex}"
66
93
 
67
94
  assume_role_policy = jsonencode({
@@ -75,7 +102,7 @@ resource "aws_iam_role" "cognito_sms_role" {
75
102
  }
76
103
  Condition = {
77
104
  StringEquals = {
78
- "sts:ExternalId" = random_uuid.sms_external_id.result
105
+ "sts:ExternalId" = random_uuid.sms_external_id[0].result
79
106
  "aws:SourceAccount" = data.aws_caller_identity.current.account_id
80
107
  }
81
108
  ArnLike = {
@@ -90,8 +117,10 @@ resource "aws_iam_role" "cognito_sms_role" {
90
117
  resource "aws_iam_role_policy" "cognito_sms_policy" {
91
118
  # checkov:skip=CKV_AWS_290:Cognito SMS requires sns:Publish with wildcard resource
92
119
  # checkov:skip=CKV_AWS_355:Cognito SMS requires sns:Publish with wildcard resource
120
+ count = var.mfa_second_factor_sms ? 1 : 0
121
+
93
122
  name = "cognito-sms-policy-${random_id.unique_suffix.hex}"
94
- role = aws_iam_role.cognito_sms_role.id
123
+ role = aws_iam_role.cognito_sms_role[0].id
95
124
 
96
125
  policy = jsonencode({
97
126
  Version = "2012-10-17"
@@ -127,18 +156,23 @@ resource "aws_cognito_user_pool" "user_pool" {
127
156
  }
128
157
 
129
158
  # MFA configuration
130
- mfa_configuration = "ON"
159
+ mfa_configuration = var.mfa
131
160
 
132
161
  # Software token MFA configuration
133
162
  software_token_mfa_configuration {
134
- enabled = true
163
+ enabled = var.mfa_second_factor_otp
135
164
  }
136
165
 
137
- # SMS MFA configuration
138
- sms_configuration {
139
- external_id = random_uuid.sms_external_id.result
140
- sns_caller_arn = aws_iam_role.cognito_sms_role.arn
141
- sns_region = data.aws_region.current.region
166
+ # SMS MFA configuration. Due to a Terraform AWS provider limitation, this also gates
167
+ # SMS-based phone number verification below - see the mfa_second_factor_sms variable.
168
+ dynamic "sms_configuration" {
169
+ for_each = var.mfa_second_factor_sms ? [1] : []
170
+
171
+ content {
172
+ external_id = random_uuid.sms_external_id[0].result
173
+ sns_caller_arn = aws_iam_role.cognito_sms_role[0].arn
174
+ sns_region = data.aws_region.current.region
175
+ }
142
176
  }
143
177
 
144
178
  depends_on = [aws_iam_role_policy.cognito_sms_policy]
@@ -158,8 +192,9 @@ resource "aws_cognito_user_pool" "user_pool" {
158
192
  }
159
193
  }
160
194
 
161
- # Auto verification
162
- auto_verified_attributes = ["email", "phone_number"]
195
+ # Auto verification. Phone number verification requires sms_configuration above, so it
196
+ # follows mfa_second_factor_sms - see that variable's description.
197
+ auto_verified_attributes = var.mfa_second_factor_sms ? ["email", "phone_number"] : ["email"]
163
198
 
164
199
  # User pool tier (equivalent to FeaturePlan.PLUS)
165
200
  user_pool_tier = "PLUS"
@@ -220,7 +255,7 @@ resource "aws_cognito_user_pool" "user_pool" {
220
255
 
221
256
  # User attribute update settings - require verification before update
222
257
  user_attribute_update_settings {
223
- attributes_require_verification_before_update = ["email", "phone_number"]
258
+ attributes_require_verification_before_update = var.mfa_second_factor_sms ? ["email", "phone_number"] : ["email"]
224
259
  }
225
260
 
226
261
  # Verification message templates
@@ -4,12 +4,38 @@ variable "enable_waf" {
4
4
  default = true
5
5
  }
6
6
 
7
+ variable "mfa" {
8
+ description = "Whether users must configure MFA, may optionally configure MFA, or cannot use MFA at all"
9
+ type = string
10
+ default = "ON"
11
+
12
+ validation {
13
+ condition = contains(["OFF", "OPTIONAL", "ON"], var.mfa)
14
+ error_message = "mfa must be one of OFF, OPTIONAL, ON"
15
+ }
16
+ }
17
+
18
+ variable "mfa_second_factor_otp" {
19
+ description = "Whether users can use a time-based one time password (TOTP), generated by a hardware or software token, as an MFA method"
20
+ type = bool
21
+ default = true
22
+ }
23
+
24
+ variable "mfa_second_factor_sms" {
25
+ description = "Whether users can use SMS to their verified phone numbers as an MFA method. Due to a Terraform AWS provider limitation, disabling this also disables SMS-based phone number verification, unlike the equivalent CDK construct prop"
26
+ type = bool
27
+ default = true
28
+ }
29
+
7
30
  module "identity" {
8
31
  source = "./identity"
9
32
 
10
33
  user_pool_domain_prefix = "<%= cognitoDomain %>"
11
34
  allow_signup = <%= allowSignup %>
12
35
  enable_waf = var.enable_waf
36
+ mfa = var.mfa
37
+ mfa_second_factor_otp = var.mfa_second_factor_otp
38
+ mfa_second_factor_sms = var.mfa_second_factor_sms
13
39
  }
14
40
 
15
41
  # Outputs
@@ -1,10 +1,19 @@
1
1
  import * as url from 'url';
2
2
  import { Construct } from 'constructs';
3
- import { StaticWebsite } from '../../core/index<% if (esm) { %>.js<% } %>';
3
+ import {
4
+ StaticWebsite,
5
+ StaticWebsiteProps,
6
+ } from '../../core/index<% if (esm) { %>.js<% } %>';
7
+
8
+ export type <%= websiteNameClassName %>Props = Omit<
9
+ StaticWebsiteProps,
10
+ 'websiteName' | 'websiteFilePath'
11
+ >;
4
12
 
5
13
  export class <%= websiteNameClassName %> extends StaticWebsite {
6
- constructor(scope: Construct, id: string) {
14
+ constructor(scope: Construct, id: string, props?: <%= websiteNameClassName %>Props) {
7
15
  super(scope, id, {
16
+ ...props,
8
17
  websiteName: '<%= websiteNameClassName %>',
9
18
  websiteFilePath: url.fileURLToPath(
10
19
  new URL('../../../../../../<%= websiteContentPath %>/bundle', <% if (esm) { %>import.meta.url<% } else { %>require('url').pathToFileURL(__filename).href<% } %>)
@@ -31,7 +31,7 @@ import {
31
31
  } from 'aws-cdk-lib/aws-s3-deployment';
32
32
  import { Construct } from 'constructs';
33
33
  import { RuntimeConfig } from './runtime-config<% if (esm) { %>.js<% } %>';
34
- import { Key } from 'aws-cdk-lib/aws-kms';
34
+ import { IKey, Key } from 'aws-cdk-lib/aws-kms';
35
35
  import {
36
36
  CfnDelivery,
37
37
  CfnDeliveryDestination,
@@ -74,6 +74,33 @@ export interface StaticWebsiteProps {
74
74
  * When provided, viewers are required to use TLS 1.2 or later.
75
75
  */
76
76
  readonly certificate?: ICertificate;
77
+ /**
78
+ * Whether to protect the CloudFront distribution with an AWS WAF Web ACL.
79
+ *
80
+ * @default true
81
+ */
82
+ readonly enableWaf?: boolean;
83
+ /**
84
+ * Server-side encryption for the website and distribution log buckets.
85
+ *
86
+ * @default BucketEncryption.KMS
87
+ */
88
+ readonly encryption?: BucketEncryption;
89
+ /**
90
+ * KMS key used to encrypt the website and distribution log buckets. Only used when `encryption` is
91
+ * `BucketEncryption.KMS`. When not provided, a new key is created. Note that a key imported via
92
+ * `Key.fromKeyArn` must already grant the CloudWatch Logs, S3 and CloudFront service principals the
93
+ * necessary permissions in its own key policy - `addToResourcePolicy` is a no-op on an imported key,
94
+ * so this construct cannot grant them on your behalf.
95
+ */
96
+ readonly encryptionKey?: IKey;
97
+ /**
98
+ * Whether the automatically created KMS key has rotation enabled. Only applies when `encryption` is
99
+ * `BucketEncryption.KMS` and no `encryptionKey` is supplied.
100
+ *
101
+ * @default true
102
+ */
103
+ readonly enableKeyRotation?: boolean;
77
104
  }
78
105
 
79
106
  /**
@@ -92,17 +119,27 @@ export class StaticWebsite extends Construct {
92
119
  constructor(
93
120
  scope: Construct,
94
121
  id: string,
95
- { websiteFilePath, websiteName, domainNames, certificate }: StaticWebsiteProps
122
+ {
123
+ websiteFilePath,
124
+ websiteName,
125
+ domainNames,
126
+ certificate,
127
+ enableWaf = true,
128
+ encryption = BucketEncryption.KMS,
129
+ encryptionKey,
130
+ enableKeyRotation = true,
131
+ }: StaticWebsiteProps
96
132
  ) {
97
133
  super(scope, id);
98
134
 
99
- const websiteKey = new Key(this, 'WebsiteKey', {
100
- enableKeyRotation: true,
101
- });
135
+ const websiteKey: IKey | undefined =
136
+ encryption === BucketEncryption.KMS
137
+ ? (encryptionKey ?? new Key(this, 'WebsiteKey', { enableKeyRotation }))
138
+ : undefined;
102
139
 
103
140
  // Allow CloudWatch Logs to use the website key for server access log delivery.
104
141
  const stack = Stack.of(this);
105
- websiteKey.addToResourcePolicy(
142
+ websiteKey?.addToResourcePolicy(
106
143
  new PolicyStatement({
107
144
  effect: Effect.ALLOW,
108
145
  principals: [
@@ -136,7 +173,7 @@ export class StaticWebsite extends Construct {
136
173
  enforceSSL: true,
137
174
  autoDeleteObjects: true,
138
175
  removalPolicy: RemovalPolicy.DESTROY,
139
- encryption: BucketEncryption.KMS,
176
+ encryption,
140
177
  encryptionKey: websiteKey,
141
178
  objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
142
179
  publicReadAccess: false,
@@ -153,7 +190,7 @@ export class StaticWebsite extends Construct {
153
190
  accessLogs,
154
191
  );
155
192
  // Web ACL
156
- const wafStack = new CloudfrontWebAcl(this, 'waf');
193
+ const wafStack = enableWaf ? new CloudfrontWebAcl(this, 'waf') : undefined;
157
194
 
158
195
  // Bucket holding CloudFront standard access logs. CloudFront delivers its
159
196
  // own logs to S3 only, so this bucket is retained; its S3 server access
@@ -162,7 +199,7 @@ export class StaticWebsite extends Construct {
162
199
  enforceSSL: true,
163
200
  autoDeleteObjects: true,
164
201
  removalPolicy: RemovalPolicy.DESTROY,
165
- encryption: BucketEncryption.KMS,
202
+ encryption,
166
203
  encryptionKey: websiteKey,
167
204
  objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
168
205
  publicReadAccess: false,
@@ -214,7 +251,7 @@ export class StaticWebsite extends Construct {
214
251
  this,
215
252
  'CloudfrontDistribution',
216
253
  {
217
- webAclId: wafStack.wafArn,
254
+ webAclId: wafStack?.wafArn,
218
255
  enableLogging: true,
219
256
  logBucket: logBucket,
220
257
  ...(certificate
@@ -8,6 +8,53 @@ terraform {
8
8
  }
9
9
  }
10
10
 
11
+ variable "custom_domain_names" {
12
+ description = "Custom domain names (aliases) for the CloudFront distribution. Requires acm_certificate_arn."
13
+ type = list(string)
14
+ default = []
15
+ }
16
+
17
+ variable "acm_certificate_arn" {
18
+ description = "ARN of an ACM certificate (in us-east-1) for the custom domain names. When set, viewers are required to use TLS 1.2 or later."
19
+ type = string
20
+ default = null
21
+ }
22
+
23
+ variable "enable_waf" {
24
+ description = "Whether to protect the CloudFront distribution with an AWS WAF Web ACL."
25
+ type = bool
26
+ default = true
27
+ }
28
+
29
+ variable "encryption" {
30
+ description = "Server-side encryption for the website and distribution log buckets. One of KMS or S3_MANAGED."
31
+ type = string
32
+ default = "KMS"
33
+
34
+ validation {
35
+ condition = contains(["KMS", "S3_MANAGED"], var.encryption)
36
+ error_message = "encryption must be one of KMS or S3_MANAGED."
37
+ }
38
+ }
39
+
40
+ variable "kms_key_arn" {
41
+ description = "ARN of an existing KMS key used to encrypt the website and distribution log buckets when encryption is KMS. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the CloudWatch Logs, S3 and CloudFront service principals the necessary permissions in its own key policy."
42
+ type = string
43
+ default = null
44
+ }
45
+
46
+ variable "create_kms_key" {
47
+ description = "Whether to create a KMS key for the website. Only applies when encryption is KMS. Set to false when supplying kms_key_arn."
48
+ type = bool
49
+ default = true
50
+ }
51
+
52
+ variable "enable_key_rotation" {
53
+ description = "Whether the automatically created KMS key has rotation enabled. Only applies when encryption is KMS and create_kms_key is true."
54
+ type = bool
55
+ default = true
56
+ }
57
+
11
58
  # Static website module configured for the web package
12
59
  module "static_website" {
13
60
  source = "../../../core/static-website"
@@ -15,6 +62,14 @@ module "static_website" {
15
62
  website_name = "<%= websiteNameKebabCase %>"
16
63
  website_file_path = "${path.module}/../../../../../../../<%= websiteContentPath %>/bundle"
17
64
 
65
+ custom_domain_names = var.custom_domain_names
66
+ acm_certificate_arn = var.acm_certificate_arn
67
+ enable_waf = var.enable_waf
68
+ encryption = var.encryption
69
+ kms_key_arn = var.kms_key_arn
70
+ create_kms_key = var.create_kms_key
71
+ enable_key_rotation = var.enable_key_rotation
72
+
18
73
  providers = {
19
74
  aws.us_east_1 = aws.us_east_1
20
75
  }