Skip to content

Commit 1d2b1d3

Browse files
feat(aws-cognito): send emails with a verified domain (#19790)
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*
1 parent 7bd7139 commit 1d2b1d3

File tree

8 files changed

+330
-1
lines changed

8 files changed

+330
-1
lines changed

packages/@aws-cdk/aws-cognito/README.md

+15
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,21 @@ new cognito.UserPool(this, 'myuserpool', {
364364

365365
```
366366

367+
When sending emails from an SES verified domain, `sesVerifiedDomain` can be used to specify the domain.
368+
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.
369+
370+
```ts
371+
new cognito.UserPool(this, 'myuserpool', {
372+
email: cognito.UserPoolEmail.withSES({
373+
sesRegion: 'us-east-1',
374+
fromEmail: '[email protected]',
375+
fromName: 'Awesome App',
376+
replyTo: '[email protected]',
377+
sesVerifiedDomain: 'myawesomeapp.com',
378+
}),
379+
});
380+
```
381+
367382
### Device Tracking
368383

369384
User pools can be configured to track devices that users have logged in to.

packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ export interface UserPoolSESOptions {
5656
* @default - The same region as the Cognito UserPool
5757
*/
5858
readonly sesRegion?: string;
59+
60+
/**
61+
* SES Verified custom domain to be used to verify the identity
62+
*
63+
* @default - no domain
64+
*/
65+
readonly sesVerifiedDomain?: string
5966
}
6067

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

174+
if (this.options.sesVerifiedDomain) {
175+
const domainFromEmail = this.options.fromEmail.split('@').pop();
176+
if (domainFromEmail !== this.options.sesVerifiedDomain) {
177+
throw new Error('"fromEmail" contains a different domain than the "sesVerifiedDomain"');
178+
}
179+
}
180+
167181
return {
168182
from: encodeAndTest(from),
169183
replyToEmailAddress: encodeAndTest(this.options.replyTo),
@@ -172,7 +186,7 @@ class SESEmail extends UserPoolEmail {
172186
sourceArn: Stack.of(scope).formatArn({
173187
service: 'ses',
174188
resource: 'identity',
175-
resourceName: encodeAndTest(this.options.fromEmail),
189+
resourceName: encodeAndTest(this.options.sesVerifiedDomain ?? this.options.fromEmail),
176190
region: this.options.sesRegion ?? region,
177191
}),
178192
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core';
2+
import { UserPool, UserPoolEmail } from '../lib';
3+
4+
5+
const app = new App();
6+
const stack = new Stack(app, 'integ-user-pool-signup-code');
7+
8+
const userpool = new UserPool(stack, 'myuserpool', {
9+
removalPolicy: RemovalPolicy.DESTROY,
10+
userPoolName: 'MyUserPool',
11+
email: UserPoolEmail.withSES({
12+
sesRegion: 'us-east-1',
13+
fromEmail: '[email protected]',
14+
replyTo: '[email protected]',
15+
sesVerifiedDomain: 'example.com',
16+
}),
17+
});
18+
19+
new CfnOutput(stack, 'user-pool-id', {
20+
value: userpool.userPoolId,
21+
});
22+
23+
app.synth();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"17.0.0"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"Resources": {
3+
"myuserpool01998219": {
4+
"Type": "AWS::Cognito::UserPool",
5+
"Properties": {
6+
"AccountRecoverySetting": {
7+
"RecoveryMechanisms": [
8+
{
9+
"Name": "verified_phone_number",
10+
"Priority": 1
11+
},
12+
{
13+
"Name": "verified_email",
14+
"Priority": 2
15+
}
16+
]
17+
},
18+
"AdminCreateUserConfig": {
19+
"AllowAdminCreateUserOnly": true
20+
},
21+
"EmailConfiguration": {
22+
"EmailSendingAccount": "DEVELOPER",
23+
"From": "[email protected]",
24+
"ReplyToEmailAddress": "[email protected]",
25+
"SourceArn": {
26+
"Fn::Join": [
27+
"",
28+
[
29+
"arn:",
30+
{
31+
"Ref": "AWS::Partition"
32+
},
33+
":ses:us-east-1:",
34+
{
35+
"Ref": "AWS::AccountId"
36+
},
37+
":identity/example.com"
38+
]
39+
]
40+
}
41+
},
42+
"EmailVerificationMessage": "The verification code to your new account is {####}",
43+
"EmailVerificationSubject": "Verify your new account",
44+
"SmsVerificationMessage": "The verification code to your new account is {####}",
45+
"UserPoolName": "MyUserPool",
46+
"VerificationMessageTemplate": {
47+
"DefaultEmailOption": "CONFIRM_WITH_CODE",
48+
"EmailMessage": "The verification code to your new account is {####}",
49+
"EmailSubject": "Verify your new account",
50+
"SmsMessage": "The verification code to your new account is {####}"
51+
}
52+
},
53+
"UpdateReplacePolicy": "Delete",
54+
"DeletionPolicy": "Delete"
55+
}
56+
},
57+
"Outputs": {
58+
"userpoolid": {
59+
"Value": {
60+
"Ref": "myuserpool01998219"
61+
}
62+
}
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"version": "17.0.0",
3+
"artifacts": {
4+
"Tree": {
5+
"type": "cdk:tree",
6+
"properties": {
7+
"file": "tree.json"
8+
},
9+
"metadata": {}
10+
},
11+
"integ-user-pool-signup-code": {
12+
"type": "aws:cloudformation:stack",
13+
"environment": "aws://unknown-account/unknown-region",
14+
"properties": {
15+
"templateFile": "integ-user-pool-signup-code.template.json",
16+
"validateOnSynth": false
17+
},
18+
"metadata": {
19+
"/integ-user-pool-signup-code/myuserpool/Resource": [
20+
{
21+
"type": "aws:cdk:logicalId",
22+
"data": "myuserpool01998219"
23+
}
24+
],
25+
"/integ-user-pool-signup-code/user-pool-id": [
26+
{
27+
"type": "aws:cdk:logicalId",
28+
"data": "userpoolid"
29+
}
30+
]
31+
},
32+
"displayName": "integ-user-pool-signup-code"
33+
}
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
{
2+
"version": "tree-0.1",
3+
"tree": {
4+
"id": "App",
5+
"path": "",
6+
"children": {
7+
"Tree": {
8+
"id": "Tree",
9+
"path": "Tree",
10+
"constructInfo": {
11+
"fqn": "@aws-cdk/core.Construct",
12+
"version": "0.0.0"
13+
}
14+
},
15+
"integ-user-pool-signup-code": {
16+
"id": "integ-user-pool-signup-code",
17+
"path": "integ-user-pool-signup-code",
18+
"children": {
19+
"myuserpool": {
20+
"id": "myuserpool",
21+
"path": "integ-user-pool-signup-code/myuserpool",
22+
"children": {
23+
"Resource": {
24+
"id": "Resource",
25+
"path": "integ-user-pool-signup-code/myuserpool/Resource",
26+
"attributes": {
27+
"aws:cdk:cloudformation:type": "AWS::Cognito::UserPool",
28+
"aws:cdk:cloudformation:props": {
29+
"accountRecoverySetting": {
30+
"recoveryMechanisms": [
31+
{
32+
"name": "verified_phone_number",
33+
"priority": 1
34+
},
35+
{
36+
"name": "verified_email",
37+
"priority": 2
38+
}
39+
]
40+
},
41+
"adminCreateUserConfig": {
42+
"allowAdminCreateUserOnly": true
43+
},
44+
"emailConfiguration": {
45+
"from": "[email protected]",
46+
"replyToEmailAddress": "[email protected]",
47+
"emailSendingAccount": "DEVELOPER",
48+
"sourceArn": {
49+
"Fn::Join": [
50+
"",
51+
[
52+
"arn:",
53+
{
54+
"Ref": "AWS::Partition"
55+
},
56+
":ses:us-east-1:",
57+
{
58+
"Ref": "AWS::AccountId"
59+
},
60+
":identity/example.com"
61+
]
62+
]
63+
}
64+
},
65+
"emailVerificationMessage": "The verification code to your new account is {####}",
66+
"emailVerificationSubject": "Verify your new account",
67+
"smsVerificationMessage": "The verification code to your new account is {####}",
68+
"userPoolName": "MyUserPool",
69+
"verificationMessageTemplate": {
70+
"defaultEmailOption": "CONFIRM_WITH_CODE",
71+
"emailMessage": "The verification code to your new account is {####}",
72+
"emailSubject": "Verify your new account",
73+
"smsMessage": "The verification code to your new account is {####}"
74+
}
75+
}
76+
},
77+
"constructInfo": {
78+
"fqn": "@aws-cdk/aws-cognito.CfnUserPool",
79+
"version": "0.0.0"
80+
}
81+
}
82+
},
83+
"constructInfo": {
84+
"fqn": "@aws-cdk/aws-cognito.UserPool",
85+
"version": "0.0.0"
86+
}
87+
},
88+
"user-pool-id": {
89+
"id": "user-pool-id",
90+
"path": "integ-user-pool-signup-code/user-pool-id",
91+
"constructInfo": {
92+
"fqn": "@aws-cdk/core.CfnOutput",
93+
"version": "0.0.0"
94+
}
95+
}
96+
},
97+
"constructInfo": {
98+
"fqn": "@aws-cdk/core.Stack",
99+
"version": "0.0.0"
100+
}
101+
}
102+
},
103+
"constructInfo": {
104+
"fqn": "@aws-cdk/core.App",
105+
"version": "0.0.0"
106+
}
107+
}
108+
}

packages/@aws-cdk/aws-cognito/test/user-pool.test.ts

+69
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,75 @@ describe('User Pool', () => {
17291729

17301730
});
17311731

1732+
test('email withSES with verified domain', () => {
1733+
// GIVEN
1734+
const stack = new Stack(undefined, undefined, {
1735+
env: {
1736+
region: 'us-east-2',
1737+
account: '11111111111',
1738+
},
1739+
});
1740+
1741+
// WHEN
1742+
new UserPool(stack, 'Pool', {
1743+
email: UserPoolEmail.withSES({
1744+
fromEmail: '[email protected]',
1745+
fromName: 'My Custom Email',
1746+
sesRegion: 'us-east-1',
1747+
replyTo: '[email protected]',
1748+
configurationSetName: 'default',
1749+
sesVerifiedDomain: 'example.com',
1750+
}),
1751+
});
1752+
1753+
// THEN
1754+
Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', {
1755+
EmailConfiguration: {
1756+
EmailSendingAccount: 'DEVELOPER',
1757+
From: 'My Custom Email <[email protected]>',
1758+
ReplyToEmailAddress: '[email protected]',
1759+
ConfigurationSet: 'default',
1760+
SourceArn: {
1761+
'Fn::Join': [
1762+
'',
1763+
[
1764+
'arn:',
1765+
{
1766+
Ref: 'AWS::Partition',
1767+
},
1768+
':ses:us-east-1:11111111111:identity/example.com',
1769+
],
1770+
],
1771+
},
1772+
},
1773+
});
1774+
});
1775+
1776+
test('email withSES throws, when "fromEmail" contains the different domain', () => {
1777+
// GIVEN
1778+
const stack = new Stack(undefined, undefined, {
1779+
env: {
1780+
region: 'us-east-2',
1781+
account: '11111111111',
1782+
},
1783+
});
1784+
1785+
expect(() => new UserPool(stack, 'Pool1', {
1786+
mfaMessage: '{####',
1787+
})).toThrow(/MFA message must contain the template string/);
1788+
1789+
// WHEN
1790+
expect(() => new UserPool(stack, 'Pool', {
1791+
email: UserPoolEmail.withSES({
1792+
fromEmail: '[email protected]',
1793+
fromName: 'My Custom Email',
1794+
sesRegion: 'us-east-1',
1795+
replyTo: '[email protected]',
1796+
configurationSetName: 'default',
1797+
sesVerifiedDomain: 'example.com',
1798+
}),
1799+
})).toThrow(/"fromEmail" contains a different domain than the "sesVerifiedDomain"/);
1800+
});
17321801
});
17331802

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

0 commit comments

Comments
 (0)