Skip to content

Commit

Permalink
Merge pull request #6207 from vector-im/feature/adm/sdk-signout-all-d…
Browse files Browse the repository at this point in the history
…evices

SDK - Logout all devices
  • Loading branch information
ouchadam authored Jun 27, 2022
2 parents 6b75e2c + e655a54 commit ccb4f2d
Show file tree
Hide file tree
Showing 25 changed files with 115 additions and 30 deletions.
1 change: 1 addition & 0 deletions changelog.d/6191.sdk
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for MSC2457 - opting in or out of logging out all devices when changing password
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ data class LoginFlowResult(
val ssoIdentityProviders: List<SsoIdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean
val isOutdatedHomeserver: Boolean,
val isLogoutDevicesSupported: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ interface LoginWizard {
* Confirm the new password, once the user has checked their email
* When this method succeed, tha account password will be effectively modified.
*
* @param newPassword the desired new password
* @param newPassword the desired new password.
* @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
* if [org.matrix.android.sdk.api.auth.data.LoginFlowResult.isLogoutDevicesSupported] is true.
*/
suspend fun resetPasswordMailConfirmed(newPassword: String)
suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
interface AccountService {
/**
* Ask the homeserver to change the password.
*
* @param password Current password.
* @param newPassword New password
* @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
* if [org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities.canControlLogoutDevices] is true.
*/
suspend fun changePassword(
password: String,
newPassword: String
)
suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean = true)

/**
* Deactivate the account.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ data class HomeServerCapabilities(
/**
* True if the home server support threading.
*/
val canUseThreading: Boolean = false
val canUseThreading: Boolean = false,

/**
* True if the home server supports controlling the logout of all devices when changing password.
*/
val canControlLogoutDevices: Boolean = false
) {

enum class RoomCapabilitySupport {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard
import org.matrix.android.sdk.internal.auth.login.DirectLoginTask
import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard
import org.matrix.android.sdk.internal.auth.version.Versions
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
import org.matrix.android.sdk.internal.di.Unauthenticated
Expand Down Expand Up @@ -292,7 +293,8 @@ internal class DefaultAuthenticationService @Inject constructor(
ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl = homeServerUrl,
isOutdatedHomeserver = !versions.isSupportedBySdk()
isOutdatedHomeserver = !versions.isSupportedBySdk(),
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,13 @@ internal class DefaultLoginWizard(
.also { pendingSessionStore.savePendingSessionData(it) }
}

override suspend fun resetPasswordMailConfirmed(newPassword: String) {
override suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean) {
val resetPasswordData = pendingSessionData.resetPasswordData ?: throw IllegalStateException("Developer error - Must call resetPassword first")
val param = ResetPasswordMailConfirmed.create(
pendingSessionData.clientSecret,
resetPasswordData.addThreePidRegistrationResponse.sid,
newPassword
newPassword,
logoutAllDevices
)

executeRequest(null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ internal data class ResetPasswordMailConfirmed(

// the new password
@Json(name = "new_password")
val newPassword: String? = null
val newPassword: String? = null,

@Json(name = "logout_devices")
val logoutDevices: Boolean? = null
) {
companion object {
fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed {
fun create(clientSecret: String, sid: String, newPassword: String, logoutDevices: Boolean?): ResetPasswordMailConfirmed {
return ResetPasswordMailConfirmed(
auth = AuthParams.createForResetPassword(clientSecret, sid),
newPassword = newPassword
newPassword = newPassword,
logoutDevices = logoutDevices
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ internal data class HomeServerVersion(
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1)
val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
}

/**
* Indicate if the server supports MSC2457 `logout_devices` parameter when setting a new password.
*
* @return true if logout_devices is supported
*/
internal fun Versions.doesServerSupportLogoutDevices(): Boolean {
return getMaxVersion() >= HomeServerVersion.r0_6_1
}

private fun Versions.getMaxVersion(): HomeServerVersion {
return supportedVersions
?.mapNotNull { HomeServerVersion.parse(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
import org.matrix.android.sdk.internal.util.Normalizer
import timber.log.Timber
import javax.inject.Inject
Expand All @@ -62,7 +63,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
override fun equals(other: Any?) = other is RealmSessionStoreMigration
override fun hashCode() = 1000

val schemaVersion = 30L
val schemaVersion = 31L

override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
Expand Down Expand Up @@ -97,5 +98,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 28) MigrateSessionTo028(realm).perform()
if (oldVersion < 29) MigrateSessionTo029(realm).perform()
if (oldVersion < 30) MigrateSessionTo030(realm).perform()
if (oldVersion < 31) MigrateSessionTo031(realm).perform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ internal object HomeServerCapabilitiesMapper {
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
roomVersions = mapRoomVersion(entity.roomVersionsJson),
canUseThreading = entity.canUseThreading
canUseThreading = entity.canUseThreading,
canControlLogoutDevices = entity.canControlLogoutDevices
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator
* Migrating to:
* Live location sharing aggregated summary: adding new field userId.
*/
internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) {
internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 29) {

override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.migration

import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
import org.matrix.android.sdk.internal.util.database.RealmMigrator

internal class MigrateSessionTo031(realm: DynamicRealm) : RealmMigrator(realm, 31) {

override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.CAN_CONTROL_LOGOUT_DEVICES, Boolean::class.java)
?.forceRefreshOfHomeServerCapabilities()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ internal open class HomeServerCapabilitiesEntity(
var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null,
var lastUpdatedTimestamp: Long = 0L,
var canUseThreading: Boolean = false
var canUseThreading: Boolean = false,
var canControlLogoutDevices: Boolean = false
) : RealmObject() {

companion object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ internal data class ChangePasswordParams(
val auth: UserPasswordAuth? = null,

@Json(name = "new_password")
val newPassword: String? = null
val newPassword: String? = null,

@Json(name = "logout_devices")
val logoutDevices: Boolean = true
) {
companion object {
fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams {
fun create(userId: String, oldPassword: String, newPassword: String, logoutAllDevices: Boolean): ChangePasswordParams {
return ChangePasswordParams(
auth = UserPasswordAuth(user = userId, password = oldPassword),
newPassword = newPassword
newPassword = newPassword,
logoutDevices = logoutAllDevices
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import javax.inject.Inject
internal interface ChangePasswordTask : Task<ChangePasswordTask.Params, Unit> {
data class Params(
val password: String,
val newPassword: String
val newPassword: String,
val logoutAllDevices: Boolean
)
}

Expand All @@ -37,7 +38,7 @@ internal class DefaultChangePasswordTask @Inject constructor(
) : ChangePasswordTask {

override suspend fun execute(params: ChangePasswordTask.Params) {
val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword)
val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword, params.logoutAllDevices)
try {
executeRequest(globalErrorReceiver) {
accountAPI.changePassword(changePasswordParams)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ internal class DefaultAccountService @Inject constructor(
private val deactivateAccountTask: DeactivateAccountTask
) : AccountService {

override suspend fun changePassword(password: String, newPassword: String) {
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
override suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean) {
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword, logoutAllDevices))
}

override suspend fun deactivateAccount(eraseAllData: Boolean, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.auth.version.Versions
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
Expand Down Expand Up @@ -142,6 +143,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(

if (getVersionResult != null) {
homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk()
homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices()
}

if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,10 +444,11 @@ class OnboardingViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch {
runCatching { safeLoginWizard.resetPassword(action.email) }.fold(
onSuccess = {
val state = awaitState()
setState {
copy(
isLoading = false,
resetState = ResetState(email = action.email, newPassword = action.newPassword)
resetState = createResetState(action, state.selectedHomeserver)
)
}
_viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
Expand All @@ -460,6 +461,12 @@ class OnboardingViewModel @AssistedInject constructor(
}
}

private fun createResetState(action: OnboardingAction.ResetPassword, selectedHomeserverState: SelectedHomeserverState) = ResetState(
email = action.email,
newPassword = action.newPassword,
supportsLogoutAllDevices = selectedHomeserverState.isLogoutDevicesSupported
)

private fun handleResetPasswordMailConfirmed() {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ data class SelectedHomeserverState(
val upstreamUrl: String? = null,
val preferredLoginMode: LoginMode = LoginMode.Unknown,
val supportedLoginTypes: List<String> = emptyList(),
val isLogoutDevicesSupported: Boolean = false,
) : Parcelable

@Parcelize
Expand All @@ -88,6 +89,7 @@ data class PersonalizationState(
data class ResetState(
val email: String? = null,
val newPassword: String? = null,
val supportsLogoutAllDevices: Boolean = false
) : Parcelable

@Parcelize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class StartAuthenticationFlowUseCase @Inject constructor(
userFacingUrl = config.homeServerUri.toString(),
upstreamUrl = authFlow.homeServerUrl,
preferredLoginMode = preferredLoginMode,
supportedLoginTypes = authFlow.supportedLoginTypes
supportedLoginTypes = authFlow.supportedLoginTypes,
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported
)

private fun matrixOrgUrl() = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,10 @@ class VectorSettingsGeneralFragment @Inject constructor(
}
}

val homeServerCapabilities = session.homeServerCapabilitiesService().getHomeServerCapabilities()
// Password
// Hide the preference if password can not be updated
if (session.homeServerCapabilitiesService().getHomeServerCapabilities().canChangePassword) {
if (homeServerCapabilities.canChangePassword) {
mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
onPasswordUpdateClick()
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a
private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true)
private const val AN_EMAIL = "[email protected]"
private const val A_PASSWORD = "a-password"

Expand Down Expand Up @@ -478,6 +479,7 @@ class OnboardingViewModelTest {

@Test
fun `given can successfully reset password, when resetting password, then emits reset done event`() = runTest {
viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES))
val test = viewModel.test()
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
Expand All @@ -488,7 +490,10 @@ class OnboardingViewModelTest {
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false, resetState = ResetState(AN_EMAIL, A_PASSWORD)) }
{
val resetState = ResetState(AN_EMAIL, A_PASSWORD, supportsLogoutAllDevices = true)
copy(isLoading = false, resetState = resetState)
}
)
.assertEvents(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
.finish()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ class StartAuthenticationFlowUseCaseTest {
ssoIdentityProviders = SSO_IDENTITY_PROVIDERS,
isLoginAndRegistrationSupported = true,
homeServerUrl = A_DECLARED_HOMESERVER_URL,
isOutdatedHomeserver = false
isOutdatedHomeserver = false,
isLogoutDevicesSupported = false
)

private fun expectedResult(
Expand Down

0 comments on commit ccb4f2d

Please sign in to comment.