diff --git a/aws-auth-cognito/api/aws-auth-cognito.api b/aws-auth-cognito/api/aws-auth-cognito.api index 3b1d9e0a4d..af4b955052 100644 --- a/aws-auth-cognito/api/aws-auth-cognito.api +++ b/aws-auth-cognito/api/aws-auth-cognito.api @@ -53,7 +53,9 @@ public final class com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin : com/ public fun signOut (Lcom/amplifyframework/auth/options/AuthSignOutOptions;Lcom/amplifyframework/core/Consumer;)V public fun signOut (Lcom/amplifyframework/core/Consumer;)V public fun signUp (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/auth/options/AuthSignUpOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V + public final fun updateMFAPreference (Lcom/amplifyframework/auth/cognito/MFAPreference;Lcom/amplifyframework/auth/cognito/MFAPreference;Lcom/amplifyframework/auth/cognito/MFAPreference;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public final fun updateMFAPreference (Lcom/amplifyframework/auth/cognito/MFAPreference;Lcom/amplifyframework/auth/cognito/MFAPreference;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V + public static synthetic fun updateMFAPreference$default (Lcom/amplifyframework/auth/cognito/AWSCognitoAuthPlugin;Lcom/amplifyframework/auth/cognito/MFAPreference;Lcom/amplifyframework/auth/cognito/MFAPreference;Lcom/amplifyframework/auth/cognito/MFAPreference;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;ILjava/lang/Object;)V public fun updatePassword (Ljava/lang/String;Ljava/lang/String;Lcom/amplifyframework/core/Action;Lcom/amplifyframework/core/Consumer;)V public fun updateUserAttribute (Lcom/amplifyframework/auth/AuthUserAttribute;Lcom/amplifyframework/auth/options/AuthUpdateUserAttributeOptions;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V public fun updateUserAttribute (Lcom/amplifyframework/auth/AuthUserAttribute;Lcom/amplifyframework/core/Consumer;Lcom/amplifyframework/core/Consumer;)V diff --git a/aws-auth-cognito/build.gradle.kts b/aws-auth-cognito/build.gradle.kts index 16a744ed92..93585fe2e9 100644 --- a/aws-auth-cognito/build.gradle.kts +++ b/aws-auth-cognito/build.gradle.kts @@ -68,9 +68,11 @@ dependencies { androidTestImplementation(libs.test.androidx.runner) androidTestImplementation(libs.test.androidx.junit) androidTestImplementation(libs.test.kotlin.coroutines) + androidTestImplementation(libs.test.kotlin.kotlinTest) androidTestImplementation(libs.test.totp) androidTestImplementation(project(":aws-api")) + androidTestImplementation(project(":aws-api-appsync")) androidTestImplementation(project(":testutils")) } diff --git a/aws-auth-cognito/src/androidTest/assets/create-mfa-subscription.graphql b/aws-auth-cognito/src/androidTest/assets/create-mfa-subscription.graphql new file mode 100644 index 0000000000..e20f6f7032 --- /dev/null +++ b/aws-auth-cognito/src/androidTest/assets/create-mfa-subscription.graphql @@ -0,0 +1,7 @@ +subscription OnCreateMfaInfo { + onCreateMfaInfo { + username + code + expirationTime + } +} diff --git a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginEmailMFATests.kt b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginEmailMFATests.kt new file mode 100644 index 0000000000..0c7a98cee1 --- /dev/null +++ b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginEmailMFATests.kt @@ -0,0 +1,192 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.amplifyframework.api.aws.AWSApiPlugin +import com.amplifyframework.api.graphql.SimpleGraphQLRequest +import com.amplifyframework.auth.AuthUserAttribute +import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.MFAType +import com.amplifyframework.auth.cognito.exceptions.service.CodeMismatchException +import com.amplifyframework.auth.cognito.test.R +import com.amplifyframework.auth.cognito.testutils.AbortableCountdownLatch +import com.amplifyframework.auth.options.AuthSignUpOptions +import com.amplifyframework.auth.result.AuthSignUpResult +import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.core.configuration.AmplifyOutputs +import com.amplifyframework.core.configuration.AmplifyOutputsData +import com.amplifyframework.datastore.generated.model.MfaInfo +import com.amplifyframework.testutils.Assets +import com.amplifyframework.testutils.sync.SynchronousAuth +import java.util.Random +import java.util.UUID +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import org.junit.After +import org.junit.BeforeClass +import org.junit.Test + +class AWSCognitoAuthPluginEmailMFATests { + + private val password = "${UUID.randomUUID()}BleepBloop1234!" + private val userName = "test${Random().nextInt()}" + private val email = "$userName@testdomain.com" + + companion object { + private var authPlugin = AWSCognitoAuthPlugin() + private var apiPlugin = AWSApiPlugin() + lateinit var synchronousAuth: SynchronousAuth + var mfaCode = "" + var abortableLatch: AbortableCountdownLatch? = null + + @JvmStatic + @BeforeClass + fun initializePlugin() { + val context = ApplicationProvider.getApplicationContext() + val config = AmplifyOutputsData + .deserialize(context, AmplifyOutputs.fromResource(R.raw.amplify_outputs_email_or_totp_mfa)) + + authPlugin.configure(config, context) + apiPlugin.configure(config, context) + synchronousAuth = SynchronousAuth.delegatingTo(authPlugin) + + apiPlugin.subscribe( + SimpleGraphQLRequest( + Assets.readAsString("create-mfa-subscription.graphql"), + MfaInfo::class.java, + null + ), + { println("====== Subscription Established ======") }, + { + println("====== Received some MFA Info ======") + mfaCode = it.data.code + abortableLatch?.abort() + }, + { println("====== Subscription Failed $it ======") }, + { } + ) + } + } + + @After + fun tearDown() { + mfaCode = "" + synchronousAuth.deleteUser() + } + + @Test + fun fresh_email_mfa_setup() { + // Step 1: Sign up a new user + signUpNewUser() + + // Step 2: Attempt to sign in with the newly created user + var signInResult = synchronousAuth.signIn(userName, password) + + // Validation 1: Validate that the next step is MFA Setup Selection + assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION, signInResult.nextStep.signInStep) + + // Validation 2: Validate that the available MFA choices are Email and TOTP + assertEquals(setOf(MFAType.EMAIL, MFAType.TOTP), signInResult.nextStep.allowedMFATypes) + + // Step 3: Select "Email" as the MFA to set up + signInResult = synchronousAuth.confirmSignIn("EMAIL_OTP") + + // Validation 2: Validate that the next step is to input the user's email address + assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP, signInResult.nextStep.signInStep) + + // Step 4: Input the email address to send the code to then wait for the MFA code + abortableLatch = AbortableCountdownLatch(1) + signInResult = synchronousAuth.confirmSignIn(email) + + // Validation 3: Validate that the next step is to confirm the emailed MFA code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_EMAIL_MFA_CODE, signInResult.nextStep.signInStep) + + // Wait until the MFA code has been received + abortableLatch?.await(20, TimeUnit.SECONDS) + + // Step 5: Input the emailed MFA code for confirmation + signInResult = synchronousAuth.confirmSignIn(mfaCode) + + // Validation 4: Validate that MFA setup is done + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun sign_in_to_existing_email_mfa() { + // Step 1: Sign up a new user with an existing email address + signUpNewUser(email) + + // Step 2: Attempt to sign in with the newly created user + abortableLatch = AbortableCountdownLatch(1) + var signInResult = synchronousAuth.signIn(userName, password) + + // Validation 1: Validate that the next step is to confirm the emailed MFA code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_EMAIL_MFA_CODE, signInResult.nextStep.signInStep) + + // Wait until the MFA code has been received + abortableLatch?.await(20, TimeUnit.SECONDS) + + // Step 4: Input the emailed MFA code for confirmation + signInResult = synchronousAuth.confirmSignIn(mfaCode) + + // Validation 2: Validate that MFA setup is done + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + @Test + fun use_an_incorrect_MFA_code_then_sign_in_using_the_correct_one() { + // Step 1: Sign up a new user with an existing email address + signUpNewUser(email) + + // Step 2: Attempt to sign in with the newly created user + abortableLatch = AbortableCountdownLatch(1) + var signInResult = synchronousAuth.signIn(userName, password) + + // Validation 1: Validate that the next step is to confirm the emailed MFA code + assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_EMAIL_MFA_CODE, signInResult.nextStep.signInStep) + + // Wait until the MFA code has been received + abortableLatch?.await(20, TimeUnit.SECONDS) + + // Step 4: Input the an incorrect MFA code + // Validation 2: Validate that an incorrect MFA code throws a CodeMismatchException + assertFailsWith { + signInResult = synchronousAuth.confirmSignIn(mfaCode.reversed()) + } + + // Step 5: Input the correct MFA code for validation + signInResult = synchronousAuth.confirmSignIn(mfaCode) + + // Validation 3: Validate that MFA setup is done + assertEquals(AuthSignInStep.DONE, signInResult.nextStep.signInStep) + } + + private fun signUpNewUser(email: String? = null): AuthSignUpResult { + val attributes = if (email == null) { + emptyList() + } else { + listOf(AuthUserAttribute(AuthUserAttributeKey.email(), email)) + } + val options = AuthSignUpOptions.builder() + .userAttributes( + attributes + ).build() + return synchronousAuth.signUp(userName, password, options) + } +} diff --git a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTOTPTests.kt b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTOTPTests.kt index f1d4ac409e..c439e34e0e 100644 --- a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTOTPTests.kt +++ b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTOTPTests.kt @@ -143,7 +143,7 @@ class AWSCognitoAuthPluginTOTPTests { ) synchronousAuth.confirmSignIn(otp) synchronousAuth.updateUserAttribute(AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+19876543210")) - updateMFAPreference(MFAPreference.ENABLED, MFAPreference.ENABLED) + updateMFAPreference(MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.ENABLED) synchronousAuth.signOut() val signInResult = synchronousAuth.signIn(userName, password) Assert.assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION, signInResult.nextStep.signInStep) @@ -168,9 +168,9 @@ class AWSCognitoAuthPluginTOTPTests { synchronousAuth.signUp(userName, password, options) } - private fun updateMFAPreference(sms: MFAPreference, totp: MFAPreference) { + private fun updateMFAPreference(sms: MFAPreference, totp: MFAPreference, email: MFAPreference) { val latch = CountDownLatch(1) - authPlugin.updateMFAPreference(sms, totp, { latch.countDown() }, { latch.countDown() }) + authPlugin.updateMFAPreference(sms, totp, email, { latch.countDown() }, { latch.countDown() }) latch.await(5, TimeUnit.SECONDS) } } diff --git a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/testutils/AbortableCountdownLatch.kt b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/testutils/AbortableCountdownLatch.kt new file mode 100644 index 0000000000..ca7dd6a422 --- /dev/null +++ b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/testutils/AbortableCountdownLatch.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.auth.cognito.testutils + +import java.util.concurrent.CountDownLatch + +class AbortableCountdownLatch(count: Int) : CountDownLatch(count) { + + fun abort() { + if (count == 0L) { + return + } + + while (count > 0) { + countDown() + } + } +} diff --git a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/datastore/generated/model/MfaInfo.java b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/datastore/generated/model/MfaInfo.java new file mode 100644 index 0000000000..de56bc6e18 --- /dev/null +++ b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/datastore/generated/model/MfaInfo.java @@ -0,0 +1,254 @@ +package com.amplifyframework.datastore.generated.model; + +import com.amplifyframework.core.model.temporal.Temporal; +import com.amplifyframework.core.model.ModelIdentifier; + +import java.util.List; +import java.util.UUID; +import java.util.Objects; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +/** This is an auto generated class representing the MfaInfo type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "MfaInfos", type = Model.Type.USER, version = 1) +public final class MfaInfo implements Model { + public static final QueryField ID = field("MfaInfo", "id"); + public static final QueryField USERNAME = field("MfaInfo", "username"); + public static final QueryField CODE = field("MfaInfo", "code"); + public static final QueryField EXPIRATION_TIME = field("MfaInfo", "expirationTime"); + private final @ModelField(targetType="ID", isRequired = true) String id; + private final @ModelField(targetType="String", isRequired = true) String username; + private final @ModelField(targetType="String", isRequired = true) String code; + private final @ModelField(targetType="AWSTimestamp", isRequired = true) Temporal.Timestamp expirationTime; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public String resolveIdentifier() { + return id; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getCode() { + return code; + } + + public Temporal.Timestamp getExpirationTime() { + return expirationTime; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private MfaInfo(String id, String username, String code, Temporal.Timestamp expirationTime) { + this.id = id; + this.username = username; + this.code = code; + this.expirationTime = expirationTime; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + MfaInfo mfaInfo = (MfaInfo) obj; + return ObjectsCompat.equals(getId(), mfaInfo.getId()) && + ObjectsCompat.equals(getUsername(), mfaInfo.getUsername()) && + ObjectsCompat.equals(getCode(), mfaInfo.getCode()) && + ObjectsCompat.equals(getExpirationTime(), mfaInfo.getExpirationTime()) && + ObjectsCompat.equals(getCreatedAt(), mfaInfo.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), mfaInfo.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getId()) + .append(getUsername()) + .append(getCode()) + .append(getExpirationTime()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("MfaInfo {") + .append("id=" + String.valueOf(getId()) + ", ") + .append("username=" + String.valueOf(getUsername()) + ", ") + .append("code=" + String.valueOf(getCode()) + ", ") + .append("expirationTime=" + String.valueOf(getExpirationTime()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static UsernameStep builder() { + return new Builder(); + } + + /** + * WARNING: This method should not be used to build an instance of this object for a CREATE mutation. + * This is a convenience method to return an instance of the object with only its ID populated + * to be used in the context of a parameter in a delete mutation or referencing a foreign key + * in a relationship. + * @param id the id of the existing item this instance will represent + * @return an instance of this model with only ID populated + */ + public static MfaInfo justId(String id) { + return new MfaInfo( + id, + null, + null, + null + ); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(id, + username, + code, + expirationTime); + } + public interface UsernameStep { + CodeStep username(String username); + } + + + public interface CodeStep { + ExpirationTimeStep code(String code); + } + + + public interface ExpirationTimeStep { + BuildStep expirationTime(Temporal.Timestamp expirationTime); + } + + + public interface BuildStep { + MfaInfo build(); + BuildStep id(String id); + } + + + public static class Builder implements UsernameStep, CodeStep, ExpirationTimeStep, BuildStep { + private String id; + private String username; + private String code; + private Temporal.Timestamp expirationTime; + public Builder() { + + } + + private Builder(String id, String username, String code, Temporal.Timestamp expirationTime) { + this.id = id; + this.username = username; + this.code = code; + this.expirationTime = expirationTime; + } + + @Override + public MfaInfo build() { + String id = this.id != null ? this.id : UUID.randomUUID().toString(); + + return new MfaInfo( + id, + username, + code, + expirationTime); + } + + @Override + public CodeStep username(String username) { + Objects.requireNonNull(username); + this.username = username; + return this; + } + + @Override + public ExpirationTimeStep code(String code) { + Objects.requireNonNull(code); + this.code = code; + return this; + } + + @Override + public BuildStep expirationTime(Temporal.Timestamp expirationTime) { + Objects.requireNonNull(expirationTime); + this.expirationTime = expirationTime; + return this; + } + + /** + * @param id id + * @return Current Builder instance, for fluent method chaining + */ + public BuildStep id(String id) { + this.id = id; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String id, String username, String code, Temporal.Timestamp expirationTime) { + super(id, username, code, expirationTime); + Objects.requireNonNull(username); + Objects.requireNonNull(code); + Objects.requireNonNull(expirationTime); + } + + @Override + public CopyOfBuilder username(String username) { + return (CopyOfBuilder) super.username(username); + } + + @Override + public CopyOfBuilder code(String code) { + return (CopyOfBuilder) super.code(code); + } + + @Override + public CopyOfBuilder expirationTime(Temporal.Timestamp expirationTime) { + return (CopyOfBuilder) super.expirationTime(expirationTime); + } + } + + + public static class MfaInfoIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public MfaInfoIdentifier(String id) { + super(id); + } + } + +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt index 5e9567e082..4771fa5403 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt @@ -474,12 +474,21 @@ class AWSCognitoAuthPlugin : AuthPlugin() { fun fetchMFAPreference(onSuccess: Consumer, onError: Consumer) = enqueue(onSuccess, onError) { queueFacade.fetchMFAPreference() } + @Deprecated("Use updateMFAPreference(sms, totp, email, onSuccess, onError) instead") fun updateMFAPreference( sms: MFAPreference?, totp: MFAPreference?, onSuccess: Action, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.updateMFAPreference(sms, totp) } + ) = enqueue(onSuccess, onError) { queueFacade.updateMFAPreference(sms, totp, null) } + + fun updateMFAPreference( + sms: MFAPreference? = null, + totp: MFAPreference? = null, + email: MFAPreference? = null, + onSuccess: Action, + onError: Consumer + ) = enqueue(onSuccess, onError) { queueFacade.updateMFAPreference(sms, totp, email) } private fun enqueue(onSuccess: Action, onError: Consumer, block: suspend () -> Unit) = enqueue({ onSuccess.call() }, onError::accept, block) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt index f64b110fcb..ae448ac3aa 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt @@ -553,12 +553,14 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth suspend fun updateMFAPreference( sms: MFAPreference?, - totp: MFAPreference? + totp: MFAPreference?, + email: MFAPreference? ) { return suspendCoroutine { continuation -> delegate.updateMFAPreference( sms, totp, + email, { continuation.resume(Unit) }, { continuation.resumeWithException(it) } ) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/MFATypeUtil.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/MFATypeUtil.kt index ca4bc57435..dccc880d3e 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/MFATypeUtil.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/MFATypeUtil.kt @@ -27,4 +27,5 @@ val MFAType.challengeResponse: String get() = when (this) { MFAType.SMS -> "SMS_MFA" MFAType.TOTP -> "SOFTWARE_TOKEN_MFA" + MFAType.EMAIL -> "EMAIL_OTP" } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt index 497f9e54e2..082eef794e 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt @@ -27,6 +27,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.AttributeType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChangePasswordRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceRememberedStatusType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.EmailMfaSettingsType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ForgetDeviceRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserAttributeVerificationCodeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserRequest @@ -71,9 +72,12 @@ import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.HostedUIHelper import com.amplifyframework.auth.cognito.helpers.SessionHelper import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper +import com.amplifyframework.auth.cognito.helpers.getAllowedMFATypesFromChallengeParameters +import com.amplifyframework.auth.cognito.helpers.getMFASetupTypeOrNull import com.amplifyframework.auth.cognito.helpers.getMFAType import com.amplifyframework.auth.cognito.helpers.getMFATypeOrNull import com.amplifyframework.auth.cognito.helpers.identityProviderName +import com.amplifyframework.auth.cognito.helpers.isMfaSetupSelectionChallenge import com.amplifyframework.auth.cognito.helpers.value import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmResetPasswordOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions @@ -588,13 +592,13 @@ internal class RealAWSCognitoAuthPlugin( ChallengeNameType.MfaSetup.value, null, null, - null + totpSetupState.challengeParams ), onSuccess, onError, totpSetupState.signInTOTPSetupData ) - totpSetupState?.hasNewResponse = false + totpSetupState.hasNewResponse = false } } } @@ -706,20 +710,33 @@ internal class RealAWSCognitoAuthPlugin( ) ) } + signInState is SignInState.ResolvingChallenge && signInState.challengeState is SignInChallengeState.WaitingForAnswer && (signInState.challengeState as SignInChallengeState.WaitingForAnswer).hasNewResponse -> { authStateMachine.cancel(token) val signInChallengeState = signInState.challengeState as SignInChallengeState.WaitingForAnswer var allowedMFATypes: Set? = null + + if (signInChallengeState.challenge.challengeName == ChallengeNameType.MfaSetup.value || + signInChallengeState.challenge.challengeName == ChallengeNameType.EmailOtp.value + ) { + SignInChallengeHelper.getNextStep( + signInChallengeState.challenge, + onSuccess, + onError + ) + (signInState.challengeState as SignInChallengeState.WaitingForAnswer).hasNewResponse = false + return@listen + } + val signInStep = when (signInChallengeState.challenge.challengeName) { "SMS_MFA" -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE "NEW_PASSWORD_REQUIRED" -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD "SOFTWARE_TOKEN_MFA" -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE "SELECT_MFA_TYPE" -> { - signInChallengeState.challenge.parameters?.get("MFAS_CAN_CHOOSE")?.let { - allowedMFATypes = SignInChallengeHelper.getAllowedMFATypes(it) - } + allowedMFATypes = + getAllowedMFATypesFromChallengeParameters(signInChallengeState.challenge.parameters) AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION } else -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE @@ -747,7 +764,7 @@ internal class RealAWSCognitoAuthPlugin( ChallengeNameType.MfaSetup.value, null, null, - null + totpSetupState.challengeParams ), onSuccess, onError, @@ -797,9 +814,19 @@ internal class RealAWSCognitoAuthPlugin( getMFATypeOrNull(challengeResponse) == null ) { val error = InvalidParameterException( - message = "Value for challengeResponse must be one of SMS_MFA or SOFTWARE_TOKEN_MFA" + message = "Value for challengeResponse must be one of " + + "SMS_MFA, EMAIL_OTP or SOFTWARE_TOKEN_MFA" + ) + onError.accept(error) + } else if (challengeState is SignInChallengeState.WaitingForAnswer && + isMfaSetupSelectionChallenge(challengeState.challenge) && + getMFASetupTypeOrNull(challengeResponse) == null + ) { + val error = InvalidParameterException( + message = "Value for challengeResponse must be one of EMAIL_OTP or SOFTWARE_TOKEN_MFA" ) onError.accept(error) + authStateMachine.cancel(token) } else { val event = SignInChallengeEvent( SignInChallengeEvent.EventType.VerifyChallengeAnswer( @@ -2278,15 +2305,17 @@ internal class RealAWSCognitoAuthPlugin( fun updateMFAPreference( sms: MFAPreference?, totp: MFAPreference?, + email: MFAPreference?, onSuccess: Action, onError: Consumer ) { - if (sms == null && totp == null) { + if (sms == null && totp == null && email == null) { onError.accept(InvalidParameterException("No mfa settings given")) return } // If either of the params have preferred setting set then ignore fetched preference preferred property - val overridePreferredSetting: Boolean = !(sms?.mfaPreferred == true || totp?.mfaPreferred == true) + val overridePreferredSetting = + !(sms?.mfaPreferred == true || totp?.mfaPreferred == true || email?.mfaPreferred == true) fetchMFAPreference({ userPreference -> authStateMachine.getCurrentState { authState -> when (authState.authNState) { @@ -2300,7 +2329,7 @@ internal class RealAWSCognitoAuthPlugin( .cognitoIdentityProviderClient ?.setUserMfaPreference { this.accessToken = token - this.smsMfaSettings = sms?.let { it -> + this.smsMfaSettings = sms?.let { val preferredMFASetting = it.mfaPreferred ?: ( overridePreferredSetting && @@ -2312,7 +2341,7 @@ internal class RealAWSCognitoAuthPlugin( preferredMfa = preferredMFASetting } } - this.softwareTokenMfaSettings = totp?.let { it -> + this.softwareTokenMfaSettings = totp?.let { val preferredMFASetting = it.mfaPreferred ?: ( overridePreferredSetting && @@ -2324,6 +2353,18 @@ internal class RealAWSCognitoAuthPlugin( preferredMfa = preferredMFASetting } } + this.emailMfaSettings = email?.let { + val preferredMFASetting = it.mfaPreferred + ?: ( + overridePreferredSetting && + userPreference.preferred == MFAType.EMAIL && + it.mfaEnabled + ) + EmailMfaSettingsType.invoke { + enabled = it.mfaEnabled + preferredMfa = preferredMFASetting + } + } }?.also { onSuccess.call() } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActions.kt index 7c00b0892d..8ac25e8cbc 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActions.kt @@ -41,7 +41,12 @@ internal object SetupTOTPCognitoActions : SetupTOTPActions { response?.secretCode?.let { secret -> SetupTOTPEvent( SetupTOTPEvent.EventType.WaitForAnswer( - SignInTOTPSetupData(secret, response.session, eventType.totpSetupDetails.username) + totpSetupDetails = SignInTOTPSetupData( + secretCode = secret, + session = response.session, + username = eventType.totpSetupDetails.username + ), + challengeParams = eventType.challengeParams ) ) } ?: SetupTOTPEvent( diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActions.kt index 6c8436476c..50738d5149 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActions.kt @@ -22,6 +22,8 @@ import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.cognito.AuthEnvironment import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper +import com.amplifyframework.auth.cognito.helpers.isEmailMfaSetupChallenge +import com.amplifyframework.auth.cognito.helpers.isMfaSetupSelectionChallenge import com.amplifyframework.auth.exceptions.UnknownException import com.amplifyframework.statemachine.Action import com.amplifyframework.statemachine.codegen.actions.SignInChallengeActions @@ -43,13 +45,29 @@ internal object SignInChallengeCognitoActions : SignInChallengeActions { logger.verbose("$id Starting execution") val evt = try { val username = challenge.username + + // If we are selecting an MFA Setup Type, Cognito doesn't want a response. + // We handle the next step locally + if (isMfaSetupSelectionChallenge(challenge)) { + val event = SignInChallengeHelper.evaluateNextStep( + username = username ?: "", + challengeNameType = ChallengeNameType.MfaSetup, + session = challenge.session, + challengeParameters = mapOf("MFAS_CAN_SETUP" to answer), + authenticationResult = null + ) + logger.verbose("$id Sending event ${event.type}") + dispatcher.send(event) + return@Action + } + val challengeResponses = mutableMapOf() if (!username.isNullOrEmpty()) { challengeResponses[KEY_USERNAME] = username } - getChallengeResponseKey(challenge.challengeName)?.also { responseKey -> + getChallengeResponseKey(challenge)?.also { responseKey -> challengeResponses[responseKey] = answer } @@ -111,12 +129,24 @@ internal object SignInChallengeCognitoActions : SignInChallengeActions { dispatcher.send(evt) } - private fun getChallengeResponseKey(challengeName: String): String? { + private fun getChallengeResponseKey(challenge: AuthChallenge): String? { + val challengeName = challenge.challengeName return when (ChallengeNameType.fromValue(challengeName)) { is ChallengeNameType.SmsMfa -> "SMS_MFA_CODE" is ChallengeNameType.NewPasswordRequired -> "NEW_PASSWORD" is ChallengeNameType.CustomChallenge, ChallengeNameType.SelectMfaType -> "ANSWER" is ChallengeNameType.SoftwareTokenMfa -> "SOFTWARE_TOKEN_MFA_CODE" + is ChallengeNameType.EmailOtp -> "EMAIL_OTP_CODE" + // TOTP is not part of this because, it follows a completely different setup path + is ChallengeNameType.MfaSetup -> { + if (isMfaSetupSelectionChallenge(challenge)) { + "MFA_SETUP" + } else if (isEmailMfaSetupChallenge(challenge)) { + "EMAIL" + } else { + null + } + } else -> null } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInCognitoActions.kt index 2dfe25a605..2a89b5c1f4 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SignInCognitoActions.kt @@ -142,7 +142,12 @@ internal object SignInCognitoActions : SignInActions { override fun initiateTOTPSetupAction(event: SignInEvent.EventType.InitiateTOTPSetup) = Action("initiateTOTPSetup") { id, dispatcher -> logger.verbose("$id Starting execution") - val evt = SetupTOTPEvent(SetupTOTPEvent.EventType.SetupTOTP(event.signInTOTPSetupData)) + val evt = SetupTOTPEvent( + SetupTOTPEvent.EventType.SetupTOTP( + totpSetupDetails = event.signInTOTPSetupData, + challengeParams = event.challengeParams + ) + ) logger.verbose("$id Sending event ${evt.type}") dispatcher.send(evt) } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt index c3bf3b8a60..f9146149b1 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt @@ -16,17 +16,27 @@ package com.amplifyframework.auth.cognito.helpers import com.amplifyframework.auth.MFAType +import com.amplifyframework.auth.exceptions.UnknownException +import com.amplifyframework.statemachine.codegen.data.AuthChallenge @Throws(IllegalArgumentException::class) internal fun getMFAType(value: String) = when (value) { "SMS_MFA" -> MFAType.SMS "SOFTWARE_TOKEN_MFA" -> MFAType.TOTP + "EMAIL_OTP" -> MFAType.EMAIL else -> throw IllegalArgumentException("Unsupported MFA type") } internal fun getMFATypeOrNull(value: String) = when (value) { "SMS_MFA" -> MFAType.SMS "SOFTWARE_TOKEN_MFA" -> MFAType.TOTP + "EMAIL_OTP" -> MFAType.EMAIL + else -> null +} + +internal fun getMFASetupTypeOrNull(value: String) = when (value) { + "SOFTWARE_TOKEN_MFA" -> MFAType.TOTP + "EMAIL_OTP" -> MFAType.EMAIL else -> null } @@ -34,4 +44,41 @@ internal val MFAType.value: String get() = when (this) { MFAType.SMS -> "SMS_MFA" MFAType.TOTP -> "SOFTWARE_TOKEN_MFA" + MFAType.EMAIL -> "EMAIL_OTP" } + +internal fun isMfaSetupSelectionChallenge(challenge: AuthChallenge) = + challenge.challengeName == "MFA_SETUP" && + getAllowedMFASetupTypesFromChallengeParameters(challenge.parameters).size > 1 + +internal fun isEmailMfaSetupChallenge(challenge: AuthChallenge) = + challenge.challengeName == "MFA_SETUP" && + getAllowedMFASetupTypesFromChallengeParameters(challenge.parameters) == setOf(MFAType.EMAIL) + +internal fun getAllowedMFATypesFromChallengeParameters(challengeParameters: Map?): Set { + val mfasCanChoose = challengeParameters?.get("MFAS_CAN_CHOOSE") ?: return emptySet() + val result = mutableSetOf() + mfasCanChoose.replace(Regex("\\[|\\]|\""), "").split(",").forEach { + when (it) { + "SMS_MFA" -> result.add(MFAType.SMS) + "SOFTWARE_TOKEN_MFA" -> result.add(MFAType.TOTP) + "EMAIL_MFA" -> result.add(MFAType.EMAIL) + else -> throw UnknownException(cause = Exception("MFA type not supported.")) + } + } + return result +} + +// We exclude SMS as a setup type +internal fun getAllowedMFASetupTypesFromChallengeParameters(challengeParameters: Map?): Set { + val mfasCanSetup = challengeParameters?.get("MFAS_CAN_SETUP") ?: return emptySet() + + val result = mutableSetOf() + mfasCanSetup.replace(Regex("\\[|\\]|\""), "").split(",").forEach { + when (it) { + "SOFTWARE_TOKEN_MFA" -> result.add(MFAType.TOTP) + "EMAIL_OTP" -> result.add(MFAType.EMAIL) + } + } + return result +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelper.kt index 61e3b47345..ff67d97739 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelper.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelper.kt @@ -19,6 +19,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthenticationResul import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.smithy.kotlin.runtime.time.Instant import com.amplifyframework.auth.AuthCodeDeliveryDetails +import com.amplifyframework.auth.AuthCodeDeliveryDetails.DeliveryMedium import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.TOTPSetupDetails @@ -83,17 +84,21 @@ internal object SignInChallengeHelper { challengeNameType is ChallengeNameType.CustomChallenge || challengeNameType is ChallengeNameType.NewPasswordRequired || challengeNameType is ChallengeNameType.SoftwareTokenMfa || + challengeNameType is ChallengeNameType.EmailOtp || challengeNameType is ChallengeNameType.SelectMfaType -> { val challenge = AuthChallenge(challengeNameType.value, username, session, challengeParameters) SignInEvent(SignInEvent.EventType.ReceivedChallenge(challenge)) } challengeNameType is ChallengeNameType.MfaSetup -> { - val allowedMFASetupTypes = challengeParameters?.get("MFAS_CAN_SETUP") - ?.let { getAllowedMFATypes(it) } ?: emptySet() - if (allowedMFASetupTypes.contains(MFAType.TOTP)) { + val allowedMFASetupTypes = getAllowedMFASetupTypesFromChallengeParameters(challengeParameters) + val challenge = AuthChallenge(challengeNameType.value, username, session, challengeParameters) + + if (allowedMFASetupTypes.contains(MFAType.EMAIL)) { + SignInEvent(SignInEvent.EventType.ReceivedChallenge(challenge)) + } else if (allowedMFASetupTypes.contains(MFAType.TOTP)) { val setupTOTPData = SignInTOTPSetupData("", session, username) - SignInEvent(SignInEvent.EventType.InitiateTOTPSetup(setupTOTPData)) + SignInEvent(SignInEvent.EventType.InitiateTOTPSetup(setupTOTPData, challenge.parameters)) } else { SignInEvent( SignInEvent.EventType.ThrowError( @@ -122,7 +127,7 @@ internal object SignInChallengeHelper { is ChallengeNameType.SmsMfa -> { val deliveryDetails = AuthCodeDeliveryDetails( challengeParams.getValue("CODE_DELIVERY_DESTINATION"), - AuthCodeDeliveryDetails.DeliveryMedium.fromString( + DeliveryMedium.fromString( challengeParams.getValue("CODE_DELIVERY_DELIVERY_MEDIUM") ) ) @@ -169,7 +174,7 @@ internal object SignInChallengeHelper { false, AuthNextSignInStep( AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE, - mapOf(), + emptyMap(), null, null, null @@ -178,19 +183,47 @@ internal object SignInChallengeHelper { onSuccess.accept(authSignInResult) } is ChallengeNameType.MfaSetup -> { - signInTOTPSetupData?.let { + val allowedMFASetupTypes = getAllowedMFASetupTypesFromChallengeParameters(challengeParams) + + if (allowedMFASetupTypes.contains(MFAType.TOTP) && allowedMFASetupTypes.contains(MFAType.EMAIL)) { + val authSignInResult = AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION, + emptyMap(), + null, + null, + allowedMFASetupTypes + ) + ) + onSuccess.accept(authSignInResult) + } else if (allowedMFASetupTypes.contains(MFAType.TOTP) && signInTOTPSetupData != null) { val authSignInResult = AuthSignInResult( false, AuthNextSignInStep( AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, challengeParams, null, - TOTPSetupDetails(it.secretCode, it.username), + TOTPSetupDetails(signInTOTPSetupData.secretCode, signInTOTPSetupData.username), + allowedMFAType + ) + ) + onSuccess.accept(authSignInResult) + } else if (allowedMFASetupTypes.contains(MFAType.EMAIL)) { + val authSignInResult = AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP, + emptyMap(), + null, + null, allowedMFAType ) ) onSuccess.accept(authSignInResult) - } ?: onError.accept(UnknownException(cause = Exception("Challenge type not supported."))) + } else { + onError.accept(UnknownException(cause = Exception("Challenge type not supported."))) + } } is ChallengeNameType.SelectMfaType -> { val authSignInResult = AuthSignInResult( @@ -200,24 +233,35 @@ internal object SignInChallengeHelper { mapOf(), null, null, - challengeParams["MFAS_CAN_CHOOSE"]?.let { getAllowedMFATypes(it) } + getAllowedMFATypesFromChallengeParameters(challengeParams) ) ) onSuccess.accept(authSignInResult) } - else -> onError.accept(UnknownException(cause = Exception("Challenge type not supported."))) - } - } + is ChallengeNameType.EmailOtp -> { + val codeDeliveryMedium = DeliveryMedium.fromString( + challengeParams["CODE_DELIVERY_DELIVERY_MEDIUM"] ?: DeliveryMedium.UNKNOWN.value + ) + val codeDeliveryDestination = challengeParams["CODE_DELIVERY_DESTINATION"] + val deliveryDetails = if (codeDeliveryDestination != null) { + AuthCodeDeliveryDetails(codeDeliveryDestination, codeDeliveryMedium) + } else { + null + } - fun getAllowedMFATypes(allowedMFAType: String): Set { - val result = mutableSetOf() - allowedMFAType.replace(Regex("\\[|\\]|\""), "").split(",").forEach { - when (it) { - "SMS_MFA" -> result.add(MFAType.SMS) - "SOFTWARE_TOKEN_MFA" -> result.add(MFAType.TOTP) - else -> throw UnknownException(cause = Exception("MFA type not supported.")) + val authSignInResult = AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONFIRM_SIGN_IN_WITH_EMAIL_MFA_CODE, + mapOf(), + deliveryDetails, + null, + null + ) + ) + onSuccess.accept(authSignInResult) } + else -> onError.accept(UnknownException(cause = Exception("Challenge type not supported."))) } - return result } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SetupTOTPEvent.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SetupTOTPEvent.kt index 9bf7adefae..f0abd5017e 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SetupTOTPEvent.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SetupTOTPEvent.kt @@ -22,8 +22,14 @@ internal class SetupTOTPEvent(val eventType: EventType, override val time: Date? StateMachineEvent { sealed class EventType { - data class SetupTOTP(val totpSetupDetails: SignInTOTPSetupData) : EventType() - data class WaitForAnswer(val totpSetupDetails: SignInTOTPSetupData) : EventType() + data class SetupTOTP( + val totpSetupDetails: SignInTOTPSetupData, + val challengeParams: Map? + ) : EventType() + data class WaitForAnswer( + val totpSetupDetails: SignInTOTPSetupData, + val challengeParams: Map? + ) : EventType() data class ThrowAuthError(val exception: Exception, val username: String, val session: String?) : EventType() data class VerifyChallengeAnswer( val answer: String, diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignInEvent.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignInEvent.kt index 7f0c367af3..14044d1da6 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignInEvent.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SignInEvent.kt @@ -62,7 +62,10 @@ internal class SignInEvent(val eventType: EventType, override val time: Date? = data class FinalizeSignIn(val id: String = "") : EventType() data class ReceivedChallenge(val challenge: AuthChallenge) : EventType() data class ThrowError(val exception: Exception) : EventType() - data class InitiateTOTPSetup(val signInTOTPSetupData: SignInTOTPSetupData) : EventType() + data class InitiateTOTPSetup( + val signInTOTPSetupData: SignInTOTPSetupData, + val challengeParams: Map? + ) : EventType() } override val type: String = eventType.javaClass.simpleName diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt index 2d9b03bfef..e3ce55329c 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt @@ -27,7 +27,8 @@ internal sealed class SetupTOTPState : State { data class SetupTOTP(val id: String = "") : SetupTOTPState() data class WaitingForAnswer( val signInTOTPSetupData: SignInTOTPSetupData, - var hasNewResponse: Boolean = false + var hasNewResponse: Boolean = false, + val challengeParams: Map? ) : SetupTOTPState() data class Verifying(val id: String = "") : SetupTOTPState() data class RespondingToAuthChallenge(val id: String = "") : SetupTOTPState() @@ -63,7 +64,13 @@ internal sealed class SetupTOTPState : State { is SetupTOTP -> when (challengeEvent) { is SetupTOTPEvent.EventType.WaitForAnswer -> { - StateResolution(WaitingForAnswer(challengeEvent.totpSetupDetails, true)) + StateResolution( + WaitingForAnswer( + signInTOTPSetupData = challengeEvent.totpSetupDetails, + hasNewResponse = true, + challengeParams = challengeEvent.challengeParams + ) + ) } is SetupTOTPEvent.EventType.ThrowAuthError -> StateResolution( @@ -130,7 +137,13 @@ internal sealed class SetupTOTPState : State { } is SetupTOTPEvent.EventType.WaitForAnswer -> { - StateResolution(WaitingForAnswer(challengeEvent.totpSetupDetails, true)) + StateResolution( + WaitingForAnswer( + signInTOTPSetupData = challengeEvent.totpSetupDetails, + hasNewResponse = true, + challengeParams = challengeEvent.challengeParams + ) + ) } else -> defaultResolution diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt index 8afb3f53c5..3dd650b065 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt @@ -722,11 +722,12 @@ class AWSCognitoAuthPluginTest { fun updateMFAPreferences() { val smsPreference = MFAPreference.ENABLED val totpPreference = MFAPreference.PREFERRED + val emailPreference = MFAPreference.NOT_PREFERRED val onSuccess = Action { } val onError = Consumer { } - authPlugin.updateMFAPreference(smsPreference, totpPreference, onSuccess, onError) + authPlugin.updateMFAPreference(smsPreference, totpPreference, emailPreference, onSuccess, onError) verify(timeout = CHANNEL_TIMEOUT) { - realPlugin.updateMFAPreference(smsPreference, totpPreference, any(), any()) + realPlugin.updateMFAPreference(smsPreference, totpPreference, emailPreference, any(), any()) } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt index 4c8ae9736d..ad1466f85b 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt @@ -30,6 +30,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmSignUpReques import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmSignUpResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeliveryMediumType import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.EmailMfaSettingsType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ForgetDeviceResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserAttributeVerificationCodeResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserResponse @@ -1576,7 +1577,7 @@ class RealAWSCognitoAuthPluginTest { username = "" } } - plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.PREFERRED, onSuccess, mockk()) + plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.PREFERRED, null, onSuccess, mockk()) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1617,7 +1618,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.ENABLED, null, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1658,7 +1661,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.ENABLED, null, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1679,10 +1684,60 @@ class RealAWSCognitoAuthPluginTest { } @Test - fun `updateMFAPreferences when both provided sms and totp preference are null and cognito throws an exception`() { + fun `updateMFAPreferences when current preference is email with additional sms and totp preferences are enabled`() { + val onSuccess = ActionWithLatch() + val setUserMFAPreferenceRequest = slot() + + coEvery { + mockCognitoIPClient.getUser { + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + }.answers { + GetUserResponse.invoke { + userMfaSettingList = listOf("SMS_MFA", "SOFTWARE_TOKEN_MFA", "EMAIL_OTP") + preferredMfaSetting = "EMAIL_OTP" + userAttributes = listOf() + username = "" + } + } + + coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { + SetUserMfaPreferenceResponse.invoke {} + } + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + ) + + assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } + assertTrue(setUserMFAPreferenceRequest.isCaptured) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = true + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) + assertEquals( + SmsMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.smsMfaSettings + ) + assertEquals( + SoftwareTokenMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings + ) + } + + @Test + fun `updateMFAPreferences when provided email sms and totp preference are null and cognito throws an exception`() { val onError = ConsumerWithLatch() coEvery { mockCognitoIPClient.setUserMfaPreference(any()) } throws Exception() - plugin.updateMFAPreference(null, null, mockk(), onError) + plugin.updateMFAPreference(null, null, null, mockk(), onError) onError.shouldBeCalled() } @@ -1696,13 +1751,13 @@ class RealAWSCognitoAuthPluginTest { accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken } } throws Exception() - plugin.updateMFAPreference(null, null, mockk(), onError) + plugin.updateMFAPreference(null, null, null, mockk(), onError) onError.shouldBeCalled() } @Test - fun `updatepref when currentpref is null and TOTP is enabled and SMS is enabled`() { + fun `updatepref when currentpref is null and TOTP, SMS, and email are enabled`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -1722,7 +1777,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1740,10 +1797,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is null and TOTP is enabled and SMS is disabled`() { + fun `updatepref when currentpref is null and TOTP is enabled and SMS and email are disabled`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -1763,7 +1827,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.DISABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.DISABLED, MFAPreference.DISABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1781,10 +1847,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is null and TOTP is disabled and SMS is enabled`() { + fun `updatepref when currentpref is null and TOTP and email are disabled and SMS is enabled`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -1804,7 +1877,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.DISABLED, MFAPreference.ENABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.DISABLED, MFAPreference.ENABLED, MFAPreference.DISABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1822,10 +1897,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is null and SMS is preferred and TOTP is enabled`() { + fun `updatepref when currentpref is null and TOTP and SMS are disabled and email is enabled`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -1845,7 +1927,59 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.PREFERRED, MFAPreference.ENABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.DISABLED, MFAPreference.DISABLED, MFAPreference.ENABLED, onSuccess, mockk() + ) + + assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } + assertTrue(setUserMFAPreferenceRequest.isCaptured) + assertEquals( + SmsMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.smsMfaSettings + ) + assertEquals( + SoftwareTokenMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings + ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) + } + + @Test + fun `updatepref when currentpref is null and SMS is preferred and TOTP and email are enabled`() { + val onSuccess = ActionWithLatch() + val setUserMFAPreferenceRequest = slot() + + coEvery { + mockCognitoIPClient.getUser { + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + }.answers { + GetUserResponse.invoke { + userMfaSettingList = null + preferredMfaSetting = null + userAttributes = listOf() + username = "" + } + } + + coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { + SetUserMfaPreferenceResponse.invoke {} + } + plugin.updateMFAPreference( + MFAPreference.PREFERRED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1863,10 +1997,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + SoftwareTokenMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings + ) } @Test - fun `updatepref when currentpref is null and SMS is enabled and TOTP is preferred`() { + fun `updatepref when currentpref is null and SMS and email are enabled and TOTP is preferred`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -1886,7 +2027,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.PREFERRED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.PREFERRED, MFAPreference.ENABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1904,10 +2047,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is null and TOTP is preferred and SMS is disabled`() { + fun `updatepref when currentpref is null and SMS and TOTP are enabled and email is preferred`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -1927,7 +2077,59 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.DISABLED, MFAPreference.PREFERRED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.PREFERRED, onSuccess, mockk() + ) + + assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } + assertTrue(setUserMFAPreferenceRequest.isCaptured) + assertEquals( + SmsMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.smsMfaSettings + ) + assertEquals( + SoftwareTokenMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings + ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = true + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) + } + + @Test + fun `updatepref when currentpref is null and TOTP is preferred and SMS and email are disabled`() { + val onSuccess = ActionWithLatch() + val setUserMFAPreferenceRequest = slot() + + coEvery { + mockCognitoIPClient.getUser { + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + }.answers { + GetUserResponse.invoke { + userMfaSettingList = null + preferredMfaSetting = null + userAttributes = listOf() + username = "" + } + } + + coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { + SetUserMfaPreferenceResponse.invoke {} + } + plugin.updateMFAPreference( + MFAPreference.DISABLED, MFAPreference.PREFERRED, MFAPreference.DISABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1945,10 +2147,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is null and TOTP is disabled and SMS is preferred`() { + fun `updatepref when currentpref is null and TOTP and email are disabled and SMS is preferred`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -1968,7 +2177,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.PREFERRED, MFAPreference.DISABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.PREFERRED, MFAPreference.DISABLED, MFAPreference.DISABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -1986,10 +2197,67 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) + } + + @Test + fun `updatepref when currentpref is null and TOTP and sms are disabled and email is preferred`() { + val onSuccess = ActionWithLatch() + val setUserMFAPreferenceRequest = slot() + + coEvery { + mockCognitoIPClient.getUser { + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + }.answers { + GetUserResponse.invoke { + userMfaSettingList = null + preferredMfaSetting = null + userAttributes = listOf() + username = "" + } + } + + coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { + SetUserMfaPreferenceResponse.invoke {} + } + plugin.updateMFAPreference( + MFAPreference.DISABLED, MFAPreference.DISABLED, MFAPreference.PREFERRED, onSuccess, mockk() + ) + + assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } + assertTrue(setUserMFAPreferenceRequest.isCaptured) + assertEquals( + SmsMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.smsMfaSettings + ) + assertEquals( + SoftwareTokenMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings + ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = true + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is TOTP preferred and TOTP parameter is disabled`() { + fun `updatepref when currentpref is TOTP preferred and TOTP parameter is disabled`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -2009,7 +2277,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.DISABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.DISABLED, MFAPreference.ENABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -2027,10 +2297,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is SMS preferred and SMS parameter is disabled`() { + fun `updatepref when currentpref is SMS preferred and SMS parameter is disabled`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -2050,7 +2327,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.DISABLED, MFAPreference.ENABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.DISABLED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -2068,6 +2347,63 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) + } + + @Test + fun `updatepref when currentpref is email preferred and email parameter is disabled`() { + val onSuccess = ActionWithLatch() + val setUserMFAPreferenceRequest = slot() + + coEvery { + mockCognitoIPClient.getUser { + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + }.answers { + GetUserResponse.invoke { + userMfaSettingList = listOf("EMAIL_OTP") + preferredMfaSetting = "EMAIL_OTP" + userAttributes = listOf() + username = "" + } + } + + coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { + SetUserMfaPreferenceResponse.invoke {} + } + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.ENABLED, MFAPreference.DISABLED, onSuccess, mockk() + ) + + assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } + assertTrue(setUserMFAPreferenceRequest.isCaptured) + assertEquals( + SmsMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.smsMfaSettings + ) + assertEquals( + SoftwareTokenMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings + ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test @@ -2092,7 +2428,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.DISABLED, MFAPreference.ENABLED, onSuccess, onError) + plugin.updateMFAPreference( + MFAPreference.DISABLED, MFAPreference.ENABLED, MFAPreference.DISABLED, onSuccess, onError + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -2110,10 +2448,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = false + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is TOTP preferred and params include SMS preferred and TOTP enabled`() { + fun `updatepref when currentpref is TOTP preferred and params include SMS preferred and TOTP and email enabled`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -2133,7 +2478,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.PREFERRED, MFAPreference.ENABLED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.PREFERRED, MFAPreference.ENABLED, MFAPreference.ENABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -2151,10 +2498,17 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test - fun `updatepref when currentpref is SMS preferred and params include SMS enabled and TOTP preferred`() { + fun `updatepref when currentpref is SMS preferred and params include SMS and email enabled and TOTP preferred`() { val onSuccess = ActionWithLatch() val setUserMFAPreferenceRequest = slot() @@ -2174,7 +2528,9 @@ class RealAWSCognitoAuthPluginTest { coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { SetUserMfaPreferenceResponse.invoke {} } - plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.PREFERRED, onSuccess, mockk()) + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.PREFERRED, MFAPreference.ENABLED, onSuccess, mockk() + ) onSuccess.shouldBeCalled() assertTrue(setUserMFAPreferenceRequest.isCaptured) @@ -2192,6 +2548,63 @@ class RealAWSCognitoAuthPluginTest { }, setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) + } + + @Test + fun `updatepref when currentpref is email preferred and params include SMS and email enabled and TOTP preferred`() { + val onSuccess = ActionWithLatch() + val setUserMFAPreferenceRequest = slot() + + coEvery { + mockCognitoIPClient.getUser { + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + }.answers { + GetUserResponse.invoke { + userMfaSettingList = listOf("EMAIL_OTP") + preferredMfaSetting = "EMAIL_OTP" + userAttributes = listOf() + username = "" + } + } + + coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { + SetUserMfaPreferenceResponse.invoke {} + } + plugin.updateMFAPreference( + MFAPreference.ENABLED, MFAPreference.PREFERRED, MFAPreference.ENABLED, onSuccess, mockk() + ) + + assertTrue { onSuccess.latch.await(5, TimeUnit.SECONDS) } + assertTrue(setUserMFAPreferenceRequest.isCaptured) + assertEquals( + SmsMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.smsMfaSettings + ) + assertEquals( + SoftwareTokenMfaSettingsType.invoke { + enabled = true + preferredMfa = true + }, + setUserMFAPreferenceRequest.captured.softwareTokenMfaSettings + ) + assertEquals( + EmailMfaSettingsType.invoke { + enabled = true + preferredMfa = false + }, + setUserMFAPreferenceRequest.captured.emailMfaSettings + ) } @Test diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActionsTest.kt index c8a7403dfd..636831178a 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActionsTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActionsTest.kt @@ -87,13 +87,17 @@ class SetupTOTPCognitoActionsTest { } val initiateAction = SetupTOTPCognitoActions.initiateTOTPSetup( SetupTOTPEvent.EventType.SetupTOTP( - SignInTOTPSetupData("", "SESSION", "USERNAME") + SignInTOTPSetupData("", "SESSION", "USERNAME"), + mapOf("MFAS_CAN_SETUP" to "SOFTWARE_TOKEN_MFA") ) ) initiateAction.execute(dispatcher, authEnvironment) val expectedEvent = SetupTOTPEvent( - SetupTOTPEvent.EventType.WaitForAnswer(SignInTOTPSetupData(secretCode, session, username)) + SetupTOTPEvent.EventType.WaitForAnswer( + SignInTOTPSetupData(secretCode, session, username), + mapOf("MFAS_CAN_SETUP" to "SOFTWARE_TOKEN_MFA") + ) ) assertEquals( expectedEvent.type, @@ -124,7 +128,8 @@ class SetupTOTPCognitoActionsTest { } val initiateAction = SetupTOTPCognitoActions.initiateTOTPSetup( SetupTOTPEvent.EventType.SetupTOTP( - SignInTOTPSetupData("", "SESSION", "USERNAME") + SignInTOTPSetupData("", "SESSION", "USERNAME"), + mapOf("MFAS_CAN_SETUP" to "SOFTWARE_TOKEN_MFA") ) ) initiateAction.execute(dispatcher, authEnvironment) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActionsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActionsTest.kt index 5c74ac0b5d..3e1c1bf952 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActionsTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SignInChallengeCognitoActionsTest.kt @@ -138,4 +138,33 @@ class SignInChallengeCognitoActionsTest { assertTrue(capturedRequest.isCaptured) assertEquals(expectedChallengeResponses, capturedRequest.captured.challengeResponses) } + + @Test + fun `verify email MFA setup selection challenge is handled`() = runTest { + val expectedChallengeResponses = mapOf( + "USERNAME" to "testUser", + ) + + val capturedRequest = slot() + coEvery { + cognitoIdentityProviderClientMock.respondToAuthChallenge(capture(capturedRequest)) + }.answers { + mockk() + } + + SignInChallengeCognitoActions.verifyChallengeAuthAction( + "EMAIL_OTP", + emptyMap(), + emptyList(), + AuthChallenge( + "MFA_SETUP", + username = "testUser", + session = null, + parameters = null + ) + ).execute(dispatcher, authEnvironment) + + assertTrue(capturedRequest.isCaptured) + assertEquals(expectedChallengeResponses, capturedRequest.captured.challengeResponses) + } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelperTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelperTest.kt new file mode 100644 index 0000000000..8d29d4569b --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/helpers/SignInChallengeHelperTest.kt @@ -0,0 +1,385 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.auth.cognito.helpers + +import com.amplifyframework.auth.AuthCodeDeliveryDetails +import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.MFAType +import com.amplifyframework.auth.TOTPSetupDetails +import com.amplifyframework.auth.exceptions.UnknownException +import com.amplifyframework.auth.result.AuthSignInResult +import com.amplifyframework.auth.result.step.AuthNextSignInStep +import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.statemachine.codegen.data.AuthChallenge +import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull + +class SignInChallengeHelperTest { + private val username = "username" + private val email = "test@testdomain.com" + + // MFA Setup + @Test + fun `User needs to select either Email OTP or TOTP to setup`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "MFA_SETUP", + username = username, + session = "session", + parameters = mapOf("MFAS_CAN_SETUP" to "\"EMAIL_OTP\",\"SOFTWARE_TOKEN_MFA\"") + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION, + emptyMap(), + null, + null, + setOf(MFAType.EMAIL, MFAType.TOTP) + ) + ), + signInResult + ) + assertNull(errorResult) + } + + @Test + fun `User needs to setup TOTP`() { + val totpSetupData = SignInTOTPSetupData( + secretCode = "secretCode", + session = "session", + username = username + ) + + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "MFA_SETUP", + username = username, + session = "session", + parameters = mapOf("MFAS_CAN_SETUP" to "\"SOFTWARE_TOKEN_MFA\"") + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = totpSetupData, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, + mapOf("MFAS_CAN_SETUP" to "\"SOFTWARE_TOKEN_MFA\""), + null, + TOTPSetupDetails( + sharedSecret = totpSetupData.secretCode, + username = totpSetupData.username + ), + null + ) + ), + signInResult + ) + assertNull(errorResult) + } + + @Test + fun `User needs to setup email OTP`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "MFA_SETUP", + username = username, + session = "session", + parameters = mapOf("MFAS_CAN_SETUP" to "\"EMAIL_OTP\"") + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP, + emptyMap(), + null, + null, + null + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // MFA Selection + @Test + fun `User needs to select which MFA type to use`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "SELECT_MFA_TYPE", + username = username, + session = "session", + parameters = mapOf("MFAS_CAN_CHOOSE" to "\"EMAIL_MFA\",\"SOFTWARE_TOKEN_MFA\",\"SMS_MFA\"") + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION, + emptyMap(), + null, + null, + setOf(MFAType.EMAIL, MFAType.TOTP, MFAType.SMS) + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // Email OTP + @Test + fun `User is asked to confirm an emailed MFA code`() { + val deliveryDetails = AuthCodeDeliveryDetails(email, AuthCodeDeliveryDetails.DeliveryMedium.EMAIL) + + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "EMAIL_OTP", + username = username, + session = "session", + parameters = mapOf( + "CODE_DELIVERY_DELIVERY_MEDIUM" to "email", + "CODE_DELIVERY_DESTINATION" to email + ) + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONFIRM_SIGN_IN_WITH_EMAIL_MFA_CODE, + emptyMap(), + deliveryDetails, + null, + null + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // TOTP + @Test + fun `User is asked to input the TOTP code`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "SOFTWARE_TOKEN_MFA", + username = username, + session = "session", + parameters = null + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE, + emptyMap(), + null, + null, + null + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // SMS + @Test + fun `User is asked to confirm an SMS MFA code`() { + val phoneNumber = "+15555555555" + val deliveryDetails = AuthCodeDeliveryDetails(phoneNumber, AuthCodeDeliveryDetails.DeliveryMedium.SMS) + + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "SMS_MFA", + username = username, + session = "session", + parameters = mapOf( + "CODE_DELIVERY_DELIVERY_MEDIUM" to "sms", + "CODE_DELIVERY_DESTINATION" to phoneNumber + ) + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertEquals( + AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE, + emptyMap(), + deliveryDetails, + null, + null + ) + ), + signInResult + ) + assertNull(errorResult) + } + + // Exceptions + @Test + fun `Exception when setting up unknown challenge type`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "MFA_SETUP", + username = username, + session = "session", + parameters = mapOf("MFAS_CAN_SETUP" to "\"UNKNOWN_CHALLENGE\"") + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertIs(errorResult) + assertNull(signInResult) + } + + @Test + fun `Exception when choosing an unknown challenge type`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "MFA_SETUP", + username = username, + session = "session", + parameters = mapOf("MFAS_CAN_CHOOSE" to "\"EMAIL_MFA\",\"UNKNOWN\",\"SMS_MFA\"") + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertIs(errorResult) + assertNull(signInResult) + } + + @Test + fun `Exception when receiving an unsupported challenge name`() { + var signInResult: AuthSignInResult? = null + var errorResult: AuthException? = null + SignInChallengeHelper.getNextStep( + challenge = AuthChallenge( + challengeName = "UNKNOWN", + username = username, + session = "session", + parameters = mapOf("MFAS_CAN_CHOOSE" to "\"EMAIL_MFA\",\"UNKNOWN\",\"SMS_MFA\"") + ), + onSuccess = { + signInResult = it + }, + onError = { + errorResult = it + }, + signInTOTPSetupData = null, + allowedMFAType = null + ) + assertIs(errorResult) + assertNull(signInResult) + } +} diff --git a/core/api/core.api b/core/api/core.api index 790095cd96..97d5a5ca0d 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -691,6 +691,7 @@ public final class com/amplifyframework/auth/AuthUserAttributeKey { } public final class com/amplifyframework/auth/MFAType : java/lang/Enum { + public static final field EMAIL Lcom/amplifyframework/auth/MFAType; public static final field SMS Lcom/amplifyframework/auth/MFAType; public static final field TOTP Lcom/amplifyframework/auth/MFAType; public static fun getEntries ()Lkotlin/enums/EnumEntries; @@ -1151,11 +1152,14 @@ public final class com/amplifyframework/auth/result/step/AuthResetPasswordStep : public final class com/amplifyframework/auth/result/step/AuthSignInStep : java/lang/Enum { public static final field CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE Lcom/amplifyframework/auth/result/step/AuthSignInStep; + public static final field CONFIRM_SIGN_IN_WITH_EMAIL_MFA_CODE Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_IN_WITH_NEW_PASSWORD Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_IN_WITH_TOTP_CODE Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONFIRM_SIGN_UP Lcom/amplifyframework/auth/result/step/AuthSignInStep; + public static final field CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONTINUE_SIGN_IN_WITH_MFA_SELECTION Lcom/amplifyframework/auth/result/step/AuthSignInStep; + public static final field CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field CONTINUE_SIGN_IN_WITH_TOTP_SETUP Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field DONE Lcom/amplifyframework/auth/result/step/AuthSignInStep; public static final field RESET_PASSWORD Lcom/amplifyframework/auth/result/step/AuthSignInStep; diff --git a/core/src/main/java/com/amplifyframework/auth/MFAType.kt b/core/src/main/java/com/amplifyframework/auth/MFAType.kt index c57472a3c8..8e60d2440c 100644 --- a/core/src/main/java/com/amplifyframework/auth/MFAType.kt +++ b/core/src/main/java/com/amplifyframework/auth/MFAType.kt @@ -27,5 +27,10 @@ enum class MFAType { /** * Time-based One Time Password linked with an authenticator app */ - TOTP; + TOTP, + + /** + * Receives MFA codes with an email + */ + EMAIL; } diff --git a/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignInStep.java b/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignInStep.java index 3373104db3..b5273b361c 100644 --- a/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignInStep.java +++ b/core/src/main/java/com/amplifyframework/auth/result/step/AuthSignInStep.java @@ -58,6 +58,13 @@ public enum AuthSignInStep { */ CONFIRM_SIGN_UP, + /** + * User selects MFA type to use. + * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} + * with MFAType to select. + */ + CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION, + /** * Admin requires user to setup TOTP. * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} @@ -65,6 +72,13 @@ public enum AuthSignInStep { */ CONTINUE_SIGN_IN_WITH_TOTP_SETUP, + /** + * Admin requires user to setup email MFA. + * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} + * with email code to verify. + */ + CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP, + /** * The user account is required to set MFA selection. * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} @@ -79,6 +93,13 @@ public enum AuthSignInStep { */ CONFIRM_SIGN_IN_WITH_TOTP_CODE, + /** + * MFA is enabled on this account and requires the user to confirm with the code received by email. + * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} + * with email code. + */ + CONFIRM_SIGN_IN_WITH_EMAIL_MFA_CODE, + /** * No further steps are needed in the sign in flow. */ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd160acae3..c99c98f4b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,9 +17,9 @@ androidx-test-orchestrator = "1.4.2" androidx-test-runner = "1.3.0" androidx-workmanager = "2.7.1" apollo = "4.0.0" -aws-kotlin = "1.2.8" # ensure proper aws-smithy version also set +aws-kotlin = "1.3.31" # ensure proper aws-smithy version also set aws-sdk = "2.62.2" -aws-smithy = "1.2.2" # ensure proper aws-kotlin version also set +aws-smithy = "1.3.8" # ensure proper aws-kotlin version also set binary-compatibility-validator = "0.14.0" coroutines = "1.7.3" desugar = "1.2.0" diff --git a/scripts/pull_backend_config_from_s3 b/scripts/pull_backend_config_from_s3 index b7671765f7..eb4c385369 100755 --- a/scripts/pull_backend_config_from_s3 +++ b/scripts/pull_backend_config_from_s3 @@ -62,6 +62,7 @@ readonly config_files=( "aws-auth-cognito/src/androidTest/res/raw/awsconfiguration.json" "aws-auth-cognito/src/androidTest/res/raw/credentials.json" "aws-auth-cognito/src/androidTest/res/raw/amplify_outputs.json" + "aws-auth-cognito/src/androidTest/res/raw/amplify_outputs_email_or_totp_mfa.json" ) # Set up output path