diff --git a/.changeset/cuddly-clowns-guess.md b/.changeset/cuddly-clowns-guess.md new file mode 100644 index 00000000000..1c64c047e7d --- /dev/null +++ b/.changeset/cuddly-clowns-guess.md @@ -0,0 +1,9 @@ +--- +'@aws-amplify/auth-construct': minor +'@aws-amplify/client-config': minor +'@aws-amplify/backend-auth': minor +'@aws-amplify/backend': minor +'@aws-amplify/seed': minor +--- + +feat(auth): Added support for email-MFA in Amplify Auth construct diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index 6c2a137ce37..cfce6840c45 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -144,13 +144,22 @@ export type MFA = { mode: 'OPTIONAL' | 'REQUIRED'; } & MFASettings); +// @public +export type MFAEmailSettings = boolean; + // @public export type MFASettings = { + totp?: MFATotpSettings; + sms?: MFASmsSettings; + email: MFAEmailSettings; +} | { totp?: MFATotpSettings; sms: MFASmsSettings; + email?: MFAEmailSettings; } | { totp: MFATotpSettings; sms?: MFASmsSettings; + email?: MFAEmailSettings; }; // @public diff --git a/packages/auth-construct/README.md b/packages/auth-construct/README.md index 90d6b814e3b..b7e5f701af8 100644 --- a/packages/auth-construct/README.md +++ b/packages/auth-construct/README.md @@ -47,6 +47,36 @@ new AmplifyAuth(stack, 'Auth', { }); ``` +### Email login with email MFA + +In this example, you will create a stack with email login and email MFA enabled. Note that email MFA requires an email sender configuration. + +```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, + sms: false, + totp: false, + }, + senders: { + email: { + fromEmail: 'noreply@example.com', + fromName: 'My App', + }, + }, +}); +``` + ### 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..b24607c6e3c 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -1169,6 +1169,62 @@ void describe('Auth construct', () => { assert.equal(outputs['mfaConfiguration']['Value'], 'ON'); }); + void it('enables email MFA when email is set to true', () => { + new AmplifyAuth(stack, 'test', { + loginWith: { email: true }, + multifactor: { mode: 'OPTIONAL', email: true }, + senders: { + email: { + fromEmail: 'noreply@example.com', + fromName: 'Example.com', + }, + }, + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Cognito::UserPool', { + EnabledMfas: ['EMAIL_OTP'], + }); + const outputs = template.findOutputs('*'); + assert.equal(outputs['mfaTypes']['Value'], '["EMAIL"]'); + assert.equal(outputs['mfaConfiguration']['Value'], 'OPTIONAL'); + }); + + void it('enables multiple MFA types including email', () => { + new AmplifyAuth(stack, 'test', { + loginWith: { email: true }, + multifactor: { mode: 'REQUIRED', sms: true, totp: true, email: true }, + senders: { + email: { + fromEmail: 'noreply@example.com', + fromName: 'Example.com', + }, + }, + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Cognito::UserPool', { + EnabledMfas: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'], + }); + const outputs = template.findOutputs('*'); + assert.equal(outputs['mfaTypes']['Value'], '["SMS","TOTP","EMAIL"]'); + assert.equal(outputs['mfaConfiguration']['Value'], 'ON'); + }); + + void it('does not enable email MFA when email is set to false', () => { + new AmplifyAuth(stack, 'test', { + loginWith: { email: true }, + multifactor: { mode: 'OPTIONAL', sms: true, email: false }, + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Cognito::UserPool', { + EnabledMfas: ['SMS_MFA'], + }); + const outputs = template.findOutputs('*'); + assert.equal(outputs['mfaTypes']['Value'], '["SMS"]'); + }); + 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..c192dc07a15 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -231,6 +231,7 @@ export class AmplifyAuth if (!(cfnUserPool instanceof CfnUserPool)) { throw Error('Could not find CfnUserPool resource in stack.'); } + const cfnUserPoolClient = userPoolClient.node.findChild( 'Resource', ) as CfnUserPoolClient; @@ -810,7 +811,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 +820,7 @@ export class AmplifyAuth ? { sms: mfa.sms ? true : false, otp: mfa.totp ? true : false, + email: mfa.email ? true : false, } : undefined; }; @@ -1222,14 +1224,18 @@ export class AmplifyAuth // extract the MFA types from the UserPool resource output.mfaTypes = Lazy.string({ produce: () => { + const enabledMfas = cfnUserPool.enabledMfas ?? []; const mfaTypes: string[] = []; - (cfnUserPool.enabledMfas ?? []).forEach((type) => { + enabledMfas.forEach((type) => { if (type === 'SMS_MFA') { mfaTypes.push('SMS'); } 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/index.ts b/packages/auth-construct/src/index.ts index 86f3dd3a4a0..f156c6a24ea 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -13,6 +13,7 @@ export { VerificationEmailWithLink, MFA, MFASmsSettings, + MFAEmailSettings, MFATotpSettings, MFASettings, PhoneNumberLogin, diff --git a/packages/auth-construct/src/types.ts b/packages/auth-construct/src/types.ts index 3823690c098..02f314621ce 100644 --- a/packages/auth-construct/src/types.ts +++ b/packages/auth-construct/src/types.ts @@ -120,20 +120,30 @@ export type MFASmsSettings = */ smsMessage: (createCode: () => string) => string; }; +/** + * If true, the MFA token is sent to the user via email. + */ +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" diff --git a/packages/backend-auth/src/lambda/reference_auth_initializer.ts b/packages/backend-auth/src/lambda/reference_auth_initializer.ts index f25e0c4ec0a..35ae897c529 100644 --- a/packages/backend-auth/src/lambda/reference_auth_initializer.ts +++ b/packages/backend-auth/src/lambda/reference_auth_initializer.ts @@ -430,6 +430,10 @@ export class ReferenceAuthInitializer { if (userPoolMFA.SoftwareTokenMfaConfiguration?.Enabled) { mfaTypes.push('TOTP'); } + + if (userPoolMFA.EmailMfaConfiguration) { + mfaTypes.push('EMAIL_MFA'); + } // social providers const socialProviders: string[] = []; if (userPoolProviders) { diff --git a/packages/client-config/API.md b/packages/client-config/API.md index 2c1f6d22c9e..55795591afd 100644 --- a/packages/client-config/API.md +++ b/packages/client-config/API.md @@ -253,7 +253,7 @@ interface AWSAmplifyBackendOutputs { user_verification_types?: ('email' | 'phone_number')[]; unauthenticated_identities_enabled?: boolean; mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; - mfa_methods?: ('SMS' | 'TOTP')[]; + mfa_methods?: ('SMS' | 'TOTP' | 'EMAIL')[]; groups?: { [k: string]: AmplifyUserGroupConfig; }[]; diff --git a/packages/client-config/src/client-config-schema/client_config_v1.4.ts b/packages/client-config/src/client-config-schema/client_config_v1.4.ts index e84598d88c6..73a2be938fc 100644 --- a/packages/client-config/src/client-config-schema/client_config_v1.4.ts +++ b/packages/client-config/src/client-config-schema/client_config_v1.4.ts @@ -185,7 +185,7 @@ export interface AWSAmplifyBackendOutputs { user_verification_types?: ('email' | 'phone_number')[]; unauthenticated_identities_enabled?: boolean; mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; - mfa_methods?: ('SMS' | 'TOTP')[]; + mfa_methods?: ('SMS' | 'TOTP' | 'EMAIL')[]; groups?: { [k: string]: AmplifyUserGroupConfig; }[]; diff --git a/packages/client-config/src/client-config-schema/schema_v1.4.json b/packages/client-config/src/client-config-schema/schema_v1.4.json index 9205353c421..66037e44776 100644 --- a/packages/client-config/src/client-config-schema/schema_v1.4.json +++ b/packages/client-config/src/client-config-schema/schema_v1.4.json @@ -187,7 +187,7 @@ "mfa_methods": { "type": "array", "items": { - "enum": ["SMS", "TOTP"] + "enum": ["SMS", "TOTP", "EMAIL"] } }, "groups": { diff --git a/packages/seed/src/auth-seed/config_reader.ts b/packages/seed/src/auth-seed/config_reader.ts index 37544bbee56..aa7438dd631 100644 --- a/packages/seed/src/auth-seed/config_reader.ts +++ b/packages/seed/src/auth-seed/config_reader.ts @@ -3,7 +3,7 @@ import { AmplifyUserError } from '@aws-amplify/platform-core'; export type AuthConfiguration = { userPoolId: string; - mfaMethods?: ('SMS' | 'TOTP')[]; + mfaMethods?: ('SMS' | 'TOTP' | 'EMAIL')[]; mfaConfig?: 'NONE' | 'REQUIRED' | 'OPTIONAL'; groups?: string[]; };