diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 75a5e549f9095..f01f8646a4d1f 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -356,6 +356,16 @@ new cognito.UserPool(this, 'UserPool', { The default for account recovery is by phone if available and by email otherwise. A user will not be allowed to reset their password via phone if they are also using it for MFA. +#### Advanced Security Mode + +User pools can be configured to use Advanced security. You can turn the user pool advanced security features on, and customize the actions that are taken in response to different risks. Or you can use audit mode to gather metrics on detected risks without taking action. In audit mode, the advanced security features publish metrics to Amazon CloudWatch. See the [documentation on Advanced security](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-advanced-security.html) to learn more. + +```ts +new cognito.UserPool(this, 'myuserpool', { + // ... + advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED, +}); +``` ### Emails diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 29e3b6e28338c..63cc7bc587631 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -496,6 +496,19 @@ export interface DeviceTracking { readonly deviceOnlyRememberedOnUserPrompt: boolean; } +/** + * The different ways in which a user pool's Advanced Security Mode can be configured. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-userpooladdons.html#cfn-cognito-userpool-userpooladdons-advancedsecuritymode + */ +export enum AdvancedSecurityMode { + /** Enable advanced security mode */ + ENFORCED = 'ENFORCED', + /** gather metrics on detected risks without taking action. Metrics are published to Amazon CloudWatch */ + AUDIT = 'AUDIT', + /** Advanced security mode is disabled */ + OFF = 'OFF' +} + /** * Props for the UserPool construct */ @@ -692,6 +705,12 @@ export interface UserPoolProps { * @default - no key ID configured */ readonly customSenderKmsKey?: IKey; + + /** + * The user pool's Advanced Security Mode + * @default - no value + */ + readonly advancedSecurityMode?: AdvancedSecurityMode; } /** @@ -934,6 +953,9 @@ export class UserPool extends UserPoolBase { emailVerificationSubject, smsVerificationMessage, verificationMessageTemplate, + userPoolAddOns: undefinedIfNoKeys({ + advancedSecurityMode: props.advancedSecurityMode, + }), schema: this.schemaConfiguration(props), mfaConfiguration: props.mfa, enabledMfas: this.mfaConfiguration(props), diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/cdk.out b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/cdk.out index 8ecc185e9dbee..145739f539580 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"21.0.0"} \ No newline at end of file +{"version":"22.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ-user-pool.assets.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ-user-pool.assets.json index 3993c765c773e..644371da0045c 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ-user-pool.assets.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ-user-pool.assets.json @@ -1,7 +1,7 @@ { - "version": "21.0.0", + "version": "22.0.0", "files": { - "7df3ca05ace569184cc645d485b05885dc2e13f745606873a57afa9d264ecc08": { + "d52bd1bc71dfc61d4ad7024fe0716269f71e86ea085d0ccd71bcbe68e8df1c34": { "source": { "path": "integ-user-pool.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "7df3ca05ace569184cc645d485b05885dc2e13f745606873a57afa9d264ecc08.json", + "objectKey": "d52bd1bc71dfc61d4ad7024fe0716269f71e86ea085d0ccd71bcbe68e8df1c34.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ-user-pool.template.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ-user-pool.template.json index e8d562b64cc60..8bf4aebbc70b3 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ-user-pool.template.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ-user-pool.template.json @@ -905,6 +905,9 @@ "phone_number" ] }, + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, "UserPoolName": "MyUserPool", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ.json index 3f63a187791d6..8ecffed9dfaf4 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "21.0.0", + "version": "22.0.0", "testCases": { "integ.user-pool-explicit-props": { "stacks": [ diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/manifest.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/manifest.json index 8934cd44cef53..6e7665f258319 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/manifest.json @@ -1,12 +1,6 @@ { - "version": "21.0.0", + "version": "22.0.0", "artifacts": { - "Tree": { - "type": "cdk:tree", - "properties": { - "file": "tree.json" - } - }, "integ-user-pool.assets": { "type": "cdk:asset-manifest", "properties": { @@ -23,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/7df3ca05ace569184cc645d485b05885dc2e13f745606873a57afa9d264ecc08.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/d52bd1bc71dfc61d4ad7024fe0716269f71e86ea085d0ccd71bcbe68e8df1c34.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -260,99 +254,15 @@ "type": "aws:cdk:logicalId", "data": "CheckBootstrapVersion" } - ], - "createAuthChallengeCreateAuthChallengeCognito57E2297E": [ - { - "type": "aws:cdk:logicalId", - "data": "createAuthChallengeCreateAuthChallengeCognito57E2297E", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "customMessageCustomMessageCognitoB4F894A6": [ - { - "type": "aws:cdk:logicalId", - "data": "customMessageCustomMessageCognitoB4F894A6", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "defineAuthChallengeDefineAuthChallengeCognito4DBD8021": [ - { - "type": "aws:cdk:logicalId", - "data": "defineAuthChallengeDefineAuthChallengeCognito4DBD8021", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "postAuthenticationPostAuthenticationCognito8B923BC3": [ - { - "type": "aws:cdk:logicalId", - "data": "postAuthenticationPostAuthenticationCognito8B923BC3", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "postConfirmationPostConfirmationCognito9D010393": [ - { - "type": "aws:cdk:logicalId", - "data": "postConfirmationPostConfirmationCognito9D010393", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "preAuthenticationPreAuthenticationCognito67FACB54": [ - { - "type": "aws:cdk:logicalId", - "data": "preAuthenticationPreAuthenticationCognito67FACB54", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "preSignUpPreSignUpCognitoE986CC53": [ - { - "type": "aws:cdk:logicalId", - "data": "preSignUpPreSignUpCognitoE986CC53", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "preTokenGenerationPreTokenGenerationCognitoC1959918": [ - { - "type": "aws:cdk:logicalId", - "data": "preTokenGenerationPreTokenGenerationCognitoC1959918", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "userMigrationUserMigrationCognito29EEC4AD": [ - { - "type": "aws:cdk:logicalId", - "data": "userMigrationUserMigrationCognito29EEC4AD", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "verifyAuthChallengeResponseVerifyAuthChallengeResponseCognito9DC48AFC": [ - { - "type": "aws:cdk:logicalId", - "data": "verifyAuthChallengeResponseVerifyAuthChallengeResponseCognito9DC48AFC", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } ] }, "displayName": "integ-user-pool" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/tree.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/tree.json index 15fe1267a751d..9ca75c368a2c5 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.js.snapshot/tree.json @@ -4,14 +4,6 @@ "id": "App", "path": "", "children": { - "Tree": { - "id": "Tree", - "path": "Tree", - "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.123" - } - }, "integ-user-pool": { "id": "integ-user-pool", "path": "integ-user-pool", @@ -24,6 +16,14 @@ "id": "ServiceRole", "path": "integ-user-pool/createAuthChallenge/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/createAuthChallenge/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/createAuthChallenge/ServiceRole/Resource", @@ -108,6 +108,14 @@ "id": "ServiceRole", "path": "integ-user-pool/customMessage/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/customMessage/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/customMessage/ServiceRole/Resource", @@ -192,6 +200,14 @@ "id": "ServiceRole", "path": "integ-user-pool/defineAuthChallenge/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/defineAuthChallenge/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/defineAuthChallenge/ServiceRole/Resource", @@ -276,6 +292,14 @@ "id": "ServiceRole", "path": "integ-user-pool/postAuthentication/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/postAuthentication/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/postAuthentication/ServiceRole/Resource", @@ -360,6 +384,14 @@ "id": "ServiceRole", "path": "integ-user-pool/postConfirmation/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/postConfirmation/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/postConfirmation/ServiceRole/Resource", @@ -444,6 +476,14 @@ "id": "ServiceRole", "path": "integ-user-pool/preAuthentication/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/preAuthentication/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/preAuthentication/ServiceRole/Resource", @@ -528,6 +568,14 @@ "id": "ServiceRole", "path": "integ-user-pool/preSignUp/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/preSignUp/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/preSignUp/ServiceRole/Resource", @@ -612,6 +660,14 @@ "id": "ServiceRole", "path": "integ-user-pool/preTokenGeneration/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/preTokenGeneration/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/preTokenGeneration/ServiceRole/Resource", @@ -696,6 +752,14 @@ "id": "ServiceRole", "path": "integ-user-pool/userMigration/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/userMigration/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/userMigration/ServiceRole/Resource", @@ -780,6 +844,14 @@ "id": "ServiceRole", "path": "integ-user-pool/verifyAuthChallengeResponse/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-user-pool/verifyAuthChallengeResponse/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/verifyAuthChallengeResponse/ServiceRole/Resource", @@ -1134,6 +1206,14 @@ "id": "smsRole", "path": "integ-user-pool/myuserpool/smsRole", "children": { + "ImportsmsRole": { + "id": "ImportsmsRole", + "path": "integ-user-pool/myuserpool/smsRole/ImportsmsRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-user-pool/myuserpool/smsRole/Resource", @@ -1356,6 +1436,9 @@ "phone_number" ] }, + "userPoolAddOns": { + "advancedSecurityMode": "ENFORCED" + }, "userPoolName": "MyUserPool", "verificationMessageTemplate": { "defaultEmailOption": "CONFIRM_WITH_CODE", @@ -1418,12 +1501,36 @@ "fqn": "@aws-cdk/core.CfnOutput", "version": "0.0.0" } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-user-pool/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-user-pool/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } } }, "constructInfo": { "fqn": "@aws-cdk/core.Stack", "version": "0.0.0" } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.168" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts index 744dba2530789..bbd322af0ce03 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts @@ -1,6 +1,6 @@ import { Code, Function, IFunction, Runtime } from '@aws-cdk/aws-lambda'; import { App, CfnOutput, Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; -import { BooleanAttribute, DateTimeAttribute, Mfa, NumberAttribute, StringAttribute, UserPool } from '../lib'; +import { AdvancedSecurityMode, BooleanAttribute, DateTimeAttribute, Mfa, NumberAttribute, StringAttribute, UserPool } from '../lib'; const app = new App(); const stack = new Stack(app, 'integ-user-pool'); @@ -73,6 +73,7 @@ const userpool = new UserPool(stack, 'myuserpool', { userMigration: dummyTrigger('userMigration'), verifyAuthChallengeResponse: dummyTrigger('verifyAuthChallengeResponse'), }, + advancedSecurityMode: AdvancedSecurityMode.ENFORCED, snsRegion: Stack.of(stack).region, }); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index d122a8f733a48..9cc1cfc5fb4a8 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -5,7 +5,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { CfnParameter, Duration, Stack, Tags } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, UserPoolEmail } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, UserPoolEmail, AdvancedSecurityMode } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -1924,6 +1924,41 @@ test('deletion protection', () => { }); }); +test.each( + [ + [AdvancedSecurityMode.ENFORCED, 'ENFORCED'], + [AdvancedSecurityMode.AUDIT, 'AUDIT'], + [AdvancedSecurityMode.OFF, 'OFF'], + ])('advanced security is configured correctly when set to (%s)', (advancedSecurityMode, compareString) => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + advancedSecurityMode: advancedSecurityMode, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + UserPoolAddOns: { + AdvancedSecurityMode: compareString, + }, + }); +}); + +test('advanced security is not present if option is not provided', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', {}); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + UserPoolAddOns: Match.absent(), + }); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { functionName: name,