Skip to content

Commit

Permalink
feat(aws-cognito): send emails with a verified domain (#19790)
Browse files Browse the repository at this point in the history
When sending emails with a verified domain, the email address does not need to be verified.
In that case, the identity of the SourceArn in EmailConfiguration is allowed to be set to the domain instead of the email address.

closes [#19762](#19762)

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)?
	* [ ] Did you use `cdk-integ` to deploy the infrastructure and generate the snapshot (i.e. `cdk-integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
hassanazharkhan committed Apr 22, 2022
1 parent 7bd7139 commit 1d2b1d3
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 1 deletion.
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,21 @@ new cognito.UserPool(this, 'myuserpool', {

```

When sending emails from an SES verified domain, `sesVerifiedDomain` can be used to specify the domain.
The email address does not need to be verified when sending emails from a verified domain, because the identity of the email configuration is can be determined from the domain alone.

```ts
new cognito.UserPool(this, 'myuserpool', {
email: cognito.UserPoolEmail.withSES({
sesRegion: 'us-east-1',
fromEmail: 'noreply@myawesomeapp.com',
fromName: 'Awesome App',
replyTo: 'support@myawesomeapp.com',
sesVerifiedDomain: 'myawesomeapp.com',
}),
});
```

### Device Tracking

User pools can be configured to track devices that users have logged in to.
Expand Down
16 changes: 15 additions & 1 deletion packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ export interface UserPoolSESOptions {
* @default - The same region as the Cognito UserPool
*/
readonly sesRegion?: string;

/**
* SES Verified custom domain to be used to verify the identity
*
* @default - no domain
*/
readonly sesVerifiedDomain?: string
}

/**
Expand Down Expand Up @@ -164,6 +171,13 @@ class SESEmail extends UserPoolEmail {
from = `${this.options.fromName} <${this.options.fromEmail}>`;
}

if (this.options.sesVerifiedDomain) {
const domainFromEmail = this.options.fromEmail.split('@').pop();
if (domainFromEmail !== this.options.sesVerifiedDomain) {
throw new Error('"fromEmail" contains a different domain than the "sesVerifiedDomain"');
}
}

return {
from: encodeAndTest(from),
replyToEmailAddress: encodeAndTest(this.options.replyTo),
Expand All @@ -172,7 +186,7 @@ class SESEmail extends UserPoolEmail {
sourceArn: Stack.of(scope).formatArn({
service: 'ses',
resource: 'identity',
resourceName: encodeAndTest(this.options.fromEmail),
resourceName: encodeAndTest(this.options.sesVerifiedDomain ?? this.options.fromEmail),
region: this.options.sesRegion ?? region,
}),
};
Expand Down
23 changes: 23 additions & 0 deletions packages/@aws-cdk/aws-cognito/test/integ.user-pool-ses-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core';
import { UserPool, UserPoolEmail } from '../lib';


const app = new App();
const stack = new Stack(app, 'integ-user-pool-signup-code');

const userpool = new UserPool(stack, 'myuserpool', {
removalPolicy: RemovalPolicy.DESTROY,
userPoolName: 'MyUserPool',
email: UserPoolEmail.withSES({
sesRegion: 'us-east-1',
fromEmail: 'noreply@example.com',
replyTo: 'support@example.com',
sesVerifiedDomain: 'example.com',
}),
});

new CfnOutput(stack, 'user-pool-id', {
value: userpool.userPoolId,
});

app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"17.0.0"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"Resources": {
"myuserpool01998219": {
"Type": "AWS::Cognito::UserPool",
"Properties": {
"AccountRecoverySetting": {
"RecoveryMechanisms": [
{
"Name": "verified_phone_number",
"Priority": 1
},
{
"Name": "verified_email",
"Priority": 2
}
]
},
"AdminCreateUserConfig": {
"AllowAdminCreateUserOnly": true
},
"EmailConfiguration": {
"EmailSendingAccount": "DEVELOPER",
"From": "noreply@example.com",
"ReplyToEmailAddress": "support@example.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":ses:us-east-1:",
{
"Ref": "AWS::AccountId"
},
":identity/example.com"
]
]
}
},
"EmailVerificationMessage": "The verification code to your new account is {####}",
"EmailVerificationSubject": "Verify your new account",
"SmsVerificationMessage": "The verification code to your new account is {####}",
"UserPoolName": "MyUserPool",
"VerificationMessageTemplate": {
"DefaultEmailOption": "CONFIRM_WITH_CODE",
"EmailMessage": "The verification code to your new account is {####}",
"EmailSubject": "Verify your new account",
"SmsMessage": "The verification code to your new account is {####}"
}
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
}
},
"Outputs": {
"userpoolid": {
"Value": {
"Ref": "myuserpool01998219"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"version": "17.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
"properties": {
"file": "tree.json"
},
"metadata": {}
},
"integ-user-pool-signup-code": {
"type": "aws:cloudformation:stack",
"environment": "aws://unknown-account/unknown-region",
"properties": {
"templateFile": "integ-user-pool-signup-code.template.json",
"validateOnSynth": false
},
"metadata": {
"/integ-user-pool-signup-code/myuserpool/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "myuserpool01998219"
}
],
"/integ-user-pool-signup-code/user-pool-id": [
{
"type": "aws:cdk:logicalId",
"data": "userpoolid"
}
]
},
"displayName": "integ-user-pool-signup-code"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{
"version": "tree-0.1",
"tree": {
"id": "App",
"path": "",
"children": {
"Tree": {
"id": "Tree",
"path": "Tree",
"constructInfo": {
"fqn": "@aws-cdk/core.Construct",
"version": "0.0.0"
}
},
"integ-user-pool-signup-code": {
"id": "integ-user-pool-signup-code",
"path": "integ-user-pool-signup-code",
"children": {
"myuserpool": {
"id": "myuserpool",
"path": "integ-user-pool-signup-code/myuserpool",
"children": {
"Resource": {
"id": "Resource",
"path": "integ-user-pool-signup-code/myuserpool/Resource",
"attributes": {
"aws:cdk:cloudformation:type": "AWS::Cognito::UserPool",
"aws:cdk:cloudformation:props": {
"accountRecoverySetting": {
"recoveryMechanisms": [
{
"name": "verified_phone_number",
"priority": 1
},
{
"name": "verified_email",
"priority": 2
}
]
},
"adminCreateUserConfig": {
"allowAdminCreateUserOnly": true
},
"emailConfiguration": {
"from": "noreply@example.com",
"replyToEmailAddress": "support@example.com",
"emailSendingAccount": "DEVELOPER",
"sourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":ses:us-east-1:",
{
"Ref": "AWS::AccountId"
},
":identity/example.com"
]
]
}
},
"emailVerificationMessage": "The verification code to your new account is {####}",
"emailVerificationSubject": "Verify your new account",
"smsVerificationMessage": "The verification code to your new account is {####}",
"userPoolName": "MyUserPool",
"verificationMessageTemplate": {
"defaultEmailOption": "CONFIRM_WITH_CODE",
"emailMessage": "The verification code to your new account is {####}",
"emailSubject": "Verify your new account",
"smsMessage": "The verification code to your new account is {####}"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-cognito.CfnUserPool",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-cognito.UserPool",
"version": "0.0.0"
}
},
"user-pool-id": {
"id": "user-pool-id",
"path": "integ-user-pool-signup-code/user-pool-id",
"constructInfo": {
"fqn": "@aws-cdk/core.CfnOutput",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/core.Stack",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/core.App",
"version": "0.0.0"
}
}
}
69 changes: 69 additions & 0 deletions packages/@aws-cdk/aws-cognito/test/user-pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,75 @@ describe('User Pool', () => {

});

test('email withSES with verified domain', () => {
// GIVEN
const stack = new Stack(undefined, undefined, {
env: {
region: 'us-east-2',
account: '11111111111',
},
});

// WHEN
new UserPool(stack, 'Pool', {
email: UserPoolEmail.withSES({
fromEmail: 'mycustomemail@example.com',
fromName: 'My Custom Email',
sesRegion: 'us-east-1',
replyTo: 'reply@example.com',
configurationSetName: 'default',
sesVerifiedDomain: 'example.com',
}),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', {
EmailConfiguration: {
EmailSendingAccount: 'DEVELOPER',
From: 'My Custom Email <mycustomemail@example.com>',
ReplyToEmailAddress: 'reply@example.com',
ConfigurationSet: 'default',
SourceArn: {
'Fn::Join': [
'',
[
'arn:',
{
Ref: 'AWS::Partition',
},
':ses:us-east-1:11111111111:identity/example.com',
],
],
},
},
});
});

test('email withSES throws, when "fromEmail" contains the different domain', () => {
// GIVEN
const stack = new Stack(undefined, undefined, {
env: {
region: 'us-east-2',
account: '11111111111',
},
});

expect(() => new UserPool(stack, 'Pool1', {
mfaMessage: '{####',
})).toThrow(/MFA message must contain the template string/);

// WHEN
expect(() => new UserPool(stack, 'Pool', {
email: UserPoolEmail.withSES({
fromEmail: 'mycustomemail@some.com',
fromName: 'My Custom Email',
sesRegion: 'us-east-1',
replyTo: 'reply@example.com',
configurationSetName: 'default',
sesVerifiedDomain: 'example.com',
}),
})).toThrow(/"fromEmail" contains a different domain than the "sesVerifiedDomain"/);
});
});

test('device tracking is configured correctly', () => {
Expand Down

0 comments on commit 1d2b1d3

Please sign in to comment.