Skip to content

Commit

Permalink
Merge pull request #8620 from vector-im/feature/bma/oidcSessionEnd
Browse files Browse the repository at this point in the history
Feature/bma/OIDC session end
  • Loading branch information
bmarty committed Sep 12, 2023
2 parents 1f41c54 + 52a0693 commit ec9a066
Show file tree
Hide file tree
Showing 21 changed files with 148 additions and 20 deletions.
1 change: 1 addition & 0 deletions changelog.d/8616.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If an external account manager is configured on the server, use it to delete other sessions and hide the multi session deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ data class HomeServerCapabilities(
* External account management url for use with MSC3824 delegated OIDC, provided in Wellknown.
*/
val externalAccountManagementUrl: String? = null,

/**
* Authentication issuer for use with MSC3824 delegated OIDC, provided in Wellknown.
*/
val authenticationIssuer: String? = null,
) {

enum class RoomCapabilitySupport {
Expand Down Expand Up @@ -141,6 +146,8 @@ data class HomeServerCapabilities(
return cap?.preferred ?: cap?.support?.lastOrNull()
}

val delegatedOidcAuthEnabled: Boolean = authenticationIssuer != null

companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L
const val ROOM_CAP_KNOCK = "knock"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ internal class DefaultAuthenticationService @Inject constructor(
}

// If an m.login.sso flow is present that is flagged as being for MSC3824 OIDC compatibility then we only return that flow
val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibilty == true }
val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibility == true }
val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows

val supportsGetLoginTokenFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.token" && it.getLoginToken == true } != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal data class LoginFlow(
* See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
*/
@Json(name = "org.matrix.msc3824.delegated_oidc_compatibility")
val delegatedOidcCompatibilty: Boolean? = null,
val delegatedOidcCompatibility: Boolean? = null,

