diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json index e1d4a0ba17a4b..9fe51c84d0b28 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json @@ -40,6 +40,12 @@ "PoolD3F588B8": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index c063e532b26a7..5f67c80b204b8 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -33,6 +33,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Attributes](#attributes) - [Security](#security) - [Multi-factor Authentication](#multi-factor-authentication-mfa) + - [Account Recovery Settings](#account-recovery-settings) - [Emails](#emails) - [Lambda Triggers](#lambda-triggers) - [Import](#importing-user-pools) @@ -268,6 +269,18 @@ new UserPool(this, 'myuserpool', { Note that, `tempPasswordValidity` can be specified only in whole days. Specifying fractional days would throw an error. +#### Account Recovery Settings + +User pools can be configured on which method a user should use when recovering the password for their account. This +can either be email and/or SMS. Read more at [Recovering User Accounts](https://docs.aws.amazon.com/cognito/latest/developerguide/how-to-recover-a-user-account.html) + +```ts +new UserPool(this, 'UserPool', { + ..., + accountRecoverySettings: AccountRecovery.EMAIL_ONLY, +}) +``` + ### Emails Cognito sends emails to users in the user pool, when particular actions take place, such as welcome emails, invitation diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index bfd38a9bd2b36..6ff23e96dde5d 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -385,6 +385,47 @@ export interface EmailSettings { readonly replyTo?: string; } +/** + * How will a user be able to recover their account? + * + * When a user forgets their password, they can have a code sent to their verified email or verified phone to recover their account. + * You can choose the preferred way to send codes below. + * We recommend not allowing phone to be used for both password resets and multi-factor authentication (MFA). + * + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/how-to-recover-a-user-account.html + */ +export enum AccountRecovery { + /** + * Email if available, otherwise phone, but don’t allow a user to reset their password via phone if they are also using it for MFA + */ + EMAIL_AND_PHONE_WITHOUT_MFA, + + /** + * Phone if available, otherwise email, but don’t allow a user to reset their password via phone if they are also using it for MFA + */ + PHONE_WITHOUT_MFA_AND_EMAIL, + + /** + * Email only + */ + EMAIL_ONLY, + + /** + * Phone only, but don’t allow a user to reset their password via phone if they are also using it for MFA + */ + PHONE_ONLY_WITHOUT_MFA, + + /** + * (Not Recommended) Phone if available, otherwise email, and do allow a user to reset their password via phone if they are also using it for MFA. + */ + PHONE_AND_EMAIL, + + /** + * None – users will have to contact an administrator to reset their passwords + */ + NONE, +} + /** * Props for the UserPool construct */ @@ -509,6 +550,13 @@ export interface UserPoolProps { * @default true */ readonly signInCaseSensitive?: boolean; + + /** + * How will a user be able to recover their account? + * + * @default AccountRecovery.PHONE_WITHOUT_MFA_AND_EMAIL + */ + readonly accountRecovery?: AccountRecovery; } /** @@ -622,7 +670,7 @@ export class UserPool extends UserPoolBase { */ public readonly userPoolProviderUrl: string; - private triggers: CfnUserPool.LambdaConfigProperty = { }; + private triggers: CfnUserPool.LambdaConfigProperty = {}; constructor(scope: Construct, id: string, props: UserPoolProps = {}) { super(scope, id); @@ -683,6 +731,7 @@ export class UserPool extends UserPoolBase { usernameConfiguration: undefinedIfNoKeys({ caseSensitive: props.signInCaseSensitive, }), + accountRecoverySetting: this.accountRecovery(props), }); this.userPoolId = userPool.ref; @@ -908,6 +957,42 @@ export class UserPool extends UserPoolBase { } return schema; } + + private accountRecovery(props: UserPoolProps): undefined | CfnUserPool.AccountRecoverySettingProperty { + const accountRecovery = props.accountRecovery ?? AccountRecovery.PHONE_WITHOUT_MFA_AND_EMAIL; + switch (accountRecovery) { + case AccountRecovery.EMAIL_AND_PHONE_WITHOUT_MFA: + return { + recoveryMechanisms: [ + { name: 'verified_email', priority: 1 }, + { name: 'verified_phone_number', priority: 2 }, + ], + }; + case AccountRecovery.PHONE_WITHOUT_MFA_AND_EMAIL: + return { + recoveryMechanisms: [ + { name: 'verified_phone_number', priority: 1 }, + { name: 'verified_email', priority: 2 }, + ], + }; + case AccountRecovery.EMAIL_ONLY: + return { + recoveryMechanisms: [{ name: 'verified_email', priority: 1 }], + }; + case AccountRecovery.PHONE_ONLY_WITHOUT_MFA: + return { + recoveryMechanisms: [{ name: 'verified_phone_number', priority: 1 }], + }; + case AccountRecovery.NONE: + return { + recoveryMechanisms: [{ name: 'admin_only', priority: 1 }], + }; + case AccountRecovery.PHONE_AND_EMAIL: + return undefined; + default: + throw new Error(`Unsupported AccountRecovery type - ${accountRecovery}`); + } + } } function undefinedIfNoKeys(struct: object): object | undefined { diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json index c39124006db33..8b5246dfedf58 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json @@ -40,6 +40,12 @@ "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json index 15fd0ce903e93..f51fe3b47866a 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json @@ -40,6 +40,12 @@ "UserPool6BA7E5F2": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json index 254b68b5d32b1..6bb3d7edab140 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json @@ -40,6 +40,12 @@ "UserPool6BA7E5F2": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json index 7625b4a9a80d7..5e2c8662f3f60 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json @@ -680,6 +680,12 @@ "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": false, "InviteMessageTemplate": { diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json index edcf082c6ca0d..3fa00974541cf 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json @@ -40,6 +40,12 @@ "pool056F3F7E": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json index 046eda0872a79..90d858978f043 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json @@ -40,6 +40,12 @@ "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": false }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json index ec00db3fd3eb2..45661be1e0766 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json @@ -40,6 +40,12 @@ "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": false }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json index 075fb3542f6ad..f2beef72d6eb4 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json @@ -40,6 +40,12 @@ "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, 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 61eb7a0ed229c..801879f6ea55a 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -3,7 +3,7 @@ import { ABSENT } from '@aws-cdk/assert/lib/assertions/have-resource'; import { Role } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnParameter, Construct, Duration, Stack, Tag } from '@aws-cdk/core'; -import { Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -975,3 +975,123 @@ function fooFunction(scope: Construct, name: string): lambda.IFunction { handler: 'index.handler', }); } + +describe('AccountRecoverySetting should be configured correctly', () => { + test('EMAIL_AND_PHONE_WITHOUT_MFA', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.EMAIL_AND_PHONE_WITHOUT_MFA }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + AccountRecoverySetting: { + RecoveryMechanisms: [ + { Name: 'verified_email', Priority: 1 }, + { Name: 'verified_phone_number', Priority: 2 }, + ], + }, + }); + }); + + test('PHONE_WITHOUT_MFA_AND_EMAIL', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.PHONE_WITHOUT_MFA_AND_EMAIL }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + AccountRecoverySetting: { + RecoveryMechanisms: [ + { Name: 'verified_phone_number', Priority: 1 }, + { Name: 'verified_email', Priority: 2 }, + ], + }, + }); + }); + + test('EMAIL_ONLY', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.EMAIL_ONLY }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + AccountRecoverySetting: { + RecoveryMechanisms: [ + { Name: 'verified_email', Priority: 1 }, + ], + }, + }); + }); + + test('PHONE_ONLY_WITHOUT_MFA', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.PHONE_ONLY_WITHOUT_MFA }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + AccountRecoverySetting: { + RecoveryMechanisms: [ + { Name: 'verified_phone_number', Priority: 1 }, + ], + }, + }); + }); + + test('NONE', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.NONE }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + AccountRecoverySetting: { + RecoveryMechanisms: [ + { Name: 'admin_only', Priority: 1 }, + ], + }, + }); + }); + + test('PHONE_AND_EMAIL', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.PHONE_AND_EMAIL }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + AccountRecoverySetting: ABSENT, + }); + }); + + test('default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool'); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + AccountRecoverySetting: { + RecoveryMechanisms: [ + { Name: 'verified_phone_number', Priority: 1 }, + { Name: 'verified_email', Priority: 2 }, + ], + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.expected.json index ca495599afaf6..e35271c92c173 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.expected.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.expected.json @@ -493,6 +493,12 @@ "UserPool6BA7E5F2": { "Type": "AWS::Cognito::UserPool", "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { "Name": "verified_phone_number", "Priority": 1 }, + { "Name": "verified_email", "Priority": 2 } + ] + }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true },