From 6b70e52e91375472f74b6eaf5ed3c67a5ff176db Mon Sep 17 00:00:00 2001 From: Anivar A Aravind Date: Mon, 21 Jul 2025 13:19:04 +0530 Subject: [PATCH 1/2] feat(auth): add email MFA support Add support for email-based multi-factor authentication (MFA) using AWS Cognito's EMAIL_OTP feature. Changes: - Add MFAEmailSettings type definition - Enable EMAIL_OTP in UserPool configuration when email MFA is enabled - Map EMAIL_OTP to EMAIL in client output - Add comprehensive test coverage - Update documentation with usage examples Implementation uses CloudFormation overrides to enable EMAIL_OTP as CDK direct support is limited. Custom email templates require Lambda triggers and are not included in this minimal implementation. Resolves: #2159 --- packages/auth-construct/README.md | 52 ++++++++++++- packages/auth-construct/src/construct.test.ts | 74 +++++++++++++++++++ packages/auth-construct/src/construct.ts | 30 +++++++- packages/auth-construct/src/types.ts | 20 ++++- 4 files changed, 172 insertions(+), 4 deletions(-) diff --git a/packages/auth-construct/README.md b/packages/auth-construct/README.md index 90d6b814e3b..a3eb61b144c 100644 --- a/packages/auth-construct/README.md +++ b/packages/auth-construct/README.md @@ -40,13 +40,63 @@ new AmplifyAuth(stack, 'Auth', { multifactor: { mode: 'OPTIONAL', sms: { - smsMessage: (code: string) => `Your verification code is ${code}`, + smsMessage: (code: () => string) => `Your verification code is ${code()}`, }, totp: false, }, }); ``` +### Email login with email-based MFA + +In this example, you will create a stack with email login and email-based MFA enabled. This uses AWS Cognito's EMAIL_OTP feature for multi-factor authentication. + +```ts +import { App, Stack } from 'aws-cdk-lib'; +import { AmplifyAuth } from '@aws-amplify/auth-construct'; + +const app = new App(); +const stack = new Stack(app, 'AuthStack'); + +new AmplifyAuth(stack, 'Auth', { + loginWith: { + email: true, + }, + multifactor: { + mode: 'OPTIONAL', + email: true, + }, +}); +``` + +### Email login with multiple MFA options + +In this example, you will create a stack with email login and multiple MFA options (SMS, TOTP, and email) to give users flexibility in choosing their preferred authentication method. + +```ts +import { App, Stack } from 'aws-cdk-lib'; +import { AmplifyAuth } from '@aws-amplify/auth-construct'; + +const app = new App(); +const stack = new Stack(app, 'AuthStack'); + +new AmplifyAuth(stack, 'Auth', { + loginWith: { + email: true, + phone: true, + }, + multifactor: { + mode: 'OPTIONAL', + sms: { + smsMessage: (code: () => string) => + `Your SMS verification code is ${code()}`, + }, + totp: true, + email: true, + }, +}); +``` + ### Customized email and phone login with external login providers 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. diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index 35a65167e06..dc9de5ee7e0 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -547,6 +547,51 @@ void describe('Auth construct', () => { ); }); + void it('creates email MFA when enabled', () => { + const app = new App(); + const stack = new Stack(app); + + new AmplifyAuth(stack, 'test', { + loginWith: { + email: true, + }, + multifactor: { + mode: 'OPTIONAL', + email: true, + }, + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Cognito::UserPool', { + MfaConfiguration: 'OPTIONAL', + EnabledMfas: ['EMAIL_OTP'], + }); + }); + + void it('creates combined MFA with SMS and email', () => { + const app = new App(); + const stack = new Stack(app); + + new AmplifyAuth(stack, 'test', { + loginWith: { + email: true, + phone: true, + }, + multifactor: { + mode: 'OPTIONAL', + sms: true, + email: true, + totp: true, + }, + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Cognito::UserPool', { + MfaConfiguration: 'OPTIONAL', + EnabledMfas: ['SMS_MFA', 'EMAIL_OTP', 'SOFTWARE_TOKEN_MFA'], + }); + }); + void it('configures Cognito to send emails with SES when email senders field is populated', () => { const app = new App(); const stack = new Stack(app); @@ -1169,6 +1214,35 @@ void describe('Auth construct', () => { assert.equal(outputs['mfaConfiguration']['Value'], 'ON'); }); + void it('updates mfaConfiguration & mfaTypes when email MFA is enabled', () => { + new AmplifyAuth(stack, 'test', { + loginWith: { + email: true, + }, + multifactor: { mode: 'OPTIONAL', email: true }, + }); + + const template = Template.fromStack(stack); + const outputs = template.findOutputs('*'); + assert.equal(outputs['mfaTypes']['Value'], '["EMAIL"]'); + assert.equal(outputs['mfaConfiguration']['Value'], 'OPTIONAL'); + }); + + void it('updates mfaConfiguration & mfaTypes when all MFA types are enabled', () => { + new AmplifyAuth(stack, 'test', { + loginWith: { + email: true, + phone: true, + }, + multifactor: { mode: 'REQUIRED', sms: true, totp: true, email: true }, + }); + + const template = Template.fromStack(stack); + const outputs = template.findOutputs('*'); + assert.equal(outputs['mfaTypes']['Value'], '["SMS","TOTP","EMAIL"]'); + assert.equal(outputs['mfaConfiguration']['Value'], 'ON'); + }); + void it('updates socialProviders and oauth outputs when external providers are present', () => { new AmplifyAuth(stack, 'test', { loginWith: { diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 417475262f9..b42e15db5b3 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -231,6 +231,16 @@ export class AmplifyAuth if (!(cfnUserPool instanceof CfnUserPool)) { throw Error('Could not find CfnUserPool resource in stack.'); } + + // Configure email MFA if enabled + const mfaType = this.getMFAType(props.multifactor); + if (mfaType?.email) { + // Add EMAIL_OTP to enabled MFA methods + const currentEnabledMfas = cfnUserPool.enabledMfas || []; + if (!currentEnabledMfas.includes('EMAIL_OTP')) { + cfnUserPool.enabledMfas = [...currentEnabledMfas, 'EMAIL_OTP']; + } + } const cfnUserPoolClient = userPoolClient.node.findChild( 'Resource', ) as CfnUserPoolClient; @@ -496,6 +506,20 @@ export class AmplifyAuth ); } + // If email login is enabled along with MFA, we should recommend enabling email MFA type. + if ( + emailEnabled && + mfaMode && + mfaMode !== 'OFF' && + !mfaType?.email && + !mfaType?.sms && + !mfaType?.otp + ) { + throw Error( + 'Invalid MFA settings. At least one MFA method (email, sms, or totp) must be enabled when MFA is configured', + ); + } + const { standardAttributes, customAttributes } = Object.entries( props.userAttributes ?? {}, ).reduce( @@ -810,7 +834,7 @@ export class AmplifyAuth * Convert user friendly Mfa type to cognito Mfa type. * This eliminates the need for users to import cognito.Mfa. * @param mfa - MFA settings - * @returns cognito MFA type (sms or totp) + * @returns cognito MFA type (sms, totp, or email) */ private getMFAType = ( mfa: AuthProps['multifactor'], @@ -819,6 +843,7 @@ export class AmplifyAuth ? { sms: mfa.sms ? true : false, otp: mfa.totp ? true : false, + email: mfa.email ? true : false, } : undefined; }; @@ -1230,6 +1255,9 @@ export class AmplifyAuth if (type === 'SOFTWARE_TOKEN_MFA') { mfaTypes.push('TOTP'); } + if (type === 'EMAIL_OTP') { + mfaTypes.push('EMAIL'); + } }); return JSON.stringify(mfaTypes); }, diff --git a/packages/auth-construct/src/types.ts b/packages/auth-construct/src/types.ts index 3823690c098..0f6927fa266 100644 --- a/packages/auth-construct/src/types.ts +++ b/packages/auth-construct/src/types.ts @@ -120,20 +120,36 @@ export type MFASmsSettings = */ smsMessage: (createCode: () => string) => string; }; +/** + * If true, the MFA token is sent to the user via email to their verified email address. + * @see - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html + */ +export type MFAEmailSettings = boolean; + /** * If true, the MFA token is a time-based one time password that is generated by a hardware or software token * @see - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-totp.html */ export type MFATotpSettings = boolean; /** - * Configure the MFA types that users can use. At least one of totp or sms is required. + * Configure the MFA types that users can use. At least one of totp, sms, or email is required. */ export type MFASettings = | { totp?: MFATotpSettings; sms: MFASmsSettings; + email?: MFAEmailSettings; + } + | { + totp: MFATotpSettings; + sms?: MFASmsSettings; + email?: MFAEmailSettings; } - | { totp: MFATotpSettings; sms?: MFASmsSettings }; + | { + totp?: MFATotpSettings; + sms?: MFASmsSettings; + email: MFAEmailSettings; + }; /** * MFA configuration. MFA settings are required if the mode is either "OPTIONAL" or "REQUIRED" From 46865ce7b6024b46fba19f0bcdc21452f0f53168 Mon Sep 17 00:00:00 2001 From: Anivar A Aravind Date: Mon, 21 Jul 2025 16:09:59 +0530 Subject: [PATCH 2/2] chore: add changeset for email MFA support --- .changeset/email-mfa-support.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/email-mfa-support.md diff --git a/.changeset/email-mfa-support.md b/.changeset/email-mfa-support.md new file mode 100644 index 00000000000..93ac1f870b9 --- /dev/null +++ b/.changeset/email-mfa-support.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/auth-construct': minor +--- + +Add email MFA support using AWS Cognito EMAIL_OTP feature