From 086697a70cac83814518383e73bbfb5a75ac8980 Mon Sep 17 00:00:00 2001 From: Saijad Dhuka <83975678+sdhuka@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:28:45 -0500 Subject: [PATCH 01/15] feat(Auth): Add TOTP Support (#2537) --- aws-auth-cognito/build.gradle.kts | 4 + .../cognito/AWSCognitoAuthPluginTOTPTests.kt | 179 ++++++++ .../auth/cognito/AWSCognitoAuthPlugin.kt | 71 +++ .../auth/cognito/AuthStateMachine.kt | 8 +- .../cognito/CognitoAuthExceptionConverter.kt | 3 + .../auth/cognito/KotlinAuthFacadeInternal.kt | 44 ++ .../auth/cognito/RealAWSCognitoAuthPlugin.kt | 409 ++++++++++++++++-- .../auth/cognito/UserMFAPreference.kt | 55 +++ .../auth/cognito/actions/SRPCognitoActions.kt | 10 +- .../actions/SetupTOTPCognitoActions.kt | 166 +++++++ .../actions/SignInChallengeCognitoActions.kt | 4 +- .../cognito/actions/SignInCognitoActions.kt | 9 + .../EnableSoftwareTokenMfaException.kt | 28 ++ .../cognito/helpers/SignInChallengeHelper.kt | 92 +++- .../AWSCognitoAuthConfirmSignInOptions.kt | 17 +- .../AWSCognitoAuthVerifyTOTPSetupOptions.java | 67 +++ .../codegen/actions/SetupTOTPActions.kt | 29 ++ .../codegen/actions/SignInActions.kt | 1 + .../codegen/data/SignInTOTPSetupData.kt | 21 + .../codegen/events/SetupTOTPEvent.kt | 41 ++ .../codegen/events/SignInEvent.kt | 2 + .../codegen/states/SetupTOTPState.kt | 147 +++++++ .../codegen/states/SignInChallengeState.kt | 2 + .../codegen/states/SignInState.kt | 71 ++- .../auth/cognito/AWSCognitoAuthPluginTest.kt | 49 +++ .../cognito/RealAWSCognitoAuthPluginTest.kt | 237 ++++++++++ .../auth/cognito/StateTransitionTestBase.kt | 10 +- .../auth/cognito/StateTransitionTests.kt | 2 + .../actions/SetupTOTPCognitoActionsTest.kt | 286 ++++++++++++ .../serializers/AuthStatesSerializer.kt | 15 +- .../com/amplifyframework/kotlin/auth/Auth.kt | 18 + .../kotlin/auth/KotlinAuthFacade.kt | 22 + .../com/amplifyframework/TOTPSetupDetails.kt | 29 ++ .../amplifyframework/auth/AuthCategory.java | 21 +- .../auth/AuthCategoryBehavior.java | 37 ++ .../java/com/amplifyframework/auth/MFAType.kt | 29 ++ .../options/AuthVerifyTOTPSetupOptions.java | 60 +++ .../auth/result/step/AuthNextSignInStep.java | 42 +- .../auth/result/step/AuthSignInStep.java | 23 +- gradle/libs.versions.toml | 2 + .../amplifyframework/rx/RxAuthBinding.java | 19 +- .../rx/RxAuthCategoryBehavior.java | 26 ++ .../rx/RxAuthBindingTest.java | 16 +- scripts/pull_backend_config_from_s3 | 1 + .../testutils/sync/SynchronousAuth.java | 12 + 45 files changed, 2355 insertions(+), 81 deletions(-) create mode 100644 aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTOTPTests.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/UserMFAPreference.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMfaException.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SetupTOTPActions.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SetupTOTPEvent.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActionsTest.kt create mode 100644 core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt create mode 100644 core/src/main/java/com/amplifyframework/auth/MFAType.kt create mode 100644 core/src/main/java/com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions.java diff --git a/aws-auth-cognito/build.gradle.kts b/aws-auth-cognito/build.gradle.kts index d741d55a95..7a002160da 100644 --- a/aws-auth-cognito/build.gradle.kts +++ b/aws-auth-cognito/build.gradle.kts @@ -39,6 +39,8 @@ dependencies { implementation(libs.aws.cognitoidentityprovider) testImplementation(project(":testutils")) + testImplementation(project(":core")) + testImplementation(project(":aws-core")) //noinspection GradleDependency testImplementation(libs.test.json) @@ -60,6 +62,8 @@ dependencies { androidTestImplementation(libs.test.androidx.runner) androidTestImplementation(libs.test.androidx.junit) androidTestImplementation(libs.test.kotlin.coroutines) + androidTestImplementation(libs.test.totp) + androidTestImplementation(project(":aws-api")) androidTestImplementation(project(":testutils")) } 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 new file mode 100644 index 0000000000..3955c18580 --- /dev/null +++ b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTOTPTests.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2023 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amplifyframework.auth.AuthUserAttribute +import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.MFAType +import com.amplifyframework.auth.cognito.test.R +import com.amplifyframework.auth.options.AuthSignUpOptions +import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.AmplifyConfiguration +import com.amplifyframework.core.category.CategoryConfiguration +import com.amplifyframework.core.category.CategoryType +import com.amplifyframework.logging.AndroidLoggingPlugin +import com.amplifyframework.logging.LogLevel +import com.amplifyframework.testutils.sync.SynchronousAuth +import dev.robinohs.totpkt.otp.totp.TotpGenerator +import dev.robinohs.totpkt.otp.totp.timesupport.generateCode +import java.util.Random +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AWSCognitoAuthPluginTOTPTests { + + private lateinit var authPlugin: AWSCognitoAuthPlugin + private lateinit var synchronousAuth: SynchronousAuth + private val password = UUID.randomUUID().toString() + private val userName = "testUser${Random().nextInt()}" + private val email = "$userName@testdomain.com" + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + Amplify.addPlugin(AndroidLoggingPlugin(LogLevel.VERBOSE)) + val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfiguration_totp) + val authConfig: CategoryConfiguration = config.forCategoryType(CategoryType.AUTH) + val authConfigJson = authConfig.getPluginConfig("awsCognitoAuthPlugin") + authPlugin = AWSCognitoAuthPlugin() + authPlugin.configure(authConfigJson, context) + synchronousAuth = SynchronousAuth.delegatingTo(authPlugin) + signUpNewUser(userName, password, email) + synchronousAuth.signOut() + } + + @After + fun tearDown() { + synchronousAuth.deleteUser() + } + + /* + * This test signs up a new user and goes thru successful MFA Setup process. + * */ + @Test + fun mfa_setup() { + val result = synchronousAuth.signIn(userName, password) + Assert.assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, result.nextStep.signInStep) + val otp = TotpGenerator().generateCode( + result.nextStep.totpSetupDetails!!.sharedSecret.toByteArray(), + System.currentTimeMillis() + ) + synchronousAuth.confirmSignIn(otp) + val currentUser = synchronousAuth.currentUser + Assert.assertEquals(userName.lowercase(), currentUser.username) + } + + /* + * This test signs up a new user, enter incorrect MFA code during verification and + * then enter correct OTP code to successfully set TOTP MFA. + * */ + @Test + fun mfasetup_with_incorrect_otp() { + val result = synchronousAuth.signIn(userName, password) + Assert.assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, result.nextStep.signInStep) + try { + synchronousAuth.confirmSignIn("123456") + } catch (e: Exception) { + Assert.assertEquals("Code mismatch", e.cause?.message) + val otp = TotpGenerator().generateCode( + result.nextStep.totpSetupDetails!!.sharedSecret.toByteArray(), + System.currentTimeMillis() + ) + synchronousAuth.confirmSignIn(otp) + val currentUser = synchronousAuth.currentUser + Assert.assertEquals(userName.lowercase(), currentUser.username) + } + } + + /* + * This test signs up a new user, successfully setup MFA, sign-out and then goes thru sign-in with TOTP. + * */ + @Test + fun signIn_with_totp_after_mfa_setup() { + val result = synchronousAuth.signIn(userName, password) + Assert.assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, result.nextStep.signInStep) + val otp = TotpGenerator().generateCode( + result.nextStep.totpSetupDetails!!.sharedSecret.toByteArray() + ) + synchronousAuth.confirmSignIn(otp) + synchronousAuth.signOut() + + val signInResult = synchronousAuth.signIn(userName, password) + Assert.assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE, signInResult.nextStep.signInStep) + val otpCode = TotpGenerator().generateCode( + result.nextStep.totpSetupDetails!!.sharedSecret.toByteArray(), + System.currentTimeMillis() + 30 * 1000 // 30 sec is added to generate new OTP code + ) + synchronousAuth.confirmSignIn(otpCode) + val currentUser = synchronousAuth.currentUser + Assert.assertEquals(userName.lowercase(), currentUser.username) + } + + /* + * This test signs up a new user, successfully setup MFA, update user attribute to add phone number, + * sign-out the user, goes thru MFA selection flow during sign-in, select TOTP MFA type, + * successfully sign-in using TOTP + * */ + @Test + fun select_mfa_type() { + val result = synchronousAuth.signIn(userName, password) + Assert.assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, result.nextStep.signInStep) + val otp = TotpGenerator().generateCode( + result.nextStep.totpSetupDetails!!.sharedSecret.toByteArray() + ) + synchronousAuth.confirmSignIn(otp) + synchronousAuth.updateUserAttribute(AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+19876543210")) + updateMFAPreference(MFAPreference.Enabled, MFAPreference.Enabled) + synchronousAuth.signOut() + val signInResult = synchronousAuth.signIn(userName, password) + Assert.assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION, signInResult.nextStep.signInStep) + val totpSignInResult = synchronousAuth.confirmSignIn(MFAType.TOTP.value) + Assert.assertEquals(AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE, totpSignInResult.nextStep.signInStep) + val otpCode = TotpGenerator().generateCode( + result.nextStep.totpSetupDetails!!.sharedSecret.toByteArray(), + System.currentTimeMillis() + 30 * 1000 // 30 sec is added to generate new OTP code + ) + synchronousAuth.confirmSignIn(otpCode) + val currentUser = synchronousAuth.currentUser + Assert.assertEquals(userName.lowercase(), currentUser.username) + } + + private fun signUpNewUser(userName: String, password: String, email: String) { + val options = AuthSignUpOptions.builder() + .userAttributes( + listOf( + AuthUserAttribute(AuthUserAttributeKey.email(), email) + ) + ).build() + synchronousAuth.signUp(userName, password, options) + } + + private fun updateMFAPreference(sms: MFAPreference, totp: MFAPreference) { + val latch = CountDownLatch(1) + authPlugin.updateMFAPreference(sms, totp, { latch.countDown() }, { latch.countDown() }) + latch.await(5, TimeUnit.SECONDS) + } +} 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 c6b10b6cd6..625b64e662 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 @@ -20,6 +20,7 @@ import android.content.Context import android.content.Intent import androidx.annotation.VisibleForTesting import com.amplifyframework.AmplifyException +import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.auth.AWSCognitoAuthMetadataType import com.amplifyframework.auth.AuthCodeDeliveryDetails @@ -32,6 +33,7 @@ import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.cognito.asf.UserContextDataProvider +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult import com.amplifyframework.auth.exceptions.ConfigurationException @@ -48,6 +50,7 @@ import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthSignUpOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult @@ -768,6 +771,40 @@ class AWSCognitoAuthPlugin : AuthPlugin() { ) } + override fun setUpTOTP(onSuccess: Consumer, onError: Consumer) { + queueChannel.trySend( + pluginScope.launch(start = CoroutineStart.LAZY) { + try { + val result = queueFacade.setupMFA() + onSuccess.accept(result) + } catch (e: Exception) { + onError.accept(e.toAuthException()) + } + } + ) + } + + override fun verifyTOTPSetup(code: String, onSuccess: Action, onError: Consumer) { + verifyTOTPSetup(code, AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), onSuccess, onError) + } + + override fun verifyTOTPSetup( + code: String, + options: AuthVerifyTOTPSetupOptions, + onSuccess: Action, + onError: Consumer + ) { + queueChannel.trySend( + pluginScope.launch(start = CoroutineStart.LAZY) { + try { + queueFacade.verifyTOTPSetup(code, options) + onSuccess.call() + } catch (e: Exception) { + onError.accept(e.toAuthException()) + } + } + ) + } override fun getEscapeHatch() = realPlugin.escapeHatch() override fun getPluginKey() = AWS_COGNITO_AUTH_PLUGIN_KEY @@ -852,4 +889,38 @@ class AWSCognitoAuthPlugin : AuthPlugin() { } ) } + + fun fetchMFAPreference( + onSuccess: Consumer, + onError: Consumer + ) { + queueChannel.trySend( + pluginScope.launch(start = CoroutineStart.LAZY) { + try { + val result = queueFacade.fetchMFAPreference() + onSuccess.accept(result) + } catch (e: Exception) { + onError.accept(e.toAuthException()) + } + } + ) + } + + fun updateMFAPreference( + sms: MFAPreference?, + totp: MFAPreference?, + onSuccess: Action, + onError: Consumer + ) { + queueChannel.trySend( + pluginScope.launch(start = CoroutineStart.LAZY) { + try { + queueFacade.updateMFAPreference(sms, totp) + onSuccess.call() + } catch (e: Exception) { + onError.accept(e.toAuthException()) + } + } + ) + } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt index c0467902a3..a781329481 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt @@ -24,6 +24,7 @@ import com.amplifyframework.auth.cognito.actions.FetchAuthSessionCognitoActions import com.amplifyframework.auth.cognito.actions.HostedUICognitoActions import com.amplifyframework.auth.cognito.actions.MigrateAuthCognitoActions import com.amplifyframework.auth.cognito.actions.SRPCognitoActions +import com.amplifyframework.auth.cognito.actions.SetupTOTPCognitoActions import com.amplifyframework.auth.cognito.actions.SignInChallengeCognitoActions import com.amplifyframework.auth.cognito.actions.SignInCognitoActions import com.amplifyframework.auth.cognito.actions.SignInCustomCognitoActions @@ -42,6 +43,7 @@ import com.amplifyframework.statemachine.codegen.states.HostedUISignInState import com.amplifyframework.statemachine.codegen.states.MigrateSignInState import com.amplifyframework.statemachine.codegen.states.RefreshSessionState import com.amplifyframework.statemachine.codegen.states.SRPSignInState +import com.amplifyframework.statemachine.codegen.states.SetupTOTPState import com.amplifyframework.statemachine.codegen.states.SignInChallengeState import com.amplifyframework.statemachine.codegen.states.SignInState import com.amplifyframework.statemachine.codegen.states.SignOutState @@ -62,10 +64,11 @@ internal class AuthStateMachine( SignInChallengeState.Resolver(SignInChallengeCognitoActions), HostedUISignInState.Resolver(HostedUICognitoActions), DeviceSRPSignInState.Resolver(DeviceSRPCognitoSignInActions), + SetupTOTPState.Resolver(SetupTOTPCognitoActions), SignInCognitoActions ), SignOutState.Resolver(SignOutCognitoActions), - AuthenticationCognitoActions, + AuthenticationCognitoActions ), AuthorizationState.Resolver( FetchAuthSessionState.Resolver(FetchAuthSessionCognitoActions), @@ -93,10 +96,11 @@ internal class AuthStateMachine( SignInChallengeState.Resolver(SignInChallengeCognitoActions).logging(), HostedUISignInState.Resolver(HostedUICognitoActions).logging(), DeviceSRPSignInState.Resolver(DeviceSRPCognitoSignInActions).logging(), + SetupTOTPState.Resolver(SetupTOTPCognitoActions).logging(), SignInCognitoActions ).logging(), SignOutState.Resolver(SignOutCognitoActions).logging(), - AuthenticationCognitoActions, + AuthenticationCognitoActions ).logging(), AuthorizationState.Resolver( FetchAuthSessionState.Resolver(FetchAuthSessionCognitoActions).logging(), diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt index e3fa1914f4..17a5f5c29f 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt @@ -18,6 +18,7 @@ package com.amplifyframework.auth.cognito import aws.sdk.kotlin.services.cognitoidentityprovider.model.AliasExistsException import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeDeliveryFailureException import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.EnableSoftwareTokenMfaException import aws.sdk.kotlin.services.cognitoidentityprovider.model.ExpiredCodeException import aws.sdk.kotlin.services.cognitoidentityprovider.model.InvalidParameterException import aws.sdk.kotlin.services.cognitoidentityprovider.model.InvalidPasswordException @@ -88,6 +89,8 @@ internal class CognitoAuthExceptionConverter { com.amplifyframework.auth.cognito.exceptions.service.TooManyRequestsException(error) is PasswordResetRequiredException -> com.amplifyframework.auth.cognito.exceptions.service.PasswordResetRequiredException(error) + is EnableSoftwareTokenMfaException -> + com.amplifyframework.auth.cognito.exceptions.service.EnableSoftwareTokenMfaException(error) else -> UnknownException(fallbackMessage, error) } } 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 ca4b4dc456..96ce06d54a 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 @@ -17,6 +17,7 @@ package com.amplifyframework.auth.cognito import android.app.Activity import android.content.Intent +import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthProvider @@ -38,6 +39,7 @@ import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthSignUpOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult @@ -520,4 +522,46 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth ) } } + + suspend fun setupMFA(): TOTPSetupDetails { + return suspendCoroutine { continuation -> + delegate.setUpTOTP( + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } + } + suspend fun verifyTOTPSetup(code: String, options: AuthVerifyTOTPSetupOptions) { + return suspendCoroutine { continuation -> + delegate.verifyTOTPSetup( + code, + options, + { continuation.resume(Unit) }, + { continuation.resumeWithException(it) } + ) + } + } + + suspend fun fetchMFAPreference(): UserMFAPreference { + return suspendCoroutine { continuation -> + delegate.fetchMFAPreference( + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } + } + + suspend fun updateMFAPreference( + sms: MFAPreference?, + totp: MFAPreference? + ) { + return suspendCoroutine { continuation -> + delegate.updateMFAPreference( + sms, + totp, + { continuation.resume(Unit) }, + { continuation.resumeWithException(it) } + ) + } + } } 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 24a2abafec..7780b6f5b7 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 @@ -18,22 +18,31 @@ package com.amplifyframework.auth.cognito import android.app.Activity import android.content.Intent import androidx.annotation.WorkerThread +import aws.sdk.kotlin.services.cognitoidentityprovider.associateSoftwareToken import aws.sdk.kotlin.services.cognitoidentityprovider.confirmForgotPassword import aws.sdk.kotlin.services.cognitoidentityprovider.confirmSignUp +import aws.sdk.kotlin.services.cognitoidentityprovider.getUser import aws.sdk.kotlin.services.cognitoidentityprovider.model.AnalyticsMetadataType 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.GetUserAttributeVerificationCodeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.ListDevicesRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SmsMfaSettingsType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SoftwareTokenMfaSettingsType import aws.sdk.kotlin.services.cognitoidentityprovider.model.UpdateDeviceStatusRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.UpdateUserAttributesRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.UpdateUserAttributesResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponseType import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifyUserAttributeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.resendConfirmationCode +import aws.sdk.kotlin.services.cognitoidentityprovider.setUserMfaPreference import aws.sdk.kotlin.services.cognitoidentityprovider.signUp +import aws.sdk.kotlin.services.cognitoidentityprovider.verifySoftwareToken import com.amplifyframework.AmplifyException +import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.auth.AWSCognitoAuthMetadataType import com.amplifyframework.auth.AWSCredentials @@ -48,6 +57,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidOauthConfigurationException import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException import com.amplifyframework.auth.cognito.exceptions.invalidstate.SignedInException @@ -70,6 +80,7 @@ import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignUpOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributesOptions +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions @@ -98,6 +109,7 @@ import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthSignUpOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult @@ -118,6 +130,7 @@ import com.amplifyframework.hub.HubEvent import com.amplifyframework.logging.Logger import com.amplifyframework.statemachine.StateChangeListenerToken import com.amplifyframework.statemachine.codegen.data.AmplifyCredential +import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.data.AuthConfiguration import com.amplifyframework.statemachine.codegen.data.DeviceMetadata import com.amplifyframework.statemachine.codegen.data.FederatedToken @@ -131,6 +144,7 @@ import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.AuthorizationEvent import com.amplifyframework.statemachine.codegen.events.DeleteUserEvent import com.amplifyframework.statemachine.codegen.events.HostedUIEvent +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent import com.amplifyframework.statemachine.codegen.events.SignOutEvent import com.amplifyframework.statemachine.codegen.states.AuthState @@ -139,6 +153,7 @@ import com.amplifyframework.statemachine.codegen.states.AuthorizationState import com.amplifyframework.statemachine.codegen.states.DeleteUserState import com.amplifyframework.statemachine.codegen.states.HostedUISignInState import com.amplifyframework.statemachine.codegen.states.SRPSignInState +import com.amplifyframework.statemachine.codegen.states.SetupTOTPState import com.amplifyframework.statemachine.codegen.states.SignInChallengeState import com.amplifyframework.statemachine.codegen.states.SignInState import com.amplifyframework.statemachine.codegen.states.SignOutState @@ -498,7 +513,8 @@ internal class RealAWSCognitoAuthPlugin( ) // Continue sign in is AuthenticationState.SignedOut, - is AuthenticationState.Configured -> { + is AuthenticationState.Configured + -> { _signIn(username, password, signInOptions, onSuccess, onError) } is AuthenticationState.SignedIn -> onError.accept(SignedInException()) @@ -543,6 +559,7 @@ internal class RealAWSCognitoAuthPlugin( val signInState = authNState.signInState val srpSignInState = (signInState as? SignInState.SigningInWithSRP)?.srpSignInState val challengeState = (signInState as? SignInState.ResolvingChallenge)?.challengeState + val totpSetupState = (signInState as? SignInState.ResolvingTOTPSetup)?.setupTOTPState when { srpSignInState is SRPSignInState.Error -> { authStateMachine.cancel(token) @@ -560,6 +577,22 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.cancel(token) SignInChallengeHelper.getNextStep(challengeState.challenge, onSuccess, onError) } + + totpSetupState is SetupTOTPState.WaitingForAnswer -> { + authStateMachine.cancel(token) + SignInChallengeHelper.getNextStep( + AuthChallenge( + ChallengeNameType.MfaSetup.value, + null, + null, + null + ), + onSuccess, + onError, + totpSetupState.signInTOTPSetupData + ) + totpSetupState?.hasNewResponse = false + } } } authNState is AuthenticationState.SignedIn && @@ -567,7 +600,7 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.cancel(token) val authSignInResult = AuthSignInResult( true, - AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null) + AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null) ) onSuccess.accept(authSignInResult) sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) @@ -616,12 +649,19 @@ internal class RealAWSCognitoAuthPlugin( if (signInState is SignInState.ResolvingChallenge) { when (signInState.challengeState) { is SignInChallengeState.WaitingForAnswer, is SignInChallengeState.Error -> { - _confirmSignIn(challengeResponse, options, onSuccess, onError) + _confirmSignIn(signInState, challengeResponse, options, onSuccess, onError) } else -> { onError.accept(InvalidStateException()) } } + } else if (signInState is SignInState.ResolvingTOTPSetup) { + when (signInState.setupTOTPState) { + is SetupTOTPState.WaitingForAnswer, is SetupTOTPState.Error -> { + _confirmSignIn(signInState, challengeResponse, options, onSuccess, onError) + } + else -> onError.accept(InvalidStateException()) + } } else { onError.accept(InvalidStateException()) } @@ -629,6 +669,7 @@ internal class RealAWSCognitoAuthPlugin( } private fun _confirmSignIn( + signInState: SignInState, challengeResponse: String, options: AuthConfirmSignInOptions, onSuccess: Consumer, @@ -641,13 +682,14 @@ internal class RealAWSCognitoAuthPlugin( val authNState = authState.authNState val authZState = authState.authZState val signInState = (authNState as? AuthenticationState.SigningIn)?.signInState + val totpSetupState = (signInState as? SignInState.ResolvingTOTPSetup)?.setupTOTPState when { authNState is AuthenticationState.SignedIn && authZState is AuthorizationState.SessionEstablished -> { authStateMachine.cancel(token) val authSignInResult = AuthSignInResult( true, - AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null) + AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null) ) onSuccess.accept(authSignInResult) sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) @@ -656,7 +698,8 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.cancel(token) onError.accept( CognitoAuthExceptionConverter.lookup( - signInState.exception, "Confirm Sign in failed." + signInState.exception, + "Confirm Sign in failed." ) ) } @@ -665,21 +708,64 @@ internal class RealAWSCognitoAuthPlugin( (signInState.challengeState as SignInChallengeState.WaitingForAnswer).hasNewResponse -> { authStateMachine.cancel(token) val signInChallengeState = signInState.challengeState as SignInChallengeState.WaitingForAnswer - var signInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE - if (signInChallengeState.challenge.challengeName == "SMS_MFA") { - signInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE - } else if (signInChallengeState.challenge.challengeName == "NEW_PASSWORD_REQUIRED") { - signInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD + var allowedMFATypes: Set? = null + 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) + } + AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION + } + else -> AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE } - val authSignInResult = AuthSignInResult( false, - AuthNextSignInStep(signInStep, signInChallengeState.challenge.parameters ?: mapOf(), null) + AuthNextSignInStep( + signInStep, + signInChallengeState.challenge.parameters ?: mapOf(), + null, + null, + allowedMFATypes + ) ) onSuccess.accept(authSignInResult) (signInState.challengeState as SignInChallengeState.WaitingForAnswer).hasNewResponse = false } + signInState is SignInState.ResolvingTOTPSetup && + totpSetupState is SetupTOTPState.WaitingForAnswer && + totpSetupState.hasNewResponse -> { + authStateMachine.cancel(token) + SignInChallengeHelper.getNextStep( + AuthChallenge( + ChallengeNameType.MfaSetup.value, + null, + null, + null + ), + onSuccess, + onError, + totpSetupState.signInTOTPSetupData + ) + totpSetupState.hasNewResponse = false + } + + signInState is SignInState.ResolvingTOTPSetup && + totpSetupState is SetupTOTPState.Error && + totpSetupState.hasNewResponse -> { + authStateMachine.cancel(token) + onError.accept( + CognitoAuthExceptionConverter.lookup( + totpSetupState.exception, + "Confirm Sign in failed." + ) + ) + totpSetupState.hasNewResponse = false + } + signInState is SignInState.ResolvingChallenge && signInState.challengeState is SignInChallengeState.Error && (signInState.challengeState as SignInChallengeState.Error).hasNewResponse -> { @@ -695,16 +781,60 @@ internal class RealAWSCognitoAuthPlugin( (signInState.challengeState as SignInChallengeState.Error).hasNewResponse = false } } - }, { - val awsCognitoConfirmSignInOptions = options as? AWSCognitoAuthConfirmSignInOptions - val event = SignInChallengeEvent( - SignInChallengeEvent.EventType.VerifyChallengeAnswer( - challengeResponse, - awsCognitoConfirmSignInOptions?.metadata ?: mapOf() - ) - ) - authStateMachine.send(event) - } + }, + { + val awsCognitoConfirmSignInOptions = options as? AWSCognitoAuthConfirmSignInOptions + when (signInState) { + is SignInState.ResolvingChallenge -> { + val event = SignInChallengeEvent( + SignInChallengeEvent.EventType.VerifyChallengeAnswer( + challengeResponse, + awsCognitoConfirmSignInOptions?.metadata ?: mapOf() + ) + ) + authStateMachine.send(event) + } + + is SignInState.ResolvingTOTPSetup -> { + when (signInState.setupTOTPState) { + is SetupTOTPState.WaitingForAnswer -> { + val setupData = + (signInState.setupTOTPState as SetupTOTPState.WaitingForAnswer).signInTOTPSetupData + + val event = SetupTOTPEvent( + SetupTOTPEvent.EventType.VerifyChallengeAnswer( + challengeResponse, + setupData.username, + setupData.session, + awsCognitoConfirmSignInOptions?.friendlyDeviceName + ) + ) + authStateMachine.send(event) + } + is SetupTOTPState.Error -> { + val username = + (signInState.setupTOTPState as SetupTOTPState.Error).username + val session = + (signInState.setupTOTPState as SetupTOTPState.Error).session + + val event = SetupTOTPEvent( + SetupTOTPEvent.EventType.VerifyChallengeAnswer( + challengeResponse, + username, + session, + awsCognitoConfirmSignInOptions?.friendlyDeviceName + ) + ) + authStateMachine.send(event) + } + + else -> onError.accept(InvalidStateException()) + } + } + + else -> onError.accept(InvalidStateException()) + } + } ) } @@ -853,7 +983,7 @@ internal class RealAWSCognitoAuthPlugin( val authSignInResult = AuthSignInResult( true, - AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null) + AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null) ) onSuccess.accept(authSignInResult) sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) @@ -1001,7 +1131,9 @@ internal class RealAWSCognitoAuthPlugin( ) } _fetchAuthSession(onSuccess, onError) - } else onSuccess.accept(credential.getCognitoSession()) + } else { + onSuccess.accept(credential.getCognitoSession()) + } } is AuthorizationState.Error -> { val error = authZState.exception @@ -1218,7 +1350,8 @@ internal class RealAWSCognitoAuthPlugin( val pinpointEndpointId = authEnvironment.getPinpointEndpointId() ResetPasswordUseCase( - cognitoIdentityProviderClient, appClient, + cognitoIdentityProviderClient, + appClient, configuration.userPool?.appClientSecret ).execute( username, @@ -1343,8 +1476,8 @@ internal class RealAWSCognitoAuthPlugin( try { authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.changePassword( - changePasswordRequest - ) + changePasswordRequest + ) onSuccess.call() } catch (e: Exception) { onError.accept(CognitoAuthExceptionConverter.lookup(e, e.toString())) @@ -1360,7 +1493,6 @@ internal class RealAWSCognitoAuthPlugin( when (authState.authNState) { // Check if user signed in is AuthenticationState.SignedIn -> { - GlobalScope.launch { try { val accessToken = getSession().userPoolTokensResult.value?.accessToken @@ -1446,9 +1578,8 @@ internal class RealAWSCognitoAuthPlugin( private suspend fun updateUserAttributes( attributes: List, - userAttributesOptionsMetadata: Map?, + userAttributesOptionsMetadata: Map? ): MutableMap { - return suspendCoroutine { continuation -> authStateMachine.getCurrentState { authState -> @@ -1472,8 +1603,8 @@ internal class RealAWSCognitoAuthPlugin( } val userAttributeResponse = authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.updateUserAttributes( - userAttributesRequest - ) + userAttributesRequest + ) continuation.resume( getUpdateUserAttributeResult(userAttributeResponse, userAttributes) @@ -1497,14 +1628,12 @@ internal class RealAWSCognitoAuthPlugin( response: UpdateUserAttributesResponse?, userAttributeList: List ): MutableMap { - val finalResult = HashMap() response?.codeDeliveryDetailsList?.let { val codeDeliveryDetailsList = it for (item in codeDeliveryDetailsList) { item.attributeName?.let { - val deliveryMedium = AuthCodeDeliveryDetails.DeliveryMedium.fromString(item.deliveryMedium?.value) val authCodeDeliveryDetails = AuthCodeDeliveryDetails( item.destination.toString(), @@ -1561,13 +1690,12 @@ internal class RealAWSCognitoAuthPlugin( val getUserAttributeVerificationCodeResponse = authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.getUserAttributeVerificationCode( - getUserAttributeVerificationCodeRequest - ) + getUserAttributeVerificationCodeRequest + ) getUserAttributeVerificationCodeResponse?.codeDeliveryDetails?.let { val codeDeliveryDetails = it codeDeliveryDetails.attributeName?.let { - val deliveryMedium = AuthCodeDeliveryDetails.DeliveryMedium.fromString( codeDeliveryDetails.deliveryMedium?.value ) @@ -1629,8 +1757,8 @@ internal class RealAWSCognitoAuthPlugin( } authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.verifyUserAttribute( - verifyUserAttributeRequest - ) + verifyUserAttributeRequest + ) onSuccess.call() } ?: onError.accept(InvalidUserPoolConfigurationException()) } catch (e: Exception) { @@ -1877,7 +2005,6 @@ internal class RealAWSCognitoAuthPlugin( authZState is AuthorizationState.SessionEstablished || authZState is AuthorizationState.Error ) -> { - val existingCredential = when (authZState) { is AuthorizationState.SessionEstablished -> authZState.amplifyCredential is AuthorizationState.Error -> { @@ -1971,10 +2098,10 @@ internal class RealAWSCognitoAuthPlugin( authNState is AuthenticationState.FederatedToIdentityPool && authZState is AuthorizationState.SessionEstablished ) || ( - authZState is AuthorizationState.Error && - authZState.exception is SessionError && - authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated - ) -> { + authZState is AuthorizationState.Error && + authZState.exception is SessionError && + authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated + ) -> { val event = AuthenticationEvent(AuthenticationEvent.EventType.ClearFederationToIdentityPool()) authStateMachine.send(event) _clearFederationToIdentityPool(onSuccess, onError) @@ -1986,6 +2113,200 @@ internal class RealAWSCognitoAuthPlugin( } } + override fun setUpTOTP(onSuccess: Consumer, onError: Consumer) { + authStateMachine.getCurrentState { authState -> + when (authState.authNState) { + is AuthenticationState.SignedIn -> { + GlobalScope.launch { + try { + val accessToken = getSession().userPoolTokensResult.value?.accessToken + accessToken?.let { token -> + SessionHelper.getUsername(token)?.let { username -> + authEnvironment.cognitoAuthService + .cognitoIdentityProviderClient?.associateSoftwareToken { + this.accessToken = token + }?.also { response -> + response.secretCode?.let { secret -> + onSuccess.accept( + TOTPSetupDetails( + secret, + username + ) + ) + } + } + } + } ?: onError.accept(SignedOutException()) + } catch (error: Exception) { + onError.accept( + CognitoAuthExceptionConverter.lookup( + error, + "Cannot find a multi-factor authentication (MFA) method." + ) + ) + } + } + } + + else -> onError.accept(InvalidStateException()) + } + } + } + + override fun verifyTOTPSetup( + code: String, + onSuccess: Action, + onError: Consumer + ) { + verifyTotp(code, null, onSuccess, onError) + } + + override fun verifyTOTPSetup( + code: String, + options: AuthVerifyTOTPSetupOptions, + onSuccess: Action, + onError: Consumer + ) { + val cognitoOptions = options as? AWSCognitoAuthVerifyTOTPSetupOptions + verifyTotp(code, cognitoOptions?.friendlyDeviceName, onSuccess, onError) + } + + fun fetchMFAPreference( + onSuccess: Consumer, + onError: Consumer + ) { + authStateMachine.getCurrentState { authState -> + when (authState.authNState) { + is AuthenticationState.SignedIn -> { + GlobalScope.launch { + try { + val accessToken = getSession().userPoolTokensResult.value?.accessToken + accessToken?.let { token -> + authEnvironment.cognitoAuthService + .cognitoIdentityProviderClient?.getUser { + this.accessToken = token + }?.also { response -> + var enabledSet: MutableSet? = null + var preferred: MFAType? = null + if (!response.userMfaSettingList.isNullOrEmpty()) { + enabledSet = mutableSetOf() + response.userMfaSettingList?.forEach { mfaType -> + enabledSet.add(MFAType.toMFAType(mfaType)) + } + } + response.preferredMfaSetting?.let { preferredMFA -> + preferred = MFAType.toMFAType(preferredMFA) + } + onSuccess.accept(UserMFAPreference(enabledSet, preferred)) + } + } ?: onError.accept(SignedOutException()) + } catch (error: Exception) { + onError.accept( + CognitoAuthExceptionConverter.lookup( + error, + "Cannot update the MFA preferences" + ) + ) + } + } + } + + else -> onError.accept(InvalidStateException()) + } + } + } + + fun updateMFAPreference( + sms: MFAPreference?, + totp: MFAPreference?, + onSuccess: Action, + onError: Consumer + ) { + authStateMachine.getCurrentState { authState -> + when (authState.authNState) { + is AuthenticationState.SignedIn -> { + GlobalScope.launch { + try { + val accessToken = getSession().userPoolTokensResult.value?.accessToken + accessToken?.let { token -> + authEnvironment.cognitoAuthService.cognitoIdentityProviderClient?.setUserMfaPreference { + this.accessToken = token + this.smsMfaSettings = sms?.let { + SmsMfaSettingsType.invoke { + enabled = it.mfaEnabled + preferredMfa = it.mfaPreferred + } + } + this.softwareTokenMfaSettings = totp?.let { + SoftwareTokenMfaSettingsType.invoke { + enabled = it.mfaEnabled + preferredMfa = it.mfaPreferred + } + } + }?.also { + onSuccess.call() + } + } ?: onError.accept(SignedOutException()) + } catch (error: Exception) { + onError.accept( + CognitoAuthExceptionConverter.lookup( + error, + "Amazon Cognito cannot update the MFA preferences" + ) + ) + } + } + } + + else -> onError.accept(InvalidStateException()) + } + } + } + + private fun verifyTotp( + code: String, + friendlyDeviceName: String?, + onSuccess: Action, + onError: Consumer + ) { + authStateMachine.getCurrentState { authState -> + when (authState.authNState) { + is AuthenticationState.SignedIn -> { + GlobalScope.launch { + try { + val accessToken = getSession().userPoolTokensResult.value?.accessToken + accessToken?.let { token -> + authEnvironment.cognitoAuthService + .cognitoIdentityProviderClient?.verifySoftwareToken { + this.userCode = code + this.friendlyDeviceName = friendlyDeviceName + this.accessToken = token + }?.also { + when (it.status) { + is VerifySoftwareTokenResponseType.Success -> onSuccess.call() + else -> throw ServiceException( + message = "An unknown service error has occurred", + recoverySuggestion = AmplifyException.TODO_RECOVERY_SUGGESTION + ) + } + } + } ?: onError.accept(SignedOutException()) + } catch (error: Exception) { + onError.accept( + CognitoAuthExceptionConverter.lookup( + error, + "Amazon Cognito cannot find a multi-factor authentication (MFA) method." + ) + ) + } + } + } + + else -> onError.accept(InvalidStateException()) + } + } + } + private fun _clearFederationToIdentityPool(onSuccess: Action, onError: Consumer) { _signOut(sendHubEvent = false) { when (it) { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/UserMFAPreference.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/UserMFAPreference.kt new file mode 100644 index 0000000000..61c629a461 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/UserMFAPreference.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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 com.amplifyframework.auth.MFAType + +public data class UserMFAPreference( + val enabled: Set?, + val preferred: MFAType? +) + +public sealed class MFAPreference { + abstract val mfaEnabled: Boolean + abstract val mfaPreferred: Boolean + + object Disabled : MFAPreference() { + override val mfaEnabled: Boolean + get() = false + override val mfaPreferred: Boolean + get() = false + } + + object Enabled : MFAPreference() { + override val mfaEnabled: Boolean + get() = true + override val mfaPreferred: Boolean + get() = false + } + + object Preferred : MFAPreference() { + override val mfaEnabled: Boolean + get() = true + override val mfaPreferred: Boolean + get() = true + } + + object NotPreferred : MFAPreference() { + override val mfaEnabled: Boolean + get() = true + override val mfaPreferred: Boolean + get() = false + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActions.kt index 4e6e211e57..ad756218a7 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SRPCognitoActions.kt @@ -83,7 +83,9 @@ internal object SRPCognitoActions : SRPActions { SRPEvent( SRPEvent.EventType.RespondPasswordVerifier( - challengeParams, event.metadata, initiateAuthResponse.session + challengeParams, + event.metadata, + initiateAuthResponse.session ) ) } ?: throw Exception("Auth challenge parameters are empty.") @@ -142,7 +144,9 @@ internal object SRPCognitoActions : SRPActions { SRPEvent( SRPEvent.EventType.RespondPasswordVerifier( - challengeParams, event.metadata, initiateAuthResponse.session + challengeParams, + event.metadata, + initiateAuthResponse.session ) ) } ?: throw ServiceException( @@ -188,7 +192,7 @@ internal object SRPCognitoActions : SRPActions { KEY_USERNAME to username, KEY_PASSWORD_CLAIM_SECRET_BLOCK to secretBlock, KEY_PASSWORD_CLAIM_SIGNATURE to srpHelper.getSignature(salt, srpB, secretBlock), - KEY_TIMESTAMP to srpHelper.dateString, + KEY_TIMESTAMP to srpHelper.dateString ) secretHash?.let { challengeParams[KEY_SECRET_HASH] = it } challengeParams[KEY_DEVICE_KEY] = deviceKey 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 new file mode 100644 index 0000000000..7c00b0892d --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActions.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2023 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.actions + +import aws.sdk.kotlin.services.cognitoidentityprovider.associateSoftwareToken +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponseType +import aws.sdk.kotlin.services.cognitoidentityprovider.respondToAuthChallenge +import aws.sdk.kotlin.services.cognitoidentityprovider.verifySoftwareToken +import com.amplifyframework.AmplifyException +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.helpers.SignInChallengeHelper +import com.amplifyframework.auth.exceptions.ServiceException +import com.amplifyframework.statemachine.Action +import com.amplifyframework.statemachine.codegen.actions.SetupTOTPActions +import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent + +internal object SetupTOTPCognitoActions : SetupTOTPActions { + private const val KEY_DEVICE_KEY = "DEVICE_KEY" + override fun initiateTOTPSetup(eventType: SetupTOTPEvent.EventType.SetupTOTP): Action = Action( + "InitiateTOTPSetup" + ) { id, dispatcher -> + logger.verbose("$id Starting execution") + val evt = try { + val response = cognitoAuthService.cognitoIdentityProviderClient?.associateSoftwareToken { + session = eventType.totpSetupDetails.session + } + response?.secretCode?.let { secret -> + SetupTOTPEvent( + SetupTOTPEvent.EventType.WaitForAnswer( + SignInTOTPSetupData(secret, response.session, eventType.totpSetupDetails.username) + ) + ) + } ?: SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError( + Exception("Software token setup failed"), + eventType.totpSetupDetails.username, + eventType.totpSetupDetails.session + ) + ) + } catch (e: Exception) { + SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError( + e, + eventType.totpSetupDetails.username, + eventType.totpSetupDetails.session + ) + ) + } + logger.verbose("$id Sending event ${evt.type}") + dispatcher.send(evt) + } + + override fun verifyChallengeAnswer( + eventType: SetupTOTPEvent.EventType.VerifyChallengeAnswer + ): Action = + Action("verifyChallengeAnswer") { id, dispatcher -> + logger.verbose("$id Starting execution") + val evt = try { + val response = cognitoAuthService.cognitoIdentityProviderClient?.verifySoftwareToken { + userCode = eventType.answer + this.session = eventType.session + this.friendlyDeviceName = eventType.friendlyDeviceName + } + + response?.let { + when (it.status) { + is VerifySoftwareTokenResponseType.Success -> { + SetupTOTPEvent( + SetupTOTPEvent.EventType.RespondToAuthChallenge( + eventType.username, + it.session + ) + ) + } + else -> { + SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError( + ServiceException( + message = "An unknown service error has occurred", + recoverySuggestion = AmplifyException.TODO_RECOVERY_SUGGESTION + ), + eventType.username, + eventType.session + ) + ) + } + } + } ?: SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError( + Exception("Software token verification failed"), + eventType.username, + eventType.session + ) + ) + } catch (exception: Exception) { + SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError( + exception, + eventType.username, + eventType.session + ) + ) + } + logger.verbose("$id Sending event ${evt.type}") + dispatcher.send(evt) + } + + override fun respondToAuthChallenge( + eventType: SetupTOTPEvent.EventType.RespondToAuthChallenge + ): Action = + Action("RespondToAuthChallenge") { id, dispatcher -> + logger.verbose("$id Starting execution") + val evt = try { + val challengeResponses = mutableMapOf() + challengeResponses["USERNAME"] = eventType.username + val deviceMetadata = getDeviceMetadata(eventType.username) + deviceMetadata?.deviceKey?.let { challengeResponses[KEY_DEVICE_KEY] = it } + val encodedContextData = getUserContextData(eventType.username) + val pinpointEndpointId = getPinpointEndpointId() + + val response = cognitoAuthService.cognitoIdentityProviderClient?.respondToAuthChallenge { + this.session = eventType.session + this.challengeResponses = challengeResponses + challengeName = ChallengeNameType.MfaSetup + clientId = configuration.userPool?.appClient + pinpointEndpointId?.let { analyticsMetadata { analyticsEndpointId = it } } + encodedContextData?.let { this.userContextData { encodedData = it } } + } + + response?.let { + SignInChallengeHelper.evaluateNextStep( + username = eventType.username, + challengeNameType = response.challengeName, + session = response.session, + challengeParameters = response.challengeParameters, + authenticationResult = response.authenticationResult + ) + } ?: SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError( + Exception("Software token verification failed"), + eventType.username, + eventType.session + ) + ) + } catch (exception: Exception) { + SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError(exception, eventType.username, eventType.session) + ) + } + dispatcher.send(evt) + } +} 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 71dc49d44f..55c92442a1 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 @@ -56,7 +56,6 @@ internal object SignInChallengeCognitoActions : SignInChallengeActions { val encodedContextData = username?.let { getUserContextData(it) } val pinpointEndpointId = getPinpointEndpointId() - val response = cognitoAuthService.cognitoIdentityProviderClient?.respondToAuthChallenge { clientId = configuration.userPool?.appClient challengeName = ChallengeNameType.fromValue(challenge.challengeName) @@ -90,7 +89,8 @@ internal object SignInChallengeCognitoActions : SignInChallengeActions { return when (ChallengeNameType.fromValue(challengeName)) { is ChallengeNameType.SmsMfa -> "SMS_MFA_CODE" is ChallengeNameType.NewPasswordRequired -> "NEW_PASSWORD" - is ChallengeNameType.CustomChallenge -> "ANSWER" + is ChallengeNameType.CustomChallenge, ChallengeNameType.SelectMfaType -> "ANSWER" + is ChallengeNameType.SoftwareTokenMfa -> "SOFTWARE_TOKEN_MFA_CODE" 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 bb92d7db1e..2dfe25a605 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 @@ -32,6 +32,7 @@ import com.amplifyframework.statemachine.codegen.events.CustomSignInEvent import com.amplifyframework.statemachine.codegen.events.DeviceSRPSignInEvent import com.amplifyframework.statemachine.codegen.events.HostedUIEvent import com.amplifyframework.statemachine.codegen.events.SRPEvent +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent import com.amplifyframework.statemachine.codegen.events.SignInEvent @@ -137,4 +138,12 @@ internal object SignInCognitoActions : SignInActions { logger.verbose("$id Sending event ${evt.type}") dispatcher.send(evt) } + + override fun initiateTOTPSetupAction(event: SignInEvent.EventType.InitiateTOTPSetup) = + Action("initiateTOTPSetup") { id, dispatcher -> + logger.verbose("$id Starting execution") + val evt = SetupTOTPEvent(SetupTOTPEvent.EventType.SetupTOTP(event.signInTOTPSetupData)) + logger.verbose("$id Sending event ${evt.type}") + dispatcher.send(evt) + } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMfaException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMfaException.kt new file mode 100644 index 0000000000..2cdd44c28a --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMfaException.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 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.exceptions.service + +import com.amplifyframework.auth.AuthException + +/** + * Software Token MFA is not enabled for the user. + * @param cause The underlying cause of this exception + */ +open class EnableSoftwareTokenMfaException(cause: Throwable?) : + AuthException( + "Software token TOTP multi-factor authentication (MFA) is not enabled for the user pool.", + "Enable the software token MFA for the user.", + cause + ) 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 42c1ac377f..b3e1c106bd 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 @@ -18,8 +18,10 @@ package com.amplifyframework.auth.cognito.helpers import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthenticationResultType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.smithy.kotlin.runtime.time.Instant +import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.exceptions.UnknownException import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.auth.result.step.AuthNextSignInStep @@ -30,6 +32,7 @@ import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.data.CognitoUserPoolTokens import com.amplifyframework.statemachine.codegen.data.DeviceMetadata import com.amplifyframework.statemachine.codegen.data.SignInMethod +import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData import com.amplifyframework.statemachine.codegen.data.SignedInData import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.SignInEvent @@ -78,11 +81,17 @@ internal object SignInChallengeHelper { } challengeNameType is ChallengeNameType.SmsMfa || challengeNameType is ChallengeNameType.CustomChallenge || - challengeNameType is ChallengeNameType.NewPasswordRequired -> { + challengeNameType is ChallengeNameType.NewPasswordRequired || + challengeNameType is ChallengeNameType.SoftwareTokenMfa || + challengeNameType is ChallengeNameType.SelectMfaType -> { val challenge = AuthChallenge(challengeNameType.value, username, session, challengeParameters) SignInEvent(SignInEvent.EventType.ReceivedChallenge(challenge)) } + challengeNameType is ChallengeNameType.MfaSetup -> { + val setupTOTPData = SignInTOTPSetupData("", session, username) + SignInEvent(SignInEvent.EventType.InitiateTOTPSetup(setupTOTPData)) + } challengeNameType is ChallengeNameType.DeviceSrpAuth -> { SignInEvent(SignInEvent.EventType.InitiateSignInWithDeviceSRP(username, mapOf())) } @@ -93,7 +102,9 @@ internal object SignInChallengeHelper { fun getNextStep( challenge: AuthChallenge, onSuccess: Consumer, - onError: Consumer + onError: Consumer, + signInTOTPSetupData: SignInTOTPSetupData? = null, + allowedMFAType: Set? = null ) { val challengeParams = challenge.parameters?.toMutableMap() ?: mapOf() @@ -107,25 +118,96 @@ internal object SignInChallengeHelper { ) val authSignInResult = AuthSignInResult( false, - AuthNextSignInStep(AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE, mapOf(), deliveryDetails) + AuthNextSignInStep( + AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE, + mapOf(), + deliveryDetails, + null, + null + ) ) onSuccess.accept(authSignInResult) } is ChallengeNameType.NewPasswordRequired -> { val authSignInResult = AuthSignInResult( false, - AuthNextSignInStep(AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD, challengeParams, null) + AuthNextSignInStep( + AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD, + challengeParams, + null, + null, + null + ) ) onSuccess.accept(authSignInResult) } is ChallengeNameType.CustomChallenge -> { val authSignInResult = AuthSignInResult( false, - AuthNextSignInStep(AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE, challengeParams, null) + AuthNextSignInStep( + AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE, + challengeParams, + null, + null, + null + ) + ) + onSuccess.accept(authSignInResult) + } + is ChallengeNameType.SoftwareTokenMfa -> { + val authSignInResult = AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE, + mapOf(), + null, + null, + null + ) + ) + onSuccess.accept(authSignInResult) + } + is ChallengeNameType.MfaSetup -> { + signInTOTPSetupData?.let { + val authSignInResult = AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP, + challengeParams, + null, + TOTPSetupDetails(it.secretCode, it.username), + allowedMFAType + ) + ) + onSuccess.accept(authSignInResult) + } ?: onError.accept(UnknownException(cause = Exception("Challenge type not supported."))) + } + is ChallengeNameType.SelectMfaType -> { + val authSignInResult = AuthSignInResult( + false, + AuthNextSignInStep( + AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION, + mapOf(), + null, + null, + challengeParams["MFAS_CAN_CHOOSE"]?.let { getAllowedMFATypes(it) } + ) ) onSuccess.accept(authSignInResult) } else -> onError.accept(UnknownException(cause = Exception("Challenge type not supported."))) } } + + 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.")) + } + } + return result + } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions.kt index 88bcc66fc1..08cf7fc78d 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthConfirmSignInOptions.kt @@ -31,7 +31,12 @@ data class AWSCognitoAuthConfirmSignInOptions internal constructor( * Get additional user attributes which should be associated with this user on confirmSignIn. * @return additional user attributes which should be associated with this user on confirmSignIn */ - val userAttributes: List + val userAttributes: List, + /** + * Get the friendly device name used to setup TOTP. + * @return friendly device name + */ + val friendlyDeviceName: String? ) : AuthConfirmSignInOptions() { companion object { @@ -53,6 +58,7 @@ data class AWSCognitoAuthConfirmSignInOptions internal constructor( class CognitoBuilder : Builder() { private var metadata: Map = mapOf() private var userAttributes: List = listOf() + private var friendlyDeviceName: String? = null /** * Returns the type of builder this is to support proper flow with it being an extended class. @@ -77,10 +83,17 @@ data class AWSCognitoAuthConfirmSignInOptions internal constructor( */ fun userAttributes(userAttributes: List) = apply { this.userAttributes = userAttributes } + /** + * Set the friendlyDeviceName field for the object being built. + * @param friendlyDeviceName friendly name of the device used to setup totp. + * @return the instance of the builder. + */ + fun friendlyDeviceName(friendlyDeviceName: String) = apply { this.friendlyDeviceName = friendlyDeviceName } + /** * Construct and return the object with the values set in the builder. * @return a new instance of AWSCognitoAuthConfirmSignInOptions with the values specified in the builder. */ - override fun build() = AWSCognitoAuthConfirmSignInOptions(metadata, userAttributes) + override fun build() = AWSCognitoAuthConfirmSignInOptions(metadata, userAttributes, friendlyDeviceName) } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java new file mode 100644 index 0000000000..f8794014d3 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 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.options; + +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; + +/** + * Cognito extension of update verify totp setup options to add the platform specific fields. + */ +public final class AWSCognitoAuthVerifyTOTPSetupOptions extends AuthVerifyTOTPSetupOptions { + + private String friendlyDeviceName; + + private AWSCognitoAuthVerifyTOTPSetupOptions(String friendlyDeviceName) { + this.friendlyDeviceName = friendlyDeviceName; + } + + /** + * Return the friendlyDeviceName to set during cognito TOTP setup. + * @return friendlyDeviceName string + * */ + public String getFriendlyDeviceName() { + return friendlyDeviceName; + } + + /** + * The builder for this class. + */ + public static final class CognitoBuilder extends Builder { + private String friendlyDeviceName; + + private String getFriendlyDeviceName() { + return friendlyDeviceName; + } + + /** + * Friendly device name to be set in Cognito. + * @param friendlyDeviceName String input for friendlyDeviceName + * @return current CognitoBuilder instance + * */ + public CognitoBuilder setFriendlyDeviceName(String friendlyDeviceName) { + this.friendlyDeviceName = friendlyDeviceName; + return this; + } + + /** + * Construct and return the object with the values set in the builder. + * @return a new instance of AWSCognitoAuthVerifyTOTPSetupOptions with the values specified in the builder. + */ + public AWSCognitoAuthVerifyTOTPSetupOptions build() { + return new AWSCognitoAuthVerifyTOTPSetupOptions(getFriendlyDeviceName()); + } + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SetupTOTPActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SetupTOTPActions.kt new file mode 100644 index 0000000000..5d47ddc599 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SetupTOTPActions.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 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.statemachine.codegen.actions + +import com.amplifyframework.statemachine.Action +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent + +internal interface SetupTOTPActions { + fun initiateTOTPSetup(eventType: SetupTOTPEvent.EventType.SetupTOTP): Action + fun verifyChallengeAnswer( + eventType: SetupTOTPEvent.EventType.VerifyChallengeAnswer + ): Action + + fun respondToAuthChallenge( + eventType: SetupTOTPEvent.EventType.RespondToAuthChallenge + ): Action +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SignInActions.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SignInActions.kt index bdb56e9184..ba8f868f3f 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SignInActions.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/actions/SignInActions.kt @@ -27,4 +27,5 @@ internal interface SignInActions { fun initResolveChallenge(event: SignInEvent.EventType.ReceivedChallenge): Action fun confirmDevice(event: SignInEvent.EventType.ConfirmDevice): Action fun startHostedUIAuthAction(event: SignInEvent.EventType.InitiateHostedUISignIn): Action + fun initiateTOTPSetupAction(event: SignInEvent.EventType.InitiateTOTPSetup): Action } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt new file mode 100644 index 0000000000..95b065a2dc --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2023 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.statemachine.codegen.data + +internal data class SignInTOTPSetupData( + val secretCode: String, + val session: String?, + val username: String +) 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 new file mode 100644 index 0000000000..9bf7adefae --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/events/SetupTOTPEvent.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2023 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.statemachine.codegen.events + +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData +import java.util.Date + +internal class SetupTOTPEvent(val eventType: EventType, override val time: Date? = null) : + StateMachineEvent { + + sealed class EventType { + data class SetupTOTP(val totpSetupDetails: SignInTOTPSetupData) : EventType() + data class WaitForAnswer(val totpSetupDetails: SignInTOTPSetupData) : EventType() + data class ThrowAuthError(val exception: Exception, val username: String, val session: String?) : EventType() + data class VerifyChallengeAnswer( + val answer: String, + val username: String, + val session: String?, + val friendlyDeviceName: String? + ) : + EventType() + + data class RespondToAuthChallenge(val username: String, val session: String?) : EventType() + data class Verified(val id: String = "") : EventType() + } + + override val type: String = eventType.javaClass.simpleName +} 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 9a2b2d9cdc..7f0c367af3 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 @@ -19,6 +19,7 @@ import com.amplifyframework.statemachine.StateMachineEvent import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.data.DeviceMetadata import com.amplifyframework.statemachine.codegen.data.SignInData +import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData import com.amplifyframework.statemachine.codegen.data.SignedInData import java.util.Date @@ -61,6 +62,7 @@ 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() } 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 new file mode 100644 index 0000000000..abc12255c9 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SetupTOTPState.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2023 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.statemachine.codegen.states + +import com.amplifyframework.statemachine.State +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.StateMachineResolver +import com.amplifyframework.statemachine.StateResolution +import com.amplifyframework.statemachine.codegen.actions.SetupTOTPActions +import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent + +internal sealed class SetupTOTPState : State { + data class NotStarted(val id: String = "") : SetupTOTPState() + data class SetupTOTP(val signInTOTPSetupData: SignInTOTPSetupData) : SetupTOTPState() + data class WaitingForAnswer( + val signInTOTPSetupData: SignInTOTPSetupData, + var hasNewResponse: Boolean = false + ) : SetupTOTPState() + data class Verifying(val code: String, val username: String, val session: String?) : SetupTOTPState() + data class RespondingToAuthChallenge(val username: String, val session: String?) : SetupTOTPState() + data class Success(val id: String = "") : SetupTOTPState() + data class Error( + val exception: Exception, + val username: String, + val session: String?, + var hasNewResponse: Boolean = false + ) : SetupTOTPState() + + class Resolver(private val setupTOTPActions: SetupTOTPActions) : StateMachineResolver { + override val defaultState = NotStarted("default") + + override fun resolve(oldState: SetupTOTPState, event: StateMachineEvent): StateResolution { + val defaultResolution = StateResolution(oldState) + val challengeEvent = (event as? SetupTOTPEvent)?.eventType + return when (oldState) { + is NotStarted -> when (challengeEvent) { + is SetupTOTPEvent.EventType.SetupTOTP -> { + StateResolution( + SetupTOTP(challengeEvent.totpSetupDetails), + listOf(setupTOTPActions.initiateTOTPSetup(challengeEvent)) + ) + } + + is SetupTOTPEvent.EventType.ThrowAuthError -> StateResolution( + Error(challengeEvent.exception, challengeEvent.username, challengeEvent.session) + ) + + else -> defaultResolution + } + + is SetupTOTP -> when (challengeEvent) { + is SetupTOTPEvent.EventType.WaitForAnswer -> { + StateResolution(WaitingForAnswer(challengeEvent.totpSetupDetails, true)) + } + + is SetupTOTPEvent.EventType.ThrowAuthError -> StateResolution( + Error(challengeEvent.exception, challengeEvent.username, challengeEvent.session) + ) + + else -> defaultResolution + } + + is WaitingForAnswer -> when (challengeEvent) { + is SetupTOTPEvent.EventType.VerifyChallengeAnswer -> { + StateResolution( + Verifying( + challengeEvent.answer, + oldState.signInTOTPSetupData.username, + oldState.signInTOTPSetupData.session + ), + listOf(setupTOTPActions.verifyChallengeAnswer(challengeEvent)) + ) + } + + is SetupTOTPEvent.EventType.ThrowAuthError -> StateResolution( + Error(challengeEvent.exception, challengeEvent.username, challengeEvent.session) + ) + + else -> defaultResolution + } + + is Verifying -> when (challengeEvent) { + is SetupTOTPEvent.EventType.RespondToAuthChallenge -> { + StateResolution( + RespondingToAuthChallenge(oldState.username, oldState.session), + listOf( + setupTOTPActions.respondToAuthChallenge( + challengeEvent + ) + ) + ) + } + + is SetupTOTPEvent.EventType.ThrowAuthError -> StateResolution( + Error(challengeEvent.exception, challengeEvent.username, challengeEvent.session, true) + ) + + else -> defaultResolution + } + + is RespondingToAuthChallenge -> when (challengeEvent) { + is SetupTOTPEvent.EventType.Verified -> { + StateResolution( + Success() + ) + } + + is SetupTOTPEvent.EventType.ThrowAuthError -> StateResolution( + Error(challengeEvent.exception, challengeEvent.username, challengeEvent.session) + ) + + else -> defaultResolution + } + + is Error -> when (challengeEvent) { + is SetupTOTPEvent.EventType.VerifyChallengeAnswer -> { + StateResolution( + Verifying(challengeEvent.answer, "", null), + listOf(setupTOTPActions.verifyChallengeAnswer(challengeEvent)) + ) + } + + is SetupTOTPEvent.EventType.WaitForAnswer -> { + StateResolution(WaitingForAnswer(challengeEvent.totpSetupDetails, true)) + } + + else -> defaultResolution + } + + else -> defaultResolution + } + } + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt index c5281987b6..d26299c83a 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt @@ -22,6 +22,7 @@ import com.amplifyframework.statemachine.StateResolution import com.amplifyframework.statemachine.codegen.actions.SignInChallengeActions import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent +import com.amplifyframework.statemachine.codegen.events.SignInEvent internal sealed class SignInChallengeState : State { data class NotStarted(val id: String = "") : SignInChallengeState() @@ -50,6 +51,7 @@ internal sealed class SignInChallengeState : State { ): StateResolution { val defaultResolution = StateResolution(oldState) val challengeEvent = asSignInChallengeEvent(event) + val signInEvent = (event as? SignInEvent)?.eventType return when (oldState) { is NotStarted -> when (challengeEvent) { is SignInChallengeEvent.EventType.WaitForAnswer -> { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInState.kt index 5fecde353d..b5317633b1 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInState.kt @@ -31,6 +31,7 @@ internal sealed class SignInState : State { data class SigningInViaMigrateAuth(override var migrateSignInState: MigrateSignInState?) : SignInState() data class ResolvingDeviceSRP(override var deviceSRPSignInState: DeviceSRPSignInState?) : SignInState() data class ResolvingChallenge(override var challengeState: SignInChallengeState?) : SignInState() + data class ResolvingTOTPSetup(override var setupTOTPState: SetupTOTPState?) : SignInState() data class ConfirmingDevice(val id: String = "") : SignInState() data class Done(val id: String = "") : SignInState() data class Error(val exception: Exception) : SignInState() @@ -42,6 +43,7 @@ internal sealed class SignInState : State { open var migrateSignInState: MigrateSignInState? = MigrateSignInState.NotStarted() open var hostedUISignInState: HostedUISignInState? = HostedUISignInState.NotStarted() open var deviceSRPSignInState: DeviceSRPSignInState? = DeviceSRPSignInState.NotStarted() + open var setupTOTPState: SetupTOTPState? = SetupTOTPState.NotStarted() class Resolver( private val srpSignInResolver: StateMachineResolver, @@ -50,7 +52,8 @@ internal sealed class SignInState : State { private val challengeResolver: StateMachineResolver, private val hostedUISignInResolver: StateMachineResolver, private val deviceSRPSignInResolver: StateMachineResolver, - private val signInActions: SignInActions, + private val setupTOTPResolver: StateMachineResolver, + private val signInActions: SignInActions ) : StateMachineResolver { override val defaultState = NotStarted() @@ -94,6 +97,10 @@ internal sealed class SignInState : State { actions += it.actions } + oldState.setupTOTPState?.let { setupTOTPResolver.resolve(it, event) }?.let { + builder.setupTOTPState = it.newState + actions += it.actions + } return StateResolution(builder.build(), actions) } @@ -109,73 +116,131 @@ internal sealed class SignInState : State { SigningInWithSRP(oldState.srpSignInState), listOf(signInActions.startSRPAuthAction(signInEvent)) ) + is SignInEvent.EventType.InitiateSignInWithCustom -> StateResolution( SigningInWithCustom(oldState.customSignInState), listOf(signInActions.startCustomAuthAction(signInEvent)) ) + is SignInEvent.EventType.InitiateHostedUISignIn -> StateResolution( SigningInWithHostedUI(HostedUISignInState.NotStarted()), listOf(signInActions.startHostedUIAuthAction(signInEvent)) ) + is SignInEvent.EventType.InitiateMigrateAuth -> StateResolution( SigningInViaMigrateAuth(MigrateSignInState.NotStarted()), listOf(signInActions.startMigrationAuthAction(signInEvent)) ) + is SignInEvent.EventType.InitiateCustomSignInWithSRP -> StateResolution( SigningInWithSRPCustom(oldState.srpSignInState), listOf(signInActions.startCustomAuthWithSRPAction(signInEvent)) ) + else -> defaultResolution } + is SigningInWithSRP, is SigningInWithCustom, is SigningInViaMigrateAuth, - is SigningInWithSRPCustom -> when (signInEvent) { + is SigningInWithSRPCustom + -> when (signInEvent) { is SignInEvent.EventType.ReceivedChallenge -> { val action = signInActions.initResolveChallenge(signInEvent) StateResolution(ResolvingChallenge(oldState.challengeState), listOf(action)) } + is SignInEvent.EventType.InitiateSignInWithDeviceSRP -> StateResolution( ResolvingDeviceSRP(DeviceSRPSignInState.NotStarted()), listOf(signInActions.startDeviceSRPAuthAction(signInEvent)) ) + is SignInEvent.EventType.ConfirmDevice -> { val action = signInActions.confirmDevice(signInEvent) StateResolution(ConfirmingDevice(), listOf(action)) } + + is SignInEvent.EventType.InitiateTOTPSetup -> StateResolution( + ResolvingTOTPSetup(oldState.setupTOTPState), + listOf(signInActions.initiateTOTPSetupAction(signInEvent)) + ) + is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) else -> defaultResolution } + is ResolvingChallenge -> when (signInEvent) { is SignInEvent.EventType.ConfirmDevice -> { val action = signInActions.confirmDevice(signInEvent) StateResolution(ConfirmingDevice(), listOf(action)) } + is SignInEvent.EventType.ReceivedChallenge -> { val action = signInActions.initResolveChallenge(signInEvent) StateResolution(ResolvingChallenge(oldState.challengeState), listOf(action)) } + + is SignInEvent.EventType.InitiateTOTPSetup -> StateResolution( + ResolvingTOTPSetup(oldState.setupTOTPState), + listOf(signInActions.initiateTOTPSetupAction(signInEvent)) + ) + is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) else -> defaultResolution } + + is ResolvingTOTPSetup -> when (signInEvent) { + is SignInEvent.EventType.ReceivedChallenge -> { + val action = signInActions.initResolveChallenge(signInEvent) + StateResolution(ResolvingChallenge(oldState.challengeState), listOf(action)) + } + + is SignInEvent.EventType.ConfirmDevice -> { + val action = signInActions.confirmDevice(signInEvent) + StateResolution(ConfirmingDevice(), listOf(action)) + } + + is SignInEvent.EventType.InitiateSignInWithDeviceSRP -> StateResolution( + ResolvingDeviceSRP(DeviceSRPSignInState.NotStarted()), + listOf(signInActions.startDeviceSRPAuthAction(signInEvent)) + ) + + is SignInEvent.EventType.FinalizeSignIn -> { + StateResolution(SignedIn()) + } + + else -> defaultResolution + } + is ResolvingDeviceSRP -> when (signInEvent) { is SignInEvent.EventType.ReceivedChallenge -> { val action = signInActions.initResolveChallenge(signInEvent) StateResolution(ResolvingChallenge(oldState.challengeState), listOf(action)) } + + is SignInEvent.EventType.InitiateTOTPSetup -> StateResolution( + ResolvingTOTPSetup(SetupTOTPState.NotStarted()), + listOf(signInActions.initiateTOTPSetupAction(signInEvent)) + ) + is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) + else -> defaultResolution } + is ConfirmingDevice -> when (signInEvent) { is SignInEvent.EventType.FinalizeSignIn -> { StateResolution(SignedIn()) } + is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) else -> defaultResolution } + is SigningInWithHostedUI -> when (signInEvent) { is SignInEvent.EventType.SignedIn -> StateResolution(Done()) is SignInEvent.EventType.ThrowError -> StateResolution(Error(signInEvent.exception)) else -> defaultResolution } + else -> defaultResolution } } @@ -189,6 +254,7 @@ internal sealed class SignInState : State { var migrateSignInState: MigrateSignInState? = null var hostedUISignInState: HostedUISignInState? = null var deviceSRPSignInState: DeviceSRPSignInState? = null + var setupTOTPState: SetupTOTPState? = null override fun build(): SignInState = when (signInState) { is SigningInWithSRP -> SigningInWithSRP(srpSignInState) @@ -198,6 +264,7 @@ internal sealed class SignInState : State { is SigningInWithHostedUI -> SigningInWithHostedUI(hostedUISignInState) is SigningInWithSRPCustom -> SigningInWithSRPCustom(srpSignInState) is ResolvingDeviceSRP -> ResolvingDeviceSRP(deviceSRPSignInState) + is ResolvingTOTPSetup -> ResolvingTOTPSetup(setupTOTPState) else -> signInState } } 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 4a7738e2be..a180bb6ef9 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 @@ -17,6 +17,7 @@ package com.amplifyframework.auth.cognito import android.app.Activity import android.content.Intent +import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthException @@ -25,6 +26,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions @@ -681,6 +683,53 @@ class AWSCognitoAuthPluginTest { verify(timeout = CHANNEL_TIMEOUT) { realPlugin.clearFederationToIdentityPool(any(), any()) } } + @Test + fun setUpTOTP() { + val expectedOnSuccess = Consumer { } + val expectedOnError = Consumer { } + authPlugin.setUpTOTP(expectedOnSuccess, expectedOnError) + verify(timeout = CHANNEL_TIMEOUT) { realPlugin.setUpTOTP(any(), any()) } + } + + @Test + fun verifyTOTPSetup() { + val code = "123456" + val expectedOnSuccess = Action { } + val expectedOnError = Consumer { } + authPlugin.verifyTOTPSetup(code, expectedOnSuccess, expectedOnError) + verify(timeout = CHANNEL_TIMEOUT) { realPlugin.verifyTOTPSetup(code, any(), any(), any()) } + } + + @Test + fun verifyTOTPSetupWithOptions() { + val code = "123456" + val options = AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().setFriendlyDeviceName("DEVICE_NAME").build() + val expectedOnSuccess = Action { } + val expectedOnError = Consumer { } + authPlugin.verifyTOTPSetup(code, options, expectedOnSuccess, expectedOnError) + verify(timeout = CHANNEL_TIMEOUT) { realPlugin.verifyTOTPSetup(code, options, any(), any()) } + } + + @Test + fun fetchMFAPreferences() { + val expectedOnSuccess = Consumer { } + val expectedOnError = Consumer { } + authPlugin.fetchMFAPreference(expectedOnSuccess, expectedOnError) + verify(timeout = CHANNEL_TIMEOUT) { realPlugin.fetchMFAPreference(any(), any()) } + } + + @Test + fun updateMFAPreferences() { + val smsPreference = MFAPreference.Enabled + val totpPreference = MFAPreference.Preferred + val onSuccess = Action { } + val onError = Consumer { } + authPlugin.updateMFAPreference(smsPreference, totpPreference, onSuccess, onError) + verify(timeout = CHANNEL_TIMEOUT) { + realPlugin.updateMFAPreference(smsPreference, totpPreference, any(), any()) + } + } + @Test fun verifyPluginKey() { assertEquals("awsCognitoAuthPlugin", authPlugin.pluginKey) 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 d1053e6c03..e35e8708d9 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 @@ -16,10 +16,13 @@ package com.amplifyframework.auth.cognito import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.getUser import aws.sdk.kotlin.services.cognitoidentityprovider.model.AnalyticsMetadataType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AssociateSoftwareTokenResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.AttributeType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChangePasswordResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeDeliveryDetailsType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchException import aws.sdk.kotlin.services.cognitoidentityprovider.model.CognitoIdentityProviderException import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmForgotPasswordRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmForgotPasswordResponse @@ -32,17 +35,27 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResendConfirmationCodeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResendConfirmationCodeResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SetUserMfaPreferenceRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SetUserMfaPreferenceResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.SignUpRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.SignUpResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SmsMfaSettingsType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SoftwareTokenMfaNotFoundException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SoftwareTokenMfaSettingsType import aws.sdk.kotlin.services.cognitoidentityprovider.model.UpdateUserAttributesRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.UpdateUserAttributesResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponseType import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifyUserAttributeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifyUserAttributeResponse +import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.SRPHelper @@ -50,6 +63,7 @@ import com.amplifyframework.auth.cognito.options.AWSCognitoAuthResendUserAttribu import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributesOptions +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.usecases.ResetPasswordUseCase import com.amplifyframework.auth.exceptions.InvalidStateException @@ -1625,4 +1639,227 @@ class RealAWSCognitoAuthPluginTest { "Auth flow types do not match expected" ) } + + @Test + fun `setupTOTP on success`() { + val currentAuthState = mockk { + every { authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + every { authZState } returns AuthorizationState.SessionEstablished(credentials) + } + every { authStateMachine.getCurrentState(captureLambda()) } answers { + lambda<(AuthState) -> Unit>().invoke(currentAuthState) + } + val listenLatch = CountDownLatch(1) + val onSuccess = mockk>() + val totpSetupDetails = slot() + every { onSuccess.accept(capture(totpSetupDetails)) }.answers { listenLatch.countDown() } + val onError = mockk>() + + val session = "SESSION" + val secretCode = "SECRET_CODE" + coEvery { mockCognitoIPClient.associateSoftwareToken(any()) }.answers { + AssociateSoftwareTokenResponse.invoke { + this.session = session + this.secretCode = secretCode + } + } + + plugin.setUpTOTP(onSuccess, onError) + + assertTrue { listenLatch.await(5, TimeUnit.SECONDS) } + assertTrue(totpSetupDetails.isCaptured) + verify(exactly = 1) { onSuccess.accept(any()) } + assertEquals(secretCode, totpSetupDetails.captured.sharedSecret) + } + + @Test + fun `setupTOTP on error`() { + val currentAuthState = mockk { + every { authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + every { authZState } returns AuthorizationState.SessionEstablished(credentials) + } + every { authStateMachine.getCurrentState(captureLambda()) } answers { + lambda<(AuthState) -> Unit>().invoke(currentAuthState) + } + + val listenLatch = CountDownLatch(1) + val onSuccess = mockk>() + val onError = mockk>() + val authException = slot() + every { onError.accept(capture(authException)) }.answers { listenLatch.countDown() } + + val expectedErrorMessage = "Software token MFA not enabled" + coEvery { mockCognitoIPClient.associateSoftwareToken(any()) }.answers { + throw SoftwareTokenMfaNotFoundException.invoke { + message = expectedErrorMessage + } + } + + plugin.setUpTOTP(onSuccess, onError) + + assertTrue { listenLatch.await(5, TimeUnit.SECONDS) } + assertTrue(authException.isCaptured) + verify(exactly = 1) { onError.accept(any()) } + assertEquals(expectedErrorMessage, authException.captured.cause?.message) + } + + @Test + fun `verifyTOTP on success`() { + val currentAuthState = mockk { + every { authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + every { authZState } returns AuthorizationState.SessionEstablished(credentials) + } + every { authStateMachine.getCurrentState(captureLambda()) } answers { + lambda<(AuthState) -> Unit>().invoke(currentAuthState) + } + val listenLatch = CountDownLatch(1) + val onSuccess = mockk() + every { onSuccess.call() }.answers { listenLatch.countDown() } + val onError = mockk>() + + val session = "SESSION" + val code = "123456" + val friendlyDeviceName = "DEVICE_NAME" + coEvery { + mockCognitoIPClient.verifySoftwareToken( + VerifySoftwareTokenRequest.invoke { + userCode = code + this.friendlyDeviceName = friendlyDeviceName + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + ) + }.answers { + VerifySoftwareTokenResponse.invoke { + status = VerifySoftwareTokenResponseType.Success + } + } + + plugin.verifyTOTPSetup( + code, + AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().setFriendlyDeviceName(friendlyDeviceName).build(), + onSuccess, + onError + ) + + assertTrue { listenLatch.await(5, TimeUnit.SECONDS) } + verify(exactly = 1) { onSuccess.call() } + } + + @Test + fun `verifyTOTP on error`() { + val currentAuthState = mockk { + every { authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + every { authZState } returns AuthorizationState.SessionEstablished(credentials) + } + every { authStateMachine.getCurrentState(captureLambda()) } answers { + lambda<(AuthState) -> Unit>().invoke(currentAuthState) + } + val listenLatch = CountDownLatch(1) + val onSuccess = mockk() + val onError = mockk>() + val authException = slot() + every { onError.accept(capture(authException)) }.answers { listenLatch.countDown() } + + val session = "SESSION" + val code = "123456" + val friendlyDeviceName = "DEVICE_NAME" + val errorMessage = "Invalid code" + coEvery { + mockCognitoIPClient.verifySoftwareToken( + VerifySoftwareTokenRequest.invoke { + userCode = code + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + ) + }.answers { + VerifySoftwareTokenResponse.invoke { + throw CodeMismatchException.invoke { + message = errorMessage + } + } + } + + plugin.verifyTOTPSetup( + code, + AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), + onSuccess, + onError + ) + + assertTrue { listenLatch.await(5, TimeUnit.SECONDS) } + assertTrue(authException.isCaptured) + assertEquals(errorMessage, authException.captured.cause?.message) + verify(exactly = 1) { onError.accept(any()) } + } + + @Test + fun fetchMFAPreferences() { + val currentAuthState = mockk { + every { authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + every { authZState } returns AuthorizationState.SessionEstablished(credentials) + } + every { authStateMachine.getCurrentState(captureLambda()) } answers { + lambda<(AuthState) -> Unit>().invoke(currentAuthState) + } + val listenLatch = CountDownLatch(1) + val userMFAPreference = slot() + val onSuccess = mockk>() + every { onSuccess.accept(capture(userMFAPreference)) }.answers { listenLatch.countDown() } + val onError = mockk>() + + coEvery { + mockCognitoIPClient.getUser { + accessToken = credentials.signedInData.cognitoUserPoolTokens.accessToken + } + }.answers { + GetUserResponse.invoke { + userMfaSettingList = listOf("SMS_MFA", "SOFTWARE_TOKEN_MFA") + preferredMfaSetting = "SOFTWARE_TOKEN_MFA" + } + } + plugin.fetchMFAPreference(onSuccess, onError) + + assertTrue { listenLatch.await(5, TimeUnit.SECONDS) } + assertTrue(userMFAPreference.isCaptured) + assertEquals(setOf(MFAType.SMS, MFAType.TOTP), userMFAPreference.captured.enabled) + assertEquals(MFAType.TOTP, userMFAPreference.captured.preferred) + } + + @Test + fun updateMFAPreferences() { + val currentAuthState = mockk { + every { authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + every { authZState } returns AuthorizationState.SessionEstablished(credentials) + } + every { authStateMachine.getCurrentState(captureLambda()) } answers { + lambda<(AuthState) -> Unit>().invoke(currentAuthState) + } + val listenLatch = CountDownLatch(1) + val onSuccess = mockk() + every { onSuccess.call() }.answers { listenLatch.countDown() } + val onError = mockk>() + val setUserMFAPreferenceRequest = slot() + coEvery { mockCognitoIPClient.setUserMfaPreference(capture(setUserMFAPreferenceRequest)) }.answers { + SetUserMfaPreferenceResponse.invoke { + } + } + plugin.updateMFAPreference(MFAPreference.Enabled, MFAPreference.Preferred, onSuccess, onError) + + assertTrue { listenLatch.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 + ) + } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTestBase.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTestBase.kt index b5f2e803a2..22f250799c 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTestBase.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTestBase.kt @@ -27,6 +27,7 @@ import com.amplifyframework.statemachine.codegen.actions.FetchAuthSessionActions import com.amplifyframework.statemachine.codegen.actions.HostedUIActions import com.amplifyframework.statemachine.codegen.actions.MigrateAuthActions import com.amplifyframework.statemachine.codegen.actions.SRPActions +import com.amplifyframework.statemachine.codegen.actions.SetupTOTPActions import com.amplifyframework.statemachine.codegen.actions.SignInActions import com.amplifyframework.statemachine.codegen.actions.SignInChallengeActions import com.amplifyframework.statemachine.codegen.actions.SignOutActions @@ -117,13 +118,16 @@ open class StateTransitionTestBase { @Mock internal lateinit var mockDeleteUserActions: DeleteUserCognitoActions + @Mock + internal lateinit var mockSetupTOTPActions: SetupTOTPActions + private val dummyCredential = AmplifyCredential.UserAndIdentityPool( SignedInData( "userId", "username", Date(0), SignInMethod.ApiBased(SignInMethod.ApiBased.AuthType.USER_SRP_AUTH), - CognitoUserPoolTokens("idToken", "accessToken", "refreshToken", 123123L), + CognitoUserPoolTokens("idToken", "accessToken", "refreshToken", 123123L) ), "identityPool", AWSCredentials( @@ -342,7 +346,9 @@ open class StateTransitionTestBase { dispatcher.send( SRPEvent( SRPEvent.EventType.RespondPasswordVerifier( - mapOf(), mapOf(), "sample_session" + mapOf(), + mapOf(), + "sample_session" ) ) ) diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTests.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTests.kt index f8a09bbbb3..0dbe3ae41f 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTests.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/StateTransitionTests.kt @@ -38,6 +38,7 @@ import com.amplifyframework.statemachine.codegen.states.HostedUISignInState import com.amplifyframework.statemachine.codegen.states.MigrateSignInState import com.amplifyframework.statemachine.codegen.states.RefreshSessionState import com.amplifyframework.statemachine.codegen.states.SRPSignInState +import com.amplifyframework.statemachine.codegen.states.SetupTOTPState import com.amplifyframework.statemachine.codegen.states.SignInChallengeState import com.amplifyframework.statemachine.codegen.states.SignInState import com.amplifyframework.statemachine.codegen.states.SignOutState @@ -92,6 +93,7 @@ class StateTransitionTests : StateTransitionTestBase() { SignInChallengeState.Resolver(mockSignInChallengeActions), HostedUISignInState.Resolver(mockHostedUIActions), DeviceSRPSignInState.Resolver(mockDeviceSRPSignInActions), + SetupTOTPState.Resolver(mockSetupTOTPActions), mockSignInActions ), SignOutState.Resolver(mockSignOutActions), 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 new file mode 100644 index 0000000000..d45ac34f64 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/actions/SetupTOTPCognitoActionsTest.kt @@ -0,0 +1,286 @@ +/* + * Copyright 2023 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.actions + +import androidx.test.core.app.ApplicationProvider +import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AssociateSoftwareTokenRequest +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AssociateSoftwareTokenResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.CodeMismatchException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.SoftwareTokenMfaNotFoundException +import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponse +import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponseType +import aws.sdk.kotlin.services.cognitoidentityprovider.verifySoftwareToken +import com.amplifyframework.auth.cognito.AWSCognitoAuthService +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.StoreClientBehavior +import com.amplifyframework.logging.Logger +import com.amplifyframework.statemachine.EventDispatcher +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.AuthConfiguration +import com.amplifyframework.statemachine.codegen.data.SignInTOTPSetupData +import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) +class SetupTOTPCognitoActionsTest { + + private val configuration = mockk() + private val cognitoAuthService = mockk() + private val credentialStoreClient = mockk() + private val logger = mockk() + private val cognitoIdentityProviderClientMock = mockk() + private val dispatcher = mockk() + + private val capturedEvent = slot() + + private lateinit var authEnvironment: AuthEnvironment + + @Before + fun setup() { + every { logger.verbose(any()) }.answers {} + every { dispatcher.send(capture(capturedEvent)) }.answers { } + every { cognitoAuthService.cognitoIdentityProviderClient }.answers { cognitoIdentityProviderClientMock } + authEnvironment = AuthEnvironment( + ApplicationProvider.getApplicationContext(), + configuration, + cognitoAuthService, + credentialStoreClient, + null, + null, + logger + ) + } + + @Test + fun `initiateTOTPSetup send waitForAnswer on success`() = runTest { + val secretCode = "SECRET_CODE" + val session = "SESSION" + val username = "USERNAME" + coEvery { + cognitoIdentityProviderClientMock.associateSoftwareToken(any()) + }.answers { + AssociateSoftwareTokenResponse.invoke { + this.secretCode = secretCode + this.session = session + } + } + val initiateAction = SetupTOTPCognitoActions.initiateTOTPSetup( + SetupTOTPEvent.EventType.SetupTOTP( + SignInTOTPSetupData("", "SESSION", "USERNAME") + ) + ) + initiateAction.execute(dispatcher, authEnvironment) + + val expectedEvent = SetupTOTPEvent( + SetupTOTPEvent.EventType.WaitForAnswer(SignInTOTPSetupData(secretCode, session, username)) + ) + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + assertEquals( + secretCode, + ( + (capturedEvent.captured as SetupTOTPEvent).eventType as SetupTOTPEvent.EventType.WaitForAnswer + ).totpSetupDetails.secretCode + ) + } + + @Test + fun `initiateTOTPSetup send waitForAnswer on failure`() = runTest { + val session = "SESSION" + val serviceException = SoftwareTokenMfaNotFoundException { + message = "TOTP is not enabled" + } + coEvery { + cognitoIdentityProviderClientMock.associateSoftwareToken( + AssociateSoftwareTokenRequest.invoke { + this.session = session + } + ) + }.answers { + throw serviceException + } + val initiateAction = SetupTOTPCognitoActions.initiateTOTPSetup( + SetupTOTPEvent.EventType.SetupTOTP( + SignInTOTPSetupData("", "SESSION", "USERNAME") + ) + ) + initiateAction.execute(dispatcher, authEnvironment) + + val expectedEvent = SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError(serviceException, "USERNAME", "SESSION") + ) + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + assertEquals( + serviceException, + ((capturedEvent.captured as SetupTOTPEvent).eventType as SetupTOTPEvent.EventType.ThrowAuthError).exception + ) + } + + @Test + fun `verifyChallengeAnswer send RespondToAuthChallenge on success`() = runTest { + val answer = "123456" + val session = "SESSION" + val username = "USERNAME" + val friendlyDeviceName = "TEST_DEVICE" + coEvery { + cognitoIdentityProviderClientMock.verifySoftwareToken { + this.userCode = answer + this.session = session + this.friendlyDeviceName = friendlyDeviceName + } + }.answers { + VerifySoftwareTokenResponse.invoke { + this.session = session + this.status = VerifySoftwareTokenResponseType.Success + } + } + val expectedEvent = SetupTOTPEvent( + SetupTOTPEvent.EventType.RespondToAuthChallenge(username, session) + ) + + val verifyChallengeAnswerAction = SetupTOTPCognitoActions.verifyChallengeAnswer( + SetupTOTPEvent.EventType.VerifyChallengeAnswer( + answer, + username, + session, + friendlyDeviceName + ) + ) + verifyChallengeAnswerAction.execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + + assertEquals( + session, + ( + (capturedEvent.captured as SetupTOTPEvent).eventType as SetupTOTPEvent.EventType.RespondToAuthChallenge + ).session + ) + } + + @Test + fun `verifyChallengeAnswer send RespondToAuthChallenge on Error`() = runTest { + val answer = "123456" + val session = "SESSION" + val username = "USERNAME" + val friendlyDeviceName = "TEST_DEVICE" + coEvery { + cognitoIdentityProviderClientMock.verifySoftwareToken { + this.userCode = answer + this.session = session + this.friendlyDeviceName = friendlyDeviceName + } + }.answers { + VerifySoftwareTokenResponse.invoke { + this.session = session + this.status = VerifySoftwareTokenResponseType.Error + } + } + val expectedEvent = SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError( + Exception("An unknown service error has occurred"), + "USERNAME", + "SESSION" + ) + ) + + val verifyChallengeAnswerAction = SetupTOTPCognitoActions.verifyChallengeAnswer( + SetupTOTPEvent.EventType.VerifyChallengeAnswer( + answer, + username, + session, + friendlyDeviceName + ) + ) + verifyChallengeAnswerAction.execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + + assertEquals( + (expectedEvent.eventType as SetupTOTPEvent.EventType.ThrowAuthError).exception.message, + ( + (capturedEvent.captured as SetupTOTPEvent).eventType as SetupTOTPEvent.EventType.ThrowAuthError + ).exception.message + ) + } + + @Test + fun `verifyChallengeAnswer send RespondToAuthChallenge on exception`() = runTest { + val answer = "123456" + val session = "SESSION" + val username = "USERNAME" + val friendlyDeviceName = "TEST_DEVICE" + val serviceException = CodeMismatchException { + message = "Invalid Code" + } + coEvery { + cognitoIdentityProviderClientMock.verifySoftwareToken { + this.userCode = answer + this.session = session + this.friendlyDeviceName = friendlyDeviceName + } + }.answers { + throw serviceException + } + val expectedEvent = SetupTOTPEvent( + SetupTOTPEvent.EventType.ThrowAuthError(serviceException, "USERNAME", "SESSION") + ) + + val verifyChallengeAnswerAction = SetupTOTPCognitoActions.verifyChallengeAnswer( + SetupTOTPEvent.EventType.VerifyChallengeAnswer( + answer, + username, + session, + friendlyDeviceName + ) + ) + verifyChallengeAnswerAction.execute(dispatcher, authEnvironment) + + assertEquals( + expectedEvent.type, + capturedEvent.captured.type + ) + + assertEquals( + (expectedEvent.eventType as SetupTOTPEvent.EventType.ThrowAuthError).exception.message, + ( + (capturedEvent.captured as SetupTOTPEvent).eventType as SetupTOTPEvent.EventType.ThrowAuthError + ).exception.message + ) + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthStatesSerializer.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthStatesSerializer.kt index f3415d6609..d3a454353d 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthStatesSerializer.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/serializers/AuthStatesSerializer.kt @@ -44,13 +44,17 @@ import kotlinx.serialization.modules.contextual @Serializable internal data class AuthStatesProxy( val type: String = "AuthState", - @Contextual @SerialName("AuthenticationState") + @Contextual + @SerialName("AuthenticationState") val authNState: AuthenticationState? = null, - @Contextual @SerialName("AuthorizationState") + @Contextual + @SerialName("AuthorizationState") val authZState: AuthorizationState? = null, - @Contextual @SerialName("SignInState") + @Contextual + @SerialName("SignInState") val signInState: SignInState = SignInState.NotStarted(), - @Contextual @SerialName("SignInChallengeState") + @Contextual + @SerialName("SignInChallengeState") val signInChallengeState: SignInChallengeState? = null, @Contextual val signedInData: SignedInData? = null, @@ -142,7 +146,7 @@ internal data class AuthStatesProxy( amplifyCredential = authState.amplifyCredential ) is AuthorizationState.SigningIn -> AuthStatesProxy( - type = "AuthorizationState.SigningIn", + type = "AuthorizationState.SigningIn" ) is AuthorizationState.SigningOut -> TODO() is AuthorizationState.StoringCredentials -> TODO() @@ -166,6 +170,7 @@ internal data class AuthStatesProxy( is SignInState.SigningInWithHostedUI -> TODO() is SignInState.SigningInWithSRP -> TODO() is SignInState.SigningInWithSRPCustom -> TODO() + is SignInState.ResolvingTOTPSetup -> TODO() } } is SignInChallengeState -> { diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt index a6e80ef849..60d31f8489 100644 --- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt +++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt @@ -17,6 +17,7 @@ package com.amplifyframework.kotlin.auth import android.app.Activity import android.content.Intent +import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthException @@ -37,6 +38,7 @@ import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthSignUpOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult @@ -327,4 +329,20 @@ interface Auth { */ @Throws(AuthException::class) suspend fun deleteUser() + + /** + * Setup TOTP for the currently signed in user. + * @return TOTP Setup details + */ + suspend fun setUpTOTP(): TOTPSetupDetails + + /** + * Verify TOTP setup for the currently signed in user. + * @param code TOTP code to verify TOTP setup + * @param options additional options to verify totp setup + */ + suspend fun verifyTOTPSetup( + code: String, + options: AuthVerifyTOTPSetupOptions = AuthVerifyTOTPSetupOptions.builder().build() + ) } diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt index 41a3d03bce..bb95055a18 100644 --- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt +++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt @@ -17,6 +17,7 @@ package com.amplifyframework.kotlin.auth import android.app.Activity import android.content.Intent +import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCategoryBehavior as Delegate import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice @@ -37,6 +38,7 @@ import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthSignUpOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult @@ -336,4 +338,24 @@ class KotlinAuthFacade(private val delegate: Delegate = Amplify.Auth) : Auth { ) } } + + override suspend fun setUpTOTP(): TOTPSetupDetails { + return suspendCoroutine { continuation -> + delegate.setUpTOTP({ + continuation.resume(it) + }, { + continuation.resumeWithException(it) + }) + } + } + + override suspend fun verifyTOTPSetup(code: String, options: AuthVerifyTOTPSetupOptions) { + return suspendCoroutine { continuation -> + delegate.verifyTOTPSetup(code, options, { + continuation.resume(Unit) + }, { + continuation.resumeWithException(it) + }) + } + } } diff --git a/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt b/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt new file mode 100644 index 0000000000..a9d77de1f1 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 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 + +import android.net.Uri + +data class TOTPSetupDetails( + val sharedSecret: String, + val username: String +) { + fun getSetupURI( + appName: String, + accountName: String = username + ): Uri { + return Uri.parse("otpauth://totp/$appName:$accountName?secret=$sharedSecret&issuer=$appName") + } +} diff --git a/core/src/main/java/com/amplifyframework/auth/AuthCategory.java b/core/src/main/java/com/amplifyframework/auth/AuthCategory.java index 51d144e202..519e7c0bdc 100644 --- a/core/src/main/java/com/amplifyframework/auth/AuthCategory.java +++ b/core/src/main/java/com/amplifyframework/auth/AuthCategory.java @@ -20,6 +20,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; @@ -32,6 +33,7 @@ import com.amplifyframework.auth.options.AuthSignUpOptions; import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions; import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions; +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.options.AuthWebUISignInOptions; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; @@ -387,12 +389,29 @@ public void signOut(@NonNull Consumer onComplete) { ) { getSelectedPlugin().signOut(options, onComplete); } - + @Override public void deleteUser( @NonNull Action onSuccess, @NonNull Consumer onError) { getSelectedPlugin().deleteUser(onSuccess, onError); } + + @Override + public void setUpTOTP(@NonNull Consumer onSuccess, @NonNull Consumer onError) { + getSelectedPlugin().setUpTOTP(onSuccess, onError); + } + + @Override + public void verifyTOTPSetup(@NonNull String code, @NonNull Action onSuccess, + @NonNull Consumer onError) { + getSelectedPlugin().verifyTOTPSetup(code, onSuccess, onError); + } + + @Override + public void verifyTOTPSetup(@NonNull String code, @NonNull AuthVerifyTOTPSetupOptions options, + @NonNull Action onSuccess, @NonNull Consumer onError) { + getSelectedPlugin().verifyTOTPSetup(code, options, onSuccess, onError); + } } diff --git a/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java b/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java index 82d7254ca7..91d4177212 100644 --- a/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java +++ b/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java @@ -20,6 +20,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; @@ -32,6 +33,7 @@ import com.amplifyframework.auth.options.AuthSignUpOptions; import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions; import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions; +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.options.AuthWebUISignInOptions; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; @@ -512,4 +514,39 @@ void signOut( void deleteUser( @NonNull Action onSuccess, @NonNull Consumer onError); + + /** + * Setup TOTP for the currently signed in user. + * @param onSuccess Success callback + * @param onError Error callback + */ + void setUpTOTP( + @NonNull Consumer onSuccess, + @NonNull Consumer onError); + + /** + * Verify TOTP setup for the currently signed in user. + * @param code TOTP code to verify TOTP setup + * @param onSuccess Success callback + * @param onError Error callback + */ + void verifyTOTPSetup( + @NonNull String code, + @NonNull Action onSuccess, + @NonNull Consumer onError + ); + + /** + * Verify TOTP setup for the currently signed in user. + * @param code TOTP code to verify TOTP setup + * @param options additional options to verify totp setup + * @param onSuccess Success callback + * @param onError Error callback + */ + void verifyTOTPSetup( + @NonNull String code, + @NonNull AuthVerifyTOTPSetupOptions options, + @NonNull Action onSuccess, + @NonNull Consumer onError + ); } diff --git a/core/src/main/java/com/amplifyframework/auth/MFAType.kt b/core/src/main/java/com/amplifyframework/auth/MFAType.kt new file mode 100644 index 0000000000..b32dc80351 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/auth/MFAType.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 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 + +enum class MFAType(val value: String) { + SMS("SMS_MFA"), + TOTP("SOFTWARE_TOKEN_MFA"); + companion object { + fun toMFAType(value: String): MFAType { + return when (value) { + MFAType.SMS.value -> MFAType.SMS + MFAType.TOTP.value -> MFAType.TOTP + else -> throw IllegalArgumentException("Unsupported MFA type") + } + } + } +} diff --git a/core/src/main/java/com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions.java b/core/src/main/java/com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions.java new file mode 100644 index 0000000000..f422623203 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 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.options; + +/** + * The shared options among all Auth plugins. + * Note: This is currently empty but exists here to support common verify totp setup options. + */ +public class AuthVerifyTOTPSetupOptions { + + /** + * protected constructor. + */ + protected AuthVerifyTOTPSetupOptions() { + } + + /** + * Get a builder to construct an instance of this object. + * @return a builder to construct an instance of this object. + */ + public static Builder builder() { + return new CoreBuilder(); + } + + /** + * The builder for this class. + * @param The type of builder - used to support plugin extensions of this. + */ + public abstract static class Builder> { + + /** + * Build an instance of AuthVerifyTOTPSetupOptions (or one of its subclasses). + * @return an instance of AuthVerifyTOTPSetupOptions (or one of its subclasses) + */ + public AuthVerifyTOTPSetupOptions build() { + return new AuthVerifyTOTPSetupOptions(); + } + + } + + /** + * The specific implementation of builder for this as the parent class. + */ + public static final class CoreBuilder extends Builder { + + } +} diff --git a/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java b/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java index ad926c956e..5736912c60 100644 --- a/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java +++ b/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java @@ -19,11 +19,14 @@ import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; +import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.AuthCodeDeliveryDetails; +import com.amplifyframework.auth.MFAType; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * This object represents all details around the next step in the sign in process. It holds an instance of the @@ -35,20 +38,29 @@ public final class AuthNextSignInStep { private final Map additionalInfo; private final AuthCodeDeliveryDetails codeDeliveryDetails; + private final TOTPSetupDetails totpSetupDetails; + private final Set allowedMFATypes; + /** * Gives details on the next step, if there is one, in the sign in flow. * @param signInStep the next step in the sign in flow (could be optional or required) * @param additionalInfo possible extra info to go with the next step (refer to plugin documentation) * @param codeDeliveryDetails Details about how a code was sent, if relevant to the current step + * @param totpSetupDetails Details to setup TOTP, if relevant to the current step + * @param allowedMFATypes Set of allowed MFA type, if relevant to the current step */ public AuthNextSignInStep( @NonNull AuthSignInStep signInStep, @NonNull Map additionalInfo, - @Nullable AuthCodeDeliveryDetails codeDeliveryDetails) { + @Nullable AuthCodeDeliveryDetails codeDeliveryDetails, + @Nullable TOTPSetupDetails totpSetupDetails, + @Nullable Set allowedMFATypes) { this.signInStep = Objects.requireNonNull(signInStep); this.additionalInfo = new HashMap<>(); this.additionalInfo.putAll(Objects.requireNonNull(additionalInfo)); this.codeDeliveryDetails = codeDeliveryDetails; + this.totpSetupDetails = totpSetupDetails; + this.allowedMFATypes = allowedMFATypes; } /** @@ -78,6 +90,24 @@ public AuthCodeDeliveryDetails getCodeDeliveryDetails() { return codeDeliveryDetails; } + /** + * Details about how to setup TOTP. + * @return Details about how to setup TOTP, if relevant to the current step - null otherwise + */ + @Nullable + public TOTPSetupDetails getTotpSetupDetails() { + return totpSetupDetails; + } + + /** + * Set of allowed MFA Types. + * @return Set of allowed MFA Types, if relevant to the current step - null otherwise + */ + @Nullable + public Set getAllowedMFATypes() { + return allowedMFATypes; + } + /** * When overriding, be sure to include signInStep, additionalInfo, and codeDeliveryDetails in the hash. * @return Hash code of this object @@ -87,7 +117,9 @@ public int hashCode() { return ObjectsCompat.hash( getSignInStep(), getAdditionalInfo(), - getCodeDeliveryDetails() + getCodeDeliveryDetails(), + getTotpSetupDetails(), + getAllowedMFATypes() ); } @@ -105,7 +137,9 @@ public boolean equals(Object obj) { AuthNextSignInStep authSignUpResult = (AuthNextSignInStep) obj; return ObjectsCompat.equals(getSignInStep(), authSignUpResult.getSignInStep()) && ObjectsCompat.equals(getAdditionalInfo(), authSignUpResult.getAdditionalInfo()) && - ObjectsCompat.equals(getCodeDeliveryDetails(), authSignUpResult.getCodeDeliveryDetails()); + ObjectsCompat.equals(getCodeDeliveryDetails(), authSignUpResult.getCodeDeliveryDetails()) && + ObjectsCompat.equals(getTotpSetupDetails(), authSignUpResult.getTotpSetupDetails()) && + ObjectsCompat.equals(getAllowedMFATypes(), authSignUpResult.getAllowedMFATypes()); } } @@ -119,6 +153,8 @@ public String toString() { "signInStep=" + getSignInStep() + ", additionalInfo=" + getAdditionalInfo() + ", codeDeliveryDetails=" + getCodeDeliveryDetails() + + ", totpSetupDetails=" + getTotpSetupDetails() + + ", allowedMFATypes=" + getAllowedMFATypes() + '}'; } } 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 4b2dd27558..3373104db3 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 @@ -27,7 +27,7 @@ public enum AuthSignInStep { * with the code sent via SMS text message to proceed with the sign in flow. */ CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE, - + /** * Custom multifactor authentication is enabled on this account and requires you to call * {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} @@ -58,6 +58,27 @@ public enum AuthSignInStep { */ CONFIRM_SIGN_UP, + /** + * Admin requires user to setup TOTP. + * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} + * with TOTP code to verify. + */ + CONTINUE_SIGN_IN_WITH_TOTP_SETUP, + + /** + * The user account is required to set MFA selection. + * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} + * with preferred MFA option. + */ + CONTINUE_SIGN_IN_WITH_MFA_SELECTION, + + /** + * TOTP is enabled on this account and requires the user to confirm with the TOTP code. + * Call {@link com.amplifyframework.auth.AuthCategoryBehavior#confirmSignIn(String, Consumer, Consumer)} + * with TOTP Code. + */ + CONFIRM_SIGN_IN_WITH_TOTP_CODE, + /** * No further steps are needed in the sign in flow. */ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b96c010de..a5f4ef7626 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ slf4j = "2.0.6" sqlcipher = "4.5.4" tensorflow = "2.0.0" uuid = "4.0.1" +totp = "1.0.1" [libraries] android-desugartools = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } @@ -116,3 +117,4 @@ test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } test-mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } test-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } test-robolectric = { module = "org.robolectric:robolectric", version.ref="robolectric" } +test-totp = { module = "dev.robinohs:totp-kt", version.ref="totp" } \ No newline at end of file diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java index f0b7d496ae..b37c3992e2 100644 --- a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java +++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.AuthCategoryBehavior; import com.amplifyframework.auth.AuthCodeDeliveryDetails; import com.amplifyframework.auth.AuthDevice; @@ -42,6 +43,7 @@ import com.amplifyframework.auth.options.AuthSignUpOptions; import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions; import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions; +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.options.AuthWebUISignInOptions; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; @@ -308,12 +310,27 @@ public Single signOut() { public Single signOut(@NonNull AuthSignOutOptions options) { return toSingle((onComplete, onError) -> delegate.signOut(options, onComplete)); } - + @Override public Completable deleteUser() { return toCompletable(delegate::deleteUser); } + @Override + public Single setUpTOTP() { + return toSingle(delegate::setUpTOTP); + } + + @Override + public Completable verifyTOTPSetup(@NonNull String code) { + return toCompletable((onComplete, onError) -> delegate.verifyTOTPSetup(code, onComplete, onError)); + } + + @Override + public Completable verifyTOTPSetup(@NonNull String code, @NonNull AuthVerifyTOTPSetupOptions options) { + return toCompletable((onComplete, onError) -> delegate.verifyTOTPSetup(code, options, onComplete, onError)); + } + private Single toSingle(VoidBehaviors.ResultEmitter behavior) { return VoidBehaviors.toSingle(behavior); } diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java index bbc810f98d..1c5b4812f6 100644 --- a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java +++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java @@ -20,6 +20,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.AuthCategoryBehavior; import com.amplifyframework.auth.AuthCodeDeliveryDetails; import com.amplifyframework.auth.AuthDevice; @@ -41,6 +42,7 @@ import com.amplifyframework.auth.options.AuthSignUpOptions; import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions; import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions; +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.options.AuthWebUISignInOptions; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; @@ -436,4 +438,28 @@ Single resendUserAttributeConfirmationCode( * emits an {@link AuthException} otherwise */ Completable deleteUser(); + + /** + * Setup TOTP for the currently signed in user. + * @return An Rx {@link Single} which emits {@link TOTPSetupDetails} on completion + */ + Single setUpTOTP(); + + /** + * Verify TOTP setup for the currently signed in user. + * @param code TOTP code to verify TOTP setup + * @return An Rx {@link Completable} which completes upon successfully verifying totp code; + * emits an {@link AuthException} otherwise + */ + Completable verifyTOTPSetup(@NonNull String code); + + /** + * Verify TOTP setup for the currently signed in user. + * @param code TOTP code to verify TOTP setup + * @param options additional options to verify totp setup + * @return An Rx {@link Completable} which completes upon successfully verifying totp code; + * emits an {@link AuthException} otherwise + */ + Completable verifyTOTPSetup(@NonNull String code, @NonNull AuthVerifyTOTPSetupOptions options); + } diff --git a/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java b/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java index 5191a2b5a4..ad39c202ab 100644 --- a/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java +++ b/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java @@ -227,7 +227,7 @@ public void testSignInSucceeds() throws InterruptedException { // Arrange a result on the result consumer AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.EMAIL); AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE; - AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null); AuthSignInResult result = new AuthSignInResult(false, nextStep); doAnswer(invocation -> { // 0 = username, 1 = password, 2 = onResult, 3 = onFailure @@ -289,7 +289,7 @@ public void testConfirmSignInSucceeds() throws InterruptedException { // Arrange a successful result. AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE; AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.UNKNOWN); - AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null); AuthSignInResult expected = new AuthSignInResult(true, nextStep); doAnswer(invocation -> { // 0 = confirm code, 1 = onResult, 2 = onFailure @@ -351,7 +351,7 @@ public void testSignInWithSocialWebUISucceeds() throws InterruptedException { // Arrange a successful result AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE; AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.PHONE); - AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null); AuthSignInResult result = new AuthSignInResult(false, nextStep); doAnswer(invocation -> { // 0 = provider, 1 = activity, 2 = result consumer, 3 = failure consumer @@ -414,7 +414,7 @@ public void testSignInWithWebUISucceeds() throws InterruptedException { // Arrange a result AuthSignInStep step = AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE; AuthCodeDeliveryDetails details = new AuthCodeDeliveryDetails(RandomString.string(), DeliveryMedium.PHONE); - AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details); + AuthNextSignInStep nextStep = new AuthNextSignInStep(step, Collections.emptyMap(), details, null, null); AuthSignInResult result = new AuthSignInResult(false, nextStep); doAnswer(invocation -> { // 0 = activity, 1 = result consumer, 2 = failure consumer @@ -1048,10 +1048,10 @@ public void testDeleteUser() throws InterruptedException { onCompletion.call(); return null; }).when(delegate).deleteUser(anyAction(), anyConsumer()); - + // Act: call the binding TestObserver observer = auth.deleteUser().test(); - + // Assert: Completable completes with success observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); observer.assertNoErrors() @@ -1073,10 +1073,10 @@ public void testDeleteUserFails() throws InterruptedException { onFailure.accept(failure); return null; }).when(delegate).deleteUser(anyAction(), anyConsumer()); - + // Act: call the binding TestObserver observer = auth.deleteUser().test(); - + // Assert: failure is furnished via Rx Completable. observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); observer.assertNotComplete() diff --git a/scripts/pull_backend_config_from_s3 b/scripts/pull_backend_config_from_s3 index c3d2985b63..e95fbcb103 100755 --- a/scripts/pull_backend_config_from_s3 +++ b/scripts/pull_backend_config_from_s3 @@ -53,6 +53,7 @@ readonly config_files=( # Auth "aws-auth-cognito/src/androidTest/res/raw/amplifyconfiguration.json" + "aws-auth-cognito/src/androidTest/res/raw/amplifyconfiguration_totp.json" "aws-auth-cognito/src/androidTest/res/raw/awsconfiguration.json" "aws-auth-cognito/src/androidTest/res/raw/credentials.json" ) diff --git a/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java b/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java index 442ee71436..a4a242eeb7 100644 --- a/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java +++ b/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java @@ -29,6 +29,7 @@ import com.amplifyframework.auth.AuthPlugin; import com.amplifyframework.auth.AuthProvider; import com.amplifyframework.auth.AuthSession; +import com.amplifyframework.auth.AuthUser; import com.amplifyframework.auth.AuthUserAttribute; import com.amplifyframework.auth.AuthUserAttributeKey; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; @@ -599,4 +600,15 @@ public void deleteUser() throws AuthException { asyncDelegate.deleteUser(() -> onResult.accept(VoidResult.instance()), onError) ); } + + /** + * Get the current signed in user. + * @return current authenticated user + * @throws AuthException exception + */ + public AuthUser getCurrentUser() throws AuthException { + return Await.result(AUTH_OPERATION_TIMEOUT_MS, (onResult, onError) -> + asyncDelegate.getCurrentUser(onResult, onError) + ); + } } From ecc404f2c8d9c863f5e588c649afb66225b5ffd0 Mon Sep 17 00:00:00 2001 From: Anshul Gupta Date: Wed, 23 Aug 2023 12:08:13 -0500 Subject: [PATCH 02/15] lint fix --- .../auth/cognito/RealAWSCognitoAuthPlugin.kt | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) 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 7780b6f5b7..67d99ec8d0 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 @@ -1476,8 +1476,8 @@ internal class RealAWSCognitoAuthPlugin( try { authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.changePassword( - changePasswordRequest - ) + changePasswordRequest + ) onSuccess.call() } catch (e: Exception) { onError.accept(CognitoAuthExceptionConverter.lookup(e, e.toString())) @@ -1603,8 +1603,8 @@ internal class RealAWSCognitoAuthPlugin( } val userAttributeResponse = authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.updateUserAttributes( - userAttributesRequest - ) + userAttributesRequest + ) continuation.resume( getUpdateUserAttributeResult(userAttributeResponse, userAttributes) @@ -1690,8 +1690,8 @@ internal class RealAWSCognitoAuthPlugin( val getUserAttributeVerificationCodeResponse = authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.getUserAttributeVerificationCode( - getUserAttributeVerificationCodeRequest - ) + getUserAttributeVerificationCodeRequest + ) getUserAttributeVerificationCodeResponse?.codeDeliveryDetails?.let { val codeDeliveryDetails = it @@ -1757,8 +1757,8 @@ internal class RealAWSCognitoAuthPlugin( } authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.verifyUserAttribute( - verifyUserAttributeRequest - ) + verifyUserAttributeRequest + ) onSuccess.call() } ?: onError.accept(InvalidUserPoolConfigurationException()) } catch (e: Exception) { @@ -2098,10 +2098,10 @@ internal class RealAWSCognitoAuthPlugin( authNState is AuthenticationState.FederatedToIdentityPool && authZState is AuthorizationState.SessionEstablished ) || ( - authZState is AuthorizationState.Error && - authZState.exception is SessionError && - authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated - ) -> { + authZState is AuthorizationState.Error && + authZState.exception is SessionError && + authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated + ) -> { val event = AuthenticationEvent(AuthenticationEvent.EventType.ClearFederationToIdentityPool()) authStateMachine.send(event) _clearFederationToIdentityPool(onSuccess, onError) @@ -2124,17 +2124,17 @@ internal class RealAWSCognitoAuthPlugin( SessionHelper.getUsername(token)?.let { username -> authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.associateSoftwareToken { - this.accessToken = token - }?.also { response -> - response.secretCode?.let { secret -> - onSuccess.accept( - TOTPSetupDetails( - secret, - username + this.accessToken = token + }?.also { response -> + response.secretCode?.let { secret -> + onSuccess.accept( + TOTPSetupDetails( + secret, + username + ) ) - ) + } } - } } } ?: onError.accept(SignedOutException()) } catch (error: Exception) { @@ -2184,21 +2184,21 @@ internal class RealAWSCognitoAuthPlugin( accessToken?.let { token -> authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.getUser { - this.accessToken = token - }?.also { response -> - var enabledSet: MutableSet? = null - var preferred: MFAType? = null - if (!response.userMfaSettingList.isNullOrEmpty()) { - enabledSet = mutableSetOf() - response.userMfaSettingList?.forEach { mfaType -> - enabledSet.add(MFAType.toMFAType(mfaType)) + this.accessToken = token + }?.also { response -> + var enabledSet: MutableSet? = null + var preferred: MFAType? = null + if (!response.userMfaSettingList.isNullOrEmpty()) { + enabledSet = mutableSetOf() + response.userMfaSettingList?.forEach { mfaType -> + enabledSet.add(MFAType.toMFAType(mfaType)) + } } + response.preferredMfaSetting?.let { preferredMFA -> + preferred = MFAType.toMFAType(preferredMFA) + } + onSuccess.accept(UserMFAPreference(enabledSet, preferred)) } - response.preferredMfaSetting?.let { preferredMFA -> - preferred = MFAType.toMFAType(preferredMFA) - } - onSuccess.accept(UserMFAPreference(enabledSet, preferred)) - } } ?: onError.accept(SignedOutException()) } catch (error: Exception) { onError.accept( @@ -2278,18 +2278,18 @@ internal class RealAWSCognitoAuthPlugin( accessToken?.let { token -> authEnvironment.cognitoAuthService .cognitoIdentityProviderClient?.verifySoftwareToken { - this.userCode = code - this.friendlyDeviceName = friendlyDeviceName - this.accessToken = token - }?.also { - when (it.status) { - is VerifySoftwareTokenResponseType.Success -> onSuccess.call() - else -> throw ServiceException( - message = "An unknown service error has occurred", - recoverySuggestion = AmplifyException.TODO_RECOVERY_SUGGESTION - ) + this.userCode = code + this.friendlyDeviceName = friendlyDeviceName + this.accessToken = token + }?.also { + when (it.status) { + is VerifySoftwareTokenResponseType.Success -> onSuccess.call() + else -> throw ServiceException( + message = "An unknown service error has occurred", + recoverySuggestion = AmplifyException.TODO_RECOVERY_SUGGESTION + ) + } } - } } ?: onError.accept(SignedOutException()) } catch (error: Exception) { onError.accept( From f5bd57795d3ecebd53ecfcd84085a181f901e77c Mon Sep 17 00:00:00 2001 From: Anshul Gupta Date: Sat, 26 Aug 2023 16:39:07 -0500 Subject: [PATCH 03/15] PR comments --- .../auth/cognito/AWSCognitoAuthPlugin.kt | 6 +++--- .../auth/cognito/CognitoAuthExceptionConverter.kt | 2 +- .../auth/cognito/KotlinAuthFacadeInternal.kt | 2 +- .../auth/cognito/RealAWSCognitoAuthPlugin.kt | 4 ++-- ...ption.kt => EnableSoftwareTokenMFAException.kt} | 2 +- ...ons.java => AWSAuthVerifyTOTPSetupOptions.java} | 14 +++++--------- .../auth/cognito/AWSCognitoAuthPluginTest.kt | 4 ++-- .../auth/cognito/RealAWSCognitoAuthPluginTest.kt | 6 +++--- .../java/com/amplifyframework/kotlin/auth/Auth.kt | 2 ++ .../java/com/amplifyframework/TOTPSetupDetails.kt | 4 ++-- 10 files changed, 22 insertions(+), 24 deletions(-) rename aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/{EnableSoftwareTokenMfaException.kt => EnableSoftwareTokenMFAException.kt} (93%) rename aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/{AWSCognitoAuthVerifyTOTPSetupOptions.java => AWSAuthVerifyTOTPSetupOptions.java} (79%) 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 625b64e662..d5bd72be14 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 @@ -33,7 +33,7 @@ import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.cognito.asf.UserContextDataProvider -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions +import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult import com.amplifyframework.auth.exceptions.ConfigurationException @@ -775,7 +775,7 @@ class AWSCognitoAuthPlugin : AuthPlugin() { queueChannel.trySend( pluginScope.launch(start = CoroutineStart.LAZY) { try { - val result = queueFacade.setupMFA() + val result = queueFacade.setUpTOTP() onSuccess.accept(result) } catch (e: Exception) { onError.accept(e.toAuthException()) @@ -785,7 +785,7 @@ class AWSCognitoAuthPlugin : AuthPlugin() { } override fun verifyTOTPSetup(code: String, onSuccess: Action, onError: Consumer) { - verifyTOTPSetup(code, AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), onSuccess, onError) + verifyTOTPSetup(code, AWSAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), onSuccess, onError) } override fun verifyTOTPSetup( diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt index ef6229ad7f..14c94d8415 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/CognitoAuthExceptionConverter.kt @@ -91,7 +91,7 @@ internal class CognitoAuthExceptionConverter { is PasswordResetRequiredException -> com.amplifyframework.auth.cognito.exceptions.service.PasswordResetRequiredException(error) is EnableSoftwareTokenMfaException -> - com.amplifyframework.auth.cognito.exceptions.service.EnableSoftwareTokenMfaException(error) + com.amplifyframework.auth.cognito.exceptions.service.EnableSoftwareTokenMFAException(error) is UserLambdaValidationException -> com.amplifyframework.auth.cognito.exceptions.service.UserLambdaValidationException( error.message, 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 96ce06d54a..e3b2e14d93 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 @@ -523,7 +523,7 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth } } - suspend fun setupMFA(): TOTPSetupDetails { + suspend fun setUpTOTP(): TOTPSetupDetails { return suspendCoroutine { continuation -> delegate.setUpTOTP( { continuation.resume(it) }, 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 67d99ec8d0..402e480483 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 @@ -80,7 +80,7 @@ import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignUpOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributesOptions -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions +import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions @@ -2167,7 +2167,7 @@ internal class RealAWSCognitoAuthPlugin( onSuccess: Action, onError: Consumer ) { - val cognitoOptions = options as? AWSCognitoAuthVerifyTOTPSetupOptions + val cognitoOptions = options as? AWSAuthVerifyTOTPSetupOptions verifyTotp(code, cognitoOptions?.friendlyDeviceName, onSuccess, onError) } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMfaException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMFAException.kt similarity index 93% rename from aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMfaException.kt rename to aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMFAException.kt index 2cdd44c28a..4c290dc28f 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMfaException.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMFAException.kt @@ -20,7 +20,7 @@ import com.amplifyframework.auth.AuthException * Software Token MFA is not enabled for the user. * @param cause The underlying cause of this exception */ -open class EnableSoftwareTokenMfaException(cause: Throwable?) : +open class EnableSoftwareTokenMFAException(cause: Throwable?) : AuthException( "Software token TOTP multi-factor authentication (MFA) is not enabled for the user pool.", "Enable the software token MFA for the user.", diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSAuthVerifyTOTPSetupOptions.java similarity index 79% rename from aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java rename to aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSAuthVerifyTOTPSetupOptions.java index f8794014d3..b92688e589 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSAuthVerifyTOTPSetupOptions.java @@ -20,11 +20,11 @@ /** * Cognito extension of update verify totp setup options to add the platform specific fields. */ -public final class AWSCognitoAuthVerifyTOTPSetupOptions extends AuthVerifyTOTPSetupOptions { +public final class AWSAuthVerifyTOTPSetupOptions extends AuthVerifyTOTPSetupOptions { private String friendlyDeviceName; - private AWSCognitoAuthVerifyTOTPSetupOptions(String friendlyDeviceName) { + private AWSAuthVerifyTOTPSetupOptions(String friendlyDeviceName) { this.friendlyDeviceName = friendlyDeviceName; } @@ -42,16 +42,12 @@ public String getFriendlyDeviceName() { public static final class CognitoBuilder extends Builder { private String friendlyDeviceName; - private String getFriendlyDeviceName() { - return friendlyDeviceName; - } - /** * Friendly device name to be set in Cognito. * @param friendlyDeviceName String input for friendlyDeviceName * @return current CognitoBuilder instance * */ - public CognitoBuilder setFriendlyDeviceName(String friendlyDeviceName) { + public CognitoBuilder friendlyDeviceName(String friendlyDeviceName) { this.friendlyDeviceName = friendlyDeviceName; return this; } @@ -60,8 +56,8 @@ public CognitoBuilder setFriendlyDeviceName(String friendlyDeviceName) { * Construct and return the object with the values set in the builder. * @return a new instance of AWSCognitoAuthVerifyTOTPSetupOptions with the values specified in the builder. */ - public AWSCognitoAuthVerifyTOTPSetupOptions build() { - return new AWSCognitoAuthVerifyTOTPSetupOptions(getFriendlyDeviceName()); + public AWSAuthVerifyTOTPSetupOptions build() { + return new AWSAuthVerifyTOTPSetupOptions(friendlyDeviceName); } } } 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 a180bb6ef9..06a5976cd1 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 @@ -26,7 +26,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions +import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions @@ -703,7 +703,7 @@ class AWSCognitoAuthPluginTest { @Test fun verifyTOTPSetupWithOptions() { val code = "123456" - val options = AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().setFriendlyDeviceName("DEVICE_NAME").build() + val options = AWSAuthVerifyTOTPSetupOptions.CognitoBuilder().friendlyDeviceName("DEVICE_NAME").build() val expectedOnSuccess = Action { } val expectedOnError = Consumer { } authPlugin.verifyTOTPSetup(code, options, expectedOnSuccess, expectedOnError) 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 e35e8708d9..65e5ee08a6 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 @@ -63,7 +63,7 @@ import com.amplifyframework.auth.cognito.options.AWSCognitoAuthResendUserAttribu import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributesOptions -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions +import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.usecases.ResetPasswordUseCase import com.amplifyframework.auth.exceptions.InvalidStateException @@ -1736,7 +1736,7 @@ class RealAWSCognitoAuthPluginTest { plugin.verifyTOTPSetup( code, - AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().setFriendlyDeviceName(friendlyDeviceName).build(), + AWSAuthVerifyTOTPSetupOptions.CognitoBuilder().friendlyDeviceName(friendlyDeviceName).build(), onSuccess, onError ) @@ -1781,7 +1781,7 @@ class RealAWSCognitoAuthPluginTest { plugin.verifyTOTPSetup( code, - AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), + AWSAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), onSuccess, onError ) diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt index 60d31f8489..970f7dfa01 100644 --- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt +++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt @@ -334,6 +334,7 @@ interface Auth { * Setup TOTP for the currently signed in user. * @return TOTP Setup details */ + @Throws(AuthException::class) suspend fun setUpTOTP(): TOTPSetupDetails /** @@ -341,6 +342,7 @@ interface Auth { * @param code TOTP code to verify TOTP setup * @param options additional options to verify totp setup */ + @Throws(AuthException::class) suspend fun verifyTOTPSetup( code: String, options: AuthVerifyTOTPSetupOptions = AuthVerifyTOTPSetupOptions.builder().build() diff --git a/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt b/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt index a9d77de1f1..2327964fe6 100644 --- a/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt +++ b/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt @@ -21,9 +21,9 @@ data class TOTPSetupDetails( val username: String ) { fun getSetupURI( - appName: String, + issuer: String, accountName: String = username ): Uri { - return Uri.parse("otpauth://totp/$appName:$accountName?secret=$sharedSecret&issuer=$appName") + return Uri.parse("otpauth://totp/$issuer:$accountName?secret=$sharedSecret&issuer=$issuer") } } From d5e5176d71802989c5d4359f2dcabb4a9d6dd97c Mon Sep 17 00:00:00 2001 From: Anshul Gupta Date: Sat, 26 Aug 2023 16:39:37 -0500 Subject: [PATCH 04/15] lint fix --- .../amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt | 2 +- .../auth/cognito/RealAWSCognitoAuthPluginTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 402e480483..4ad7a2e910 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 @@ -70,6 +70,7 @@ 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.identityProviderName +import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmResetPasswordOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignUpOptions @@ -80,7 +81,6 @@ import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignUpOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributesOptions -import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions 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 65e5ee08a6..be2d51b994 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 @@ -59,11 +59,11 @@ import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.SRPHelper +import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthResendUserAttributeConfirmationCodeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributesOptions -import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.usecases.ResetPasswordUseCase import com.amplifyframework.auth.exceptions.InvalidStateException From ce97990d8d858604febfa9f2782b549d9930ae40 Mon Sep 17 00:00:00 2001 From: Anshul Gupta Date: Sun, 27 Aug 2023 14:38:57 -0500 Subject: [PATCH 05/15] pr comments --- .../auth/cognito/AWSCognitoAuthPluginTOTPTests.kt | 4 ---- 1 file changed, 4 deletions(-) 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 3955c18580..f7050ea1a3 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 @@ -23,12 +23,9 @@ import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.cognito.test.R import com.amplifyframework.auth.options.AuthSignUpOptions import com.amplifyframework.auth.result.step.AuthSignInStep -import com.amplifyframework.core.Amplify import com.amplifyframework.core.AmplifyConfiguration import com.amplifyframework.core.category.CategoryConfiguration import com.amplifyframework.core.category.CategoryType -import com.amplifyframework.logging.AndroidLoggingPlugin -import com.amplifyframework.logging.LogLevel import com.amplifyframework.testutils.sync.SynchronousAuth import dev.robinohs.totpkt.otp.totp.TotpGenerator import dev.robinohs.totpkt.otp.totp.timesupport.generateCode @@ -54,7 +51,6 @@ class AWSCognitoAuthPluginTOTPTests { @Before fun setup() { val context = ApplicationProvider.getApplicationContext() - Amplify.addPlugin(AndroidLoggingPlugin(LogLevel.VERBOSE)) val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfiguration_totp) val authConfig: CategoryConfiguration = config.forCategoryType(CategoryType.AUTH) val authConfigJson = authConfig.getPluginConfig("awsCognitoAuthPlugin") From 89c73ef335a8c1b7b052be2a7b96f047de1c2c80 Mon Sep 17 00:00:00 2001 From: Anshul Gupta Date: Sun, 27 Aug 2023 16:47:02 -0500 Subject: [PATCH 06/15] PR comments --- .../amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt | 4 ++-- .../auth/cognito/RealAWSCognitoAuthPlugin.kt | 4 ++-- ...ons.java => AWSCognitoAuthVerifyTOTPSetupOptions.java} | 8 ++++---- .../auth/cognito/AWSCognitoAuthPluginTest.kt | 4 ++-- .../auth/cognito/RealAWSCognitoAuthPluginTest.kt | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) rename aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/{AWSAuthVerifyTOTPSetupOptions.java => AWSCognitoAuthVerifyTOTPSetupOptions.java} (85%) 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 d5bd72be14..e866152966 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 @@ -33,7 +33,7 @@ import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.cognito.asf.UserContextDataProvider -import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult import com.amplifyframework.auth.exceptions.ConfigurationException @@ -785,7 +785,7 @@ class AWSCognitoAuthPlugin : AuthPlugin() { } override fun verifyTOTPSetup(code: String, onSuccess: Action, onError: Consumer) { - verifyTOTPSetup(code, AWSAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), onSuccess, onError) + verifyTOTPSetup(code, AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), onSuccess, onError) } override fun verifyTOTPSetup( 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 4ad7a2e910..67d99ec8d0 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 @@ -70,7 +70,6 @@ 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.identityProviderName -import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmResetPasswordOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignUpOptions @@ -81,6 +80,7 @@ import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignUpOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributesOptions +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions @@ -2167,7 +2167,7 @@ internal class RealAWSCognitoAuthPlugin( onSuccess: Action, onError: Consumer ) { - val cognitoOptions = options as? AWSAuthVerifyTOTPSetupOptions + val cognitoOptions = options as? AWSCognitoAuthVerifyTOTPSetupOptions verifyTotp(code, cognitoOptions?.friendlyDeviceName, onSuccess, onError) } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSAuthVerifyTOTPSetupOptions.java b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java similarity index 85% rename from aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSAuthVerifyTOTPSetupOptions.java rename to aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java index b92688e589..d3253b12cc 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSAuthVerifyTOTPSetupOptions.java +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java @@ -20,11 +20,11 @@ /** * Cognito extension of update verify totp setup options to add the platform specific fields. */ -public final class AWSAuthVerifyTOTPSetupOptions extends AuthVerifyTOTPSetupOptions { +public final class AWSCognitoAuthVerifyTOTPSetupOptions extends AuthVerifyTOTPSetupOptions { private String friendlyDeviceName; - private AWSAuthVerifyTOTPSetupOptions(String friendlyDeviceName) { + private AWSCognitoAuthVerifyTOTPSetupOptions(String friendlyDeviceName) { this.friendlyDeviceName = friendlyDeviceName; } @@ -56,8 +56,8 @@ public CognitoBuilder friendlyDeviceName(String friendlyDeviceName) { * Construct and return the object with the values set in the builder. * @return a new instance of AWSCognitoAuthVerifyTOTPSetupOptions with the values specified in the builder. */ - public AWSAuthVerifyTOTPSetupOptions build() { - return new AWSAuthVerifyTOTPSetupOptions(friendlyDeviceName); + public AWSCognitoAuthVerifyTOTPSetupOptions build() { + return new AWSCognitoAuthVerifyTOTPSetupOptions(friendlyDeviceName); } } } 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 06a5976cd1..7eff130622 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 @@ -26,7 +26,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey -import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions @@ -703,7 +703,7 @@ class AWSCognitoAuthPluginTest { @Test fun verifyTOTPSetupWithOptions() { val code = "123456" - val options = AWSAuthVerifyTOTPSetupOptions.CognitoBuilder().friendlyDeviceName("DEVICE_NAME").build() + val options = AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().friendlyDeviceName("DEVICE_NAME").build() val expectedOnSuccess = Action { } val expectedOnError = Consumer { } authPlugin.verifyTOTPSetup(code, options, expectedOnSuccess, expectedOnError) 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 be2d51b994..11b1793deb 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 @@ -59,11 +59,11 @@ import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.SRPHelper -import com.amplifyframework.auth.cognito.options.AWSAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthResendUserAttributeConfirmationCodeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributeOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthUpdateUserAttributesOptions +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.AuthFlowType import com.amplifyframework.auth.cognito.usecases.ResetPasswordUseCase import com.amplifyframework.auth.exceptions.InvalidStateException @@ -1736,7 +1736,7 @@ class RealAWSCognitoAuthPluginTest { plugin.verifyTOTPSetup( code, - AWSAuthVerifyTOTPSetupOptions.CognitoBuilder().friendlyDeviceName(friendlyDeviceName).build(), + AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().friendlyDeviceName(friendlyDeviceName).build(), onSuccess, onError ) @@ -1781,7 +1781,7 @@ class RealAWSCognitoAuthPluginTest { plugin.verifyTOTPSetup( code, - AWSAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), + AWSCognitoAuthVerifyTOTPSetupOptions.CognitoBuilder().build(), onSuccess, onError ) From 5d2b6ecdf0cc9000af6d68a1941f25fc9f4bc915 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 28 Aug 2023 18:00:54 -0400 Subject: [PATCH 07/15] fix(auth): TOTP contract fixes (#2573) --- .../cognito/AWSCognitoAuthPluginTOTPTests.kt | 3 +- .../auth/cognito/AWSCognitoAuthPlugin.kt | 2 +- .../auth/cognito/KotlinAuthFacadeInternal.kt | 2 +- .../auth/cognito/RealAWSCognitoAuthPlugin.kt | 11 +- .../auth/cognito/UserMFAPreference.kt | 56 +++--- .../auth/cognito/helpers/MFAHelper.kt | 19 ++ .../cognito/helpers/SignInChallengeHelper.kt | 2 +- ...> AWSCognitoAuthVerifyTOTPSetupOptions.kt} | 57 +++--- .../codegen/states/SignInChallengeState.kt | 2 - .../auth/TOTPSetupDetailsTest.kt | 35 ++++ .../auth/cognito/AWSCognitoAuthPluginTest.kt | 6 +- .../cognito/RealAWSCognitoAuthPluginTest.kt | 4 +- .../com/amplifyframework/kotlin/auth/Auth.kt | 4 +- .../kotlin/auth/KotlinAuthFacade.kt | 2 +- .../kotlin/auth/KotlinAuthFacadeTest.kt | 73 ++++++++ .../amplifyframework/auth/AuthCategory.java | 1 - .../auth/AuthCategoryBehavior.java | 1 - .../java/com/amplifyframework/auth/MFAType.kt | 26 +-- .../{ => auth}/TOTPSetupDetails.kt | 21 ++- .../options/AuthVerifyTOTPSetupOptions.java | 51 ++++-- .../auth/result/step/AuthNextSignInStep.java | 2 +- .../amplifyframework/rx/RxAuthBinding.java | 2 +- .../rx/RxAuthCategoryBehavior.java | 2 +- .../rx/RxAuthBindingTest.java | 162 ++++++++++++++++++ 24 files changed, 439 insertions(+), 107 deletions(-) create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt rename aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/{AWSCognitoAuthVerifyTOTPSetupOptions.java => AWSCognitoAuthVerifyTOTPSetupOptions.kt} (51%) create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/TOTPSetupDetailsTest.kt rename core/src/main/java/com/amplifyframework/{ => auth}/TOTPSetupDetails.kt (53%) 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 f7050ea1a3..f1d4ac409e 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 @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.MFAType +import com.amplifyframework.auth.cognito.helpers.value import com.amplifyframework.auth.cognito.test.R import com.amplifyframework.auth.options.AuthSignUpOptions import com.amplifyframework.auth.result.step.AuthSignInStep @@ -142,7 +143,7 @@ class AWSCognitoAuthPluginTOTPTests { ) synchronousAuth.confirmSignIn(otp) synchronousAuth.updateUserAttribute(AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+19876543210")) - updateMFAPreference(MFAPreference.Enabled, MFAPreference.Enabled) + updateMFAPreference(MFAPreference.ENABLED, MFAPreference.ENABLED) synchronousAuth.signOut() val signInResult = synchronousAuth.signIn(userName, password) Assert.assertEquals(AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION, signInResult.nextStep.signInStep) 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 e866152966..6e5ad9173c 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 @@ -20,7 +20,6 @@ import android.content.Context import android.content.Intent import androidx.annotation.VisibleForTesting import com.amplifyframework.AmplifyException -import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.auth.AWSCognitoAuthMetadataType import com.amplifyframework.auth.AuthCodeDeliveryDetails @@ -32,6 +31,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.cognito.asf.UserContextDataProvider import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions 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 e3b2e14d93..f64b110fcb 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 @@ -17,7 +17,6 @@ package com.amplifyframework.auth.cognito import android.app.Activity import android.content.Intent -import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthProvider @@ -25,6 +24,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions 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 67d99ec8d0..d5a71788d9 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 @@ -42,7 +42,6 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.setUserMfaPreference import aws.sdk.kotlin.services.cognitoidentityprovider.signUp import aws.sdk.kotlin.services.cognitoidentityprovider.verifySoftwareToken import com.amplifyframework.AmplifyException -import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.auth.AWSCognitoAuthMetadataType import com.amplifyframework.auth.AWSCredentials @@ -58,6 +57,7 @@ import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.MFAType +import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidOauthConfigurationException import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException import com.amplifyframework.auth.cognito.exceptions.invalidstate.SignedInException @@ -69,6 +69,7 @@ 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.getMFAType import com.amplifyframework.auth.cognito.helpers.identityProviderName import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmResetPasswordOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions @@ -2191,11 +2192,11 @@ internal class RealAWSCognitoAuthPlugin( if (!response.userMfaSettingList.isNullOrEmpty()) { enabledSet = mutableSetOf() response.userMfaSettingList?.forEach { mfaType -> - enabledSet.add(MFAType.toMFAType(mfaType)) + enabledSet.add(getMFAType(mfaType)) } } response.preferredMfaSetting?.let { preferredMFA -> - preferred = MFAType.toMFAType(preferredMFA) + preferred = getMFAType(preferredMFA) } onSuccess.accept(UserMFAPreference(enabledSet, preferred)) } @@ -2234,13 +2235,13 @@ internal class RealAWSCognitoAuthPlugin( this.smsMfaSettings = sms?.let { SmsMfaSettingsType.invoke { enabled = it.mfaEnabled - preferredMfa = it.mfaPreferred + it.mfaPreferred ?.let { preferred -> preferredMfa = preferred } } } this.softwareTokenMfaSettings = totp?.let { SoftwareTokenMfaSettingsType.invoke { enabled = it.mfaEnabled - preferredMfa = it.mfaPreferred + it.mfaPreferred ?.let { preferred -> preferredMfa = preferred } } } }?.also { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/UserMFAPreference.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/UserMFAPreference.kt index 61c629a461..8caac44a83 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/UserMFAPreference.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/UserMFAPreference.kt @@ -16,40 +16,38 @@ package com.amplifyframework.auth.cognito import com.amplifyframework.auth.MFAType -public data class UserMFAPreference( +/** + * Output for fetching MFA preference. + * + * @param enabled MFA types + * @param preferred MFA type. null if not set + */ +data class UserMFAPreference( val enabled: Set?, val preferred: MFAType? ) -public sealed class MFAPreference { - abstract val mfaEnabled: Boolean - abstract val mfaPreferred: Boolean - - object Disabled : MFAPreference() { - override val mfaEnabled: Boolean - get() = false - override val mfaPreferred: Boolean - get() = false - } +/** + * Input for updating the MFA preference for a MFA Type + */ +enum class MFAPreference(internal val mfaEnabled: Boolean, internal val mfaPreferred: Boolean? = null) { + /** + * MFA not enabled + */ + DISABLED(false), - object Enabled : MFAPreference() { - override val mfaEnabled: Boolean - get() = true - override val mfaPreferred: Boolean - get() = false - } + /** + * MFA enabled + */ + ENABLED(true), - object Preferred : MFAPreference() { - override val mfaEnabled: Boolean - get() = true - override val mfaPreferred: Boolean - get() = true - } + /** + * MFA enabled and preferred + */ + PREFERRED(true, true), - object NotPreferred : MFAPreference() { - override val mfaEnabled: Boolean - get() = true - override val mfaPreferred: Boolean - get() = false - } + /** + * MFA enabled and not preferred + */ + NOT_PREFERRED(true, false) } 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 new file mode 100644 index 0000000000..2b6e29a56e --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt @@ -0,0 +1,19 @@ +package com.amplifyframework.auth.cognito.helpers + +import com.amplifyframework.auth.MFAType +import kotlin.jvm.Throws + +@Throws(IllegalArgumentException::class) +internal fun getMFAType(value: String): MFAType { + return when (value) { + "SMS_MFA" -> MFAType.SMS + "SOFTWARE_TOKEN_MFA" -> MFAType.TOTP + else -> throw IllegalArgumentException("Unsupported MFA type") + } +} + +internal val MFAType.value: String + get() = when (this) { + MFAType.SMS -> "SMS_MFA" + MFAType.TOTP -> "SOFTWARE_TOKEN_MFA" + } 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 b3e1c106bd..edd9db687d 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 @@ -18,10 +18,10 @@ package com.amplifyframework.auth.cognito.helpers import aws.sdk.kotlin.services.cognitoidentityprovider.model.AuthenticationResultType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.smithy.kotlin.runtime.time.Instant -import com.amplifyframework.TOTPSetupDetails 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 diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.kt similarity index 51% rename from aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java rename to aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.kt index d3253b12cc..b6ece0404c 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.java +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthVerifyTOTPSetupOptions.kt @@ -12,52 +12,65 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ +package com.amplifyframework.auth.cognito.options -package com.amplifyframework.auth.cognito.options; - -import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions /** * Cognito extension of update verify totp setup options to add the platform specific fields. */ -public final class AWSCognitoAuthVerifyTOTPSetupOptions extends AuthVerifyTOTPSetupOptions { - - private String friendlyDeviceName; - - private AWSCognitoAuthVerifyTOTPSetupOptions(String friendlyDeviceName) { - this.friendlyDeviceName = friendlyDeviceName; - } - +class AWSCognitoAuthVerifyTOTPSetupOptions private constructor( /** * Return the friendlyDeviceName to set during cognito TOTP setup. * @return friendlyDeviceName string - * */ - public String getFriendlyDeviceName() { - return friendlyDeviceName; + */ + val friendlyDeviceName: String? +) : AuthVerifyTOTPSetupOptions() { + + companion object { + /** + * Get a builder object. + * @return a builder object. + */ + @JvmStatic + fun builder(): CognitoBuilder { + return CognitoBuilder() + } + + inline operator fun invoke(block: CognitoBuilder.() -> Unit) = CognitoBuilder() + .apply(block).build() } /** * The builder for this class. */ - public static final class CognitoBuilder extends Builder { - private String friendlyDeviceName; + class CognitoBuilder : Builder() { + private var friendlyDeviceName: String? = null + + /** + * Returns the type of builder this is to support proper flow with it being an extended class. + * @return the type of builder this is to support proper flow with it being an extended class. + */ + override fun getThis(): CognitoBuilder { + return this + } /** * Friendly device name to be set in Cognito. * @param friendlyDeviceName String input for friendlyDeviceName * @return current CognitoBuilder instance - * */ - public CognitoBuilder friendlyDeviceName(String friendlyDeviceName) { - this.friendlyDeviceName = friendlyDeviceName; - return this; + */ + fun friendlyDeviceName(friendlyDeviceName: String): CognitoBuilder { + this.friendlyDeviceName = friendlyDeviceName + return this } /** * Construct and return the object with the values set in the builder. * @return a new instance of AWSCognitoAuthVerifyTOTPSetupOptions with the values specified in the builder. */ - public AWSCognitoAuthVerifyTOTPSetupOptions build() { - return new AWSCognitoAuthVerifyTOTPSetupOptions(friendlyDeviceName); + override fun build(): AWSCognitoAuthVerifyTOTPSetupOptions { + return AWSCognitoAuthVerifyTOTPSetupOptions(friendlyDeviceName) } } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt index d26299c83a..c5281987b6 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/SignInChallengeState.kt @@ -22,7 +22,6 @@ import com.amplifyframework.statemachine.StateResolution import com.amplifyframework.statemachine.codegen.actions.SignInChallengeActions import com.amplifyframework.statemachine.codegen.data.AuthChallenge import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent -import com.amplifyframework.statemachine.codegen.events.SignInEvent internal sealed class SignInChallengeState : State { data class NotStarted(val id: String = "") : SignInChallengeState() @@ -51,7 +50,6 @@ internal sealed class SignInChallengeState : State { ): StateResolution { val defaultResolution = StateResolution(oldState) val challengeEvent = asSignInChallengeEvent(event) - val signInEvent = (event as? SignInEvent)?.eventType return when (oldState) { is NotStarted -> when (challengeEvent) { is SignInChallengeEvent.EventType.WaitForAnswer -> { diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/TOTPSetupDetailsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/TOTPSetupDetailsTest.kt new file mode 100644 index 0000000000..11b2197955 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/TOTPSetupDetailsTest.kt @@ -0,0 +1,35 @@ +package com.amplifyframework.auth + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class TOTPSetupDetailsTest { + + @Test + fun getSetupURI() { + val ss = "SS123" + val username = "User123" + val appName = "MyApp" + val expectedSetupURI = "otpauth://totp/MyApp:User123?secret=SS123&issuer=MyApp" + + val actual = TOTPSetupDetails(ss, username).getSetupURI(appName) + + assertEquals(expectedSetupURI, actual.toString()) + } + + @Test + fun getSetupURIWithAccountNameOverride() { + val ss = "SS123" + val username = "User123" + val accountNameOverride = "AccountOverride" + val appName = "MyApp" + val expectedSetupURI = "otpauth://totp/MyApp:AccountOverride?secret=SS123&issuer=MyApp" + + val actual = TOTPSetupDetails(ss, username).getSetupURI(appName, accountNameOverride) + + assertEquals(expectedSetupURI, actual.toString()) + } +} 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 7eff130622..8afb3f53c5 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 @@ -17,7 +17,6 @@ package com.amplifyframework.auth.cognito import android.app.Activity import android.content.Intent -import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthException @@ -26,6 +25,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.cognito.options.AWSCognitoAuthVerifyTOTPSetupOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult @@ -720,8 +720,8 @@ class AWSCognitoAuthPluginTest { @Test fun updateMFAPreferences() { - val smsPreference = MFAPreference.Enabled - val totpPreference = MFAPreference.Preferred + val smsPreference = MFAPreference.ENABLED + val totpPreference = MFAPreference.PREFERRED val onSuccess = Action { } val onError = Consumer { } authPlugin.updateMFAPreference(smsPreference, totpPreference, onSuccess, onError) 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 11b1793deb..1f73082a4f 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 @@ -49,13 +49,13 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareToken import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponseType import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifyUserAttributeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifyUserAttributeResponse -import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.MFAType +import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.helpers.SRPHelper @@ -1843,7 +1843,7 @@ class RealAWSCognitoAuthPluginTest { SetUserMfaPreferenceResponse.invoke { } } - plugin.updateMFAPreference(MFAPreference.Enabled, MFAPreference.Preferred, onSuccess, onError) + plugin.updateMFAPreference(MFAPreference.ENABLED, MFAPreference.PREFERRED, onSuccess, onError) assertTrue { listenLatch.await(5, TimeUnit.SECONDS) } assertTrue(setUserMFAPreferenceRequest.isCaptured) diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt index 970f7dfa01..8b66c28bba 100644 --- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt +++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/Auth.kt @@ -17,7 +17,6 @@ package com.amplifyframework.kotlin.auth import android.app.Activity import android.content.Intent -import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthException @@ -26,6 +25,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthConfirmSignUpOptions @@ -345,6 +345,6 @@ interface Auth { @Throws(AuthException::class) suspend fun verifyTOTPSetup( code: String, - options: AuthVerifyTOTPSetupOptions = AuthVerifyTOTPSetupOptions.builder().build() + options: AuthVerifyTOTPSetupOptions = AuthVerifyTOTPSetupOptions.defaults() ) } diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt index bb95055a18..ab9ab28267 100644 --- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt +++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/auth/KotlinAuthFacade.kt @@ -17,7 +17,6 @@ package com.amplifyframework.kotlin.auth import android.app.Activity import android.content.Intent -import com.amplifyframework.TOTPSetupDetails import com.amplifyframework.auth.AuthCategoryBehavior as Delegate import com.amplifyframework.auth.AuthCodeDeliveryDetails import com.amplifyframework.auth.AuthDevice @@ -26,6 +25,7 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions import com.amplifyframework.auth.options.AuthConfirmSignInOptions import com.amplifyframework.auth.options.AuthConfirmSignUpOptions diff --git a/core-kotlin/src/test/java/com/amplifyframework/kotlin/auth/KotlinAuthFacadeTest.kt b/core-kotlin/src/test/java/com/amplifyframework/kotlin/auth/KotlinAuthFacadeTest.kt index 9825263b43..f651f409e6 100644 --- a/core-kotlin/src/test/java/com/amplifyframework/kotlin/auth/KotlinAuthFacadeTest.kt +++ b/core-kotlin/src/test/java/com/amplifyframework/kotlin/auth/KotlinAuthFacadeTest.kt @@ -27,9 +27,11 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.exceptions.SessionExpiredException import com.amplifyframework.auth.exceptions.SignedOutException import com.amplifyframework.auth.options.AuthFetchSessionOptions +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions import com.amplifyframework.auth.result.AuthResetPasswordResult import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.auth.result.AuthSignOutResult @@ -964,4 +966,75 @@ class KotlinAuthFacadeTest { } auth.deleteUser() } + + /** + * When the setUpTOTP() delegate emits a success, it should + * be bubbled up through the coroutine API as well. + */ + @Test + fun setUpTOTPSucceeds(): Unit = runBlocking { + val setupDetails = TOTPSetupDetails("ss", "u") + every { + delegate.setUpTOTP(any(), any()) + } answers { + val indexOfErrorConsumer = 0 + val onComplete = it.invocation.args[indexOfErrorConsumer] as Consumer + onComplete.accept(setupDetails) + } + auth.setUpTOTP() + } + + /** + * When the setUpTOTP() delegate emits an error, it should + * be bubbled up through the coroutine API as well. + */ + @Test(expected = AuthException::class) + fun setUpTOTPThrows(): Unit = runBlocking { + val error = AuthException("uh", "oh") + every { + delegate.setUpTOTP(any(), any()) + } answers { + val indexOfErrorConsumer = 1 + val onError = it.invocation.args[indexOfErrorConsumer] as Consumer + onError.accept(error) + } + auth.setUpTOTP() + } + + /** + * When the verifyTOTPSetup() delegate emits a success, it should + * be bubbled up through the coroutine API as well. + */ + @Test + fun verifyTOTPSetupSucceeds(): Unit = runBlocking { + val code = "abc123" + val options = AuthVerifyTOTPSetupOptions.defaults() + every { + delegate.verifyTOTPSetup(code, options, any(), any()) + } answers { + val indexOfErrorConsumer = 2 + val onComplete = it.invocation.args[indexOfErrorConsumer] as Action + onComplete.call() + } + auth.verifyTOTPSetup(code, options) + } + + /** + * When the verifyTOTPSetup() delegate emits an error, it should + * be bubbled up through the coroutine API as well. + */ + @Test(expected = AuthException::class) + fun verifyTOTPSetupThrows(): Unit = runBlocking { + val code = "abc123" + val options = AuthVerifyTOTPSetupOptions.defaults() + val error = AuthException("uh", "oh") + every { + delegate.verifyTOTPSetup(code, options, any(), any()) + } answers { + val indexOfErrorConsumer = 3 + val onError = it.invocation.args[indexOfErrorConsumer] as Consumer + onError.accept(error) + } + auth.verifyTOTPSetup(code) + } } diff --git a/core/src/main/java/com/amplifyframework/auth/AuthCategory.java b/core/src/main/java/com/amplifyframework/auth/AuthCategory.java index 519e7c0bdc..12bab4b2a8 100644 --- a/core/src/main/java/com/amplifyframework/auth/AuthCategory.java +++ b/core/src/main/java/com/amplifyframework/auth/AuthCategory.java @@ -20,7 +20,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; diff --git a/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java b/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java index 91d4177212..6ccebd4c83 100644 --- a/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java +++ b/core/src/main/java/com/amplifyframework/auth/AuthCategoryBehavior.java @@ -20,7 +20,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; diff --git a/core/src/main/java/com/amplifyframework/auth/MFAType.kt b/core/src/main/java/com/amplifyframework/auth/MFAType.kt index b32dc80351..c57472a3c8 100644 --- a/core/src/main/java/com/amplifyframework/auth/MFAType.kt +++ b/core/src/main/java/com/amplifyframework/auth/MFAType.kt @@ -14,16 +14,18 @@ */ package com.amplifyframework.auth -enum class MFAType(val value: String) { - SMS("SMS_MFA"), - TOTP("SOFTWARE_TOKEN_MFA"); - companion object { - fun toMFAType(value: String): MFAType { - return when (value) { - MFAType.SMS.value -> MFAType.SMS - MFAType.TOTP.value -> MFAType.TOTP - else -> throw IllegalArgumentException("Unsupported MFA type") - } - } - } +/** + * Type of MFA for authentication. + */ +enum class MFAType { + + /** + * Short Messaging Service linked with a phone number + */ + SMS, + + /** + * Time-based One Time Password linked with an authenticator app + */ + TOTP; } diff --git a/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt b/core/src/main/java/com/amplifyframework/auth/TOTPSetupDetails.kt similarity index 53% rename from core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt rename to core/src/main/java/com/amplifyframework/auth/TOTPSetupDetails.kt index 2327964fe6..debb9444a0 100644 --- a/core/src/main/java/com/amplifyframework/TOTPSetupDetails.kt +++ b/core/src/main/java/com/amplifyframework/auth/TOTPSetupDetails.kt @@ -12,18 +12,33 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ -package com.amplifyframework +package com.amplifyframework.auth import android.net.Uri +/** + * Details of TOTP Setup that help launch into TOTP manager. + * + * @param sharedSecret Secret code returned by the service to help setting up TOTP + * @param username username that will be used to construct the URI + */ data class TOTPSetupDetails( val sharedSecret: String, val username: String ) { + + /** + * Returns a TOTP setup URI that can help avoid barcode scanning and use native password manager + * to handle TOTP association. + * + * @param appName of TOTP manager + * @param accountName for TOTP manager. Defaults to stored username value. + */ + @JvmOverloads fun getSetupURI( - issuer: String, + appName: String, accountName: String = username ): Uri { - return Uri.parse("otpauth://totp/$issuer:$accountName?secret=$sharedSecret&issuer=$issuer") + return Uri.parse("otpauth://totp/$appName:$accountName?secret=$sharedSecret&issuer=$appName") } } diff --git a/core/src/main/java/com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions.java b/core/src/main/java/com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions.java index f422623203..6d39a494de 100644 --- a/core/src/main/java/com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions.java +++ b/core/src/main/java/com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions.java @@ -15,24 +15,21 @@ package com.amplifyframework.auth.options; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + /** * The shared options among all Auth plugins. * Note: This is currently empty but exists here to support common verify totp setup options. */ -public class AuthVerifyTOTPSetupOptions { - - /** - * protected constructor. - */ - protected AuthVerifyTOTPSetupOptions() { - } +public abstract class AuthVerifyTOTPSetupOptions { /** - * Get a builder to construct an instance of this object. - * @return a builder to construct an instance of this object. + * Use the default verify totp setup options. + * @return Default verify totp setup options. */ - public static Builder builder() { - return new CoreBuilder(); + public static DefaultAuthVerifyTOTPSetupOptions defaults() { + return new DefaultAuthVerifyTOTPSetupOptions(); } /** @@ -41,20 +38,40 @@ public static Builder builder() { */ public abstract static class Builder> { + /** + * Return the type of builder this is so that chaining can work correctly without implicit casting. + * @return the type of builder this is + */ + public abstract T getThis(); + /** * Build an instance of AuthVerifyTOTPSetupOptions (or one of its subclasses). * @return an instance of AuthVerifyTOTPSetupOptions (or one of its subclasses) */ - public AuthVerifyTOTPSetupOptions build() { - return new AuthVerifyTOTPSetupOptions(); - } - + public abstract AuthVerifyTOTPSetupOptions build(); } /** - * The specific implementation of builder for this as the parent class. + * Default verify totp setup options. This works like a sentinel, to be used instead of "null". + * The only way to create this is by calling {@link AuthVerifyTOTPSetupOptions#defaults()}. */ - public static final class CoreBuilder extends Builder { + public static final class DefaultAuthVerifyTOTPSetupOptions extends AuthVerifyTOTPSetupOptions { + private DefaultAuthVerifyTOTPSetupOptions() {} + + @Override + public int hashCode() { + return DefaultAuthVerifyTOTPSetupOptions.class.hashCode(); + } + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof DefaultAuthVerifyTOTPSetupOptions; + } + + @NonNull + @Override + public String toString() { + return DefaultAuthVerifyTOTPSetupOptions.class.getSimpleName(); + } } } diff --git a/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java b/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java index 5736912c60..d404ec185c 100644 --- a/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java +++ b/core/src/main/java/com/amplifyframework/auth/result/step/AuthNextSignInStep.java @@ -19,9 +19,9 @@ import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; -import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.AuthCodeDeliveryDetails; import com.amplifyframework.auth.MFAType; +import com.amplifyframework.auth.TOTPSetupDetails; import java.util.HashMap; import java.util.Map; diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java index b37c3992e2..15d231db83 100644 --- a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java +++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthBinding.java @@ -21,7 +21,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.AuthCategoryBehavior; import com.amplifyframework.auth.AuthCodeDeliveryDetails; import com.amplifyframework.auth.AuthDevice; @@ -31,6 +30,7 @@ import com.amplifyframework.auth.AuthUser; import com.amplifyframework.auth.AuthUserAttribute; import com.amplifyframework.auth.AuthUserAttributeKey; +import com.amplifyframework.auth.TOTPSetupDetails; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java index 1c5b4812f6..e751f04a98 100644 --- a/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java +++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxAuthCategoryBehavior.java @@ -20,7 +20,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.AuthCategoryBehavior; import com.amplifyframework.auth.AuthCodeDeliveryDetails; import com.amplifyframework.auth.AuthDevice; @@ -30,6 +29,7 @@ import com.amplifyframework.auth.AuthUser; import com.amplifyframework.auth.AuthUserAttribute; import com.amplifyframework.auth.AuthUserAttributeKey; +import com.amplifyframework.auth.TOTPSetupDetails; import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions; import com.amplifyframework.auth.options.AuthConfirmSignInOptions; import com.amplifyframework.auth.options.AuthConfirmSignUpOptions; diff --git a/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java b/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java index ad39c202ab..c301211fec 100644 --- a/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java +++ b/rxbindings/src/test/java/com/amplifyframework/rx/RxAuthBindingTest.java @@ -28,7 +28,9 @@ import com.amplifyframework.auth.AuthUser; import com.amplifyframework.auth.AuthUserAttribute; import com.amplifyframework.auth.AuthUserAttributeKey; +import com.amplifyframework.auth.TOTPSetupDetails; import com.amplifyframework.auth.options.AuthSignUpOptions; +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.auth.result.AuthResetPasswordResult; import com.amplifyframework.auth.result.AuthSignInResult; import com.amplifyframework.auth.result.AuthSignOutResult; @@ -62,6 +64,7 @@ import static com.amplifyframework.rx.Matchers.anyConsumer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -1082,4 +1085,163 @@ public void testDeleteUserFails() throws InterruptedException { observer.assertNotComplete() .assertError(failure); } + + /** + * Tests that a successful request to set up totp will + * propagate a completion back through the binding. + * @throws InterruptedException If test observer is interrupted while awaiting terminal event + */ + @Test + public void testSetUpTOTPSucceeds() throws InterruptedException { + // Arrange an invocation of the success action + doAnswer(invocation -> { + // 0 = onComplete, 1 = onFailure + int positionOfCompletionConsumer = 0; + Consumer onComplete = invocation.getArgument(positionOfCompletionConsumer); + onComplete.accept(new TOTPSetupDetails("ss", "u")); + return null; + }).when(delegate).setUpTOTP(anyConsumer(), anyConsumer()); + + // Act: call the binding + TestObserver observer = auth.setUpTOTP().test(); + + // Assert: Completable completes successfully + observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + observer.assertComplete(); + } + + /** + * Validate that a setUp TOTP failure is propagated up through the binding. + * @throws InterruptedException If test observer is interrupted while awaiting terminal event + */ + @Test + public void testSetUpTOTPFails() throws InterruptedException { + AuthException failure = new AuthException("", "", new Exception()); + + // Arrange an invocation of the success action + doAnswer(invocation -> { + // 0 = onComplete, 1 = onFailure + int positionOfCompletionConsumer = 1; + Consumer onError = invocation.getArgument(positionOfCompletionConsumer); + onError.accept(failure); + return null; + }).when(delegate).setUpTOTP(anyConsumer(), anyConsumer()); + + // Act: call the binding + TestObserver observer = auth.setUpTOTP().test(); + + // Assert: Completable completes successfully + observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + observer.assertNotComplete() + .assertError(failure); + } + + /** + * Tests that a successful request to verify totp setup will + * propagate a completion back through the binding. + * @throws InterruptedException If test observer is interrupted while awaiting terminal event + */ + @Test + public void testVerifyTOTPSetupSucceeds() throws InterruptedException { + // Arrange an invocation of the success action + String myCode = "123"; + doAnswer(invocation -> { + // 0 = code, 1 = onComplete, 2 = onFailure + int positionOfCompletionConsumer = 1; + Action onComplete = invocation.getArgument(positionOfCompletionConsumer); + onComplete.call(); + return null; + }).when(delegate).verifyTOTPSetup(matches(myCode), anyAction(), anyConsumer()); + + // Act: call the binding + TestObserver observer = auth.verifyTOTPSetup(myCode).test(); + + // Assert: Completable completes successfully + observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + observer.assertComplete(); + } + + /** + * Validate that a verify TOTP setup failure is propagated up through the binding. + * @throws InterruptedException If test observer is interrupted while awaiting terminal event + */ + @Test + public void testVerifyTOTPSetupFails() throws InterruptedException { + // Arrange an invocation of the success action + String myCode = "123"; + AuthException failure = new AuthException("", "", new Exception()); + doAnswer(invocation -> { + // 0 = code, 1 = onComplete, 2 = onFailure + int positionOfCompletionConsumer = 2; + Consumer onError = invocation.getArgument(positionOfCompletionConsumer); + onError.accept(failure); + return null; + }).when(delegate).verifyTOTPSetup(matches(myCode), anyAction(), anyConsumer()); + + // Act: call the binding + TestObserver observer = auth.verifyTOTPSetup(myCode).test(); + + // Assert: Completable completes successfully + observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + observer.assertNotComplete() + .assertError(failure); + } + + /** + * Tests that a successful request to verify totp setup will + * propagate a completion back through the binding. + * @throws InterruptedException If test observer is interrupted while awaiting terminal event + */ + @Test + public void testVerifyTOTPSetupWithOptionsSucceeds() throws InterruptedException { + // Arrange an invocation of the success action + String myCode = "123"; + AuthVerifyTOTPSetupOptions myOptions = AuthVerifyTOTPSetupOptions.defaults(); + doAnswer(invocation -> { + // 0 = code, 1 = options, 2 = onComplete, 3 = onFailure + int positionOfCompletionConsumer = 2; + Action onComplete = invocation.getArgument(positionOfCompletionConsumer); + onComplete.call(); + return null; + }).when(delegate).verifyTOTPSetup( + matches(myCode), + any(), + anyAction(), + anyConsumer() + ); + + // Act: call the binding + TestObserver observer = auth.verifyTOTPSetup(myCode, myOptions).test(); + + // Assert: Completable completes successfully + observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + observer.assertComplete(); + } + + /** + * Validate that a verify TOTP setup failure is propagated up through the binding. + * @throws InterruptedException If test observer is interrupted while awaiting terminal event + */ + @Test + public void testVerifyTOTPSetupWithOptionsFails() throws InterruptedException { + // Arrange an invocation of the success action + String myCode = "123"; + AuthVerifyTOTPSetupOptions myOptions = AuthVerifyTOTPSetupOptions.defaults(); + AuthException failure = new AuthException("", "", new Exception()); + doAnswer(invocation -> { + // 0 = code, 1 = options, 2 = onComplete, 3 = onFailure + int positionOfCompletionConsumer = 3; + Consumer onError = invocation.getArgument(positionOfCompletionConsumer); + onError.accept(failure); + return null; + }).when(delegate).verifyTOTPSetup(matches(myCode), any(), anyAction(), anyConsumer()); + + // Act: call the binding + TestObserver observer = auth.verifyTOTPSetup(myCode, myOptions).test(); + + // Assert: Completable completes successfully + observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + observer.assertNotComplete() + .assertError(failure); + } } From 51f7326a155129339909da1f7f8b1c0f99c75de7 Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Tue, 29 Aug 2023 11:53:10 -0300 Subject: [PATCH 08/15] Add default implementations for TOTP APIs in AuthPlugin --- .../com/amplifyframework/auth/AuthPlugin.java | 46 ++++ .../amplifyframework/auth/AuthPluginTest.kt | 239 ++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 core/src/test/java/com/amplifyframework/auth/AuthPluginTest.kt diff --git a/core/src/main/java/com/amplifyframework/auth/AuthPlugin.java b/core/src/main/java/com/amplifyframework/auth/AuthPlugin.java index bec4eb7a06..c8fefd11c1 100644 --- a/core/src/main/java/com/amplifyframework/auth/AuthPlugin.java +++ b/core/src/main/java/com/amplifyframework/auth/AuthPlugin.java @@ -20,6 +20,10 @@ import androidx.annotation.WorkerThread; import com.amplifyframework.AmplifyException; +import com.amplifyframework.TOTPSetupDetails; +import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; +import com.amplifyframework.core.Action; +import com.amplifyframework.core.Consumer; import com.amplifyframework.core.category.CategoryType; import com.amplifyframework.core.plugin.Plugin; @@ -39,4 +43,46 @@ public final CategoryType getCategoryType() { @WorkerThread @Override public void initialize(@NonNull Context context) throws AmplifyException {} + + /** + * Default implementation that throws UnsupportedOperationException. + * @param onSuccess Success callback + * @param onError Error callback + */ + @Override + public void setUpTOTP(@NonNull Consumer onSuccess, @NonNull Consumer onError) { + throw new UnsupportedOperationException("TOTP is not implemented in this plugin"); + } + + /** + * Default implementation that throws UnsupportedOperationException. + * @param code TOTP code to verify TOTP setup + * @param onSuccess Success callback + * @param onError Error callback + */ + @Override + public void verifyTOTPSetup( + @NonNull String code, + @NonNull Action onSuccess, + @NonNull Consumer onError + ) { + throw new UnsupportedOperationException("TOTP is not implemented in this plugin"); + } + + /** + * Default implementation that throws UnsupportedOperationException. + * @param code TOTP code to verify TOTP setup + * @param options additional options to verify totp setup + * @param onSuccess Success callback + * @param onError Error callback + */ + @Override + public void verifyTOTPSetup( + @NonNull String code, + @NonNull AuthVerifyTOTPSetupOptions options, + @NonNull Action onSuccess, + @NonNull Consumer onError + ) { + throw new UnsupportedOperationException("TOTP is not implemented in this plugin"); + } } diff --git a/core/src/test/java/com/amplifyframework/auth/AuthPluginTest.kt b/core/src/test/java/com/amplifyframework/auth/AuthPluginTest.kt new file mode 100644 index 0000000000..5ea3013fcb --- /dev/null +++ b/core/src/test/java/com/amplifyframework/auth/AuthPluginTest.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2023 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 + +import android.app.Activity +import android.content.Context +import android.content.Intent +import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions +import com.amplifyframework.auth.options.AuthConfirmSignInOptions +import com.amplifyframework.auth.options.AuthConfirmSignUpOptions +import com.amplifyframework.auth.options.AuthFetchSessionOptions +import com.amplifyframework.auth.options.AuthResendSignUpCodeOptions +import com.amplifyframework.auth.options.AuthResendUserAttributeConfirmationCodeOptions +import com.amplifyframework.auth.options.AuthResetPasswordOptions +import com.amplifyframework.auth.options.AuthSignInOptions +import com.amplifyframework.auth.options.AuthSignOutOptions +import com.amplifyframework.auth.options.AuthSignUpOptions +import com.amplifyframework.auth.options.AuthUpdateUserAttributeOptions +import com.amplifyframework.auth.options.AuthUpdateUserAttributesOptions +import com.amplifyframework.auth.options.AuthWebUISignInOptions +import com.amplifyframework.auth.result.AuthResetPasswordResult +import com.amplifyframework.auth.result.AuthSignInResult +import com.amplifyframework.auth.result.AuthSignOutResult +import com.amplifyframework.auth.result.AuthSignUpResult +import com.amplifyframework.auth.result.AuthUpdateAttributeResult +import com.amplifyframework.core.Action +import com.amplifyframework.core.Consumer +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Test that ensures new Auth category APIs have a default implementation in the [AuthPlugin] class. This allows + * 3rd party Auth plugins to compile against newer versions of Amplify. + */ +class AuthPluginTest { + + @Test + fun `test plugin compiles`() { + // The purpose of this test is to ensure that TestPlugin compiles, the assertion is irrelevant + val plugin = TestPlugin() + assertEquals("testVersion", plugin.version) + } + + /** + * DO NOT add any implementations to this class. The purpose of this test is to ensure that any new methods added + * to the Auth category have default implementations in AuthPlugin. + */ + private class TestPlugin : AuthPlugin() { + override fun signUp( + username: String, + password: String, + options: AuthSignUpOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun confirmSignUp( + username: String, + confirmationCode: String, + options: AuthConfirmSignUpOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun confirmSignUp( + username: String, + confirmationCode: String, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun resendSignUpCode( + username: String, + options: AuthResendSignUpCodeOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun resendSignUpCode( + username: String, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun signIn( + username: String?, + password: String?, + options: AuthSignInOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun signIn( + username: String?, + password: String?, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun confirmSignIn( + challengeResponse: String, + options: AuthConfirmSignInOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun confirmSignIn( + challengeResponse: String, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun signInWithSocialWebUI( + provider: AuthProvider, + callingActivity: Activity, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun signInWithSocialWebUI( + provider: AuthProvider, + callingActivity: Activity, + options: AuthWebUISignInOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun signInWithWebUI( + callingActivity: Activity, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun signInWithWebUI( + callingActivity: Activity, + options: AuthWebUISignInOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun handleWebUISignInResponse(intent: Intent?) {} + override fun fetchAuthSession(onSuccess: Consumer, onError: Consumer) {} + override fun fetchAuthSession( + options: AuthFetchSessionOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun rememberDevice(onSuccess: Action, onError: Consumer) {} + override fun forgetDevice(onSuccess: Action, onError: Consumer) {} + override fun forgetDevice(device: AuthDevice, onSuccess: Action, onError: Consumer) {} + override fun fetchDevices(onSuccess: Consumer>, onError: Consumer) {} + override fun resetPassword( + username: String, + options: AuthResetPasswordOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun resetPassword( + username: String, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun confirmResetPassword( + username: String, + newPassword: String, + confirmationCode: String, + options: AuthConfirmResetPasswordOptions, + onSuccess: Action, + onError: Consumer + ) {} + override fun confirmResetPassword( + username: String, + newPassword: String, + confirmationCode: String, + onSuccess: Action, + onError: Consumer + ) {} + override fun updatePassword( + oldPassword: String, + newPassword: String, + onSuccess: Action, + onError: Consumer + ) {} + override fun fetchUserAttributes( + onSuccess: Consumer>, + onError: Consumer + ) {} + override fun updateUserAttribute( + attribute: AuthUserAttribute, + options: AuthUpdateUserAttributeOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun updateUserAttribute( + attribute: AuthUserAttribute, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun updateUserAttributes( + attributes: MutableList, + options: AuthUpdateUserAttributesOptions, + onSuccess: Consumer>, + onError: Consumer + ) {} + override fun updateUserAttributes( + attributes: MutableList, + onSuccess: Consumer>, + onError: Consumer + ) {} + override fun resendUserAttributeConfirmationCode( + attributeKey: AuthUserAttributeKey, + options: AuthResendUserAttributeConfirmationCodeOptions, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun resendUserAttributeConfirmationCode( + attributeKey: AuthUserAttributeKey, + onSuccess: Consumer, + onError: Consumer + ) {} + override fun confirmUserAttribute( + attributeKey: AuthUserAttributeKey, + confirmationCode: String, + onSuccess: Action, + onError: Consumer + ) {} + override fun getCurrentUser(onSuccess: Consumer, onError: Consumer) {} + override fun signOut(onComplete: Consumer) {} + override fun signOut(options: AuthSignOutOptions, onComplete: Consumer) {} + override fun deleteUser(onSuccess: Action, onError: Consumer) {} + override fun getPluginKey() = "" + override fun configure(pluginConfiguration: JSONObject?, context: Context) {} + override fun getEscapeHatch() = Unit + override fun getVersion() = "testVersion" + + // DO NOT add any additional overrides. New APIs must have a default implementation in the AuthPlugin base class. + } +} From 04ddbf479e20b49c41b166202726cd76bcf09e86 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 29 Aug 2023 11:41:13 -0400 Subject: [PATCH 09/15] mask totp setup data --- .../codegen/data/SignInTOTPSetupData.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt index 95b065a2dc..a4284327d5 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt @@ -18,4 +18,20 @@ internal data class SignInTOTPSetupData( val secretCode: String, val session: String?, val username: String -) +) { + override fun toString(): String { + return "SignInTOTPSetupData(" + + "secretCode = ${mask(secretCode)}, " + + "session = ${mask(session)}, " + + "username = ${mask(username)}" + + ")" + } + + private fun mask(value: String?): String { + return if (value == null || value.length <= 4) { + "***" + } else { + "${value.substring(0..4)}***" + } + } +} From 4a273b8264afff879f7fb5d327a7250c64b2238f Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 29 Aug 2023 11:41:50 -0400 Subject: [PATCH 10/15] Check if MFAs can setup --- .../auth/cognito/helpers/SignInChallengeHelper.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 edd9db687d..61e3b47345 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 @@ -89,8 +89,18 @@ internal object SignInChallengeHelper { SignInEvent(SignInEvent.EventType.ReceivedChallenge(challenge)) } challengeNameType is ChallengeNameType.MfaSetup -> { - val setupTOTPData = SignInTOTPSetupData("", session, username) - SignInEvent(SignInEvent.EventType.InitiateTOTPSetup(setupTOTPData)) + val allowedMFASetupTypes = challengeParameters?.get("MFAS_CAN_SETUP") + ?.let { getAllowedMFATypes(it) } ?: emptySet() + if (allowedMFASetupTypes.contains(MFAType.TOTP)) { + val setupTOTPData = SignInTOTPSetupData("", session, username) + SignInEvent(SignInEvent.EventType.InitiateTOTPSetup(setupTOTPData)) + } else { + SignInEvent( + SignInEvent.EventType.ThrowError( + Exception("Cannot initiate MFA setup from available Types: $allowedMFASetupTypes") + ) + ) + } } challengeNameType is ChallengeNameType.DeviceSrpAuth -> { SignInEvent(SignInEvent.EventType.InitiateSignInWithDeviceSRP(username, mapOf())) From a8764da37cc64589ed198c5261f86eb3122b604e Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 29 Aug 2023 15:09:12 -0400 Subject: [PATCH 11/15] Update messaging --- .../exceptions/service/EnableSoftwareTokenMFAException.kt | 2 +- .../exceptions/service/SoftwareTokenMFANotFoundException.kt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMFAException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMFAException.kt index 4c290dc28f..08f623c5d7 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMFAException.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/EnableSoftwareTokenMFAException.kt @@ -22,7 +22,7 @@ import com.amplifyframework.auth.AuthException */ open class EnableSoftwareTokenMFAException(cause: Throwable?) : AuthException( - "Software token TOTP multi-factor authentication (MFA) is not enabled for the user pool.", + "Unable to enable software token MFA", "Enable the software token MFA for the user.", cause ) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/SoftwareTokenMFANotFoundException.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/SoftwareTokenMFANotFoundException.kt index 856d0b5152..25f00dab7e 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/SoftwareTokenMFANotFoundException.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/exceptions/service/SoftwareTokenMFANotFoundException.kt @@ -21,4 +21,8 @@ import com.amplifyframework.auth.exceptions.ServiceException * @param cause The underlying cause of this exception */ open class SoftwareTokenMFANotFoundException(cause: Throwable?) : - ServiceException("Could not find software token MFA.", "Enable the software token MFA for the user.", cause) + ServiceException( + "Software token TOTP multi-factor authentication (MFA) is not enabled for the user pool.", + "Enable the software token MFA for the user.", + cause + ) From 4493003e5b5a0427b9956cb7b3711df541bdced8 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 29 Aug 2023 15:10:24 -0400 Subject: [PATCH 12/15] Update verifying during error state --- .../statemachine/codegen/states/SetupTOTPState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 abc12255c9..1dde89269e 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 @@ -128,7 +128,7 @@ internal sealed class SetupTOTPState : State { is Error -> when (challengeEvent) { is SetupTOTPEvent.EventType.VerifyChallengeAnswer -> { StateResolution( - Verifying(challengeEvent.answer, "", null), + Verifying(challengeEvent.answer, challengeEvent.username, challengeEvent.session), listOf(setupTOTPActions.verifyChallengeAnswer(challengeEvent)) ) } From 77f3793d31637e7979ca3db6aeb9285d5f52364c Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 29 Aug 2023 15:16:16 -0400 Subject: [PATCH 13/15] fix compile --- core/src/main/java/com/amplifyframework/auth/AuthPlugin.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/com/amplifyframework/auth/AuthPlugin.java b/core/src/main/java/com/amplifyframework/auth/AuthPlugin.java index c8fefd11c1..b1f5556e32 100644 --- a/core/src/main/java/com/amplifyframework/auth/AuthPlugin.java +++ b/core/src/main/java/com/amplifyframework/auth/AuthPlugin.java @@ -20,7 +20,6 @@ import androidx.annotation.WorkerThread; import com.amplifyframework.AmplifyException; -import com.amplifyframework.TOTPSetupDetails; import com.amplifyframework.auth.options.AuthVerifyTOTPSetupOptions; import com.amplifyframework.core.Action; import com.amplifyframework.core.Consumer; From 5d8e4e70e0a5c2eb727f0a808d0f66be1a509dee Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 29 Aug 2023 15:38:53 -0400 Subject: [PATCH 14/15] Remove unused data --- .../statemachine/codegen/states/SetupTOTPState.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 1dde89269e..e4697bcf57 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 @@ -29,8 +29,8 @@ internal sealed class SetupTOTPState : State { val signInTOTPSetupData: SignInTOTPSetupData, var hasNewResponse: Boolean = false ) : SetupTOTPState() - data class Verifying(val code: String, val username: String, val session: String?) : SetupTOTPState() - data class RespondingToAuthChallenge(val username: String, val session: String?) : SetupTOTPState() + data class Verifying(val id: String = "") : SetupTOTPState() + data class RespondingToAuthChallenge(val id: String = "") : SetupTOTPState() data class Success(val id: String = "") : SetupTOTPState() data class Error( val exception: Exception, @@ -76,11 +76,7 @@ internal sealed class SetupTOTPState : State { is WaitingForAnswer -> when (challengeEvent) { is SetupTOTPEvent.EventType.VerifyChallengeAnswer -> { StateResolution( - Verifying( - challengeEvent.answer, - oldState.signInTOTPSetupData.username, - oldState.signInTOTPSetupData.session - ), + Verifying(), listOf(setupTOTPActions.verifyChallengeAnswer(challengeEvent)) ) } @@ -95,7 +91,7 @@ internal sealed class SetupTOTPState : State { is Verifying -> when (challengeEvent) { is SetupTOTPEvent.EventType.RespondToAuthChallenge -> { StateResolution( - RespondingToAuthChallenge(oldState.username, oldState.session), + RespondingToAuthChallenge(), listOf( setupTOTPActions.respondToAuthChallenge( challengeEvent @@ -128,7 +124,7 @@ internal sealed class SetupTOTPState : State { is Error -> when (challengeEvent) { is SetupTOTPEvent.EventType.VerifyChallengeAnswer -> { StateResolution( - Verifying(challengeEvent.answer, challengeEvent.username, challengeEvent.session), + Verifying(), listOf(setupTOTPActions.verifyChallengeAnswer(challengeEvent)) ) } From 0cc7d2fc35c0dfd38a27b459f24e00cee269ce47 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 29 Aug 2023 15:55:25 -0400 Subject: [PATCH 15/15] Minor SM changes on TOTP --- .../statemachine/codegen/data/SignInTOTPSetupData.kt | 4 ++-- .../statemachine/codegen/states/SetupTOTPState.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt index a4284327d5..284cd6ce80 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/SignInTOTPSetupData.kt @@ -23,7 +23,7 @@ internal data class SignInTOTPSetupData( return "SignInTOTPSetupData(" + "secretCode = ${mask(secretCode)}, " + "session = ${mask(session)}, " + - "username = ${mask(username)}" + + "username = $username}" + ")" } @@ -31,7 +31,7 @@ internal data class SignInTOTPSetupData( return if (value == null || value.length <= 4) { "***" } else { - "${value.substring(0..4)}***" + "${value.substring(0 until 4)}***" } } } 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 e4697bcf57..2d9b03bfef 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 @@ -24,7 +24,7 @@ import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent internal sealed class SetupTOTPState : State { data class NotStarted(val id: String = "") : SetupTOTPState() - data class SetupTOTP(val signInTOTPSetupData: SignInTOTPSetupData) : SetupTOTPState() + data class SetupTOTP(val id: String = "") : SetupTOTPState() data class WaitingForAnswer( val signInTOTPSetupData: SignInTOTPSetupData, var hasNewResponse: Boolean = false @@ -49,7 +49,7 @@ internal sealed class SetupTOTPState : State { is NotStarted -> when (challengeEvent) { is SetupTOTPEvent.EventType.SetupTOTP -> { StateResolution( - SetupTOTP(challengeEvent.totpSetupDetails), + SetupTOTP(), listOf(setupTOTPActions.initiateTOTPSetup(challengeEvent)) ) }