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
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
9 changes: 9 additions & 0 deletions packages/auth-construct/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
56 changes: 56 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',
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is senders with email (custom email sending) required to enabled email MFA?
(not really a blocker, just curious about why senders with email is enabled in both of the tests where we want email MFA to be enabled)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property already exists and does not require enable email MFA as it can be used for other email purposes 1) email verification upon user registration and forget password

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's not necessary for email MFA, can you write a test that doesn't use it (you can just alter one of you existing tests) so when people look at these tests in the future they are not under the impression that enabling senders with email is necessary to set up in order to use email MFA?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it is required, if we did not pass it we will get the following error.

[ERROR] [BackendBuildError] Unable to deploy due to CDK Assembly Error
  ∟ Caused by: [AssemblyError] Assembly builder failed
    ∟ Caused by: [AmplifyAuthConstructInitializationError] Failed to instantiate auth construct
      ∟ Caused by: [ValidationError] To enable email-based MFA, set `email` property to the Amazon SES email-sending configuration.
    Resolution: See the underlying error message for more details.
Resolution: Check the Caused by error and fix any issues in your backend code
12:14:54 PM Stack Trace for BackendBuildError
12:14:54 PM BackendBuildError: Unable to deploy d

Apologies for the confusion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, make sure to put that information in the docs if it is not already there 👍

},
});

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
10 changes: 8 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 @@ -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'],
Expand All @@ -819,6 +820,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 +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);
},
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
14 changes: 12 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
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
Loading