Skip to content
9 changes: 9 additions & 0 deletions .changeset/cuddly-clowns-guess.md
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions packages/auth-construct/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type AttributeMapping = {
// @public
export type AuthProps = {
name?: string;
featurePlan?: aws_cognito.FeaturePlan;
loginWith: {
email?: EmailLogin;
phone?: PhoneNumberLogin;
Expand Down Expand Up @@ -144,13 +145,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
Expand Down
30 changes: 30 additions & 0 deletions packages/auth-construct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]',
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.
Expand Down
81 changes: 81 additions & 0 deletions packages/auth-construct/src/construct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]',
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: '[email protected]',
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: {
Expand Down Expand Up @@ -3088,4 +3144,29 @@ void describe('Auth construct', () => {
UserPoolName: Match.absent(),
});
});

void it('sets featurePlan when provided', () => {
const app = new App();
const stack = new Stack(app);
new AmplifyAuth(stack, 'test', {
loginWith: { email: true },
featurePlan: aws_cognito.FeaturePlan.ESSENTIALS,
});
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::Cognito::UserPool', {
UserPoolTier: 'ESSENTIALS',
});
});

void it('does not set featurePlan when not provided', () => {
const app = new App();
const stack = new Stack(app);
new AmplifyAuth(stack, 'test', {
loginWith: { email: true },
});
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::Cognito::UserPool', {
UserPoolTier: Match.absent(),
});
});
});
11 changes: 9 additions & 2 deletions packages/auth-construct/src/construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -614,6 +615,7 @@ export class AmplifyAuth
snsRegion: smsConfiguration?.snsRegion,
enableSmsRole: smsConfiguration?.enableSMSRole,
selfSignUpEnabled: DEFAULTS.ALLOW_SELF_SIGN_UP,
featurePlan: props?.featurePlan,
mfa: mfaMode,
mfaMessage: this.getMFAMessage(props.multifactor),
mfaSecondFactor: mfaType,
Expand Down Expand Up @@ -810,7 +812,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 +821,7 @@ export class AmplifyAuth
? {
sms: mfa.sms ? true : false,
otp: mfa.totp ? true : false,
email: mfa.email ? true : false,
}
: undefined;
};
Expand Down Expand Up @@ -1222,14 +1225,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);
},
Expand Down
1 change: 1 addition & 0 deletions packages/auth-construct/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
VerificationEmailWithLink,
MFA,
MFASmsSettings,
MFAEmailSettings,
MFATotpSettings,
MFASettings,
PhoneNumberLogin,
Expand Down
19 changes: 17 additions & 2 deletions packages/auth-construct/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -431,6 +441,11 @@ export type AuthProps = {
* Specify a name which will aid in generating resource names.
*/
name?: string;
/**
* Specify the feature plan for the user pool.
* @example cognito.FeaturePlan.PLUS
*/
featurePlan?: cognito.FeaturePlan;
/**
* Specify how you would like users to log in. You can choose from email, phone, and even external providers such as LoginWithAmazon.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/client-config/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
"mfa_methods": {
"type": "array",
"items": {
"enum": ["SMS", "TOTP"]
"enum": ["SMS", "TOTP", "EMAIL"]
}
},
"groups": {
Expand Down
2 changes: 1 addition & 1 deletion packages/seed/src/auth-seed/config_reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};
Expand Down