Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cognito): user pool: send emails using Amazon SES #17117

Merged
merged 10 commits into from Nov 15, 2021
61 changes: 61 additions & 0 deletions packages/@aws-cdk/aws-cognito/README.md
Expand Up @@ -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
corymhall marked this conversation as resolved.
Show resolved Hide resolved

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).
corymhall marked this conversation as resolved.
Show resolved Hide resolved

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
corymhall marked this conversation as resolved.
Show resolved Hide resolved
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
corymhall marked this conversation as resolved.
Show resolved Hide resolved
that allows Cognito to send emails. See the section `Step 3: Grant Email Permissions to Amazon Cognito` of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must have an authorization policy that allows Cognito to send emails

can we auto-configure this policy as part of the cognito module? What would that take?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to configure this policy by using a custom resource that calls put-identity-policy. A prerequisite would be creating the email in SES, but it wouldn't need to be verified or fully configured to attach this policy.

I wasn't sure about configuring this one piece in SES since it feels like SES should have it's own package that controls it's configuration. I think ideally the user experience would look like this, although I don't know if SES will get CloudFormation resources anytime soon.

const sesIdentity = new ses.Identity(this, 'Identity', {
  email: 'user@example.com',
});

sesIdentity.addAuthorization(new authorizations.CognitoAuthorization(userpool));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Email verification is clearly an out-of-band process, and we cannot automate that in the cdk.

Calling put-identity-policy via a custom resource is something we can do, i.e., add it to the aws-ses module and use it from here.
If you're interested, you could consider something like this in a subsequent PR. I'm ok with shipping it without this feature available yet.

[developer guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-configure).
corymhall marked this conversation as resolved.
Show resolved Hide resolved


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.
nija-at marked this conversation as resolved.
Show resolved Hide resolved

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',
},
corymhall marked this conversation as resolved.
Show resolved Hide resolved
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`.
corymhall marked this conversation as resolved.
Show resolved Hide resolved
corymhall marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
264 changes: 260 additions & 4 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Expand Up @@ -370,6 +370,251 @@ export interface PasswordPolicy {
readonly requireSymbols?: boolean;
}

/**
corymhall marked this conversation as resolved.
Show resolved Hide resolved
* Configuration for what from email address and name Cognito will
corymhall marked this conversation as resolved.
Show resolved Hide resolved
* 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The name of a configuration set in SES that should
* be applied to emails sent via Cognito.
* 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.
*
* @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'];
corymhall marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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') {
corymhall marked this conversation as resolved.
Show resolved Hide resolved
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'`);
}

let from = this.options.from.email;
if (this.options.from.name) {
from = `${this.options.from.name} <${this.options.from.email}>`;
}

return {
emailConfig: {
from: encodePuny(from),
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.
*/
Expand Down Expand Up @@ -574,6 +819,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
Expand Down Expand Up @@ -788,6 +1039,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,
Expand All @@ -805,10 +1064,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,
}),
Expand Down