From a5416d1c2c6ebfabd9aa091887dc208ac3e590b3 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:31:43 +0000 Subject: [PATCH 1/8] feat(aws-cognito): add support for user pool SES integration add support for SES integration by introducing a new property for configuring email settings for a user pool. This feature supports both types of integration with SES. 1. Using the COGNITO_DEFAULT sending account, but providing a custom email address 2. Using the DEVELOPER sending account This feature does not automate any configuration on SES since that is not currently possible with CloudFormation and requires a manual verification step. To use the SES integration introduced in this feature the user will have had to already configured a verified email address in Amazon SES and followed the steps outlined here: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html closes #6768 --- packages/@aws-cdk/aws-cognito/README.md | 61 +++++ .../@aws-cdk/aws-cognito/lib/user-pool.ts | 259 +++++++++++++++++- .../aws-cognito/test/user-pool.test.ts | 220 ++++++++++++++- 3 files changed, 535 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 2315662f49d10..6de798a0dc8f4 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -338,6 +338,67 @@ layer](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) to configure If an email address contains non-ASCII characters, it will be encoded using the [punycode encoding](https://en.wikipedia.org/wiki/Punycode) when generating the template for Cloudformation. +### EmailsBeta1 + +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). + +By default, user pools are configured to use Cognito's built in email capability, which by default will send emails +from `no-reply@verificationemail.com`. If you want to customize the from address, while still using the Cognito built-in +email capability, you can do so by specifying a custom email address that has been configured in Amazon SES. + +```ts +new cognito.UserPool(this, 'myuserpool', { + email: EmailBeta1.withCognito({ + fromEmail: 'noreply@myawesomeapp.com', + replyTo: 'support@myawesomeapp.com', + }), +}); +``` + +In the above example a custom email address is specified as `noreply@myawesomeapp.com`. In order for this to work +this email must be a verified email address in Amazon SES, and that email address must have an authorization policy +that allows Cognito to send emails. See the section `Step 3: Grant Email Permissions to Amazon Cognito` of the +[developer guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-configure). + + +For production applications it is recommended to configure your UserPool to send emails through Amazon SES. To do +so you must first have followed the steps in the [Cognito Developer Guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-developer) +to verify an email address, move your account out of the SES sandbox, and grant Cognito email permissions via an +authorization policy. + +Once the SES setup is complete, you can configure your UserPool to use the email configured in SES. + +```ts +new cognito.UserPool(this, 'myuserpool', { + email: EmailBeta1.withSES({ + from: { + email: 'noreply@myawesomeapp.com', + name: 'Awesome App', + }, + replyTo: 'support@myawesomeapp.com', + }), +}); +``` + +Sending emails through SES requires that SES be configured in either `us-east-1`, `us-west-1`, or `eu-west-1`. +If your UserPool is being created in a different region you must specify which SES region to use. + +```ts +new cognito.UserPool(this, 'myuserpool', { + email: EmailBeta1.withSES({ + sesRegion: SESRegionBeta1.US_EAST_1, + from: { + email: 'noreply@myawesomeapp.com', + name: 'Awesome App', + }, + replyTo: 'support@myawesomeapp.com', + }), +}); + +``` + ### Device Tracking User pools can be configured to track devices that users have logged in to. diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 6277ee0f467ee..98ebebc422213 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -370,6 +370,246 @@ export interface PasswordPolicy { readonly requireSymbols?: boolean; } +/** + * Configuration for what from email address and name Cognito will + * use to send emails via SES + */ +export interface EmailFromBeta1 { + /** + * The verified Amazon SES email address that Cognito should + * use to send emails. + */ + readonly email: string; + + /** + * An optional name that should be used as the sender's name + * along with the email. + * + * @default - no name + */ + readonly name?: string; +} + +/** + * Valid Amazon SES configuration regions + */ +export enum SESRegionBeta1 { + /** + * Amazon SES region in 'us-east-1' + */ + US_EAST_1 = 'us-east-1', + + /** + * Amazon SES region in 'us-west-2' + */ + US_WEST_2 = 'us-west-2', + + /** + * Amazon SES region in 'eu-west-1' + */ + EU_WEST_1 = 'eu-west-1', +} + +/** + * Configuration for Cognito sending emails via Amazon SES + */ +export interface SESOptionsBeta1 { + /** + * Identifies either the sender's email address or the + * sender's name with their email address. + * + * The email address used must be a verified email address + * in Amazon SES and must be configured to allow Cognito to + * send emails. + * + * https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html + */ + readonly from: EmailFromBeta1; + + /** + * The destination to which the receiver of the email should reploy to. + * + * @default - same as the fromEmail + */ + readonly replyTo?: string; + + /** + * The name of a configuration set in SES that should + * be applied to emails sent via Cognito. + * + * @default - no configuration set + */ + readonly configurationSetName?: string; + + /** + * Required if the UserPool region is different than the SES region. + * + * If sending emails with a Amazon SES verified email address, + * and the region that SES is configured is different than the + * region in which the UserPool is deployed, you must specify that + * region here. + * + * @default - The same region as the Cognito UserPool + */ + readonly sesRegion?: SESRegionBeta1; +} + +/** + * Configuration settings for Cognito default email + */ +export interface CognitoEmailOptionsBeta1 { + /** + * The verified email address in Amazon SES that + * Cognito will use to send emails. You must have already + * configured Amazon SES to allow Cognito to send Emails + * through this address. + * + * https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html + * + * @default - Cognito default email address will be used + * 'no-reply@verificationemail.com' + */ + readonly fromEmail?: string; + + /** + * The destination to which the receiver of the email should reploy to. + * + * @default - same as the fromEmail + */ + readonly replyTo?: string; + + /** + * Required if the UserPool region is different than the SES region. + * + * If sending emails with a Amazon SES verified email address, + * and the region that SES is configured is different than the + * region in which the UserPool is deployed, you must specify that + * region here. + * + * @default - The same region as the Cognito UserPool + */ + readonly sesRegion?: SESRegionBeta1; +} + +/** + * Configuration for Cognito email settings + */ +export interface EmailConfigurationBeta1 { + /** + * UserPool CFN configuration for email configuration + */ + readonly emailConfig: CfnUserPool.EmailConfigurationProperty; +} + +/** + * Configure how Cognito sends emails + */ +export abstract class EmailBeta1 { + /** + * Send email using Cognito + */ + public static withCognito(options?: CognitoEmailOptionsBeta1): EmailBeta1 { + return new CognitoEmail(options); + } + + /** + * Send email using SES + */ + public static withSES(options: SESOptionsBeta1): EmailBeta1 { + return new SESEmail(options); + } + + /** + * The valid Amazon SES configuration regions + */ + protected readonly regions = ['us-east-1', 'us-west-2', 'eu-west-1']; + + /** + * Returns the email configuration for a Cognito UserPool + * that controls how Cognito will send emails + */ + public abstract bind(scope: Construct): EmailConfigurationBeta1; + +} + +class CognitoEmail extends EmailBeta1 { + constructor(private readonly options?: CognitoEmailOptionsBeta1) { + super(); + } + + public bind(scope: Construct): EmailConfigurationBeta1 { + const region = Stack.of(scope).region; + + // if a custom email is provided that means that cognito is going to use an SES email + // and we need to provide the sourceArn which requires a valid region + let sourceArn: string | undefined = undefined; + if (this.options?.fromEmail) { + if (this.options.fromEmail != 'no-reply@verificationemail.com') { + if (Token.isUnresolved(region) && !this.options.sesRegion) { + throw new Error('Your stack region cannot be determined so "sesRegion" is required in CognitoEmailOptions'); + } + if (this.options?.sesRegion && !this.regions.includes(this.options.sesRegion)) { + throw new Error(`sesRegion must be one of 'us-east-1', 'us-west-2', 'eu-west-1'. received ${this.options.sesRegion}`); + } else if (!this.options?.sesRegion && !this.regions.includes(region)) { + throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); + } + sourceArn = Stack.of(scope).formatArn({ + service: 'ses', + resource: 'identity', + resourceName: this.options.fromEmail, + region: this.options.sesRegion ?? region, + }); + } + } + + + return { + emailConfig: { + replyToEmailAddress: encodePuny(this.options?.replyTo), + emailSendingAccount: 'COGNITO_DEFAULT', + sourceArn, + }, + }; + + } +} + +class SESEmail extends EmailBeta1 { + constructor(private readonly options: SESOptionsBeta1) { + super(); + } + + public bind(scope: Construct): EmailConfigurationBeta1 { + const region = Stack.of(scope).region; + + if (Token.isUnresolved(region) && !this.options.sesRegion) { + throw new Error('Your stack region cannot be determined so "sesRegion" is required in SESOptions'); + } + + if (this.options.sesRegion && !this.regions.includes(this.options.sesRegion)) { + throw new Error(`sesRegion must be one of 'us-east-1', 'us-west-2', 'eu-west-1'. received ${this.options.sesRegion}`); + } else if (!this.options.sesRegion && !this.regions.includes(region)) { + throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); + } + + return { + emailConfig: { + from: encodePuny(`${this.options.from.name} <${this.options.from.email}>`), + replyToEmailAddress: encodePuny(this.options.replyTo), + configurationSet: this.options.configurationSetName, + emailSendingAccount: 'DEVELOPER', + sourceArn: Stack.of(scope).formatArn({ + service: 'ses', + resource: 'identity', + resourceName: this.options.from.email, + region: this.options.sesRegion ?? region, + }), + }, + }; + } +} + + /** * Email settings for the user pool. */ @@ -574,6 +814,12 @@ export interface UserPoolProps { */ readonly emailSettings?: EmailSettings; + /** + * Email settings for a user pool. + * @default - cognito will use the default email configuration + */ + readonly email?: EmailBeta1; + /** * Lambda functions to use for supported Cognito triggers. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html @@ -788,6 +1034,14 @@ export class UserPool extends UserPoolBase { const passwordPolicy = this.configurePasswordPolicy(props); + if (props.email && props.emailSettings) { + throw new Error('you must either provide "email" or "emailSettings", but not both'); + } + const emailConfiguration = props.email ? props.email.bind(this).emailConfig : undefinedIfNoKeys({ + from: encodePuny(props.emailSettings?.from), + replyToEmailAddress: encodePuny(props.emailSettings?.replyTo), + }); + const userPool = new CfnUserPool(this, 'Resource', { userPoolName: props.userPoolName, usernameAttributes: signIn.usernameAttrs, @@ -805,10 +1059,7 @@ export class UserPool extends UserPoolBase { mfaConfiguration: props.mfa, enabledMfas: this.mfaConfiguration(props), policies: passwordPolicy !== undefined ? { passwordPolicy } : undefined, - emailConfiguration: undefinedIfNoKeys({ - from: encodePuny(props.emailSettings?.from), - replyToEmailAddress: encodePuny(props.emailSettings?.replyTo), - }), + emailConfiguration, usernameConfiguration: undefinedIfNoKeys({ caseSensitive: props.signInCaseSensitive, }), 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 7b132803da2d6..a53ca600b46d9 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 { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnParameter, Duration, Stack, Tags } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, EmailBeta1, SESRegionBeta1 } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -1388,6 +1388,224 @@ describe('User Pool', () => { }, }); }); + + test('email withCognito', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + email: EmailBeta1.withCognito(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'COGNITO_DEFAULT', + }, + }); + }); + + test('email withCognito with custom email', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-1', + account: '11111111111', + }, + }); + + // WHEN + new UserPool(stack, 'Pool', { + email: EmailBeta1.withCognito({ + fromEmail: 'mycustomemail@example.com', + replyTo: 'reply@example.com', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'COGNITO_DEFAULT', + ReplyToEmailAddress: 'reply@example.com', + SourceArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ses:us-east-1:11111111111:identity/mycustomemail@example.com', + ], + ], + }, + }, + }); + + }); + + test('email withCognito with custom email and no region', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() =>new UserPool(stack, 'Pool', { + email: EmailBeta1.withCognito({ + fromEmail: 'mycustomemail@example.com', + replyTo: 'reply@example.com', + }), + })).toThrow(/Your stack region cannot be determined/); + + }); + + test('email withSES with custom email and no region', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() =>new UserPool(stack, 'Pool', { + email: EmailBeta1.withSES({ + from: { + email: 'mycustomemail@example.com', + }, + replyTo: 'reply@example.com', + }), + })).toThrow(/Your stack region cannot be determined/); + + }); + + test('email withSES', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-1', + account: '11111111111', + }, + }); + + // WHEN + new UserPool(stack, 'Pool', { + email: EmailBeta1.withSES({ + from: { + email: 'mycustomemail@example.com', + name: 'My Custom Email', + }, + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'DEVELOPER', + From: 'My Custom Email ', + ReplyToEmailAddress: 'reply@example.com', + ConfigurationSet: 'default', + SourceArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ses:us-east-1:11111111111:identity/mycustomemail@example.com', + ], + ], + }, + }, + }); + + }); + + test('email withSES with valid region', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-2', + account: '11111111111', + }, + }); + + // WHEN + new UserPool(stack, 'Pool', { + email: EmailBeta1.withSES({ + from: { + email: 'mycustomemail@example.com', + name: 'My Custom Email', + }, + sesRegion: SESRegionBeta1.US_EAST_1, + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'DEVELOPER', + From: 'My Custom Email ', + ReplyToEmailAddress: 'reply@example.com', + ConfigurationSet: 'default', + SourceArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ses:us-east-1:11111111111:identity/mycustomemail@example.com', + ], + ], + }, + }, + }); + + }); + test('email withSES invalid region throws error', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-2', + account: '11111111111', + }, + }); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: EmailBeta1.withSES({ + from: { + email: 'mycustomemail@example.com', + name: 'My Custom Email', + }, + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + })).toThrow(/Please provide a valid value/); + + }); + + test('email withCognito invalid region throws error', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-2', + account: '11111111111', + }, + }); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: EmailBeta1.withCognito({ + fromEmail: 'mycustomemail@example.com', + replyTo: 'reply@example.com', + }), + })).toThrow(/Please provide a valid value/); + + }); }); test('device tracking is configured correctly', () => { From 57bc195ba7254e4844bb0851a5e714ad56060ce0 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:59:54 +0000 Subject: [PATCH 2/8] fixing from when no name is provided --- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 7 ++- .../aws-cognito/test/user-pool.test.ts | 48 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 98ebebc422213..4cd68972809d4 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -592,9 +592,14 @@ class SESEmail extends EmailBeta1 { throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); } + let from = this.options.from.email; + if (this.options.from.name) { + from = `${this.options.from.name} <${this.options.from.email}>`; + } + return { emailConfig: { - from: encodePuny(`${this.options.from.name} <${this.options.from.email}>`), + from: encodePuny(from), replyToEmailAddress: encodePuny(this.options.replyTo), configurationSet: this.options.configurationSetName, emailSendingAccount: 'DEVELOPER', 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 a53ca600b46d9..698c1d417042f 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -1450,7 +1450,7 @@ describe('User Pool', () => { const stack = new Stack(); // WHEN - expect(() =>new UserPool(stack, 'Pool', { + expect(() => new UserPool(stack, 'Pool', { email: EmailBeta1.withCognito({ fromEmail: 'mycustomemail@example.com', replyTo: 'reply@example.com', @@ -1464,7 +1464,7 @@ describe('User Pool', () => { const stack = new Stack(); // WHEN - expect(() =>new UserPool(stack, 'Pool', { + expect(() => new UserPool(stack, 'Pool', { email: EmailBeta1.withSES({ from: { email: 'mycustomemail@example.com', @@ -1475,6 +1475,50 @@ describe('User Pool', () => { }); + test('email withSES with no name', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-1', + account: '11111111111', + }, + }); + + // WHEN + new UserPool(stack, 'Pool', { + email: EmailBeta1.withSES({ + from: { + email: 'mycustomemail@example.com', + }, + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'DEVELOPER', + From: 'mycustomemail@example.com', + ReplyToEmailAddress: 'reply@example.com', + ConfigurationSet: 'default', + SourceArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ses:us-east-1:11111111111:identity/mycustomemail@example.com', + ], + ], + }, + }, + }); + + }); + test('email withSES', () => { // GIVEN const stack = new Stack(undefined, undefined, { From 08409b8e2e39666700e7e00d9568cefaca5605a5 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Wed, 27 Oct 2021 17:31:20 +0000 Subject: [PATCH 3/8] updates based on review --- packages/@aws-cdk/aws-cognito/README.md | 68 +++++++---------- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 76 ++++++++----------- .../aws-cognito/test/user-pool.test.ts | 48 +++++------- 3 files changed, 80 insertions(+), 112 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 6de798a0dc8f4..49159349501e3 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -314,35 +314,12 @@ 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. -### 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 cognito.UserPool(this, 'myuserpool', { - // ... - emailSettings: { - 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. 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. - -If an email address contains non-ASCII characters, it will be encoded using the [punycode -encoding](https://en.wikipedia.org/wiki/Punycode) when generating the template for Cloudformation. - -### EmailsBeta1 +### 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). +Read more at [Email settings for User Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html). By default, user pools are configured to use Cognito's built in email capability, which by default will send emails from `no-reply@verificationemail.com`. If you want to customize the from address, while still using the Cognito built-in @@ -350,7 +327,7 @@ email capability, you can do so by specifying a custom email address that has be ```ts new cognito.UserPool(this, 'myuserpool', { - email: EmailBeta1.withCognito({ + email: Email.withCognito({ fromEmail: 'noreply@myawesomeapp.com', replyTo: 'support@myawesomeapp.com', }), @@ -359,8 +336,7 @@ new cognito.UserPool(this, 'myuserpool', { In the above example a custom email address is specified as `noreply@myawesomeapp.com`. In order for this to work this email must be a verified email address in Amazon SES, and that email address must have an authorization policy -that allows Cognito to send emails. See the section `Step 3: Grant Email Permissions to Amazon Cognito` of the -[developer guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-configure). +that allows Cognito to send emails. Read more at [Configuring Email for your User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-configure). For production applications it is recommended to configure your UserPool to send emails through Amazon SES. To do @@ -372,11 +348,9 @@ Once the SES setup is complete, you can configure your UserPool to use the email ```ts new cognito.UserPool(this, 'myuserpool', { - email: EmailBeta1.withSES({ - from: { - email: 'noreply@myawesomeapp.com', - name: 'Awesome App', - }, + email: Email.withSES({ + fromEmail: 'noreply@myawesomeapp.com', + fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', }), }); @@ -387,18 +361,34 @@ If your UserPool is being created in a different region you must specify which S ```ts new cognito.UserPool(this, 'myuserpool', { - email: EmailBeta1.withSES({ - sesRegion: SESRegionBeta1.US_EAST_1, - from: { - email: 'noreply@myawesomeapp.com', - name: 'Awesome App', - }, + email: Email.withSES({ + sesRegion: SESRegion.US_EAST_1, + fromEmail: 'noreply@myawesomeapp.com', + fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', }), }); ``` +#### Emails (Legacy) + +The legacy `emailSettings` can still be used for Cognito default settings. + +```ts +new cognito.UserPool(this, 'myuserpool', { + // ... + emailSettings: { + from: 'noreply@myawesomeapp.com', + replyTo: 'support@myawesomeapp.com', + }, +}); +``` + +If an email address contains non-ASCII characters, it will be encoded using the [punycode +encoding](https://en.wikipedia.org/wiki/Punycode) when generating the template for CloudFormation. + + ### Device Tracking User pools can be configured to track devices that users have logged in to. diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 4cd68972809d4..56823548079f4 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -370,30 +370,10 @@ export interface PasswordPolicy { readonly requireSymbols?: boolean; } -/** - * Configuration for what from email address and name Cognito will - * use to send emails via SES - */ -export interface EmailFromBeta1 { - /** - * The verified Amazon SES email address that Cognito should - * use to send emails. - */ - readonly email: string; - - /** - * An optional name that should be used as the sender's name - * along with the email. - * - * @default - no name - */ - readonly name?: string; -} - /** * Valid Amazon SES configuration regions */ -export enum SESRegionBeta1 { +export enum SESRegion { /** * Amazon SES region in 'us-east-1' */ @@ -413,10 +393,10 @@ export enum SESRegionBeta1 { /** * Configuration for Cognito sending emails via Amazon SES */ -export interface SESOptionsBeta1 { +export interface SESOptions { /** - * Identifies either the sender's email address or the - * sender's name with their email address. + * The verified Amazon SES email address that Cognito should + * use to send emails. * * The email address used must be a verified email address * in Amazon SES and must be configured to allow Cognito to @@ -424,7 +404,15 @@ export interface SESOptionsBeta1 { * * https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html */ - readonly from: EmailFromBeta1; + readonly fromEmail: string; + + /** + * An optional name that should be used as the sender's name + * along with the email. + * + * @default - no name + */ + readonly fromName?: string; /** * The destination to which the receiver of the email should reploy to. @@ -451,13 +439,13 @@ export interface SESOptionsBeta1 { * * @default - The same region as the Cognito UserPool */ - readonly sesRegion?: SESRegionBeta1; + readonly sesRegion?: SESRegion; } /** * Configuration settings for Cognito default email */ -export interface CognitoEmailOptionsBeta1 { +export interface CognitoEmailOptions { /** * The verified email address in Amazon SES that * Cognito will use to send emails. You must have already @@ -488,13 +476,13 @@ export interface CognitoEmailOptionsBeta1 { * * @default - The same region as the Cognito UserPool */ - readonly sesRegion?: SESRegionBeta1; + readonly sesRegion?: SESRegion; } /** * Configuration for Cognito email settings */ -export interface EmailConfigurationBeta1 { +export interface EmailConfiguration { /** * UserPool CFN configuration for email configuration */ @@ -504,18 +492,18 @@ export interface EmailConfigurationBeta1 { /** * Configure how Cognito sends emails */ -export abstract class EmailBeta1 { +export abstract class Email { /** * Send email using Cognito */ - public static withCognito(options?: CognitoEmailOptionsBeta1): EmailBeta1 { + public static withCognito(options?: CognitoEmailOptions): Email { return new CognitoEmail(options); } /** * Send email using SES */ - public static withSES(options: SESOptionsBeta1): EmailBeta1 { + public static withSES(options: SESOptions): Email { return new SESEmail(options); } @@ -528,16 +516,16 @@ export abstract class EmailBeta1 { * Returns the email configuration for a Cognito UserPool * that controls how Cognito will send emails */ - public abstract bind(scope: Construct): EmailConfigurationBeta1; + public abstract bind(scope: Construct): EmailConfiguration; } -class CognitoEmail extends EmailBeta1 { - constructor(private readonly options?: CognitoEmailOptionsBeta1) { +class CognitoEmail extends Email { + constructor(private readonly options?: CognitoEmailOptions) { super(); } - public bind(scope: Construct): EmailConfigurationBeta1 { + public bind(scope: Construct): EmailConfiguration { const region = Stack.of(scope).region; // if a custom email is provided that means that cognito is going to use an SES email @@ -574,12 +562,12 @@ class CognitoEmail extends EmailBeta1 { } } -class SESEmail extends EmailBeta1 { - constructor(private readonly options: SESOptionsBeta1) { +class SESEmail extends Email { + constructor(private readonly options: SESOptions) { super(); } - public bind(scope: Construct): EmailConfigurationBeta1 { + public bind(scope: Construct): EmailConfiguration { const region = Stack.of(scope).region; if (Token.isUnresolved(region) && !this.options.sesRegion) { @@ -592,9 +580,9 @@ class SESEmail extends EmailBeta1 { throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); } - let from = this.options.from.email; - if (this.options.from.name) { - from = `${this.options.from.name} <${this.options.from.email}>`; + let from = this.options.fromEmail; + if (this.options.fromName) { + from = `${this.options.fromName} <${this.options.fromEmail}>`; } return { @@ -606,7 +594,7 @@ class SESEmail extends EmailBeta1 { sourceArn: Stack.of(scope).formatArn({ service: 'ses', resource: 'identity', - resourceName: this.options.from.email, + resourceName: this.options.fromEmail, region: this.options.sesRegion ?? region, }), }, @@ -823,7 +811,7 @@ export interface UserPoolProps { * Email settings for a user pool. * @default - cognito will use the default email configuration */ - readonly email?: EmailBeta1; + readonly email?: Email; /** * 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 698c1d417042f..c7a89bd053e38 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 { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnParameter, Duration, Stack, Tags } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, EmailBeta1, SESRegionBeta1 } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, Email, SESRegion } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -1395,7 +1395,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: EmailBeta1.withCognito(), + email: Email.withCognito(), }); // THEN @@ -1417,7 +1417,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: EmailBeta1.withCognito({ + email: Email.withCognito({ fromEmail: 'mycustomemail@example.com', replyTo: 'reply@example.com', }), @@ -1451,7 +1451,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: EmailBeta1.withCognito({ + email: Email.withCognito({ fromEmail: 'mycustomemail@example.com', replyTo: 'reply@example.com', }), @@ -1465,10 +1465,8 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: EmailBeta1.withSES({ - from: { - email: 'mycustomemail@example.com', - }, + email: Email.withSES({ + fromEmail: 'mycustomemail@example.com', replyTo: 'reply@example.com', }), })).toThrow(/Your stack region cannot be determined/); @@ -1486,10 +1484,8 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: EmailBeta1.withSES({ - from: { - email: 'mycustomemail@example.com', - }, + email: Email.withSES({ + fromEmail: 'mycustomemail@example.com', replyTo: 'reply@example.com', configurationSetName: 'default', }), @@ -1530,11 +1526,9 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: EmailBeta1.withSES({ - from: { - email: 'mycustomemail@example.com', - name: 'My Custom Email', - }, + email: Email.withSES({ + fromEmail: 'mycustomemail@example.com', + fromName: 'My Custom Email', replyTo: 'reply@example.com', configurationSetName: 'default', }), @@ -1575,12 +1569,10 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: EmailBeta1.withSES({ - from: { - email: 'mycustomemail@example.com', - name: 'My Custom Email', - }, - sesRegion: SESRegionBeta1.US_EAST_1, + email: Email.withSES({ + fromEmail: 'mycustomemail@example.com', + fromName: 'My Custom Email', + sesRegion: SESRegion.US_EAST_1, replyTo: 'reply@example.com', configurationSetName: 'default', }), @@ -1620,11 +1612,9 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: EmailBeta1.withSES({ - from: { - email: 'mycustomemail@example.com', - name: 'My Custom Email', - }, + email: Email.withSES({ + fromEmail: 'mycustomemail@example.com', + fromName: 'My Custom Email', replyTo: 'reply@example.com', configurationSetName: 'default', }), @@ -1643,7 +1633,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: EmailBeta1.withCognito({ + email: Email.withCognito({ fromEmail: 'mycustomemail@example.com', replyTo: 'reply@example.com', }), From 5a0daae3cfa78502f2e270eb5985d1a1889f5f01 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Wed, 27 Oct 2021 18:33:51 +0000 Subject: [PATCH 4/8] add validation on email addresses. The local part of the email address (before the '@') must only include ASCII characters, but the domain can be punycode encoded. --- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 21 +++-- .../aws-cognito/test/user-pool.test.ts | 78 +++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 56823548079f4..14e3f36fa0aec 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -544,7 +544,7 @@ class CognitoEmail extends Email { sourceArn = Stack.of(scope).formatArn({ service: 'ses', resource: 'identity', - resourceName: this.options.fromEmail, + resourceName: encodeAndTest(this.options.fromEmail), region: this.options.sesRegion ?? region, }); } @@ -553,7 +553,7 @@ class CognitoEmail extends Email { return { emailConfig: { - replyToEmailAddress: encodePuny(this.options?.replyTo), + replyToEmailAddress: encodeAndTest(this.options?.replyTo), emailSendingAccount: 'COGNITO_DEFAULT', sourceArn, }, @@ -587,14 +587,14 @@ class SESEmail extends Email { return { emailConfig: { - from: encodePuny(from), - replyToEmailAddress: encodePuny(this.options.replyTo), + from: encodeAndTest(from), + replyToEmailAddress: encodeAndTest(this.options.replyTo), configurationSet: this.options.configurationSetName, emailSendingAccount: 'DEVELOPER', sourceArn: Stack.of(scope).formatArn({ service: 'ses', resource: 'identity', - resourceName: this.options.fromEmail, + resourceName: encodeAndTest(this.options.fromEmail), region: this.options.sesRegion ?? region, }), }, @@ -1355,6 +1355,17 @@ export class UserPool extends UserPoolBase { } } +function encodeAndTest(input: string | undefined): string | undefined { + if (input) { + const local = input.split('@')[0]; + if (!/[\p{ASCII}]+/u.test(local)) { + throw new Error('the local part of the email address must use ASCII characters only'); + } + return punycodeEncode(input); + } else { + return undefined; + } +} function undefinedIfNoKeys(struct: object): object | undefined { const allUndefined = Object.values(struct).every(val => val === undefined); return allUndefined ? undefined : struct; 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 c7a89bd053e38..5e7679cfc73e7 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -1389,6 +1389,84 @@ describe('User Pool', () => { }); }); + test('email transmission with cyrillic characters in the domain are encoded', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + email: Email.withSES({ + sesRegion: SESRegion.US_EAST_1, + fromEmail: 'user@домен.рф', + replyTo: 'user@домен.рф', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + From: 'user@xn--d1acufc.xn--p1ai', + ReplyToEmailAddress: 'user@xn--d1acufc.xn--p1ai', + }, + }); + }); + + test('email transmission with cyrillic characters in the local part throw error', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: Email.withSES({ + sesRegion: SESRegion.US_EAST_1, + fromEmail: 'от@домен.рф', + replyTo: 'user@домен.рф', + }), + })).toThrow(/the local part of the email address must use ASCII characters only/); + }); + + test('email withCognito transmission with cyrillic characters in the local part throw error', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: Email.withCognito({ + sesRegion: SESRegion.US_EAST_1, + fromEmail: 'от@домен.рф', + replyTo: 'user@домен.рф', + }), + })).toThrow(/the local part of the email address must use ASCII characters only/); + }); + + test('email transmission with cyrillic characters in the local part of replyTo throw error', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: Email.withSES({ + sesRegion: SESRegion.US_EAST_1, + fromEmail: 'user@домен.рф', + replyTo: 'от@домен.рф', + }), + })).toThrow(/the local part of the email address must use ASCII characters only/); + }); + + test('email withCognito transmission with cyrillic characters in the local part of replyTo throw error', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: Email.withCognito({ + sesRegion: SESRegion.US_EAST_1, + fromEmail: 'user@домен.рф', + replyTo: 'от@домен.рф', + }), + })).toThrow(/the local part of the email address must use ASCII characters only/); + }); + test('email withCognito', () => { // GIVEN const stack = new Stack(); From 641d8abf6f5bb03ca40fe30be2d39129e340fa7a Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Fri, 29 Oct 2021 13:54:52 +0000 Subject: [PATCH 5/8] moving email settings to separate file removing regions property in favor of a const switching sesRegions to take a string deprecating emailSettings property other small doc updates --- packages/@aws-cdk/aws-cognito/README.md | 43 +-- packages/@aws-cdk/aws-cognito/lib/index.ts | 1 + .../aws-cognito/lib/user-pool-email.ts | 267 ++++++++++++++++++ .../@aws-cdk/aws-cognito/lib/user-pool.ts | 249 +--------------- .../aws-cognito/test/user-pool.test.ts | 53 +++- 5 files changed, 330 insertions(+), 283 deletions(-) create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 49159349501e3..5019b72bf9f5a 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -322,8 +322,8 @@ emails, password resets, etc. The address from which these emails are sent can b Read more at [Email settings for User Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html). By default, user pools are configured to use Cognito's built in email capability, which by default will send emails -from `no-reply@verificationemail.com`. If you want to customize the from address, while still using the Cognito built-in -email capability, you can do so by specifying a custom email address that has been configured in Amazon SES. +from `no-reply@verificationemail.com`. To customize the from address, while still using the Cognito built-in +email capability, specify a custom email address that has been configured in Amazon SES. ```ts new cognito.UserPool(this, 'myuserpool', { @@ -334,61 +334,42 @@ new cognito.UserPool(this, 'myuserpool', { }); ``` -In the above example a custom email address is specified as `noreply@myawesomeapp.com`. In order for this to work -this email must be a verified email address in Amazon SES, and that email address must have an authorization policy +The custom email address specified must first be verified in Amazon SES, and associated with an authorization policy that allows Cognito to send emails. Read more at [Configuring Email for your User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-configure). -For production applications it is recommended to configure your UserPool to send emails through Amazon SES. To do -so you must first have followed the steps in the [Cognito Developer Guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-developer) -to verify an email address, move your account out of the SES sandbox, and grant Cognito email permissions via an +For production applications it is recommended to configure the UserPool to send emails through Amazon SES. To do +so, follow the steps in the [Cognito Developer Guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-developer) +to verify an email address, move the account out of the SES sandbox, and grant Cognito email permissions via an authorization policy. -Once the SES setup is complete, you can configure your UserPool to use the email configured in SES. +Once the SES setup is complete, the UserPool can be configured to use the SES email. ```ts new cognito.UserPool(this, 'myuserpool', { email: Email.withSES({ - fromEmail: 'noreply@myawesomeapp.com', - fromName: 'Awesome App', + fromEmail: 'noreply@myawesomeapp.com', + fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', }), }); ``` Sending emails through SES requires that SES be configured in either `us-east-1`, `us-west-1`, or `eu-west-1`. -If your UserPool is being created in a different region you must specify which SES region to use. +If the UserPool is being created in a different region, `sesRegion` must be used to specify the correct SES region. ```ts new cognito.UserPool(this, 'myuserpool', { email: Email.withSES({ sesRegion: SESRegion.US_EAST_1, - fromEmail: 'noreply@myawesomeapp.com', - fromName: 'Awesome App', + fromEmail: 'noreply@myawesomeapp.com', + fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', }), }); ``` -#### Emails (Legacy) - -The legacy `emailSettings` can still be used for Cognito default settings. - -```ts -new cognito.UserPool(this, 'myuserpool', { - // ... - emailSettings: { - from: 'noreply@myawesomeapp.com', - replyTo: 'support@myawesomeapp.com', - }, -}); -``` - -If an email address contains non-ASCII characters, it will be encoded using the [punycode -encoding](https://en.wikipedia.org/wiki/Punycode) when generating the template for CloudFormation. - - ### Device Tracking User pools can be configured to track devices that users have logged in to. diff --git a/packages/@aws-cdk/aws-cognito/lib/index.ts b/packages/@aws-cdk/aws-cognito/lib/index.ts index cab56671c2b9e..7d5ce97fc2c76 100644 --- a/packages/@aws-cdk/aws-cognito/lib/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/index.ts @@ -4,6 +4,7 @@ export * from './user-pool'; export * from './user-pool-attr'; export * from './user-pool-client'; export * from './user-pool-domain'; +export * from './user-pool-email'; export * from './user-pool-idp'; export * from './user-pool-idps'; export * from './user-pool-resource-server'; diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts new file mode 100644 index 0000000000000..3f03b6f3751a2 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts @@ -0,0 +1,267 @@ +import { Stack, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { toASCII as punycodeEncode } from 'punycode/'; + +/** + * The valid Amazon SES configuration regions + */ +const REGIONS = ['us-east-1', 'us-west-2', 'eu-west-1']; + +/** + * Configuration for Cognito sending emails via Amazon SES + */ +export interface SESOptions { + /** + * The verified Amazon SES email address that Cognito should + * use to send emails. + * + * The email address used must be a verified email address + * in Amazon SES and must be configured to allow Cognito to + * send emails. + * + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html + */ + readonly fromEmail: string; + + /** + * An optional name that should be used as the sender's name + * along with the email. + * + * @default - no name + */ + readonly fromName?: string; + + /** + * The destination to which the receiver of the email should reploy to. + * + * @default - same as the fromEmail + */ + readonly replyTo?: string; + + /** + * The name of a configuration set in Amazon SES that should + * be applied to emails sent via Cognito. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-emailconfiguration.html#cfn-cognito-userpool-emailconfiguration-configurationset + * + * @default - no configuration set + */ + readonly configurationSetName?: string; + + /** + * Required if the UserPool region is different than the SES region. + * + * If sending emails with a Amazon SES verified email address, + * and the region that SES is configured is different than the + * region in which the UserPool is deployed, you must specify that + * region here. + * + * Must be 'us-east-1', 'us-west-2', or 'eu-west-1' + * + * @default - The same region as the Cognito UserPool + */ + readonly sesRegion?: string; +} + +/** + * Configuration settings for Cognito default email + */ +export interface CognitoEmailOptions { + /** + * The verified email address in Amazon SES that + * Cognito will use to send emails. You must have already + * configured Amazon SES to allow Cognito to send Emails + * through this address. + * + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html + * + * @default - Cognito default email address will be used + * 'no-reply@verificationemail.com' + */ + readonly fromEmail?: string; + + /** + * The destination to which the receiver of the email should reploy to. + * + * @default - same as the fromEmail + */ + readonly replyTo?: string; + + /** + * Required if the UserPool region is different than the SES region. + * + * If sending emails with a Amazon SES verified email address, + * and the region that SES is configured is different than the + * region in which the UserPool is deployed, you must specify that + * region here. + * + * Must be 'us-east-1', 'us-west-2', or 'eu-west-1' + * + * @default - The same region as the Cognito UserPool + */ + readonly sesRegion?: string; +} + +/** + * Configuration for Cognito email settings + */ +export interface EmailConfiguration { + /** + * The name of the configuration set in SES. + * + * @default - none + */ + readonly configurationSet?: string; + + /** + * Specifies whether to use Cognito's built in email functionality + * or SES. + * + * @default - COGNITO_DEFAULT + */ + readonly emailSendingAccount?: string; + + /** + * Identifies either the sender's email address or the sender's + * name with their email address. + * + * If emailSendingAccount is DEVELOPER then this cannot be specified. + * + * @default - no-reply@verificationemail.com + */ + readonly from?: string; + + /** + * The destination to which the receiver of the email should reply to. + * + * @default - none + */ + readonly replyToEmailAddress?: string; + + /** + * The ARN of a verified email address in Amazon SES. + * + * required if emailSendingAccount is DEVELOPER or if + * 'from' is provided. + * + * @default - none + */ + readonly sourceArn?: string; +} + +/** + * Configure how Cognito sends emails + */ +export abstract class Email { + /** + * Send email using Cognito + */ + public static withCognito(options?: CognitoEmailOptions): Email { + return new CognitoEmail(options); + } + + /** + * Send email using SES + */ + public static withSES(options: SESOptions): Email { + return new SESEmail(options); + } + + + /** + * Returns the email configuration for a Cognito UserPool + * that controls how Cognito will send emails + */ + public abstract bind(scope: Construct): EmailConfiguration; + +} + +class CognitoEmail extends Email { + constructor(private readonly options?: CognitoEmailOptions) { + super(); + } + + public bind(scope: Construct): EmailConfiguration { + const region = Stack.of(scope).region; + + // if a custom email is provided that means that cognito is going to use an SES email + // and we need to provide the sourceArn which requires a valid region + let sourceArn: string | undefined = undefined; + if (this.options?.fromEmail) { + if (this.options.fromEmail !== 'no-reply@verificationemail.com') { + if (Token.isUnresolved(region) && !this.options.sesRegion) { + throw new Error('Your stack region cannot be determined so "sesRegion" is required in CognitoEmailOptions'); + } + if (this.options?.sesRegion && !REGIONS.includes(this.options.sesRegion)) { + throw new Error(`sesRegion must be one of 'us-east-1', 'us-west-2', 'eu-west-1'. received ${this.options.sesRegion}`); + } else if (!this.options?.sesRegion && !REGIONS.includes(region)) { + throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); + } + sourceArn = Stack.of(scope).formatArn({ + service: 'ses', + resource: 'identity', + resourceName: encodeAndTest(this.options.fromEmail), + region: this.options.sesRegion ?? region, + }); + } + } + + + return { + replyToEmailAddress: encodeAndTest(this.options?.replyTo), + emailSendingAccount: 'COGNITO_DEFAULT', + sourceArn, + }; + + } +} + +class SESEmail extends Email { + constructor(private readonly options: SESOptions) { + super(); + } + + public bind(scope: Construct): EmailConfiguration { + const region = Stack.of(scope).region; + + if (Token.isUnresolved(region) && !this.options.sesRegion) { + throw new Error('Your stack region cannot be determined so "sesRegion" is required in SESOptions'); + } + + if (this.options.sesRegion && !REGIONS.includes(this.options.sesRegion)) { + throw new Error(`sesRegion must be one of 'us-east-1', 'us-west-2', 'eu-west-1'. received ${this.options.sesRegion}`); + } else if (!this.options.sesRegion && !REGIONS.includes(region)) { + throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); + } + + let from = this.options.fromEmail; + if (this.options.fromName) { + from = `${this.options.fromName} <${this.options.fromEmail}>`; + } + + return { + from: encodeAndTest(from), + replyToEmailAddress: encodeAndTest(this.options.replyTo), + configurationSet: this.options.configurationSetName, + emailSendingAccount: 'DEVELOPER', + sourceArn: Stack.of(scope).formatArn({ + service: 'ses', + resource: 'identity', + resourceName: encodeAndTest(this.options.fromEmail), + region: this.options.sesRegion ?? region, + }), + }; + } +} + +function encodeAndTest(input: string | undefined): string | undefined { + if (input) { + const local = input.split('@')[0]; + if (!/[\p{ASCII}]+/u.test(local)) { + throw new Error('the local part of the email address must use ASCII characters only'); + } + return punycodeEncode(input); + } else { + return undefined; + } +} diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 14e3f36fa0aec..46d0be2a53d38 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -8,6 +8,7 @@ import { StandardAttributeNames } from './private/attr-names'; import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr'; import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; +import { Email } from './user-pool-email'; import { IUserPoolIdentityProvider } from './user-pool-idp'; import { UserPoolResourceServer, UserPoolResourceServerOptions } from './user-pool-resource-server'; @@ -370,239 +371,6 @@ export interface PasswordPolicy { readonly requireSymbols?: boolean; } -/** - * Valid Amazon SES configuration regions - */ -export enum SESRegion { - /** - * Amazon SES region in 'us-east-1' - */ - US_EAST_1 = 'us-east-1', - - /** - * Amazon SES region in 'us-west-2' - */ - US_WEST_2 = 'us-west-2', - - /** - * Amazon SES region in 'eu-west-1' - */ - EU_WEST_1 = 'eu-west-1', -} - -/** - * Configuration for Cognito sending emails via Amazon SES - */ -export interface SESOptions { - /** - * The verified Amazon SES email address that Cognito should - * use to send emails. - * - * The email address used must be a verified email address - * in Amazon SES and must be configured to allow Cognito to - * send emails. - * - * https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html - */ - readonly fromEmail: string; - - /** - * An optional name that should be used as the sender's name - * along with the email. - * - * @default - no name - */ - readonly fromName?: string; - - /** - * The destination to which the receiver of the email should reploy to. - * - * @default - same as the fromEmail - */ - readonly replyTo?: string; - - /** - * The name of a configuration set in SES that should - * be applied to emails sent via Cognito. - * - * @default - no configuration set - */ - readonly configurationSetName?: string; - - /** - * Required if the UserPool region is different than the SES region. - * - * If sending emails with a Amazon SES verified email address, - * and the region that SES is configured is different than the - * region in which the UserPool is deployed, you must specify that - * region here. - * - * @default - The same region as the Cognito UserPool - */ - readonly sesRegion?: SESRegion; -} - -/** - * Configuration settings for Cognito default email - */ -export interface CognitoEmailOptions { - /** - * The verified email address in Amazon SES that - * Cognito will use to send emails. You must have already - * configured Amazon SES to allow Cognito to send Emails - * through this address. - * - * https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html - * - * @default - Cognito default email address will be used - * 'no-reply@verificationemail.com' - */ - readonly fromEmail?: string; - - /** - * The destination to which the receiver of the email should reploy to. - * - * @default - same as the fromEmail - */ - readonly replyTo?: string; - - /** - * Required if the UserPool region is different than the SES region. - * - * If sending emails with a Amazon SES verified email address, - * and the region that SES is configured is different than the - * region in which the UserPool is deployed, you must specify that - * region here. - * - * @default - The same region as the Cognito UserPool - */ - readonly sesRegion?: SESRegion; -} - -/** - * Configuration for Cognito email settings - */ -export interface EmailConfiguration { - /** - * UserPool CFN configuration for email configuration - */ - readonly emailConfig: CfnUserPool.EmailConfigurationProperty; -} - -/** - * Configure how Cognito sends emails - */ -export abstract class Email { - /** - * Send email using Cognito - */ - public static withCognito(options?: CognitoEmailOptions): Email { - return new CognitoEmail(options); - } - - /** - * Send email using SES - */ - public static withSES(options: SESOptions): Email { - return new SESEmail(options); - } - - /** - * The valid Amazon SES configuration regions - */ - protected readonly regions = ['us-east-1', 'us-west-2', 'eu-west-1']; - - /** - * Returns the email configuration for a Cognito UserPool - * that controls how Cognito will send emails - */ - public abstract bind(scope: Construct): EmailConfiguration; - -} - -class CognitoEmail extends Email { - constructor(private readonly options?: CognitoEmailOptions) { - super(); - } - - public bind(scope: Construct): EmailConfiguration { - const region = Stack.of(scope).region; - - // if a custom email is provided that means that cognito is going to use an SES email - // and we need to provide the sourceArn which requires a valid region - let sourceArn: string | undefined = undefined; - if (this.options?.fromEmail) { - if (this.options.fromEmail != 'no-reply@verificationemail.com') { - if (Token.isUnresolved(region) && !this.options.sesRegion) { - throw new Error('Your stack region cannot be determined so "sesRegion" is required in CognitoEmailOptions'); - } - if (this.options?.sesRegion && !this.regions.includes(this.options.sesRegion)) { - throw new Error(`sesRegion must be one of 'us-east-1', 'us-west-2', 'eu-west-1'. received ${this.options.sesRegion}`); - } else if (!this.options?.sesRegion && !this.regions.includes(region)) { - throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); - } - sourceArn = Stack.of(scope).formatArn({ - service: 'ses', - resource: 'identity', - resourceName: encodeAndTest(this.options.fromEmail), - region: this.options.sesRegion ?? region, - }); - } - } - - - return { - emailConfig: { - replyToEmailAddress: encodeAndTest(this.options?.replyTo), - emailSendingAccount: 'COGNITO_DEFAULT', - sourceArn, - }, - }; - - } -} - -class SESEmail extends Email { - constructor(private readonly options: SESOptions) { - super(); - } - - public bind(scope: Construct): EmailConfiguration { - const region = Stack.of(scope).region; - - if (Token.isUnresolved(region) && !this.options.sesRegion) { - throw new Error('Your stack region cannot be determined so "sesRegion" is required in SESOptions'); - } - - if (this.options.sesRegion && !this.regions.includes(this.options.sesRegion)) { - throw new Error(`sesRegion must be one of 'us-east-1', 'us-west-2', 'eu-west-1'. received ${this.options.sesRegion}`); - } else if (!this.options.sesRegion && !this.regions.includes(region)) { - throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); - } - - let from = this.options.fromEmail; - if (this.options.fromName) { - from = `${this.options.fromName} <${this.options.fromEmail}>`; - } - - return { - emailConfig: { - from: encodeAndTest(from), - replyToEmailAddress: encodeAndTest(this.options.replyTo), - configurationSet: this.options.configurationSetName, - emailSendingAccount: 'DEVELOPER', - sourceArn: Stack.of(scope).formatArn({ - service: 'ses', - resource: 'identity', - resourceName: encodeAndTest(this.options.fromEmail), - region: this.options.sesRegion ?? region, - }), - }, - }; - } -} - - /** * Email settings for the user pool. */ @@ -803,7 +571,9 @@ export interface UserPoolProps { /** * Email settings for a user pool. + * * @default - see defaults on each property of EmailSettings. + * @deprecated Use 'email' instead. */ readonly emailSettings?: EmailSettings; @@ -1030,7 +800,7 @@ export class UserPool extends UserPoolBase { if (props.email && props.emailSettings) { throw new Error('you must either provide "email" or "emailSettings", but not both'); } - const emailConfiguration = props.email ? props.email.bind(this).emailConfig : undefinedIfNoKeys({ + const emailConfiguration = props.email ? props.email.bind(this) : undefinedIfNoKeys({ from: encodePuny(props.emailSettings?.from), replyToEmailAddress: encodePuny(props.emailSettings?.replyTo), }); @@ -1355,17 +1125,6 @@ export class UserPool extends UserPoolBase { } } -function encodeAndTest(input: string | undefined): string | undefined { - if (input) { - const local = input.split('@')[0]; - if (!/[\p{ASCII}]+/u.test(local)) { - throw new Error('the local part of the email address must use ASCII characters only'); - } - return punycodeEncode(input); - } else { - return undefined; - } -} function undefinedIfNoKeys(struct: object): object | undefined { const allUndefined = Object.values(struct).every(val => val === undefined); return allUndefined ? undefined : struct; 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 5e7679cfc73e7..8057be4dd214d 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 { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnParameter, Duration, Stack, Tags } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, Email, SESRegion } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, Email } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -1396,7 +1396,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { email: Email.withSES({ - sesRegion: SESRegion.US_EAST_1, + sesRegion: 'us-east-1', fromEmail: 'user@домен.рф', replyTo: 'user@домен.рф', }), @@ -1418,7 +1418,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { email: Email.withSES({ - sesRegion: SESRegion.US_EAST_1, + sesRegion: 'us-east-1', fromEmail: 'от@домен.рф', replyTo: 'user@домен.рф', }), @@ -1432,7 +1432,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { email: Email.withCognito({ - sesRegion: SESRegion.US_EAST_1, + sesRegion: 'us-east-1', fromEmail: 'от@домен.рф', replyTo: 'user@домен.рф', }), @@ -1446,7 +1446,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { email: Email.withSES({ - sesRegion: SESRegion.US_EAST_1, + sesRegion: 'us-east-1', fromEmail: 'user@домен.рф', replyTo: 'от@домен.рф', }), @@ -1460,7 +1460,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { email: Email.withCognito({ - sesRegion: SESRegion.US_EAST_1, + sesRegion: 'us-east-1', fromEmail: 'user@домен.рф', replyTo: 'от@домен.рф', }), @@ -1650,7 +1650,7 @@ describe('User Pool', () => { email: Email.withSES({ fromEmail: 'mycustomemail@example.com', fromName: 'My Custom Email', - sesRegion: SESRegion.US_EAST_1, + sesRegion: 'us-east-1', replyTo: 'reply@example.com', configurationSetName: 'default', }), @@ -1718,6 +1718,45 @@ describe('User Pool', () => { })).toThrow(/Please provide a valid value/); }); + test('email withSES invalid sesRegion throws error', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + account: '11111111111', + }, + }); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: Email.withSES({ + sesRegion: 'us-east-2', + fromEmail: 'mycustomemail@example.com', + fromName: 'My Custom Email', + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + })).toThrow(/sesRegion must be one of/); + + }); + + test('email withCognito invalid region throws error', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + account: '11111111111', + }, + }); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: Email.withCognito({ + sesRegion: 'us-east-2', + fromEmail: 'mycustomemail@example.com', + replyTo: 'reply@example.com', + }), + })).toThrow(/sesRegion must be one of/); + + }); }); test('device tracking is configured correctly', () => { From d59fc24eedb22fb4d17ab0381219885725c4fbdd Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Fri, 5 Nov 2021 18:09:18 +0000 Subject: [PATCH 6/8] updates based on review comments --- packages/@aws-cdk/aws-cognito/README.md | 21 +-- .../aws-cognito/lib/user-pool-email.ts | 101 +++------------ .../@aws-cdk/aws-cognito/lib/user-pool.ts | 4 +- .../aws-cognito/test/user-pool.test.ts | 120 +++--------------- 4 files changed, 42 insertions(+), 204 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 5019b72bf9f5a..9c9e81c599fc3 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -321,23 +321,16 @@ Cognito sends emails to users in the user pool, when particular actions take pla emails, password resets, etc. The address from which these emails are sent can be configured on the user pool. Read more at [Email settings for User Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html). -By default, user pools are configured to use Cognito's built in email capability, which by default will send emails -from `no-reply@verificationemail.com`. To customize the from address, while still using the Cognito built-in -email capability, specify a custom email address that has been configured in Amazon SES. +By default, user pools are configured to use Cognito's built in email capability, which will send emails +from `no-reply@verificationemail.com`. If you want to use a custom email address you can configure +Cognito to send emails through Amazon SES, which is detailed below. ```ts new cognito.UserPool(this, 'myuserpool', { - email: Email.withCognito({ - fromEmail: 'noreply@myawesomeapp.com', - replyTo: 'support@myawesomeapp.com', - }), + email: UserPoolEmail.withCognito('support@myawesomeapp.com'), }); ``` -The custom email address specified must first be verified in Amazon SES, and associated with an authorization policy -that allows Cognito to send emails. Read more at [Configuring Email for your User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-configure). - - For production applications it is recommended to configure the UserPool to send emails through Amazon SES. To do so, follow the steps in the [Cognito Developer Guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-developer) to verify an email address, move the account out of the SES sandbox, and grant Cognito email permissions via an @@ -347,7 +340,7 @@ Once the SES setup is complete, the UserPool can be configured to use the SES em ```ts new cognito.UserPool(this, 'myuserpool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ fromEmail: 'noreply@myawesomeapp.com', fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', @@ -360,8 +353,8 @@ If the UserPool is being created in a different region, `sesRegion` must be used ```ts new cognito.UserPool(this, 'myuserpool', { - email: Email.withSES({ - sesRegion: SESRegion.US_EAST_1, + email: UserPoolEmail.withSES({ + sesRegion: 'us-east-1', fromEmail: 'noreply@myawesomeapp.com', fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts index 3f03b6f3751a2..b4153a499baff 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts @@ -10,7 +10,7 @@ const REGIONS = ['us-east-1', 'us-west-2', 'eu-west-1']; /** * Configuration for Cognito sending emails via Amazon SES */ -export interface SESOptions { +export interface UserPoolSESOptions { /** * The verified Amazon SES email address that Cognito should * use to send emails. @@ -42,7 +42,7 @@ export interface SESOptions { * The name of a configuration set in Amazon SES that should * be applied to emails sent via Cognito. * - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-emailconfiguration.html#cfn-cognito-userpool-emailconfiguration-configurationset + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-emailconfiguration.html#cfn-cognito-userpool-emailconfiguration-configurationset * * @default - no configuration set */ @@ -63,49 +63,10 @@ export interface SESOptions { readonly sesRegion?: string; } -/** - * Configuration settings for Cognito default email - */ -export interface CognitoEmailOptions { - /** - * The verified email address in Amazon SES that - * Cognito will use to send emails. You must have already - * configured Amazon SES to allow Cognito to send Emails - * through this address. - * - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html - * - * @default - Cognito default email address will be used - * 'no-reply@verificationemail.com' - */ - readonly fromEmail?: string; - - /** - * The destination to which the receiver of the email should reploy to. - * - * @default - same as the fromEmail - */ - readonly replyTo?: string; - - /** - * Required if the UserPool region is different than the SES region. - * - * If sending emails with a Amazon SES verified email address, - * and the region that SES is configured is different than the - * region in which the UserPool is deployed, you must specify that - * region here. - * - * Must be 'us-east-1', 'us-west-2', or 'eu-west-1' - * - * @default - The same region as the Cognito UserPool - */ - readonly sesRegion?: string; -} - /** * Configuration for Cognito email settings */ -export interface EmailConfiguration { +export interface UserPoolEmailConfig { /** * The name of the configuration set in SES. * @@ -117,7 +78,7 @@ export interface EmailConfiguration { * Specifies whether to use Cognito's built in email functionality * or SES. * - * @default - COGNITO_DEFAULT + * @default - Cognito build in email functionality */ readonly emailSendingAccount?: string; @@ -127,14 +88,14 @@ export interface EmailConfiguration { * * If emailSendingAccount is DEVELOPER then this cannot be specified. * - * @default - no-reply@verificationemail.com + * @default 'no-reply@verificationemail.com' */ readonly from?: string; /** * The destination to which the receiver of the email should reply to. * - * @default - none + * @default - same as `from` */ readonly replyToEmailAddress?: string; @@ -152,18 +113,18 @@ export interface EmailConfiguration { /** * Configure how Cognito sends emails */ -export abstract class Email { +export abstract class UserPoolEmail { /** * Send email using Cognito */ - public static withCognito(options?: CognitoEmailOptions): Email { - return new CognitoEmail(options); + public static withCognito(replyTo?: string): UserPoolEmail { + return new CognitoEmail(replyTo); } /** * Send email using SES */ - public static withSES(options: SESOptions): Email { + public static withSES(options: UserPoolSESOptions): UserPoolEmail { return new SESEmail(options); } @@ -172,56 +133,30 @@ export abstract class Email { * Returns the email configuration for a Cognito UserPool * that controls how Cognito will send emails */ - public abstract bind(scope: Construct): EmailConfiguration; + public abstract bind(scope: Construct): UserPoolEmailConfig; } -class CognitoEmail extends Email { - constructor(private readonly options?: CognitoEmailOptions) { +class CognitoEmail extends UserPoolEmail { + constructor(private readonly replyTo?: string) { super(); } - public bind(scope: Construct): EmailConfiguration { - const region = Stack.of(scope).region; - - // if a custom email is provided that means that cognito is going to use an SES email - // and we need to provide the sourceArn which requires a valid region - let sourceArn: string | undefined = undefined; - if (this.options?.fromEmail) { - if (this.options.fromEmail !== 'no-reply@verificationemail.com') { - if (Token.isUnresolved(region) && !this.options.sesRegion) { - throw new Error('Your stack region cannot be determined so "sesRegion" is required in CognitoEmailOptions'); - } - if (this.options?.sesRegion && !REGIONS.includes(this.options.sesRegion)) { - throw new Error(`sesRegion must be one of 'us-east-1', 'us-west-2', 'eu-west-1'. received ${this.options.sesRegion}`); - } else if (!this.options?.sesRegion && !REGIONS.includes(region)) { - throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); - } - sourceArn = Stack.of(scope).formatArn({ - service: 'ses', - resource: 'identity', - resourceName: encodeAndTest(this.options.fromEmail), - region: this.options.sesRegion ?? region, - }); - } - } - - + public bind(_scope: Construct): UserPoolEmailConfig { return { - replyToEmailAddress: encodeAndTest(this.options?.replyTo), + replyToEmailAddress: encodeAndTest(this.replyTo), emailSendingAccount: 'COGNITO_DEFAULT', - sourceArn, }; } } -class SESEmail extends Email { - constructor(private readonly options: SESOptions) { +class SESEmail extends UserPoolEmail { + constructor(private readonly options: UserPoolSESOptions) { super(); } - public bind(scope: Construct): EmailConfiguration { + public bind(scope: Construct): UserPoolEmailConfig { const region = Stack.of(scope).region; if (Token.isUnresolved(region) && !this.options.sesRegion) { diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 46d0be2a53d38..1b1a908bbe421 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -8,7 +8,7 @@ import { StandardAttributeNames } from './private/attr-names'; import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr'; import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; -import { Email } from './user-pool-email'; +import { UserPoolEmail } from './user-pool-email'; import { IUserPoolIdentityProvider } from './user-pool-idp'; import { UserPoolResourceServer, UserPoolResourceServerOptions } from './user-pool-resource-server'; @@ -581,7 +581,7 @@ export interface UserPoolProps { * Email settings for a user pool. * @default - cognito will use the default email configuration */ - readonly email?: Email; + readonly email?: UserPoolEmail; /** * 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 8057be4dd214d..900c4305d6137 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 { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnParameter, Duration, Stack, Tags } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, Email } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, UserPoolEmail } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -1395,7 +1395,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ sesRegion: 'us-east-1', fromEmail: 'user@домен.рф', replyTo: 'user@домен.рф', @@ -1417,21 +1417,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: Email.withSES({ - sesRegion: 'us-east-1', - fromEmail: 'от@домен.рф', - replyTo: 'user@домен.рф', - }), - })).toThrow(/the local part of the email address must use ASCII characters only/); - }); - - test('email withCognito transmission with cyrillic characters in the local part throw error', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - expect(() => new UserPool(stack, 'Pool', { - email: Email.withCognito({ + email: UserPoolEmail.withSES({ sesRegion: 'us-east-1', fromEmail: 'от@домен.рф', replyTo: 'user@домен.рф', @@ -1445,7 +1431,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ sesRegion: 'us-east-1', fromEmail: 'user@домен.рф', replyTo: 'от@домен.рф', @@ -1459,11 +1445,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: Email.withCognito({ - sesRegion: 'us-east-1', - fromEmail: 'user@домен.рф', - replyTo: 'от@домен.рф', - }), + email: UserPoolEmail.withCognito('от@домен.рф'), })).toThrow(/the local part of the email address must use ASCII characters only/); }); @@ -1473,7 +1455,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: Email.withCognito(), + email: UserPoolEmail.withCognito(), }); // THEN @@ -1484,21 +1466,13 @@ describe('User Pool', () => { }); }); - test('email withCognito with custom email', () => { + test('email withCognito and replyTo', () => { // GIVEN - const stack = new Stack(undefined, undefined, { - env: { - region: 'us-east-1', - account: '11111111111', - }, - }); + const stack = new Stack(); // WHEN new UserPool(stack, 'Pool', { - email: Email.withCognito({ - fromEmail: 'mycustomemail@example.com', - replyTo: 'reply@example.com', - }), + email: UserPoolEmail.withCognito('reply@example.com'), }); // THEN @@ -1506,35 +1480,8 @@ describe('User Pool', () => { EmailConfiguration: { EmailSendingAccount: 'COGNITO_DEFAULT', ReplyToEmailAddress: 'reply@example.com', - SourceArn: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':ses:us-east-1:11111111111:identity/mycustomemail@example.com', - ], - ], - }, }, }); - - }); - - test('email withCognito with custom email and no region', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - expect(() => new UserPool(stack, 'Pool', { - email: Email.withCognito({ - fromEmail: 'mycustomemail@example.com', - replyTo: 'reply@example.com', - }), - })).toThrow(/Your stack region cannot be determined/); - }); test('email withSES with custom email and no region', () => { @@ -1543,7 +1490,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ fromEmail: 'mycustomemail@example.com', replyTo: 'reply@example.com', }), @@ -1562,7 +1509,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ fromEmail: 'mycustomemail@example.com', replyTo: 'reply@example.com', configurationSetName: 'default', @@ -1604,7 +1551,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ fromEmail: 'mycustomemail@example.com', fromName: 'My Custom Email', replyTo: 'reply@example.com', @@ -1647,7 +1594,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ fromEmail: 'mycustomemail@example.com', fromName: 'My Custom Email', sesRegion: 'us-east-1', @@ -1690,7 +1637,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ fromEmail: 'mycustomemail@example.com', fromName: 'My Custom Email', replyTo: 'reply@example.com', @@ -1700,24 +1647,6 @@ describe('User Pool', () => { }); - test('email withCognito invalid region throws error', () => { - // GIVEN - const stack = new Stack(undefined, undefined, { - env: { - region: 'us-east-2', - account: '11111111111', - }, - }); - - // WHEN - expect(() => new UserPool(stack, 'Pool', { - email: Email.withCognito({ - fromEmail: 'mycustomemail@example.com', - replyTo: 'reply@example.com', - }), - })).toThrow(/Please provide a valid value/); - - }); test('email withSES invalid sesRegion throws error', () => { // GIVEN const stack = new Stack(undefined, undefined, { @@ -1728,7 +1657,7 @@ describe('User Pool', () => { // WHEN expect(() => new UserPool(stack, 'Pool', { - email: Email.withSES({ + email: UserPoolEmail.withSES({ sesRegion: 'us-east-2', fromEmail: 'mycustomemail@example.com', fromName: 'My Custom Email', @@ -1738,25 +1667,6 @@ describe('User Pool', () => { })).toThrow(/sesRegion must be one of/); }); - - test('email withCognito invalid region throws error', () => { - // GIVEN - const stack = new Stack(undefined, undefined, { - env: { - account: '11111111111', - }, - }); - - // WHEN - expect(() => new UserPool(stack, 'Pool', { - email: Email.withCognito({ - sesRegion: 'us-east-2', - fromEmail: 'mycustomemail@example.com', - replyTo: 'reply@example.com', - }), - })).toThrow(/sesRegion must be one of/); - - }); }); test('device tracking is configured correctly', () => { From cc6871d5c5208eb65a643d74708206f6bfb286c2 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Fri, 5 Nov 2021 18:15:59 +0000 Subject: [PATCH 7/8] fixing typo --- packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts index b4153a499baff..5c7b3fd2c794c 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts @@ -78,7 +78,7 @@ export interface UserPoolEmailConfig { * Specifies whether to use Cognito's built in email functionality * or SES. * - * @default - Cognito build in email functionality + * @default - Cognito built in email functionality */ readonly emailSendingAccount?: string; From bd209b544c40cfeb17252c030d063bae6165b147 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Mon, 15 Nov 2021 17:16:44 +0000 Subject: [PATCH 8/8] updates based on review --- packages/@aws-cdk/aws-cognito/README.md | 5 +++-- packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts | 11 ++++++----- packages/@aws-cdk/aws-cognito/lib/user-pool.ts | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 9c9e81c599fc3..906480586b769 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -331,7 +331,8 @@ new cognito.UserPool(this, 'myuserpool', { }); ``` -For production applications it is recommended to configure the UserPool to send emails through Amazon SES. To do +For typical production environments, the default email limit is below the required delivery volume. +To enable a higher delivery volume, you can configure the UserPool to send emails through Amazon SES. To do so, follow the steps in the [Cognito Developer Guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-developer) to verify an email address, move the account out of the SES sandbox, and grant Cognito email permissions via an authorization policy. @@ -348,7 +349,7 @@ new cognito.UserPool(this, 'myuserpool', { }); ``` -Sending emails through SES requires that SES be configured in either `us-east-1`, `us-west-1`, or `eu-west-1`. +Sending emails through SES requires that SES be configured (as described above) in one of the regions - `us-east-1`, `us-west-1`, or `eu-west-1`. If the UserPool is being created in a different region, `sesRegion` must be used to specify the correct SES region. ```ts diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts index 5c7b3fd2c794c..2d5b8af06447f 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts @@ -64,9 +64,9 @@ export interface UserPoolSESOptions { } /** - * Configuration for Cognito email settings + * Result of binding email settings with a user pool */ -export interface UserPoolEmailConfig { +interface UserPoolEmailConfig { /** * The name of the configuration set in SES. * @@ -132,8 +132,9 @@ export abstract class UserPoolEmail { /** * Returns the email configuration for a Cognito UserPool * that controls how Cognito will send emails + * @internal */ - public abstract bind(scope: Construct): UserPoolEmailConfig; + public abstract _bind(scope: Construct): UserPoolEmailConfig; } @@ -142,7 +143,7 @@ class CognitoEmail extends UserPoolEmail { super(); } - public bind(_scope: Construct): UserPoolEmailConfig { + public _bind(_scope: Construct): UserPoolEmailConfig { return { replyToEmailAddress: encodeAndTest(this.replyTo), emailSendingAccount: 'COGNITO_DEFAULT', @@ -156,7 +157,7 @@ class SESEmail extends UserPoolEmail { super(); } - public bind(scope: Construct): UserPoolEmailConfig { + public _bind(scope: Construct): UserPoolEmailConfig { const region = Stack.of(scope).region; if (Token.isUnresolved(region) && !this.options.sesRegion) { diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 1b1a908bbe421..aebcec0af301a 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -800,7 +800,7 @@ export class UserPool extends UserPoolBase { if (props.email && props.emailSettings) { throw new Error('you must either provide "email" or "emailSettings", but not both'); } - const emailConfiguration = props.email ? props.email.bind(this) : undefinedIfNoKeys({ + const emailConfiguration = props.email ? props.email._bind(this) : undefinedIfNoKeys({ from: encodePuny(props.emailSettings?.from), replyToEmailAddress: encodePuny(props.emailSettings?.replyTo), });