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 package im.vector.matrix.android.api.session.signout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
/** /**
@ -31,6 +32,12 @@ interface SignOutService {
fun signInAgain(password: String, fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable 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 * 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 * @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 package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.MatrixCallback 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.session.signout.SignOutService
import im.vector.matrix.android.api.util.Cancelable 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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith 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 import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask, private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : SignOutService { private val taskExecutor: TaskExecutor) : SignOutService {
override fun signInAgain(password: String, override fun signInAgain(password: String,
@ -36,6 +43,13 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask
.executeBy(taskExecutor) .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, override fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable { callback: MatrixCallback<Unit>): Cancelable {
return signOutTask return signOutTask

View File

@ -55,4 +55,7 @@ sealed class LoginAction : VectorViewModelAction {
object ResetSignMode : ResetAction() object ResetSignMode : ResetAction()
object ResetLogin : ResetAction() object ResetLogin : ResetAction()
object ResetResetPassword : 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.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.RegisterAction -> handleRegisterAction(action)
is LoginAction.ResetAction -> handleResetAction(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, val resetPasswordEmail: String? = null,
@PersistState @PersistState
val homeServerUrl: String? = null, val homeServerUrl: String? = null,
// For SSO session recovery
@PersistState
val deviceId: String? = null,
// Network result // Network result
@PersistState @PersistState

View File

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

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.signout.soft package im.vector.riotx.features.signout.soft
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class SoftLogoutAction : VectorViewModelAction { sealed class SoftLogoutAction : VectorViewModelAction {
@ -24,5 +25,5 @@ sealed class SoftLogoutAction : VectorViewModelAction {
object TogglePassword : SoftLogoutAction() object TogglePassword : SoftLogoutAction()
data class SignInAgain(val password: String) : 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.R
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter 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.core.resources.StringProvider
import im.vector.riotx.features.login.LoginMode import im.vector.riotx.features.login.LoginMode
import im.vector.riotx.features.signout.soft.epoxy.* import im.vector.riotx.features.signout.soft.epoxy.*
@ -71,7 +72,7 @@ class SoftLogoutController @Inject constructor(
loginTextItem { loginTextItem {
id("signText1") id("signText1")
text(stringProvider.getString(R.string.soft_logout_signin_notice, text(stringProvider.getString(R.string.soft_logout_signin_notice,
state.homeServerUrl, state.homeServerUrl.toReducedUrl(),
state.userDisplayName, state.userDisplayName,
state.userId)) 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.MainActivity
import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.login.AbstractLoginFragment 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 im.vector.riotx.features.login.LoginNavigation
import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject import javax.inject.Inject
@ -56,6 +58,14 @@ class SoftLogoutFragment @Inject constructor(
softLogoutViewModel.subscribe(this) { softLogoutViewState -> softLogoutViewModel.subscribe(this) { softLogoutViewState ->
softLogoutController.update(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() { override fun ssoSubmit() {
// TODO loginSharedActionViewModel.post(LoginNavigation.Sso) loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
} }
override fun clearData() { override fun clearData() {

View File

@ -54,8 +54,9 @@ class SoftLogoutViewModel @AssistedInject constructor(
val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity()
val userId = activity.session.myUserId val userId = activity.session.myUserId
return SoftLogoutViewState( return SoftLogoutViewState(
homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString().toReducedUrl(), homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString(),
userId = userId, userId = userId,
deviceId = activity.session.sessionParams.credentials.deviceId ?: "",
userDisplayName = activity.session.getUser(userId)?.displayName ?: userId, userDisplayName = activity.session.getUser(userId)?.displayName ?: userId,
hasUnsavedKeys = activity.session.hasUnsavedKeys() 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) { override fun handle(action: SoftLogoutAction) {
when (action) { when (action) {
is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow() is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow()
is SoftLogoutAction.SignInAgain -> handleSignInAgain(action) is SoftLogoutAction.SignInAgain -> handleSignInAgain(action)
is SoftLogoutAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is SoftLogoutAction.PasswordChanged -> handlePasswordChange(action) is SoftLogoutAction.PasswordChanged -> handlePasswordChange(action)
is SoftLogoutAction.TogglePassword -> handleTogglePassword() 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) { private fun handleSignInAgain(action: SoftLogoutAction.SignInAgain) {
setState { setState {
copy( copy(
@ -190,21 +212,25 @@ class SoftLogoutViewModel @AssistedInject constructor(
} }
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
activeSessionHolder.setActiveSession(session) onSessionRestored()
// Start the sync
session.startSync(true)
// TODO Configure and start ? Check that the push still works...
setState {
copy(
asyncLoginAction = Success(Unit)
)
}
} }
} }
) )
} }
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() { override fun onCleared() {
super.onCleared() super.onCleared()

View File

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