Skip to content

Commit 477139e

Browse files
authored
feat(auth): Add email MFA support to Amplify Auth construct (#3036)
## Problem This PR adds support for email-based multi-factor authentication (MFA) to the Amplify Auth construct. Previously, the Auth construct only supported SMS and TOTP MFA methods, limiting authentication options for applications that prefer email-based verification. Issue number, if available: - aws-amplify/amplify-ui#6590 - #2159 ## Changes • **Auth Construct**: Added email MFA configuration support in construct.ts with new type definitions in types.ts • **API Updates**: Extended public API to include email MFA options with TypeScript types • **Client Configuration**: Updated all client config schema versions (v1, v1.1-v1.4) to support email MFA settings • **Documentation**: Updated API.md and README.md with email MFA usage examples • **Test Coverage**: Added comprehensive unit tests for email MFA functionality **Corresponding docs PR, if applicable:** - aws-amplify/docs#8465 ## Validation • **Unit Tests**: Added 56 lines of test coverage in construct.test.ts validating email MFA configuration and behavior • **Manual testing**: I tested on a amplify sample app which is linked locally with amplify-backend. I set the email MFA to true and false and checked it updates correctly in the Cognito user pool ## Checklist - [ ] If this PR includes a functional change to the runtime behavior of the code, I have added or updated automated test coverage for this change. - [ ] If this PR requires a change to the [Project Architecture README](../PROJECT_ARCHITECTURE.md), I have included that update in this PR. - [ ] If this PR requires a docs update, I have linked to that docs PR above. - [ ] If this PR modifies E2E tests, makes changes to resource provisioning, or makes SDK calls, I have run the PR checks with the `run-e2e` label set. _By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license._
1 parent 5d4b72f commit 477139e

File tree

12 files changed

+133
-8
lines changed

12 files changed

+133
-8
lines changed

.changeset/cuddly-clowns-guess.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@aws-amplify/auth-construct': minor
3+
'@aws-amplify/client-config': minor
4+
'@aws-amplify/backend-auth': minor
5+
'@aws-amplify/backend': minor
6+
'@aws-amplify/seed': minor
7+
---
8+
9+
feat(auth): Added support for email-MFA in Amplify Auth construct

packages/auth-construct/API.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,22 @@ export type MFA = {
144144
mode: 'OPTIONAL' | 'REQUIRED';
145145
} & MFASettings);
146146

147+
// @public
148+
export type MFAEmailSettings = boolean;
149+
147150
// @public
148151
export type MFASettings = {
152+
totp?: MFATotpSettings;
153+
sms?: MFASmsSettings;
154+
email: MFAEmailSettings;
155+
} | {
149156
totp?: MFATotpSettings;
150157
sms: MFASmsSettings;
158+
email?: MFAEmailSettings;
151159
} | {
152160
totp: MFATotpSettings;
153161
sms?: MFASmsSettings;
162+
email?: MFAEmailSettings;
154163
};
155164

156165
// @public

