diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 8d8df6ff82..a0983161af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean { && error.message == "Invalid password" } +fun Throwable.isInvalidUIAAuth(): Boolean { + return this is Failure.ServerError + && error.code == MatrixError.M_FORBIDDEN + && error.flows != null +} + /** * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt index 8915202f35..eb327dfd56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.account +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor + /** * This interface defines methods to manage the account. It's implemented at the session level. */ @@ -43,5 +45,5 @@ interface AccountService { * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see * an incomplete view of conversations */ - suspend fun deactivateAccount(password: String, eraseAllData: Boolean) + suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt index 6c2e8b4a4e..a99a589ec4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt @@ -18,21 +18,21 @@ package org.matrix.android.sdk.internal.session.account import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth @JsonClass(generateAdapter = true) internal data class DeactivateAccountParams( - @Json(name = "auth") - val auth: UserPasswordAuth? = null, - // Set to true to erase all data of the account @Json(name = "erase") - val erase: Boolean + val erase: Boolean, + + @Json(name = "auth") + val auth: Map? = null ) { companion object { - fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { + fun create(auth: UIABaseAuth?, erase: Boolean): DeactivateAccountParams { return DeactivateAccountParams( - auth = UserPasswordAuth(user = userId, password = password), + auth = auth?.asMap(), erase = erase ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt index 9fb1cbb7d7..148afa7c90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.internal.session.account +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.internal.auth.registration.handleUIA +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest @@ -27,8 +30,9 @@ import javax.inject.Inject internal interface DeactivateAccountTask : Task { data class Params( - val password: String, - val eraseAllData: Boolean + val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, + val eraseAllData: Boolean, + val userAuthParam: UIABaseAuth? = null ) } @@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor( ) : DeactivateAccountTask { override suspend fun execute(params: DeactivateAccountTask.Params) { - val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) + val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData) - executeRequest(globalErrorReceiver) { - apiCall = accountAPI.deactivate(deactivateAccountParams) + try { + executeRequest(globalErrorReceiver) { + apiCall = accountAPI.deactivate(deactivateAccountParams) + } + } catch (throwable: Throwable) { + if (!handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth -> + execute(params.copy(userAuthParam = auth)) + } + ) { + Timber.d("## UIA: propagate failure") + throw throwable + } } - // Logout from identity server if any, ignoring errors runCatching { identityDisconnectTask.execute(Unit) } .onFailure { Timber.w(it, "Unable to disconnect identity server") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt index 1165d2116b..25b67159a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.account +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.account.AccountService import javax.inject.Inject @@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword)) } - override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) { - deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData)) + override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) { + deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData)) } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt index a95c2b73cc..b44639750e 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -154,7 +154,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras") super.onNavigationEvent(navigationEvent, extras) if (navigationEvent == NAVIGATION_FINISHED) { - sharedViewModel.handle(ReAuthActions.FallBackPageLoaded) +// sharedViewModel.handle(ReAuthActions.FallBackPageLoaded) } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt index 4b477990c0..a946a91ced 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -54,6 +54,7 @@ class ReAuthViewModel @AssistedInject constructor( when (action) { ReAuthActions.StartSSOFallback -> { if (state.flowType == LoginFlowTypes.SSO) { + setState { copy(ssoFallbackPageWasShown = true) } val ssoURL = session.getUIASsoFallbackUrl(initialState.session ?: "") _viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL)) } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt new file mode 100644 index 0000000000..c3fa844805 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 im.vector.app.features.settings.account.deactivation + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class DeactivateAccountAction : VectorViewModelAction { + object TogglePassword : DeactivateAccountAction() + data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction() + + object SsoAuthDone: DeactivateAccountAction() + data class PasswordAuthDone(val password: String): DeactivateAccountAction() + object ReAuthCancelled: DeactivateAccountAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index 3d128eb755..2cc80bfa23 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.account.deactivation +import android.app.Activity import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -23,16 +24,16 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.extensions.showPassword +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentDeactivateAccountBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.VectorSettingsActivity +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import javax.inject.Inject @@ -46,6 +47,25 @@ class DeactivateAccountFragment @Inject constructor( return FragmentDeactivateAccountBinding.inflate(inflater, container, false) } + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(DeactivateAccountAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(DeactivateAccountAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(DeactivateAccountAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(DeactivateAccountAction.ReAuthCancelled) + } + } + override fun onResume() { super.onResume() (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title) @@ -66,59 +86,46 @@ class DeactivateAccountFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupUi() setupViewListeners() observeViewEvents() } - private fun setupUi() { - views.deactivateAccountPassword.textChanges() - .subscribe { - views.deactivateAccountPasswordTil.error = null - views.deactivateAccountSubmit.isEnabled = it.isNotEmpty() - } - .disposeOnDestroyView() - } - private fun setupViewListeners() { - views.deactivateAccountPasswordReveal.setOnClickListener { - viewModel.handle(DeactivateAccountAction.TogglePassword) - } - views.deactivateAccountSubmit.debouncedClicks { viewModel.handle(DeactivateAccountAction.DeactivateAccount( - views.deactivateAccountPassword.text.toString(), - views.deactivateAccountEraseCheckbox.isChecked)) + views.deactivateAccountEraseCheckbox.isChecked) + ) } } private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is DeactivateAccountViewEvents.Loading -> { + is DeactivateAccountViewEvents.Loading -> { settingsActivity?.ignoreInvalidTokenError = true showLoadingDialog(it.message) } - DeactivateAccountViewEvents.EmptyPassword -> { + DeactivateAccountViewEvents.InvalidAuth -> { + dismissLoadingDialog() settingsActivity?.ignoreInvalidTokenError = false - views.deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password) } - DeactivateAccountViewEvents.InvalidPassword -> { - settingsActivity?.ignoreInvalidTokenError = false - views.deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password) - } - is DeactivateAccountViewEvents.OtherFailure -> { + is DeactivateAccountViewEvents.OtherFailure -> { settingsActivity?.ignoreInvalidTokenError = false + dismissLoadingDialog() displayErrorDialog(it.throwable) } - DeactivateAccountViewEvents.Done -> + DeactivateAccountViewEvents.Done -> { MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) + } + is DeactivateAccountViewEvents.RequestReAuth -> { + ReAuthActivity.newIntent(requireContext(), + it.registrationFlowResponse, + it.lastErrorCode, + getString(R.string.deactivate_account_title)).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } }.exhaustive } } - - override fun invalidate() = withState(viewModel) { state -> - views.deactivateAccountPassword.showPassword(state.passwordShown) - views.deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed else R.drawable.ic_eye) - } } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt index 46acb4aee4..05200c3aa3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt @@ -17,14 +17,15 @@ package im.vector.app.features.settings.account.deactivation import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse /** * Transient events for deactivate account settings screen */ sealed class DeactivateAccountViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents() - object EmptyPassword : DeactivateAccountViewEvents() - object InvalidPassword : DeactivateAccountViewEvents() + object InvalidAuth : DeactivateAccountViewEvents() data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents() object Done : DeactivateAccountViewEvents() + data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DeactivateAccountViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt index 6a7084fb81..dc5415a6bb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -21,25 +21,28 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.auth.ReAuthActivity import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.failure.isInvalidPassword +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.failure.isInvalidUIAAuth import org.matrix.android.sdk.api.session.Session -import java.lang.Exception +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume data class DeactivateAccountViewState( val passwordShown: Boolean = false ) : MvRxState -sealed class DeactivateAccountAction : VectorViewModelAction { - object TogglePassword : DeactivateAccountAction() - data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction() -} - class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState, private val session: Session) : VectorViewModel(initialState) { @@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel } + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + override fun handle(action: DeactivateAccountAction) { when (action) { - DeactivateAccountAction.TogglePassword -> handleTogglePassword() + DeactivateAccountAction.TogglePassword -> handleTogglePassword() is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) + DeactivateAccountAction.SsoAuthDone -> { + Timber.d("## UIA - FallBack success") + if (pendingAuth != null) { + uiaContinuation?.resume(pendingAuth!!) + } else { + uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + } + } + is DeactivateAccountAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = decryptedPass, + user = session.myUserId + ) + ) + } + DeactivateAccountAction.ReAuthCancelled -> { + Timber.d("## UIA - Reauth cancelled") + uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation = null + pendingAuth = null + } }.exhaustive } @@ -63,20 +93,22 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v } private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) { - if (action.password.isEmpty()) { - _viewEvents.post(DeactivateAccountViewEvents.EmptyPassword) - return - } - _viewEvents.post(DeactivateAccountViewEvents.Loading()) viewModelScope.launch { val event = try { - session.deactivateAccount(action.password, action.eraseAllData) + session.deactivateAccount( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + _viewEvents.post(DeactivateAccountViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise + } + }, action.eraseAllData) DeactivateAccountViewEvents.Done } catch (failure: Exception) { - if (failure.isInvalidPassword()) { - DeactivateAccountViewEvents.InvalidPassword + if (failure.isInvalidUIAAuth()) { + DeactivateAccountViewEvents.InvalidAuth } else { DeactivateAccountViewEvents.OtherFailure(failure) } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index d29ecefff1..ceb216ca42 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -124,16 +124,12 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( Unit } is CrossSigningSettingsAction.SsoAuthDone -> { - // we should use token based auth - // _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) - // will release the interactive auth interceptor - Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation") + Timber.d("## UIA - FallBack success") if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) } else { uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) } - Unit } is CrossSigningSettingsAction.PasswordAuthDone -> { val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) diff --git a/vector/src/main/res/layout/fragment_deactivate_account.xml b/vector/src/main/res/layout/fragment_deactivate_account.xml index db85c607e1..4bbf0a496c 100644 --- a/vector/src/main/res/layout/fragment_deactivate_account.xml +++ b/vector/src/main/res/layout/fragment_deactivate_account.xml @@ -31,75 +31,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" /> - - - - - - - - - - - - - - -