diff --git a/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs b/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs index f7f34748a2b..dc5612c56b0 100644 --- a/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs +++ b/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs @@ -1,4 +1,5 @@ <% var autoVerifiedAttributes = props.autoVerifiedAttributes ? props.autoVerifiedAttributes.concat(props.aliasAttributes ? props.aliasAttributes : []).filter((attr, i, aliasAttributeArray) => ['email', 'phone_number'].includes(attr) && aliasAttributeArray.indexOf(attr) === i) : [] %> +<% var configureSMS = ((props.autoVerifiedAttributes && props.autoVerifiedAttributes.includes('phone_number')) || (props.mfaConfiguration != 'OFF' && props.mfaTypes && props.mfaTypes.includes('SMS Text Message')) || (props.requiredAttributes && props.requiredAttributes.includes('phone_number'))) %> AWSTemplateFormatVersion: 2010-09-09 Parameters: @@ -78,6 +79,7 @@ Resources: MaxAge: 3000 <% } %> <%if (props.authSelections !== 'identityPoolOnly') { %> + <% if(!props.useEnabledMfas || configureSMS) { %> # BEGIN SNS ROLE RESOURCE SNSRole: # Created to allow the UserPool SMS Config to publish via the Simple Notification Service during MFA Process @@ -107,6 +109,7 @@ Resources: Action: - "sns:Publish" Resource: "*" + <% } %> # BEGIN USER POOL RESOURCES UserPool: # Created upon user selection @@ -182,12 +185,23 @@ Resources: AliasAttributes: !Ref aliasAttributes <% } %> MfaConfiguration: !Ref mfaConfiguration + <% if(props.useEnabledMfas && props.mfaConfiguration != 'OFF') {%> + EnabledMfas: + <% if(configureSMS) {%> + - SMS_MFA + <% } %> + <% if(props.mfaTypes.includes('TOTP')) {%> + - SOFTWARE_TOKEN_MFA + <% } %> + <% } %> + <% if(!props.useEnabledMfas || configureSMS) {%> SmsVerificationMessage: !Ref smsVerificationMessage SmsAuthenticationMessage: !Ref smsAuthenticationMessage SmsConfiguration: SnsCallerArn: !GetAtt SNSRole.Arn ExternalId: <%=`${props.resourceNameTruncated}_role_external_id`%> - <%if (props.mfaConfiguration != 'OFF') { %> + <% } %> + <%if (configureSMS) { %> DependsOn: SNSRole <% } %> <%if (!props.breakCircularDependency && props.triggers && props.dependsOn) { %> @@ -818,7 +832,7 @@ Resources: DependsOn: OAuthCustomResourceLogPolicy <% } %> - <%if (props.mfaConfiguration != 'OFF') { %> + <%if (!props.useEnabledMfas && props.mfaConfiguration != 'OFF') { %> # BEGIN MFA LAMBDA RESOURCES MFALambdaRole: # Created to execute Lambda which sets MFA config values @@ -1208,7 +1222,7 @@ Outputs : AppClientSecret: Value: !GetAtt UserPoolClientInputs.appSecret Condition: ShouldOutputAppClientSecrets - <%if (props.mfaConfiguration != 'OFF') { %> + <%if (!props.useEnabledMfas || configureSMS) { %> CreatedSNSRole: Value: !GetAtt SNSRole.Arn Description: role arn diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/service-walkthrough-types.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/service-walkthrough-types.ts index 617e47b9d8d..f8ad9ebd244 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/service-walkthrough-types.ts +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/service-walkthrough-types.ts @@ -27,6 +27,7 @@ export interface ServiceQuestionsBaseResult { userpoolClientReadAttributes: string[]; userpoolClientWriteAttributes: string[]; usernameCaseSensitive?: boolean; + useEnabledMfas?: boolean; authTriggerConnections?: string; } diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/auth-defaults-appliers.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/auth-defaults-appliers.ts index 65c22de9c2b..017b85f7838 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/auth-defaults-appliers.ts +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/auth-defaults-appliers.ts @@ -29,6 +29,9 @@ export const getAddAuthDefaultsApplier = (context: any, defaultValuesFilename: s if (FeatureFlags.getBoolean('auth.enableCaseInsensitivity')) { result.usernameCaseSensitive = false; } + // If the feature flag is enabled the MFA TOTP can only be enabled + + result.useEnabledMfas = FeatureFlags.getBoolean('auth.useEnabledMfas'); /* merge actual answers object into props object, * ensuring that manual entries override defaults */ diff --git a/packages/amplify-cli-core/src/feature-flags/featureFlags.ts b/packages/amplify-cli-core/src/feature-flags/featureFlags.ts index bd8bb9d2810..ee5b7a6020d 100644 --- a/packages/amplify-cli-core/src/feature-flags/featureFlags.ts +++ b/packages/amplify-cli-core/src/feature-flags/featureFlags.ts @@ -593,6 +593,12 @@ export class FeatureFlags { defaultValueForExistingProjects: false, defaultValueForNewProjects: false, }, + { + name: 'useEnabledMfas', + type: 'boolean', + defaultValueForExistingProjects: false, + defaultValueForNewProjects: true, + }, ]); this.registerFlag('codegen', [ diff --git a/packages/amplify-e2e-core/src/utils/sdk-calls.ts b/packages/amplify-e2e-core/src/utils/sdk-calls.ts index 3b4da4e0f8f..3f4f1aa7c55 100644 --- a/packages/amplify-e2e-core/src/utils/sdk-calls.ts +++ b/packages/amplify-e2e-core/src/utils/sdk-calls.ts @@ -128,6 +128,14 @@ export const getUserPool = async (userpoolId, region) => { return res; }; +export const getMFAConfiguration = async ( + userPoolId: string, + region: string, +): Promise => { + config.update({ region }); + return await new CognitoIdentityServiceProvider().getUserPoolMfaConfig({ UserPoolId: userPoolId }).promise(); +}; + export const getLambdaFunction = async (functionName: string, region: string) => { const lambda = new Lambda({ region }); try { diff --git a/packages/amplify-e2e-tests/src/__tests__/auth_5.test.ts b/packages/amplify-e2e-tests/src/__tests__/auth_5.test.ts index d485fe24904..26a208c642f 100644 --- a/packages/amplify-e2e-tests/src/__tests__/auth_5.test.ts +++ b/packages/amplify-e2e-tests/src/__tests__/auth_5.test.ts @@ -9,10 +9,11 @@ import { headlessAuthImport, } from 'amplify-e2e-core'; import { addAuthWithDefault, getBackendAmplifyMeta } from 'amplify-e2e-core'; -import { createNewProjectDir, deleteProjectDir, getProjectMeta, getUserPool } from 'amplify-e2e-core'; +import { createNewProjectDir, deleteProjectDir, getProjectMeta, getUserPool, getMFAConfiguration } from 'amplify-e2e-core'; import { AddAuthRequest, CognitoUserPoolSigninMethod, + CognitoPasswordRecoveryConfiguration, CognitoUserProperty, ImportAuthRequest, UpdateAuthRequest, @@ -65,6 +66,105 @@ describe('headless auth', () => { const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); expect(userPool.UserPool).toBeDefined(); }); + it('adds auth resource with TOTP only', async () => { + const addAuthRequest: AddAuthRequest = { + version: 1, + resourceName: 'myAuthResource', + serviceConfiguration: { + serviceName: 'Cognito', + includeIdentityPool: false, + userPoolConfiguration: { + requiredSignupAttributes: [CognitoUserProperty.EMAIL], + signinMethod: CognitoUserPoolSigninMethod.PHONE_NUMBER, + mfa: { + mode: 'OPTIONAL', + mfaTypes: ['TOTP'], + smsMessage: 'The verification code is', + }, + }, + }, + }; + + await initJSProjectWithProfile(projRoot, defaultsSettings); + await addHeadlessAuth(projRoot, addAuthRequest); + await amplifyPushAuth(projRoot); + const meta = getProjectMeta(projRoot); + const id = Object.keys(meta.auth).map(key => meta.auth[key])[0].output.UserPoolId; + const region = meta.providers.awscloudformation.Region; + const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); + const mfaconfig = await getMFAConfiguration(id, region); + expect(mfaconfig.SoftwareTokenMfaConfiguration.Enabled).toBeTruthy(); + expect(mfaconfig.SmsMfaConfiguration).toBeUndefined(); + expect(userPool.UserPool).toBeDefined(); + }); + + it('adds auth resource with TOTP only but enable SMS through signUp Attributes', async () => { + const addAuthRequest: AddAuthRequest = { + version: 1, + resourceName: 'myAuthResource', + serviceConfiguration: { + serviceName: 'Cognito', + includeIdentityPool: false, + userPoolConfiguration: { + requiredSignupAttributes: [CognitoUserProperty.EMAIL, CognitoUserProperty.PHONE_NUMBER], + signinMethod: CognitoUserPoolSigninMethod.PHONE_NUMBER, + mfa: { + mode: 'OPTIONAL', + mfaTypes: ['TOTP'], + smsMessage: 'The verification code is {####}', + }, + }, + }, + }; + + await initJSProjectWithProfile(projRoot, defaultsSettings); + await addHeadlessAuth(projRoot, addAuthRequest); + await amplifyPushAuth(projRoot); + const meta = getProjectMeta(projRoot); + const id = Object.keys(meta.auth).map(key => meta.auth[key])[0].output.UserPoolId; + const region = meta.providers.awscloudformation.Region; + const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); + const mfaconfig = await getMFAConfiguration(id, region); + expect(mfaconfig.SoftwareTokenMfaConfiguration.Enabled).toBeTruthy(); + expect(mfaconfig.SmsMfaConfiguration.SmsConfiguration).toBeDefined(); + expect(userPool.UserPool).toBeDefined(); + }); + + it('adds auth resource with TOTP only but enables SMS through password recovery', async () => { + const addAuthRequest: AddAuthRequest = { + version: 1, + resourceName: 'myAuthResource', + serviceConfiguration: { + serviceName: 'Cognito', + includeIdentityPool: false, + userPoolConfiguration: { + requiredSignupAttributes: [CognitoUserProperty.EMAIL], + passwordRecovery: { + deliveryMethod: 'SMS', + smsMessage: 'The verification code is {####}', + }, + signinMethod: CognitoUserPoolSigninMethod.PHONE_NUMBER, + mfa: { + mode: 'OPTIONAL', + mfaTypes: ['TOTP'], + smsMessage: 'The verification code is {####}', + }, + }, + }, + }; + + await initJSProjectWithProfile(projRoot, defaultsSettings); + await addHeadlessAuth(projRoot, addAuthRequest); + await amplifyPushAuth(projRoot); + const meta = getProjectMeta(projRoot); + const id = Object.keys(meta.auth).map(key => meta.auth[key])[0].output.UserPoolId; + const region = meta.providers.awscloudformation.Region; + const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); + const mfaconfig = await getMFAConfiguration(id, region); + expect(mfaconfig.SoftwareTokenMfaConfiguration.Enabled).toBeTruthy(); + expect(mfaconfig.SmsMfaConfiguration.SmsConfiguration).toBeDefined(); + expect(userPool.UserPool).toBeDefined(); + }); it('updates existing auth resource', async () => { const updateAuthRequest: UpdateAuthRequest = {