packages/auth-construct/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,36 @@ new AmplifyAuth(stack, 'Auth', {
4747
});
4848
```
4949

50+
### Email login with email MFA
51+
52+
In this example, you will create a stack with email login and email MFA enabled. Note that email MFA requires an email sender configuration.
53+
54+
```ts
55+
import { App, Stack } from 'aws-cdk-lib';
56+
import { AmplifyAuth } from '@aws-amplify/auth-construct';
57+
58+
const app = new App();
59+
const stack = new Stack(app, 'AuthStack');
60+
61+
new AmplifyAuth(stack, 'Auth', {
62+
loginWith: {
63+
email: true,
64+
},
65+
multifactor: {
66+
mode: 'OPTIONAL',
67+
email: true,
68+
sms: false,
69+
totp: false,
70+
},
71+
senders: {
72+
email: {
73+
fromEmail: '[email protected]',
74+
fromName: 'My App',
75+
},
76+
},
77+
});
78+
```
79+
5080
### Customized email and phone login with external login providers
5181

5282
In this example, you will create a stack with email, phone, and external login providers. Additionally, you can customize the email and phone verification messages.

packages/auth-construct/src/construct.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,62 @@ void describe('Auth construct', () => {
11691169
assert.equal(outputs['mfaConfiguration']['Value'], 'ON');
11701170
});
11711171

1172+
void it('enables email MFA when email is set to true', () => {
1173+
new AmplifyAuth(stack, 'test', {
1174+
loginWith: { email: true },
1175+
multifactor: { mode: 'OPTIONAL', email: true },
1176+
senders: {
1177+
email: {
1178+
fromEmail: '[email protected]',
1179+
fromName: 'Example.com',
1180+
},
1181+
},
1182+
});
1183+
1184+
const template = Template.fromStack(stack);
1185+
template.hasResourceProperties('AWS::Cognito::UserPool', {
1186+
EnabledMfas: ['EMAIL_OTP'],
1187+
});
1188+
const outputs = template.findOutputs('*');
1189+
assert.equal(outputs['mfaTypes']['Value'], '["EMAIL"]');
1190+
assert.equal(outputs['mfaConfiguration']['Value'], 'OPTIONAL');
1191+
});
1192+
1193+
void it('enables multiple MFA types including email', () => {
1194+
new AmplifyAuth(stack, 'test', {
1195+
loginWith: { email: true },
1196+
multifactor: { mode: 'REQUIRED', sms: true, totp: true, email: true },
1197+
senders: {
1198+
email: {
1199+
fromEmail: '[email protected]',
1200+
fromName: 'Example.com',
1201+
},
1202+
},
1203+
});
1204+
1205+
const template = Template.fromStack(stack);
1206+
template.hasResourceProperties('AWS::Cognito::UserPool', {
1207+
EnabledMfas: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
1208+
});
1209+
const outputs = template.findOutputs('*');
1210+
assert.equal(outputs['mfaTypes']['Value'], '["SMS","TOTP","EMAIL"]');
1211+
assert.equal(outputs['mfaConfiguration']['Value'], 'ON');
1212+
});
1213+
1214+
void it('does not enable email MFA when email is set to false', () => {
1215+
new AmplifyAuth(stack, 'test', {
1216+
loginWith: { email: true },
1217+
multifactor: { mode: 'OPTIONAL', sms: true, email: false },
1218+
});
1219+
1220+
const template = Template.fromStack(stack);
1221+
template.hasResourceProperties('AWS::Cognito::UserPool', {
1222+
EnabledMfas: ['SMS_MFA'],
1223+
});
1224+
const outputs = template.findOutputs('*');
1225+
assert.equal(outputs['mfaTypes']['Value'], '["SMS"]');
1226+
});
1227+
11721228
void it('updates socialProviders and oauth outputs when external providers are present', () => {
11731229
new AmplifyAuth(stack, 'test', {
11741230
loginWith: {

packages/auth-construct/src/construct.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export class AmplifyAuth
231231
if (!(cfnUserPool instanceof CfnUserPool)) {
232232
throw Error('Could not find CfnUserPool resource in stack.');
233233
}
234+
234235
const cfnUserPoolClient = userPoolClient.node.findChild(
235236
'Resource',
236237
) as CfnUserPoolClient;
@@ -810,7 +811,7 @@ export class AmplifyAuth
810811
* Convert user friendly Mfa type to cognito Mfa type.
811812
* This eliminates the need for users to import cognito.Mfa.
812813
* @param mfa - MFA settings
813-
* @returns cognito MFA type (sms or totp)
814+
* @returns cognito MFA type (sms, totp, or email)
814815
*/
815816
private getMFAType = (
816817
mfa: AuthProps['multifactor'],
@@ -819,6 +820,7 @@ export class AmplifyAuth
819820
? {
820821
sms: mfa.sms ? true : false,
821822
otp: mfa.totp ? true : false,
823+
email: mfa.email ? true : false,
822824
}
823825
: undefined;
824826
};
@@ -1222,14 +1224,18 @@ export class AmplifyAuth
12221224
// extract the MFA types from the UserPool resource
12231225
output.mfaTypes = Lazy.string({
12241226
produce: () => {
1227+
const enabledMfas = cfnUserPool.enabledMfas ?? [];
12251228
const mfaTypes: string[] = [];
1226-
(cfnUserPool.enabledMfas ?? []).forEach((type) => {
1229+
enabledMfas.forEach((type) => {
12271230
if (type === 'SMS_MFA') {
12281231
mfaTypes.push('SMS');
12291232
}
12301233
if (type === 'SOFTWARE_TOKEN_MFA') {
12311234
mfaTypes.push('TOTP');
12321235
}
1236+
if (type === 'EMAIL_OTP') {
1237+
mfaTypes.push('EMAIL');
1238+
}
12331239
});
12341240
return JSON.stringify(mfaTypes);
12351241
},

packages/auth-construct/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export {
1313
VerificationEmailWithLink,
1414
MFA,
1515
MFASmsSettings,
16+
MFAEmailSettings,
1617
MFATotpSettings,
1718
MFASettings,
1819
PhoneNumberLogin,

packages/auth-construct/src/types.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,20 +120,30 @@ export type MFASmsSettings =
120120
*/
121121
smsMessage: (createCode: () => string) => string;
122122
};
123+
/**
124+
* If true, the MFA token is sent to the user via email.
125+
*/
126+
export type MFAEmailSettings = boolean;
123127
/**
124128
* If true, the MFA token is a time-based one time password that is generated by a hardware or software token
125129
* @see - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-totp.html
126130
*/
127131
export type MFATotpSettings = boolean;
128132
/**
129-
* Configure the MFA types that users can use. At least one of totp or sms is required.
133+
* Configure the MFA types that users can use. At least one of totp, sms, or email is required.
130134
*/
131135
export type MFASettings =
136+
| {
137+
totp?: MFATotpSettings;
138+
sms?: MFASmsSettings;
139+
email: MFAEmailSettings;
140+
}
132141
| {
133142
totp?: MFATotpSettings;
134143
sms: MFASmsSettings;
144+
email?: MFAEmailSettings;
135145
}
136-
| { totp: MFATotpSettings; sms?: MFASmsSettings };
146+
| { totp: MFATotpSettings; sms?: MFASmsSettings; email?: MFAEmailSettings };
137147

138148
/**
139149
* MFA configuration. MFA settings are required if the mode is either "OPTIONAL" or "REQUIRED"

packages/backend-auth/src/lambda/reference_auth_initializer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,10 @@ export class ReferenceAuthInitializer {
430430
if (userPoolMFA.SoftwareTokenMfaConfiguration?.Enabled) {
431431
mfaTypes.push('TOTP');
432432
}
433+
434+
if (userPoolMFA.EmailMfaConfiguration) {
435+
mfaTypes.push('EMAIL_MFA');
436+
}
433437
// social providers
434438
const socialProviders: string[] = [];
435439
if (userPoolProviders) {

packages/client-config/API.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ interface AWSAmplifyBackendOutputs {
253253
user_verification_types?: ('email' | 'phone_number')[];
254254
unauthenticated_identities_enabled?: boolean;
255255
mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED';
256-
mfa_methods?: ('SMS' | 'TOTP')[];
256+
mfa_methods?: ('SMS' | 'TOTP' | 'EMAIL')[];
257257
groups?: {
258258
[k: string]: AmplifyUserGroupConfig;
259259
}[];

packages/client-config/src/client-config-schema/client_config_v1.4.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export interface AWSAmplifyBackendOutputs {
185185
user_verification_types?: ('email' | 'phone_number')[];
186186
unauthenticated_identities_enabled?: boolean;
187187
mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED';
188-
mfa_methods?: ('SMS' | 'TOTP')[];
188+
mfa_methods?: ('SMS' | 'TOTP' | 'EMAIL')[];
189189
groups?: {
190190
[k: string]: AmplifyUserGroupConfig;
191191
}[];

0 commit comments

Comments
 (0)