SoftLogout: recovery with SSO

This commit is contained in:
Benoit Marty 2019-12-13 01:25:58 +01:00
parent 183d6b53bd
commit 4e74b545ad
11 changed files with 130 additions and 31 deletions

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.signout
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.util.Cancelable
/**
@ -31,6 +32,12 @@ interface SignOutService {
fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Update the session with credentials received after SSO
*/
fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable
/**
* Sign out, and release the session, clear all the session data, including crypto data
* @param sigOutFromHomeserver true if the sign out request has to be done

View File

@ -17,14 +17,21 @@
package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : SignOutService {
override fun signInAgain(password: String,
@ -36,6 +43,13 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask
.executeBy(taskExecutor)
}
override fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials)
}
}
override fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable {
return signOutTask

View File

@ -55,4 +55,7 @@ sealed class LoginAction : VectorViewModelAction {
object ResetSignMode : ResetAction()
object ResetLogin : ResetAction()
object ResetResetPassword : ResetAction()
// For the soft logout case
data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction()
}

View File

@ -102,6 +102,18 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
is LoginAction.RegisterAction -> handleRegisterAction(action)
is LoginAction.ResetAction -> handleResetAction(action)
is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action)
}
}
private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) {
setState {
copy(
signMode = SignMode.SignIn,
loginMode = LoginMode.Sso,
homeServerUrl = action.homeServerUrl,
deviceId = action.deviceId
)
}
}

View File

@ -34,6 +34,9 @@ data class LoginViewState(
val resetPasswordEmail: String? = null,
@PersistState
val homeServerUrl: String? = null,
// For SSO session recovery
@PersistState
val deviceId: String? = null,
// Network result
@PersistState

View File

@ -30,10 +30,14 @@ import android.webkit.SslErrorHandler
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.features.signout.soft.SoftLogoutAction
import im.vector.riotx.features.signout.soft.SoftLogoutViewModel
import kotlinx.android.synthetic.main.fragment_login_web.*
import timber.log.Timber
import java.net.URLDecoder
@ -48,9 +52,12 @@ class LoginWebFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
private val softLogoutViewModel: SoftLogoutViewModel by activityViewModel()
override fun getLayoutResId() = R.layout.fragment_login_web
private var isWebViewLoaded = false
private var isForSessionRecovery = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -60,6 +67,9 @@ class LoginWebFragment @Inject constructor(
override fun updateWithState(state: LoginViewState) {
setupTitle(state)
isForSessionRecovery = state.deviceId?.isNotBlank() == true
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
@ -110,13 +120,22 @@ class LoginWebFragment @Inject constructor(
}
private fun launchWebView(state: LoginViewState) {
if (state.signMode == SignMode.SignIn) {
loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/login/")
} else {
// MODE_REGISTER
loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/register/")
val url = buildString {
append(state.homeServerUrl?.trim { it == '/' })
if (state.signMode == SignMode.SignIn) {
append("/_matrix/static/client/login/")
state.deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755
append("?device_id=$it")
}
} else {
// MODE_REGISTER
append("/_matrix/static/client/register/")
}
}
loginWebWebView.loadUrl(url)
loginWebWebView.webViewClient = object : WebViewClient() {
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler,
error: SslError) {
@ -212,10 +231,7 @@ class LoginWebFragment @Inject constructor(
if (state.signMode == SignMode.SignIn) {
try {
if (action == "onLogin") {
val credentials = javascriptResponse.credentials
if (credentials != null) {
loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
javascriptResponse.credentials?.let { notifyViewModel(it) }
}
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading() : failed")
@ -224,10 +240,7 @@ class LoginWebFragment @Inject constructor(
// MODE_REGISTER
// check the required parameters
if (action == "onRegistered") {
val credentials = javascriptResponse.credentials
if (credentials != null) {
loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
javascriptResponse.credentials?.let { notifyViewModel(it) }
}
}
}
@ -239,6 +252,14 @@ class LoginWebFragment @Inject constructor(
}
}
private fun notifyViewModel(credentials: Credentials) {
if (isForSessionRecovery) {
softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials))
} else {
loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.signout.soft
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class SoftLogoutAction : VectorViewModelAction {
@ -24,5 +25,5 @@ sealed class SoftLogoutAction : VectorViewModelAction {
object TogglePassword : SoftLogoutAction()
data class SignInAgain(val password: String) : SoftLogoutAction()
// TODO Add reset pwd...
data class WebLoginSuccess(val credentials: Credentials) : SoftLogoutAction()
}

View File

@ -23,6 +23,7 @@ import com.airbnb.mvrx.Success
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.toReducedUrl
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.login.LoginMode
import im.vector.riotx.features.signout.soft.epoxy.*
@ -71,7 +72,7 @@ class SoftLogoutController @Inject constructor(
loginTextItem {
id("signText1")
text(stringProvider.getString(R.string.soft_logout_signin_notice,
state.homeServerUrl,
state.homeServerUrl.toReducedUrl(),
state.userDisplayName,
state.userId))
}

View File

@ -31,6 +31,8 @@ import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction
import im.vector.riotx.features.login.LoginMode
import im.vector.riotx.features.login.LoginNavigation
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
@ -56,6 +58,14 @@ class SoftLogoutFragment @Inject constructor(
softLogoutViewModel.subscribe(this) { softLogoutViewState ->
softLogoutController.update(softLogoutViewState)
if (softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke() == LoginMode.Sso) {
// Prepare the loginViewModel for a SSO recovery
loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId
))
}
}
}
@ -84,7 +94,7 @@ class SoftLogoutFragment @Inject constructor(
}
override fun ssoSubmit() {
// TODO loginSharedActionViewModel.post(LoginNavigation.Sso)
loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
}
override fun clearData() {

View File

@ -54,8 +54,9 @@ class SoftLogoutViewModel @AssistedInject constructor(
val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity()
val userId = activity.session.myUserId
return SoftLogoutViewState(
homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString().toReducedUrl(),
homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString(),
userId = userId,
deviceId = activity.session.sessionParams.credentials.deviceId ?: "",
userDisplayName = activity.session.getUser(userId)?.displayName ?: userId,
hasUnsavedKeys = activity.session.hasUnsavedKeys()
)
@ -139,14 +140,11 @@ class SoftLogoutViewModel @AssistedInject constructor(
})
}
// TODO Cleanup
// private val _viewEvents = PublishDataSource<LoginViewEvents>()
// val viewEvents: DataSource<LoginViewEvents> = _viewEvents
override fun handle(action: SoftLogoutAction) {
when (action) {
is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow()
is SoftLogoutAction.SignInAgain -> handleSignInAgain(action)
is SoftLogoutAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is SoftLogoutAction.PasswordChanged -> handlePasswordChange(action)
is SoftLogoutAction.TogglePassword -> handleTogglePassword()
}
@ -171,6 +169,30 @@ class SoftLogoutViewModel @AssistedInject constructor(
}
}
private fun handleWebLoginSuccess(action: SoftLogoutAction.WebLoginSuccess) {
setState {
copy(
asyncLoginAction = Loading()
)
}
currentTask = session.updateCredentials(action.credentials,
object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
setState {
copy(
asyncLoginAction = Fail(failure)
)
}
}
override fun onSuccess(data: Unit) {
onSessionRestored()
}
}
)
}
private fun handleSignInAgain(action: SoftLogoutAction.SignInAgain) {
setState {
copy(
@ -190,21 +212,25 @@ class SoftLogoutViewModel @AssistedInject constructor(
}
override fun onSuccess(data: Unit) {
activeSessionHolder.setActiveSession(session)
// Start the sync
session.startSync(true)
// TODO Configure and start ? Check that the push still works...
setState {
copy(
asyncLoginAction = Success(Unit)
)
}
onSessionRestored()
}
}
)
}
private fun onSessionRestored() {
activeSessionHolder.setActiveSession(session)
// Start the sync
session.startSync(true)
// TODO Configure and start ? Check that the push still works...
setState {
copy(
asyncLoginAction = Success(Unit)
)
}
}
override fun onCleared() {
super.onCleared()

View File

@ -24,6 +24,7 @@ data class SoftLogoutViewState(
val asyncLoginAction: Async<Unit> = Uninitialized,
val homeServerUrl: String,
val userId: String,
val deviceId: String,
val userDisplayName: String,
val hasUnsavedKeys: Boolean,
val passwordShown: Boolean = false,