From de908755d71f27d04c7e0efc88c5375c94f1765a Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 12 Mar 2020 18:03:34 +0000 Subject: [PATCH 1/8] feat(cognito): user pool - MFA, password policy and email settings Add support in user pool for configuring MFA, password policy and settings for transmitted emails. --- packages/@aws-cdk/aws-cognito/README.md | 66 +++++++ .../@aws-cdk/aws-cognito/lib/user-pool.ts | 172 ++++++++++++++++- ...teg.user-pool-explicit-props.expected.json | 19 ++ .../test/integ.user-pool-explicit-props.ts | 21 ++- .../aws-cognito/test/user-pool.test.ts | 174 +++++++++++++++++- 5 files changed, 446 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 487e538e4bedd..618eeecaa87c2 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -162,6 +162,72 @@ When the `smsRole` property is specified, the `smsRoleExternalId` may also be sp assume role policy should be configured to accept this value as the ExternalId. Learn more about [ExternalId here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html). +User pools can be configured to enable MFA. It can either be turned off, set to optional or made required. Setting MFA +to optional means that individual users can choose to enable it. +Additionally, the MFA code can be sent either via SMS text message or via a time-based software token. +See the [documentation on MFA](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html) to +learn more. + +The following code snippet marks MFA for the user pool as required. This means that all users are required to +configure an MFA token and use it for sign in. It also allows for the users to use both SMS based MFA, as well, +time-based one time password. + +```ts +new UserPool(this, 'myuserpool', { + // ... + mfaEnforcement: MfaEnforcement.REQUIRED, + mfaToken: { + sms: true, + oneTimePassword: true, + }, +}); +``` + +User pools can be configured with policies around a user's password. This includes the password length and the +character sets that they must contain. + +Further to this, it can also be configured with the validity of the auto-generated temporary password. A temporary +password is generated by the user pool either when an admin signs up a user or when a password reset is requested. +The validity of this password dictates how long to give the user to use this password before expiring it. + +The following code snippet configures these properties - + +```ts +new UserPool(this, 'myuserpool', { + // ... + passwordPolicy: { + minLength: 12, + requireLowercase: true, + requireUppercase: true, + requireDigits: true, + requireSymbols: true, + tempPasswordValidity: Duration.days(3), + }, +}); +``` + +Note that, `tempPasswordValidity` can be specified only in days. + +### Emails + +Cognito sends emails to users in the user pool, when particular actions take place, such as welcome emails, invitation +emails, password resets, etc. The address from which these emails are sent can be configured on the user pool. + +```ts +new UserPool(this, 'myuserpool', { + // ... + emailTransmission: { + from: 'noreply@myawesomeapp.com', + replyTo: 'support@myawesomeapp.com', + }, +}); +``` + +By default, user pools are configured to use Cognito's built-in email capability, but it can also be configured to use +Amazon SES, however, support for Amazon SES is not available in the CDK yet. You may use the [cfn +layer](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) to configure this. Read more about [email settings +here](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html). + ### Importing User Pools Any user pool that has been created outside of this stack, can be imported into the CDK app. Importing a user pool diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 43a41541ac136..e54ce339370eb 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -1,6 +1,6 @@ import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { Construct, Duration, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; /** @@ -303,6 +303,97 @@ export interface UserInvitationConfig { readonly smsMessage?: string; } +/** + * The different ways in which a user pool's MFA enforcement can be configured. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html + */ +export enum MfaEnforcement { + /** Users are not required to use MFA for sign in, and cannot configure one. */ + OFF = 'OFF', + /** Users are not required to use MFA for sign in, but can configure one if they so choose to. */ + OPTIONAL = 'OPTIONAL', + /** Users are required to configure an MFA, and have to use it to sign in. */ + REQUIRED = 'ON', +} + +/** + * The different ways in which a user pool can obtain their MFA token for sign in. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html + */ +export interface MfaTypes { + /** + * The MFA token is sent to the user via SMS to their verified phone numbers + * @default true + */ + readonly sms: boolean; + + /** + * The MFA token is a time-based one time password that is generated by a hardware or software token + * @default false + */ + readonly oneTimePassword: boolean; +} + +/** + * Password policy for User Pools. + */ +export interface PasswordPolicy { + /** + * The length of time the temporary password generated by an admin is valid. + * + * @default Duration.days(7) + */ + readonly tempPasswordValidity?: Duration; + + /** + * Minimum length required for a user's password. + * @default 8 + */ + readonly minLength?: number; + + /** + * Whether the user is required to have lowercase characters in their password. + * @default true + */ + readonly requireLowercase?: boolean; + + /** + * Whether the user is required to have uppercase characters in their password. + * @default true + */ + readonly requireUppercase?: boolean; + + /** + * Whether the user is required to have digits in their password. + * @default true + */ + readonly requireDigits?: boolean; + + /** + * Whether the user is required to have symbols in their password. + * @default true + */ + readonly requireSymbols?: boolean; +} + +/** + * Email transmission settings for a user pool. + */ +export interface EmailTransmission { + /** + * The 'from' address on the emails received by the user. + * @default noreply@verificationemail.com + */ + readonly from?: string; + + /** + * The 'replyTo' address on the emails received by the user as defined by IETF RFC-5322. + * When set, most email clients recognize to change 'to' line to this address when a reply is drafted. + * @default - Not set. + */ + readonly replyTo?: string; +} + /** * Props for the UserPool construct */ @@ -372,6 +463,33 @@ export interface UserPoolProps { */ readonly autoVerify?: AutoVerifiedAttrs; + /** + * Configure on whether users of this user pool can or are required use MFA to sign in. + * + * @default MfaEnforcement.OFF + */ + readonly mfaEnforcement?: MfaEnforcement; + + /** + * Configure the MFA types that users can use in this user pool. Ignored if `mfaEnforcement` is set to `OFF`. + * + * @default - { sms: true, oneTimePassword: false }, if `mfaEnforcement` is set to `OPTIONAL` or `REQUIRED`. + * { sms: false, oneTimePassword: false }, otherwise + */ + readonly mfaTypes?: MfaTypes; + + /** + * Password policy for this user pool. + * @default - see defaults on each property of `PasswordPolicy`. + */ + readonly passwordPolicy?: PasswordPolicy; + + /** + * Configure the different properties for transmitting email. + * @default - see defaults on each property of `EmailTransmission`. + */ + readonly emailTransmission?: EmailTransmission; + /** * Lambda functions to use for supported Cognito triggers. * @@ -495,6 +613,8 @@ export class UserPool extends Resource implements IUserPool { inviteMessageTemplate: props.userInvitation !== undefined ? inviteMessageTemplate : undefined, }; + const passwordPolicy = this.configurePasswordPolicy(props); + const userPool = new CfnUserPool(this, 'Resource', { userPoolName: props.userPoolName, usernameAttributes: signIn.usernameAttrs, @@ -507,6 +627,13 @@ export class UserPool extends Resource implements IUserPool { emailVerificationSubject, smsVerificationMessage, verificationMessageTemplate, + mfaConfiguration: props.mfaEnforcement !== undefined ? props.mfaEnforcement : undefined, + enabledMfas: this.mfaConfiguration(props), + policies: passwordPolicy !== undefined ? { passwordPolicy } : undefined, + emailConfiguration: undefinedIfNoKeys({ + from: props.emailTransmission?.from, + replyToEmailAddress: props.emailTransmission?.replyTo, + }), }); this.userPoolId = userPool.ref; @@ -706,4 +833,47 @@ export class UserPool extends Resource implements IUserPool { }; } } + + private mfaConfiguration(props: UserPoolProps): string[] | undefined { + if (props.mfaEnforcement === undefined || props.mfaEnforcement === MfaEnforcement.OFF) { + // since default is OFF, treat undefined and OFF the same way + return undefined; + } else if (props.mfaTypes === undefined && + (props.mfaEnforcement === MfaEnforcement.OPTIONAL || props.mfaEnforcement === MfaEnforcement.REQUIRED)) { + return [ 'SMS_MFA' ]; + } else { + const enabledMfas = []; + if (props.mfaTypes!.sms) { + enabledMfas.push('SMS_MFA'); + } + if (props.mfaTypes!.oneTimePassword) { + enabledMfas.push('SOFTWARE_TOKEN_MFA'); + } + return enabledMfas; + } + } + + private configurePasswordPolicy(props: UserPoolProps): CfnUserPool.PasswordPolicyProperty | undefined { + const tempPasswordValidity = props.passwordPolicy?.tempPasswordValidity; + if (tempPasswordValidity !== undefined && tempPasswordValidity.toDays() > Duration.days(365).toDays()) { + throw new Error(`tempPasswordValidity cannot be greater than 365 days (received: ${tempPasswordValidity.toDays()})`); + } + const minLength = props.passwordPolicy?.minLength; + if (minLength !== undefined && (minLength < 6 || minLength > 99)) { + throw new Error(`minLength for password must be between 6 and 99 (received: ${minLength})`); + } + return undefinedIfNoKeys({ + temporaryPasswordValidityDays: tempPasswordValidity?.toDays({ integral: true }), + minimumLength: minLength, + requireLowercase: props.passwordPolicy?.requireLowercase, + requireUppercase: props.passwordPolicy?.requireUppercase, + requireNumbers: props.passwordPolicy?.requireDigits, + requireSymbols: props.passwordPolicy?.requireSymbols, + }); + } +} + +function undefinedIfNoKeys(struct: object): object | undefined { + const allUndefined = Object.values(struct).reduce((acc, v) => acc && (v === undefined), true); + return allUndefined ? undefined : struct; } \ No newline at end of file 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 f7b8a29230d8d..35d031301237b 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 @@ -55,9 +55,28 @@ "email", "phone_number" ], + "EmailConfiguration": { + "From": "noreply@myawesomeapp.com", + "ReplyToEmailAddress": "support@myawesomeapp.com" + }, "EmailVerificationMessage": "verification email body from the integ test. Code is {####}.", "EmailVerificationSubject": "verification email subject from the integ test", + "EnabledMfas": [ + "SMS_MFA", + "SOFTWARE_TOKEN_MFA" + ], "LambdaConfig": {}, + "MfaConfiguration": "ON", + "Policies": { + "PasswordPolicy": { + "MinimumLength": 12, + "RequireLowercase": true, + "RequireNumbers": true, + "RequireSymbols": true, + "RequireUppercase": true, + "TemporaryPasswordValidityDays": 10 + } + }, "SmsConfiguration": { "ExternalId": "integuserpoolmyuserpoolDA38443C", "SnsCallerArn": { 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 316beb5d3a494..c3b0b705e170f 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,5 +1,5 @@ -import { App, Stack } from '@aws-cdk/core'; -import { UserPool } from '../lib'; +import { App, Duration, Stack } from '@aws-cdk/core'; +import { MfaEnforcement, UserPool } from '../lib'; const app = new App(); const stack = new Stack(app, 'integ-user-pool'); @@ -25,4 +25,21 @@ new UserPool(stack, 'myuserpool', { email: true, phone: true, }, + mfaEnforcement: MfaEnforcement.REQUIRED, + mfaTypes: { + sms: true, + oneTimePassword: true, + }, + passwordPolicy: { + tempPasswordValidity: Duration.days(10), + minLength: 12, + requireDigits: true, + requireLowercase: true, + requireUppercase: true, + requireSymbols: true, + }, + emailTransmission: { + from: 'noreply@myawesomeapp.com', + replyTo: 'support@myawesomeapp.com', + }, }); \ No newline at end of file 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 16cfc7b9b051b..a49cbabb37b5e 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -2,8 +2,8 @@ import '@aws-cdk/assert/jest'; 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 { Stack, Tag } from '@aws-cdk/core'; -import { UserPool, VerificationEmailStyle } from '../lib'; +import { Duration, Stack, Tag } from '@aws-cdk/core'; +import { MfaEnforcement, UserPool, VerificationEmailStyle } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -33,7 +33,7 @@ describe('User Pool', () => { 'Fn::GetAtt': [ 'PoolsmsRoleC3352CE6', 'Arn' ], }, ExternalId: 'Pool' - } + }, }); expect(stack).toHaveResource('AWS::IAM::Role', { @@ -436,4 +436,172 @@ describe('User Pool', () => { AutoVerifiedAttributes: [ 'email', 'phone_number' ], }); }); + + test('mfaTypes is ignored when mfaEnforcement is undefined or set to OFF', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool1', { + userPoolName: 'Pool1', + mfaTypes: { + sms: true, + oneTimePassword: true, + } + }); + new UserPool(stack, 'Pool2', { + userPoolName: 'Pool2', + mfaEnforcement: MfaEnforcement.OFF, + mfaTypes: { + sms: true, + oneTimePassword: true, + } + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool1', + MfaConfiguration: ABSENT, + EnabledMfas: ABSENT, + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool2', + MfaConfiguration: 'OFF', + EnabledMfas: ABSENT, + }); + }); + + test('sms mfa type is the default when mfaEnforcement is set to REQUIRED or OPTIONAL', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool1', { + userPoolName: 'Pool1', + mfaEnforcement: MfaEnforcement.OPTIONAL, + }); + new UserPool(stack, 'Pool2', { + userPoolName: 'Pool2', + mfaEnforcement: MfaEnforcement.REQUIRED, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool1', + MfaConfiguration: 'OPTIONAL', + EnabledMfas: [ 'SMS_MFA' ], + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool2', + MfaConfiguration: 'ON', + EnabledMfas: [ 'SMS_MFA' ], + }); + }); + + test('mfa type is correctly picked up when specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + mfaEnforcement: MfaEnforcement.REQUIRED, + mfaTypes: { + sms: true, + oneTimePassword: true, + } + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + EnabledMfas: [ 'SMS_MFA', 'SOFTWARE_TOKEN_MFA' ], + }); + }); + + test('password policy is correctly set', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + passwordPolicy: { + tempPasswordValidity: Duration.days(2), + minLength: 15, + requireDigits: true, + requireLowercase: true, + requireUppercase: true, + requireSymbols: true, + } + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + Policies: { + PasswordPolicy: { + TemporaryPasswordValidityDays: 2, + MinimumLength: 15, + RequireLowercase: true, + RequireUppercase: true, + RequireNumbers: true, + RequireSymbols: true, + }, + }, + }); + }); + + test('throws when tempPassword validity is not in round days', () => { + const stack = new Stack(); + + expect(() => new UserPool(stack, 'Pool', { + passwordPolicy: { + tempPasswordValidity: Duration.hours(30), + } + })).toThrow(); + }); + + test('temp password throws an error when above the max', () => { + const stack = new Stack(); + + expect(() => new UserPool(stack, 'Pool', { + passwordPolicy: { + tempPasswordValidity: Duration.days(400), + } + })).toThrow(/tempPasswordValidity cannot be greater than/); + }); + + test('throws when minLength is out of range', () => { + const stack = new Stack(); + + expect(() => new UserPool(stack, 'Pool1', { + passwordPolicy: { + minLength: 5, + }, + })).toThrow(/minLength for password must be between/); + + expect(() => new UserPool(stack, 'Pool2', { + passwordPolicy: { + minLength: 100, + }, + })).toThrow(/minLength for password must be between/); + }); + + test('email transmission settings are recognized correctly', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + emailTransmission: { + from: 'from@myawesomeapp.com', + replyTo: 'replyTo@myawesomeapp.com' + } + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + EmailConfiguration: { + From: 'from@myawesomeapp.com', + ReplyToEmailAddress: 'replyTo@myawesomeapp.com' + } + }); + }); }); \ No newline at end of file From e38c9e143e08b6a3eef8adf1fd051bc8934c0291 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 17 Mar 2020 12:35:07 +0000 Subject: [PATCH 2/8] Rename to otp --- packages/@aws-cdk/aws-cognito/README.md | 2 +- packages/@aws-cdk/aws-cognito/lib/user-pool.ts | 4 ++-- .../aws-cognito/test/integ.user-pool-explicit-props.ts | 2 +- packages/@aws-cdk/aws-cognito/test/user-pool.test.ts | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 618eeecaa87c2..a1841590f02bc 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -178,7 +178,7 @@ new UserPool(this, 'myuserpool', { mfaEnforcement: MfaEnforcement.REQUIRED, mfaToken: { sms: true, - oneTimePassword: true, + otp: true, }, }); ``` diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index e54ce339370eb..c09f622b461cc 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -331,7 +331,7 @@ export interface MfaTypes { * The MFA token is a time-based one time password that is generated by a hardware or software token * @default false */ - readonly oneTimePassword: boolean; + readonly otp: boolean; } /** @@ -846,7 +846,7 @@ export class UserPool extends Resource implements IUserPool { if (props.mfaTypes!.sms) { enabledMfas.push('SMS_MFA'); } - if (props.mfaTypes!.oneTimePassword) { + if (props.mfaTypes!.otp) { enabledMfas.push('SOFTWARE_TOKEN_MFA'); } return enabledMfas; 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 c3b0b705e170f..2560ade195fb6 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 @@ -28,7 +28,7 @@ new UserPool(stack, 'myuserpool', { mfaEnforcement: MfaEnforcement.REQUIRED, mfaTypes: { sms: true, - oneTimePassword: true, + otp: true, }, passwordPolicy: { tempPasswordValidity: Duration.days(10), 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 a49cbabb37b5e..1891f15048607 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -446,7 +446,7 @@ describe('User Pool', () => { userPoolName: 'Pool1', mfaTypes: { sms: true, - oneTimePassword: true, + otp: true, } }); new UserPool(stack, 'Pool2', { @@ -454,7 +454,7 @@ describe('User Pool', () => { mfaEnforcement: MfaEnforcement.OFF, mfaTypes: { sms: true, - oneTimePassword: true, + otp: true, } }); @@ -507,7 +507,7 @@ describe('User Pool', () => { mfaEnforcement: MfaEnforcement.REQUIRED, mfaTypes: { sms: true, - oneTimePassword: true, + otp: true, } }); From 7a2ff56a773df5d7d441b3f4e720848eabf9f1f7 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 17 Mar 2020 12:44:37 +0000 Subject: [PATCH 3/8] updates to README --- packages/@aws-cdk/aws-cognito/README.md | 28 ++++++++++++++----- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 4 ++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index a1841590f02bc..90527b4b86249 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -29,6 +29,16 @@ other AWS services. This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +## Table of Contents + +- [User Pools](#user-pools) + - [Sign Up](#sign-up) + - [Sign In](#sign-in) + - [Security](#security) + - [Multi-factor Authentication](#multi-factor-authentication) + - [Emails](#emails) + - [Import](#importing-user-pools) + ## User Pools User pools allow creating and managing your own directory of users that can sign up and sign in. They enable easy @@ -162,15 +172,18 @@ When the `smsRole` property is specified, the `smsRoleExternalId` may also be sp assume role policy should be configured to accept this value as the ExternalId. Learn more about [ExternalId here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html). -User pools can be configured to enable MFA. It can either be turned off, set to optional or made required. Setting MFA -to optional means that individual users can choose to enable it. +#### Multi-factor Authentication (MFA) + +User pools can be configured to enable multi-factor authentication (MFA). It can either be turned off, set to optional +or made required. Setting MFA to optional means that individual users can choose to enable it. Additionally, the MFA code can be sent either via SMS text message or via a time-based software token. See the [documentation on MFA](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html) to learn more. The following code snippet marks MFA for the user pool as required. This means that all users are required to configure an MFA token and use it for sign in. It also allows for the users to use both SMS based MFA, as well, -time-based one time password. +[time-based one time password +(TOTP)](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-totp.html). ```ts new UserPool(this, 'myuserpool', { @@ -206,12 +219,13 @@ new UserPool(this, 'myuserpool', { }); ``` -Note that, `tempPasswordValidity` can be specified only in days. +Note that, `tempPasswordValidity` can be specified only in whole days. Specifying fractional days would throw an error. ### Emails Cognito sends emails to users in the user pool, when particular actions take place, such as welcome emails, invitation emails, password resets, etc. The address from which these emails are sent can be configured on the user pool. +Read more about [email settings here](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html). ```ts new UserPool(this, 'myuserpool', { @@ -224,9 +238,9 @@ new UserPool(this, 'myuserpool', { ``` By default, user pools are configured to use Cognito's built-in email capability, but it can also be configured to use -Amazon SES, however, support for Amazon SES is not available in the CDK yet. You may use the [cfn -layer](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) to configure this. Read more about [email settings -here](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html). +Amazon SES, however, support for Amazon SES is not available in the CDK yet. If you would like this to be implemented, +give [this issue](https://github.com/aws/aws-cdk/issues/6768) a +1. Until then, you can use the [cfn +layer](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) to configure this. ### Importing User Pools diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index c09f622b461cc..003cceec1f401 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -323,12 +323,14 @@ export enum MfaEnforcement { export interface MfaTypes { /** * The MFA token is sent to the user via SMS to their verified phone numbers + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-sms-text-message.html * @default true */ readonly sms: boolean; /** * The MFA token is a time-based one time password that is generated by a hardware or software token + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-totp.html * @default false */ readonly otp: boolean; @@ -627,7 +629,7 @@ export class UserPool extends Resource implements IUserPool { emailVerificationSubject, smsVerificationMessage, verificationMessageTemplate, - mfaConfiguration: props.mfaEnforcement !== undefined ? props.mfaEnforcement : undefined, + mfaConfiguration: props.mfaEnforcement, enabledMfas: this.mfaConfiguration(props), policies: passwordPolicy !== undefined ? { passwordPolicy } : undefined, emailConfiguration: undefinedIfNoKeys({ From 24fb3cfbb7062c2df0c7d480595d2fc487d33dc4 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 17 Mar 2020 12:48:31 +0000 Subject: [PATCH 4/8] rename mfa --- packages/@aws-cdk/aws-cognito/README.md | 4 ++-- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 22 +++++++++---------- .../test/integ.user-pool-explicit-props.ts | 6 ++--- .../aws-cognito/test/user-pool.test.ts | 16 +++++++------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 90527b4b86249..318b4300a89b0 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -188,8 +188,8 @@ configure an MFA token and use it for sign in. It also allows for the users to u ```ts new UserPool(this, 'myuserpool', { // ... - mfaEnforcement: MfaEnforcement.REQUIRED, - mfaToken: { + mfa: Mfa.REQUIRED, + mfaSecondFactor: { sms: true, otp: true, }, diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 003cceec1f401..b2f03e9b26051 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -307,7 +307,7 @@ export interface UserInvitationConfig { * The different ways in which a user pool's MFA enforcement can be configured. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html */ -export enum MfaEnforcement { +export enum Mfa { /** Users are not required to use MFA for sign in, and cannot configure one. */ OFF = 'OFF', /** Users are not required to use MFA for sign in, but can configure one if they so choose to. */ @@ -320,7 +320,7 @@ export enum MfaEnforcement { * The different ways in which a user pool can obtain their MFA token for sign in. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html */ -export interface MfaTypes { +export interface MfaSecondFactor { /** * The MFA token is sent to the user via SMS to their verified phone numbers * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-sms-text-message.html @@ -468,9 +468,9 @@ export interface UserPoolProps { /** * Configure on whether users of this user pool can or are required use MFA to sign in. * - * @default MfaEnforcement.OFF + * @default Mfa.OFF */ - readonly mfaEnforcement?: MfaEnforcement; + readonly mfa?: Mfa; /** * Configure the MFA types that users can use in this user pool. Ignored if `mfaEnforcement` is set to `OFF`. @@ -478,7 +478,7 @@ export interface UserPoolProps { * @default - { sms: true, oneTimePassword: false }, if `mfaEnforcement` is set to `OPTIONAL` or `REQUIRED`. * { sms: false, oneTimePassword: false }, otherwise */ - readonly mfaTypes?: MfaTypes; + readonly mfaSecondFactor?: MfaSecondFactor; /** * Password policy for this user pool. @@ -629,7 +629,7 @@ export class UserPool extends Resource implements IUserPool { emailVerificationSubject, smsVerificationMessage, verificationMessageTemplate, - mfaConfiguration: props.mfaEnforcement, + mfaConfiguration: props.mfa, enabledMfas: this.mfaConfiguration(props), policies: passwordPolicy !== undefined ? { passwordPolicy } : undefined, emailConfiguration: undefinedIfNoKeys({ @@ -837,18 +837,18 @@ export class UserPool extends Resource implements IUserPool { } private mfaConfiguration(props: UserPoolProps): string[] | undefined { - if (props.mfaEnforcement === undefined || props.mfaEnforcement === MfaEnforcement.OFF) { + if (props.mfa === undefined || props.mfa === Mfa.OFF) { // since default is OFF, treat undefined and OFF the same way return undefined; - } else if (props.mfaTypes === undefined && - (props.mfaEnforcement === MfaEnforcement.OPTIONAL || props.mfaEnforcement === MfaEnforcement.REQUIRED)) { + } else if (props.mfaSecondFactor === undefined && + (props.mfa === Mfa.OPTIONAL || props.mfa === Mfa.REQUIRED)) { return [ 'SMS_MFA' ]; } else { const enabledMfas = []; - if (props.mfaTypes!.sms) { + if (props.mfaSecondFactor!.sms) { enabledMfas.push('SMS_MFA'); } - if (props.mfaTypes!.otp) { + if (props.mfaSecondFactor!.otp) { enabledMfas.push('SOFTWARE_TOKEN_MFA'); } return enabledMfas; 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 2560ade195fb6..d0a37d4f5c968 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,5 +1,5 @@ import { App, Duration, Stack } from '@aws-cdk/core'; -import { MfaEnforcement, UserPool } from '../lib'; +import { Mfa, UserPool } from '../lib'; const app = new App(); const stack = new Stack(app, 'integ-user-pool'); @@ -25,8 +25,8 @@ new UserPool(stack, 'myuserpool', { email: true, phone: true, }, - mfaEnforcement: MfaEnforcement.REQUIRED, - mfaTypes: { + mfa: Mfa.REQUIRED, + mfaSecondFactor: { sms: true, otp: 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 1891f15048607..9b8e5795661a3 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 { Duration, Stack, Tag } from '@aws-cdk/core'; -import { MfaEnforcement, UserPool, VerificationEmailStyle } from '../lib'; +import { Mfa, UserPool, VerificationEmailStyle } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -444,15 +444,15 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool1', { userPoolName: 'Pool1', - mfaTypes: { + mfaSecondFactor: { sms: true, otp: true, } }); new UserPool(stack, 'Pool2', { userPoolName: 'Pool2', - mfaEnforcement: MfaEnforcement.OFF, - mfaTypes: { + mfa: Mfa.OFF, + mfaSecondFactor: { sms: true, otp: true, } @@ -478,11 +478,11 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool1', { userPoolName: 'Pool1', - mfaEnforcement: MfaEnforcement.OPTIONAL, + mfa: Mfa.OPTIONAL, }); new UserPool(stack, 'Pool2', { userPoolName: 'Pool2', - mfaEnforcement: MfaEnforcement.REQUIRED, + mfa: Mfa.REQUIRED, }); // THEN @@ -504,8 +504,8 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - mfaEnforcement: MfaEnforcement.REQUIRED, - mfaTypes: { + mfa: Mfa.REQUIRED, + mfaSecondFactor: { sms: true, otp: true, } From 013a416b5e9b0eaaaf05e4329b42b2eded3a711b Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 17 Mar 2020 13:02:40 +0000 Subject: [PATCH 5/8] minor fix on toc --- packages/@aws-cdk/aws-cognito/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 318b4300a89b0..0b5833a03ae77 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -35,7 +35,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Sign Up](#sign-up) - [Sign In](#sign-in) - [Security](#security) - - [Multi-factor Authentication](#multi-factor-authentication) + - [Multi-factor Authentication](#multi-factor-authentication-mfa) - [Emails](#emails) - [Import](#importing-user-pools) From 27a1636413cf086320b97c88a9c635b11f11e2b7 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 17 Mar 2020 14:37:11 +0000 Subject: [PATCH 6/8] clarify whole days --- packages/@aws-cdk/aws-cognito/lib/user-pool.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index b2f03e9b26051..62bde3e89ff31 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -342,7 +342,8 @@ export interface MfaSecondFactor { export interface PasswordPolicy { /** * The length of time the temporary password generated by an admin is valid. - * + * This must be provided as whole days, like Duration.days(3) or Duration.hours(48). + * Fractional days, such as Duration.hours(20), will generate an error. * @default Duration.days(7) */ readonly tempPasswordValidity?: Duration; From a5c438806b5df93bf5804973befee627af65d358 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 17 Mar 2020 14:47:07 +0000 Subject: [PATCH 7/8] emailTransmission -> emailSettings --- packages/@aws-cdk/aws-cognito/lib/user-pool.ts | 16 ++++++++-------- .../test/integ.user-pool-explicit-props.ts | 2 +- .../@aws-cdk/aws-cognito/test/user-pool.test.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 62bde3e89ff31..e64b7453966fa 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -380,9 +380,9 @@ export interface PasswordPolicy { } /** - * Email transmission settings for a user pool. + * Email settings for the user pool. */ -export interface EmailTransmission { +export interface EmailSettings { /** * The 'from' address on the emails received by the user. * @default noreply@verificationemail.com @@ -483,15 +483,15 @@ export interface UserPoolProps { /** * Password policy for this user pool. - * @default - see defaults on each property of `PasswordPolicy`. + * @default - see defaults on each property of PasswordPolicy. */ readonly passwordPolicy?: PasswordPolicy; /** - * Configure the different properties for transmitting email. - * @default - see defaults on each property of `EmailTransmission`. + * Email settings for a user pool. + * @default - see defaults on each property of EmailSettings. */ - readonly emailTransmission?: EmailTransmission; + readonly emailSettings?: EmailSettings; /** * Lambda functions to use for supported Cognito triggers. @@ -634,8 +634,8 @@ export class UserPool extends Resource implements IUserPool { enabledMfas: this.mfaConfiguration(props), policies: passwordPolicy !== undefined ? { passwordPolicy } : undefined, emailConfiguration: undefinedIfNoKeys({ - from: props.emailTransmission?.from, - replyToEmailAddress: props.emailTransmission?.replyTo, + from: props.emailSettings?.from, + replyToEmailAddress: props.emailSettings?.replyTo, }), }); 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 d0a37d4f5c968..9ad4105d942e2 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 @@ -38,7 +38,7 @@ new UserPool(stack, 'myuserpool', { requireUppercase: true, requireSymbols: true, }, - emailTransmission: { + emailSettings: { from: 'noreply@myawesomeapp.com', replyTo: 'support@myawesomeapp.com', }, 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 9b8e5795661a3..8b01098940e26 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -590,7 +590,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - emailTransmission: { + emailSettings: { from: 'from@myawesomeapp.com', replyTo: 'replyTo@myawesomeapp.com' } From b1f00de81c11f30dfc42f3ae533ba169a1f5af56 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 17 Mar 2020 18:42:42 +0000 Subject: [PATCH 8/8] correct a bunch of issues introduced during conflict resolution --- packages/@aws-cdk/aws-cognito/lib/user-pool.ts | 4 ++-- packages/@aws-cdk/aws-cognito/test/user-pool.test.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 4110335d01b1e..3bc593733c634 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -365,7 +365,7 @@ export interface UserPoolProps { */ readonly customAttributes?: { [key: string]: ICustomAttribute }; - /* + /** * Configure whether users of this user pool can or are required use MFA to sign in. * * @default Mfa.OFF @@ -390,7 +390,7 @@ export interface UserPoolProps { * Email settings for a user pool. * @default - see defaults on each property of EmailSettings. */ - readonly emailSettings?: EmailSettings; + readonly emailSettings?: EmailSettings; /** * Lambda functions to use for supported Cognito triggers. 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 3d79d97460421..9576295290a72 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -2,6 +2,7 @@ import '@aws-cdk/assert/jest'; 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 { Duration, Stack, Tag } from '@aws-cdk/core'; import { Mfa, NumberAttribute, StringAttribute, UserPool, VerificationEmailStyle } from '../lib'; describe('User Pool', () => { @@ -554,8 +555,8 @@ describe('User Pool', () => { ] }); }); - - test('mfaTypes is ignored when mfaEnforcement is undefined or set to OFF', () => { + + test('mfaTypes is ignored when mfaEnforcement is undefined or set to OFF', () => { // GIVEN const stack = new Stack();