diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 4674fb7570ef8..b4f5a86dc5c26 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -201,8 +201,13 @@ The default value is `false`. Cognito sends various messages to its users via SMS, for different actions, ranging from account verification to marketing. In order to send SMS messages, Cognito needs an IAM role that it can assume, with permissions that allow it -to send SMS messages. By default, CDK will create this IAM role but can also be explicily specified to an existing IAM -role using the `smsRole` property. +to send SMS messages. + +By default, the CDK looks at all of the specified properties (and their defaults when not explicitly specified) and +automatically creates an SMS role, when needed. For example, if MFA second factor by SMS is enabled, the CDK will +create a new role. The `smsRole` property can be used to specify the user supplied role that should be used instead. +Additionally, the property `enableSmsRole` can be used to override the CDK's default behaviour to either enable or +suppress automatic role creation. ```ts import { Role } from '@aws-cdk/aws-iam'; diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 6ff23e96dde5d..3ffe26e1c4a01 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -471,6 +471,13 @@ export interface UserPoolProps { */ readonly smsRoleExternalId?: string; + /** + * Setting this would explicitly enable or disable SMS role creation. + * When left unspecified, CDK will determine based on other properties if a role is needed or not. + * @default - CDK will determine based on other properties of the user pool if an SMS role should be created or not. + */ + readonly enableSmsRole?: boolean; + /** * Methods in which a user registers or signs in to a user pool. * Allows either username with aliases OR sign in with email, phone, or both. @@ -835,41 +842,57 @@ export class UserPool extends UserPoolBase { return { usernameAttrs, aliasAttrs, autoVerifyAttrs }; } - private smsConfiguration(props: UserPoolProps): CfnUserPool.SmsConfigurationProperty { + private smsConfiguration(props: UserPoolProps): CfnUserPool.SmsConfigurationProperty | undefined { + if (props.enableSmsRole === false && props.smsRole) { + throw new Error('enableSmsRole cannot be disabled when smsRole is specified'); + } + if (props.smsRole) { return { snsCallerArn: props.smsRole.roleArn, externalId: props.smsRoleExternalId, }; - } else { - const smsRoleExternalId = this.node.uniqueId.substr(0, 1223); // sts:ExternalId max length of 1224 - const smsRole = props.smsRole ?? new Role(this, 'smsRole', { - assumedBy: new ServicePrincipal('cognito-idp.amazonaws.com', { - conditions: { - StringEquals: { 'sts:ExternalId': smsRoleExternalId }, - }, - }), - inlinePolicies: { - /* - * The UserPool is very particular that it must contain an 'sns:Publish' action as an inline policy. - * Ideally, a conditional that restricts this action to 'sms' protocol needs to be attached, but the UserPool deployment fails validation. - * Seems like a case of being excessively strict. - */ - 'sns-publish': new PolicyDocument({ - statements: [ - new PolicyStatement({ - actions: [ 'sns:Publish' ], - resources: [ '*' ], - }), - ], - }), - }, - }); - return { - externalId: smsRoleExternalId, - snsCallerArn: smsRole.roleArn, - }; } + + if (props.enableSmsRole === false) { + return undefined; + } + + const mfaEnabled = props.mfa && props.mfa !== Mfa.OFF; + const mfaSms = !props.mfaSecondFactor || props.mfaSecondFactor.sms; // mfaSecondFactor.sms is true, by default if MFA is 'on' + const phoneVerification = props.signInAliases?.phone === true || props.autoVerify?.phone === true; + const roleRequired = (mfaEnabled && mfaSms) || phoneVerification; + if (!roleRequired && props.enableSmsRole === undefined) { + return undefined; + } + + const smsRoleExternalId = this.node.uniqueId.substr(0, 1223); // sts:ExternalId max length of 1224 + const smsRole = props.smsRole ?? new Role(this, 'smsRole', { + assumedBy: new ServicePrincipal('cognito-idp.amazonaws.com', { + conditions: { + StringEquals: { 'sts:ExternalId': smsRoleExternalId }, + }, + }), + inlinePolicies: { + /* + * The UserPool is very particular that it must contain an 'sns:Publish' action as an inline policy. + * Ideally, a conditional that restricts this action to 'sms' protocol needs to be attached, but the UserPool deployment fails validation. + * Seems like a case of being excessively strict. + */ + 'sns-publish': new PolicyDocument({ + statements: [ + new PolicyStatement({ + actions: [ 'sns:Publish' ], + resources: [ '*' ], + }), + ], + }), + }, + }); + return { + externalId: smsRoleExternalId, + snsCallerArn: smsRole.roleArn, + }; } private mfaConfiguration(props: UserPoolProps): string[] | 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 8b5246dfedf58..be2e268a29eac 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 @@ -1,49 +1,18 @@ { "Resources": { - "myuserpoolsmsRole0E16FDD9": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Condition": { - "StringEquals": { - "sts:ExternalId": "integuserpoolclientexplicitpropsmyuserpoolFC6541FF" - } - }, - "Effect": "Allow", - "Principal": { - "Service": "cognito-idp.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "sns-publish" - } - ] - } - }, "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { - "AccountRecoverySetting": { + "AccountRecoverySetting": { "RecoveryMechanisms": [ - { "Name": "verified_phone_number", "Priority": 1 }, - { "Name": "verified_email", "Priority": 2 } + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } ] }, "AdminCreateUserConfig": { @@ -51,15 +20,6 @@ }, "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", - "SmsConfiguration": { - "ExternalId": "integuserpoolclientexplicitpropsmyuserpoolFC6541FF", - "SnsCallerArn": { - "Fn::GetAtt": [ - "myuserpoolsmsRole0E16FDD9", - "Arn" - ] - } - }, "SmsVerificationMessage": "The verification code to your new account is {####}", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", 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 511d200ed5d90..368a5945b361b 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 @@ -1,49 +1,18 @@ { "Resources": { - "UserPoolsmsRole4EA729DD": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Condition": { - "StringEquals": { - "sts:ExternalId": "integuserpooldomaincfdistUserPool17475E8A" - } - }, - "Effect": "Allow", - "Principal": { - "Service": "cognito-idp.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "sns-publish" - } - ] - } - }, "UserPool6BA7E5F2": { "Type": "AWS::Cognito::UserPool", "Properties": { - "AccountRecoverySetting": { + "AccountRecoverySetting": { "RecoveryMechanisms": [ - { "Name": "verified_phone_number", "Priority": 1 }, - { "Name": "verified_email", "Priority": 2 } + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } ] }, "AdminCreateUserConfig": { @@ -51,15 +20,6 @@ }, "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", - "SmsConfiguration": { - "ExternalId": "integuserpooldomaincfdistUserPool17475E8A", - "SnsCallerArn": { - "Fn::GetAtt": [ - "UserPoolsmsRole4EA729DD", - "Arn" - ] - } - }, "SmsVerificationMessage": "The verification code to your new account is {####}", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", @@ -176,7 +136,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersa75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4S3BucketC6CBC09E" + "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3Bucket67234880" }, "S3Key": { "Fn::Join": [ @@ -189,7 +149,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4S3VersionKeyB194AB23" + "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96" } ] } @@ -202,7 +162,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4S3VersionKeyB194AB23" + "Ref": "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96" } ] } @@ -244,17 +204,17 @@ } }, "Parameters": { - "AssetParametersa75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4S3BucketC6CBC09E": { + "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3Bucket67234880": { "Type": "String", - "Description": "S3 bucket for asset \"a75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4\"" + "Description": "S3 bucket for asset \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" }, - "AssetParametersa75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4S3VersionKeyB194AB23": { + "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061S3VersionKey9802AE96": { "Type": "String", - "Description": "S3 key for asset version \"a75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4\"" + "Description": "S3 key for asset version \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" }, - "AssetParametersa75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4ArtifactHashBE5BD63C": { + "AssetParameters8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061ArtifactHash9212BF97": { "Type": "String", - "Description": "Artifact hash for asset \"a75563f489fb6bc4064bc85b91ef607f671326e647bcd9d9bcab0731de62edd4\"" + "Description": "Artifact hash for asset \"8ae75ec4aaae0510b0918d3a69fac5c978d780ae0d60bb94c65c7f5b4c498061\"" } } -} +} \ No newline at end of file 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 6bb3d7edab140..694cf43b5f5ea 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 @@ -1,49 +1,18 @@ { "Resources": { - "UserPoolsmsRole4EA729DD": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Condition": { - "StringEquals": { - "sts:ExternalId": "integuserpooldomainsigninurlUserPool1325E89F" - } - }, - "Effect": "Allow", - "Principal": { - "Service": "cognito-idp.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "sns-publish" - } - ] - } - }, "UserPool6BA7E5F2": { "Type": "AWS::Cognito::UserPool", "Properties": { - "AccountRecoverySetting": { + "AccountRecoverySetting": { "RecoveryMechanisms": [ - { "Name": "verified_phone_number", "Priority": 1 }, - { "Name": "verified_email", "Priority": 2 } + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } ] }, "AdminCreateUserConfig": { @@ -51,15 +20,6 @@ }, "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", - "SmsConfiguration": { - "ExternalId": "integuserpooldomainsigninurlUserPool1325E89F", - "SnsCallerArn": { - "Fn::GetAtt": [ - "UserPoolsmsRole4EA729DD", - "Arn" - ] - } - }, "SmsVerificationMessage": "The verification code to your new account is {####}", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", 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 3fa00974541cf..e68a262eb7ee3 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 @@ -1,49 +1,18 @@ { "Resources": { - "poolsmsRole04048F13": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Condition": { - "StringEquals": { - "sts:ExternalId": "integuserpoolidppoolAE0BD80C" - } - }, - "Effect": "Allow", - "Principal": { - "Service": "cognito-idp.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "sns-publish" - } - ] - } - }, "pool056F3F7E": { "Type": "AWS::Cognito::UserPool", "Properties": { - "AccountRecoverySetting": { + "AccountRecoverySetting": { "RecoveryMechanisms": [ - { "Name": "verified_phone_number", "Priority": 1 }, - { "Name": "verified_email", "Priority": 2 } + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } ] }, "AdminCreateUserConfig": { @@ -51,15 +20,6 @@ }, "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", - "SmsConfiguration": { - "ExternalId": "integuserpoolidppoolAE0BD80C", - "SnsCallerArn": { - "Fn::GetAtt": [ - "poolsmsRole04048F13", - "Arn" - ] - } - }, "SmsVerificationMessage": "The verification code to your new account is {####}", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", 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 90d858978f043..5cc13052434f2 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,10 +40,16 @@ "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { - "AccountRecoverySetting": { + "AccountRecoverySetting": { "RecoveryMechanisms": [ - { "Name": "verified_phone_number", "Priority": 1 }, - { "Name": "verified_email", "Priority": 2 } + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } ] }, "AdminCreateUserConfig": { 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 45661be1e0766..53c9f89ed8031 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,10 +40,16 @@ "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { - "AccountRecoverySetting": { + "AccountRecoverySetting": { "RecoveryMechanisms": [ - { "Name": "verified_phone_number", "Priority": 1 }, - { "Name": "verified_email", "Priority": 2 } + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } ] }, "AdminCreateUserConfig": { 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 f2beef72d6eb4..85214615c050d 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 @@ -1,49 +1,18 @@ { "Resources": { - "myuserpoolsmsRole0E16FDD9": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Condition": { - "StringEquals": { - "sts:ExternalId": "integuserpoolmyuserpoolDA38443C" - } - }, - "Effect": "Allow", - "Principal": { - "Service": "cognito-idp.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "sns-publish" - } - ] - } - }, "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { - "AccountRecoverySetting": { + "AccountRecoverySetting": { "RecoveryMechanisms": [ - { "Name": "verified_phone_number", "Priority": 1 }, - { "Name": "verified_email", "Priority": 2 } + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } ] }, "AdminCreateUserConfig": { @@ -51,15 +20,6 @@ }, "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", - "SmsConfiguration": { - "ExternalId": "integuserpoolmyuserpoolDA38443C", - "SnsCallerArn": { - "Fn::GetAtt": [ - "myuserpoolsmsRole0E16FDD9", - "Arn" - ] - } - }, "SmsVerificationMessage": "The verification code to your new account is {####}", "UserPoolName": "MyUserPool", "VerificationMessageTemplate": { 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 801879f6ea55a..17a09ff9863b6 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { ABSENT } from '@aws-cdk/assert/lib/assertions/have-resource'; -import { Role } from '@aws-cdk/aws-iam'; +import { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnParameter, Construct, Duration, Stack, Tag } from '@aws-cdk/core'; import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle } from '../lib'; @@ -28,49 +28,8 @@ describe('User Pool', () => { EmailSubject: 'Verify your new account', SmsMessage: 'The verification code to your new account is {####}', }, - SmsConfiguration: { - SnsCallerArn: { - 'Fn::GetAtt': [ 'PoolsmsRoleC3352CE6', 'Arn' ], - }, - ExternalId: 'Pool', - }, lambdaTriggers: ABSENT, }); - - expect(stack).toHaveResource('AWS::IAM::Role', { - AssumeRolePolicyDocument: { - Statement: [ - { - Action: 'sts:AssumeRole', - Condition: { - StringEquals: { - 'sts:ExternalId': 'Pool', - }, - }, - Effect: 'Allow', - Principal: { - Service: 'cognito-idp.amazonaws.com', - }, - }, - ], - Version: '2012-10-17', - }, - Policies: [ - { - PolicyDocument: { - Statement: [ - { - Action: 'sns:Publish', - Effect: 'Allow', - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - PolicyName: 'sns-publish', - }, - ], - }); }); test('self sign up option is correctly configured', () => { @@ -896,202 +855,427 @@ describe('User Pool', () => { }, }); }); -}); - -test('addClient', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const userpool = new UserPool(stack, 'Pool'); - userpool.addClient('UserPoolClient', { - userPoolClientName: 'userpoolclient', - }); - const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); - imported.addClient('UserPoolImportedClient', { - userPoolClientName: 'userpoolimportedclient', - }); - - // THEN - expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { - ClientName: 'userpoolclient', - UserPoolId: stack.resolve(userpool.userPoolId), - }); - expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { - ClientName: 'userpoolimportedclient', - UserPoolId: stack.resolve(imported.userPoolId), - }); -}); -test('addDomain', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const userpool = new UserPool(stack, 'Pool'); - userpool.addDomain('UserPoolDomain', { - cognitoDomain: { - domainPrefix: 'userpooldomain', - }, - }); - const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); - imported.addDomain('UserPoolImportedDomain', { - cognitoDomain: { - domainPrefix: 'userpoolimporteddomain', - }, - }); - - // THEN - expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { - Domain: 'userpooldomain', - UserPoolId: stack.resolve(userpool.userPoolId), - }); - expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { - Domain: 'userpoolimporteddomain', - UserPoolId: stack.resolve(imported.userPoolId), - }); -}); - -test('registered identity providers', () => { - // GIVEN - const stack = new Stack(); - const userPool = new UserPool(stack, 'pool'); - const provider1 = UserPoolIdentityProvider.fromProviderName(stack, 'provider1', 'provider1'); - const provider2 = UserPoolIdentityProvider.fromProviderName(stack, 'provider2', 'provider2'); - - // WHEN - userPool.registerIdentityProvider(provider1); - userPool.registerIdentityProvider(provider2); - - // THEN - expect(userPool.identityProviders).toEqual([provider1, provider2]); -}); - -function fooFunction(scope: Construct, name: string): lambda.IFunction { - return new lambda.Function(scope, name, { - functionName: name, - code: lambda.Code.inline('foo'), - runtime: lambda.Runtime.NODEJS_12_X, - handler: 'index.handler', - }); -} - -describe('AccountRecoverySetting should be configured correctly', () => { - test('EMAIL_AND_PHONE_WITHOUT_MFA', () => { + test('addClient', () => { // GIVEN const stack = new Stack(); // WHEN - new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.EMAIL_AND_PHONE_WITHOUT_MFA }); + const userpool = new UserPool(stack, 'Pool'); + userpool.addClient('UserPoolClient', { + userPoolClientName: 'userpoolclient', + }); + const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); + imported.addClient('UserPoolImportedClient', { + userPoolClientName: 'userpoolimportedclient', + }); // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPool', { - AccountRecoverySetting: { - RecoveryMechanisms: [ - { Name: 'verified_email', Priority: 1 }, - { Name: 'verified_phone_number', Priority: 2 }, - ], - }, + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'userpoolclient', + UserPoolId: stack.resolve(userpool.userPoolId), + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'userpoolimportedclient', + UserPoolId: stack.resolve(imported.userPoolId), }); }); - test('PHONE_WITHOUT_MFA_AND_EMAIL', () => { + test('addDomain', () => { // GIVEN const stack = new Stack(); // WHEN - new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.PHONE_WITHOUT_MFA_AND_EMAIL }); + const userpool = new UserPool(stack, 'Pool'); + userpool.addDomain('UserPoolDomain', { + cognitoDomain: { + domainPrefix: 'userpooldomain', + }, + }); + const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); + imported.addDomain('UserPoolImportedDomain', { + cognitoDomain: { + domainPrefix: 'userpoolimporteddomain', + }, + }); // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPool', { - AccountRecoverySetting: { - RecoveryMechanisms: [ - { Name: 'verified_phone_number', Priority: 1 }, - { Name: 'verified_email', Priority: 2 }, - ], - }, + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpooldomain', + UserPoolId: stack.resolve(userpool.userPoolId), + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpoolimporteddomain', + UserPoolId: stack.resolve(imported.userPoolId), }); }); - test('EMAIL_ONLY', () => { + test('registered identity providers', () => { // GIVEN const stack = new Stack(); + const userPool = new UserPool(stack, 'pool'); + const provider1 = UserPoolIdentityProvider.fromProviderName(stack, 'provider1', 'provider1'); + const provider2 = UserPoolIdentityProvider.fromProviderName(stack, 'provider2', 'provider2'); // WHEN - new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.EMAIL_ONLY }); + userPool.registerIdentityProvider(provider1); + userPool.registerIdentityProvider(provider2); // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPool', { - AccountRecoverySetting: { - RecoveryMechanisms: [ - { Name: 'verified_email', Priority: 1 }, - ], - }, - }); + expect(userPool.identityProviders).toEqual([provider1, provider2]); }); - test('PHONE_ONLY_WITHOUT_MFA', () => { - // GIVEN - const stack = new Stack(); + 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 }, + ], + }, + }); + }); - // WHEN - new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.PHONE_ONLY_WITHOUT_MFA }); + test('PHONE_WITHOUT_MFA_AND_EMAIL', () => { + // GIVEN + const stack = new Stack(); - // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPool', { - AccountRecoverySetting: { - RecoveryMechanisms: [ - { Name: 'verified_phone_number', Priority: 1 }, - ], - }, + // 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('NONE', () => { - // GIVEN - const stack = new Stack(); + test('EMAIL_ONLY', () => { + // GIVEN + const stack = new Stack(); - // WHEN - new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.NONE }); + // WHEN + new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.EMAIL_ONLY }); - // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPool', { - AccountRecoverySetting: { - RecoveryMechanisms: [ - { Name: 'admin_only', Priority: 1 }, - ], - }, + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + AccountRecoverySetting: { + RecoveryMechanisms: [ + { Name: 'verified_email', Priority: 1 }, + ], + }, + }); }); - }); - test('PHONE_AND_EMAIL', () => { - // GIVEN - const stack = new Stack(); + test('PHONE_ONLY_WITHOUT_MFA', () => { + // GIVEN + const stack = new Stack(); - // WHEN - new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.PHONE_AND_EMAIL }); + // WHEN + new UserPool(stack, 'pool', { accountRecovery: AccountRecovery.PHONE_ONLY_WITHOUT_MFA }); - // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPool', { - AccountRecoverySetting: ABSENT, + // 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 }, + ], + }, + }); }); }); - test('default', () => { - // GIVEN - const stack = new Stack(); + describe('sms roles', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); - // WHEN - new UserPool(stack, 'pool'); + // 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 }, + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: ABSENT, + }); + }); + + test('smsRole and smsExternalId is set', () => { + // GIVEN + const stack = new Stack(); + const smsRole = new Role(stack, 'smsRole', { + assumedBy: new ServicePrincipal('service.amazonaws.com'), + }); + + // WHEN + new UserPool(stack, 'pool', { + smsRole, + smsRoleExternalId: 'role-external-id', + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: { + ExternalId: 'role-external-id', + SnsCallerArn: { 'Fn::GetAtt': [ 'smsRoleA4587CE8', 'Arn' ] }, + }, + }); + }); + + test('setting enableSmsRole creates an sms role', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { + enableSmsRole: true, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: { + ExternalId: 'pool', + SnsCallerArn: { 'Fn::GetAtt': [ 'poolsmsRole04048F13', 'Arn' ] }, + }, + }); + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Condition: { + StringEquals: { + 'sts:ExternalId': 'pool', + }, + }, + Effect: 'Allow', + Principal: { + Service: 'cognito-idp.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: 'sns:Publish', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'sns-publish', + }, ], - }, + }); + }); + + test('auto sms role is not created when MFA and phoneVerification is off', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { + mfa: Mfa.OFF, + signInAliases: { + phone: false, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: ABSENT, + }); + }); + + test('auto sms role is not created when OTP-based MFA is enabled and phoneVerification is off', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { + mfa: Mfa.REQUIRED, + mfaSecondFactor: { + otp: true, + sms: false, + }, + signInAliases: { + phone: false, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: ABSENT, + }); + }); + + test('auto sms role is created when phone verification is turned on', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { + mfa: Mfa.OFF, + signInAliases: { phone: true }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: { + ExternalId: 'pool', + SnsCallerArn: { 'Fn::GetAtt': [ 'poolsmsRole04048F13', 'Arn' ] }, + }, + }); + }); + + test('auto sms role is created when phone auto-verification is set', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { + mfa: Mfa.OFF, + signInAliases: { phone: false }, + autoVerify: { phone: true }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: { + ExternalId: 'pool', + SnsCallerArn: { 'Fn::GetAtt': [ 'poolsmsRole04048F13', 'Arn' ] }, + }, + }); + }); + + test('auto sms role is created when MFA is turned on', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { + mfa: Mfa.REQUIRED, + mfaSecondFactor: { + sms: true, + otp: false, + }, + signInAliases: { + phone: false, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: { + ExternalId: 'pool', + SnsCallerArn: { 'Fn::GetAtt': [ 'poolsmsRole04048F13', 'Arn' ] }, + }, + }); + }); + + test('auto sms role is not created when enableSmsRole is unset, even when MFA is configured', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'pool', { + mfa: Mfa.REQUIRED, + mfaSecondFactor: { + sms: true, + otp: false, + }, + enableSmsRole: false, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + SmsConfiguration: ABSENT, + }); + }); + + test('throws an error when smsRole is specified but enableSmsRole is unset', () => { + const stack = new Stack(); + const smsRole = new Role(stack, 'smsRole', { + assumedBy: new ServicePrincipal('service.amazonaws.com'), + }); + + expect(() => new UserPool(stack, 'pool', { + smsRole, + enableSmsRole: false, + })).toThrow(/enableSmsRole cannot be disabled/); }); }); }); + +function fooFunction(scope: Construct, name: string): lambda.IFunction { + return new lambda.Function(scope, name, { + functionName: name, + code: lambda.Code.inline('foo'), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + }); +} \ No newline at end of file