Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/email-mfa-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/auth-construct': minor
---

Add email MFA support using AWS Cognito EMAIL_OTP feature
52 changes: 51 additions & 1 deletion packages/auth-construct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 74 additions & 0 deletions packages/auth-construct/src/construct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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: {
Expand Down
30 changes: 29 additions & 1 deletion packages/auth-construct/src/construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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'],
Expand All @@ -819,6 +843,7 @@ export class AmplifyAuth
? {
sms: mfa.sms ? true : false,
otp: mfa.totp ? true : false,
email: mfa.email ? true : false,
}
: undefined;
};
Expand Down Expand Up @@ -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);
},
Expand Down
20 changes: 18 additions & 2 deletions packages/auth-construct/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down