/**
* Whether a login flow of type m.login.token could accept a token issued using /login/get_token.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo052
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo053
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
Expand All @@ -77,7 +78,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 52L,
schemaVersion = 53L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
Expand Down Expand Up @@ -139,5 +140,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 50) MigrateSessionTo050(realm).perform()
if (oldVersion < 51) MigrateSessionTo051(realm).perform()
if (oldVersion < 52) MigrateSessionTo052(realm).perform()
if (oldVersion < 53) MigrateSessionTo053(realm).perform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ internal object HomeServerCapabilitiesMapper {
canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
canRedactRelatedEvents = entity.canRedactEventWithRelations,
externalAccountManagementUrl = entity.externalAccountManagementUrl,
authenticationIssuer = entity.authenticationIssuer,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 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 MigrateSessionTo053(realm: DynamicRealm) : RealmMigrator(realm, 53) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.AUTHENTICATION_ISSUER, String::class.java)
?.forceRefreshOfHomeServerCapabilities()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal open class HomeServerCapabilitiesEntity(
var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
var canRedactEventWithRelations: Boolean = false,
var externalAccountManagementUrl: String? = null,
var authenticationIssuer: String? = null,
) : RealmObject() {

companion object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
Timber.v("Extracted integration config : $config")
realm.insertOrUpdate(config)
}
homeServerCapabilitiesEntity.authenticationIssuer = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.issuer
homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ sealed class DevicesViewEvents : VectorViewEvents {
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvents()

object PromptResetSecrets : DevicesViewEvents()

data class OpenBrowser(val url: String) : DevicesViewEvents()
}
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,20 @@ class DevicesViewModel @AssistedInject constructor(
private fun handleDelete(action: DevicesAction.Delete) {
val deviceId = action.deviceId

val accountManagementUrl = session.homeServerCapabilitiesService().getHomeServerCapabilities().externalAccountManagementUrl
if (accountManagementUrl != null) {
// Open external browser to delete this session
_viewEvents.post(
DevicesViewEvents.OpenBrowser(
url = accountManagementUrl.removeSuffix("/") + "?action=session_end&device_id=$deviceId"
)
)
} else {
doDelete(deviceId)
}
}

private fun doDelete(deviceId: String) {
setState {
copy(
request = Loading()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.FragmentGenericRecyclerBinding
import im.vector.app.features.auth.ReAuthActivity
Expand Down Expand Up @@ -95,6 +96,9 @@ class VectorSettingsDevicesFragment :
is DevicesViewEvents.PromptResetSecrets -> {
navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
}
is DevicesViewEvents.OpenBrowser -> {
openUrlInChromeCustomTab(requireContext(), null, it.url)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber

class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState,
activeSessionHolder: ActiveSessionHolder,
private val activeSessionHolder: ActiveSessionHolder,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
Expand Down Expand Up @@ -69,6 +70,19 @@ class DevicesViewModel @AssistedInject constructor(
refreshDeviceList()
refreshIpAddressVisibility()
observePreferences()
initDelegatedOidcAuthEnabled()
}

private fun initDelegatedOidcAuthEnabled() {
setState {
copy(
delegatedOidcAuthEnabled = activeSessionHolder.getSafeActiveSession()
?.homeServerCapabilitiesService()
?.getHomeServerCapabilities()
?.delegatedOidcAuthEnabled
.orFalse()
)
}
}

override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ data class DevicesViewState(
val devices: Async<DeviceFullInfoList> = Uninitialized,
val isLoading: Boolean = false,
val isShowingIpAddress: Boolean = false,
val delegatedOidcAuthEnabled: Boolean = false,
) : MavericksState

data class DeviceFullInfoList(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,8 @@ class VectorSettingsDevicesFragment :
val unverifiedSessionsCount = deviceFullInfoList?.unverifiedSessionsCount ?: 0

renderSecurityRecommendations(inactiveSessionsCount, unverifiedSessionsCount)
renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse())
renderOtherSessionsView(otherDevices, state.isShowingIpAddress)
renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse(), state)
renderOtherSessionsView(otherDevices, state)
} else {
hideSecurityRecommendations()
hideCurrentSessionView()
Expand Down Expand Up @@ -347,13 +347,16 @@ class VectorSettingsDevicesFragment :
hideInactiveSessionsRecommendation()
}

private fun renderOtherSessionsView(otherDevices: List<DeviceFullInfo>?, isShowingIpAddress: Boolean) {
private fun renderOtherSessionsView(otherDevices: List<DeviceFullInfo>?, state: DevicesViewState) {
val isShowingIpAddress = state.isShowingIpAddress
if (otherDevices.isNullOrEmpty()) {
hideOtherSessionsView()
} else {
views.deviceListHeaderOtherSessions.isVisible = true
val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout)
// Hide multi signout if the homeserver delegates the account management
multiSignoutItem.isVisible = state.delegatedOidcAuthEnabled.not()
val nbDevices = otherDevices.size
multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
multiSignoutItem.setTextColor(colorDestructive)
Expand All @@ -377,23 +380,24 @@ class VectorSettingsDevicesFragment :
views.deviceListOtherSessions.isVisible = false
}

private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) {
private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean, state: DevicesViewState) {
currentDeviceInfo?.let {
renderCurrentSessionHeaderView(hasOtherDevices)
renderCurrentSessionHeaderView(hasOtherDevices, state)
renderCurrentSessionListView(it)
} ?: run {
hideCurrentSessionView()
}
}

private fun renderCurrentSessionHeaderView(hasOtherDevices: Boolean) {
private fun renderCurrentSessionHeaderView(hasOtherDevices: Boolean, state: DevicesViewState) {
views.deviceListHeaderCurrentSession.isVisible = true
val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
val signoutSessionItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignout)
signoutSessionItem.setTextColor(colorDestructive)
val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions)
signoutOtherSessionsItem.setTextColor(colorDestructive)
signoutOtherSessionsItem.isVisible = hasOtherDevices
// Hide signout other sessions if the homeserver delegates the account management
signoutOtherSessionsItem.isVisible = hasOtherDevices && state.delegatedOidcAuthEnabled.not()
}

private fun renderCurrentSessionListView(currentDeviceInfo: DeviceFullInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,15 @@ class OtherSessionsFragment :
val nbDevices = viewState.devices()?.size ?: 0
stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
}
multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) {
viewState.devices.invoke()?.any { it.isSelected }.orFalse()
multiSignoutItem.isVisible = if (viewState.delegatedOidcAuthEnabled) {
// Hide multi signout if the homeserver delegates the account management
false
} else {
viewState.devices.invoke()?.isNotEmpty().orFalse()
if (viewState.isSelectModeEnabled) {
viewState.devices.invoke()?.any { it.isSelected }.orFalse()
} else {
viewState.devices.invoke()?.isNotEmpty().orFalse()
}
}
val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER
multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT)
Expand Down Expand Up @@ -308,7 +313,10 @@ class OtherSessionsFragment :
)
)
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_inactive_sessions_found)
updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_inactive_title, R.string.device_manager_learn_more_sessions_inactive)
updateSecurityLearnMoreButton(
R.string.device_manager_learn_more_sessions_inactive_title,
R.string.device_manager_learn_more_sessions_inactive
)
}
DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthN
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber

class OtherSessionsViewModel @AssistedInject constructor(
@Assisted private val initialState: OtherSessionsViewState,
activeSessionHolder: ActiveSessionHolder,
private val activeSessionHolder: ActiveSessionHolder,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val pendingAuthHandler: PendingAuthHandler,
Expand All @@ -65,6 +66,19 @@ class OtherSessionsViewModel @AssistedInject constructor(
observeDevices(initialState.currentFilter)
refreshIpAddressVisibility()
observePreferences()
initDelegatedOidcAuthEnabled()
}

private fun initDelegatedOidcAuthEnabled() {
setState {
copy(
delegatedOidcAuthEnabled = activeSessionHolder.getSafeActiveSession()
?.homeServerCapabilitiesService()
?.getHomeServerCapabilities()
?.delegatedOidcAuthEnabled
.orFalse()
)
}
}

override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ data class OtherSessionsViewState(
val isSelectModeEnabled: Boolean = false,
val isLoading: Boolean = false,
val isShowingIpAddress: Boolean = false,
val delegatedOidcAuthEnabled: Boolean = false,
) : MavericksState {

constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.databinding.FragmentSessionOverviewBinding
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.crypto.recover.SetupMode
Expand Down Expand Up @@ -135,10 +136,19 @@ class SessionOverviewFragment :
activity?.let { SignOutUiWorker(it).perform() }
}

private fun confirmSignoutOtherSession() {
activity?.let {
buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession)
.show()
private fun confirmSignoutOtherSession() = withState(viewModel) { state ->
if (state.externalAccountManagementUrl != null) {
// Manage in external account manager
openUrlInChromeCustomTab(
requireContext(),
null,
state.externalAccountManagementUrl.removeSuffix("/") + "?action=session_end&device_id=${state.deviceId}"
)
} else {
activity?.let {
buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession)
.show()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ class SessionOverviewViewModel @AssistedInject constructor(
observeNotificationsStatus(initialState.deviceId)
refreshIpAddressVisibility()
observePreferences()
initExternalAccountManagementUrl()
}

private fun initExternalAccountManagementUrl() {
setState {
copy(
externalAccountManagementUrl = activeSessionHolder.getSafeActiveSession()
?.homeServerCapabilitiesService()
?.getHomeServerCapabilities()
?.externalAccountManagementUrl
)
}
}

override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ data class SessionOverviewViewState(
val isLoading: Boolean = false,
val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED,
val isShowingIpAddress: Boolean = false,
val externalAccountManagementUrl: String? = null,
) : MavericksState {
constructor(args: SessionOverviewArgs) : this(
deviceId = args.deviceId
Expand Down

0 comments on commit ec9a066

Please sign in to comment.