Merge pull request #7877 from element-hq/bugfix/bma/sessionDelegatePerClient

Fix the wrong account being signed out when the session of another account is revoked
This commit is contained in:
Benoit Marty
2026-10-07 15:29:35 +02:00
committed by GitHub
7 changed files with 88 additions and 2 deletions
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl
import org.matrix.rustcomponents.sdk.ClientSessionDelegate
import org.matrix.rustcomponents.sdk.Session
/**
* A [ClientSessionDelegate] which does nothing, to be used for the clients which are not associated to a stored session,
* for instance the temporary clients, or the clients used to perform a login. The session is only stored once the login
* is done, with a client created by [RustMatrixClientFactory.create], which uses a [RustClientSessionDelegate].
*/
internal object NoOpClientSessionDelegate : ClientSessionDelegate {
override fun saveSessionInKeychain(session: Session) = Unit
override fun retrieveSessionFromKeychain(userId: String): Session {
// This should never be called, as it's only used for multi-process setups
error("retrieveSessionFromKeychain should never be called for Android")
}
}
@@ -37,6 +37,7 @@ private val loggerTag = LoggerTag("RustClientSessionDelegate")
* It implements both [ClientSessionDelegate] and [ClientDelegate] to react to session data updates and auth errors.
*
* IMPORTANT: you must set the [client] property as soon as possible so [didReceiveAuthError] can work properly.
* Since [didReceiveAuthError] does not tell which session is concerned, each client must have its own instance.
*/
class RustClientSessionDelegate(
private val sessionStore: SessionStore,
@@ -33,6 +33,7 @@ import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.ClientSessionDelegate
import org.matrix.rustcomponents.sdk.CrossProcessLockConfig
import org.matrix.rustcomponents.sdk.RequestConfig
import org.matrix.rustcomponents.sdk.Session
@@ -65,7 +66,11 @@ class RustMatrixClientFactory(
private val clientBuilderEnterpriseHook: ClientBuilderEnterpriseHook,
private val innerMatrixClientFactory: RustMatrixClient.Factory,
) {
private val sessionDelegate = RustClientSessionDelegate(
/**
* Create a new [RustClientSessionDelegate]. Each client must have its own instance, since the delegate
* keeps a reference to the client it handles the auth errors for.
*/
private fun createSessionDelegate() = RustClientSessionDelegate(
sessionStore = sessionStore,
appCoroutineScope = appCoroutineScope,
analyticsService = analyticsService,
@@ -86,11 +91,14 @@ class RustMatrixClientFactory(
indexDirectory.deleteRecursively()
}
// Use a dedicated delegate for this client, so that an auth error only affects this session
val sessionDelegate = createSessionDelegate()
val client = getBaseClientBuilder(
sessionPaths = sessionPaths,
clientSecret = clientSecret,
slidingSyncType = ClientBuilderSlidingSync.Restored,
isMessageSearchAvailable = isMessageSearchAvailable,
sessionDelegate = sessionDelegate,
)
.homeserverUrl(sessionData.homeserverUrl)
.enableAutomaticBackPagination(featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination))
@@ -144,6 +152,7 @@ class RustMatrixClientFactory(
clientSecret: ClientSecret?,
slidingSyncType: ClientBuilderSlidingSync,
isMessageSearchAvailable: Boolean,
sessionDelegate: ClientSessionDelegate,
): ClientBuilder {
return clientBuilderProvider.provide()
.run {
@@ -35,6 +35,7 @@ class RustTemporaryMatrixClientFactory(
clientSecret = null,
slidingSyncType = ClientBuilderSlidingSync.Native,
isMessageSearchAvailable = false,
sessionDelegate = NoOpClientSessionDelegate,
)
.serverName(formattedServerName)
.build()
@@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.NoOpClientSessionDelegate
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.RustTemporaryMatrixClient
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
@@ -407,6 +408,7 @@ class RustMatrixAuthenticationService(
clientSecret = pendingKey,
slidingSyncType = ClientBuilderSlidingSync.Discovered,
isMessageSearchAvailable = isMessageSearchAvailable(),
sessionDelegate = NoOpClientSessionDelegate,
)
.config()
.build()
@@ -440,6 +442,7 @@ class RustMatrixAuthenticationService(
clientSecret = pendingKey,
slidingSyncType = ClientBuilderSlidingSync.Discovered,
isMessageSearchAvailable = isMessageSearchAvailable(),
sessionDelegate = NoOpClientSessionDelegate,
)
.serverNameOrHomeserverUrl(baseUrlOrServerName)
.build()
@@ -15,6 +15,8 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.scanner.ContentScanner
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.search.RustSearchBackfillService
import io.element.android.libraries.matrix.impl.search.workmanager.SearchBackfillRequestBuilder
@@ -35,6 +37,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import java.io.File
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
@@ -51,6 +54,46 @@ class RustMatrixClientFactoryTest {
scheduleVacuumLambda.assertions().isCalledOnce()
result.destroy()
}
@Test
fun `an auth error on a client only invalidates its own session`() = runTest {
val aliceSessionId = "@alice:server.org"
val bobSessionId = "@bob:server.org"
val sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = aliceSessionId, isTokenValid = true),
aSessionData(sessionId = bobSessionId, isTokenValid = true),
),
updateUserProfileResult = { _, _, _, _ -> },
)
val userIds = ArrayDeque(listOf(aliceSessionId, bobSessionId))
val delegates = mutableMapOf<String, ClientDelegate?>()
val sut = createRustMatrixClientFactory(
sessionStore = sessionStore,
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}, cancelLambda = { _, _ -> }),
clientBuilderProvider = FakeClientBuilderProvider {
val userId = userIds.removeFirst()
FakeFfiClientBuilder(
buildResult = {
FakeFfiClient(
userId = userId,
withUtdHook = {},
setDelegateResult = { delegates[userId] = it },
)
}
)
},
)
val aliceClient = sut.create(sessionStore.getSession(aliceSessionId)!!)
val bobClient = sut.create(sessionStore.getSession(bobSessionId)!!)
delegates.getValue(aliceSessionId)!!.didReceiveAuthError(isSoftLogout = false)
assertThat(sessionStore.getSession(aliceSessionId)!!.isTokenValid).isFalse()
assertThat(sessionStore.getSession(bobSessionId)!!.isTokenValid).isTrue()
aliceClient.destroy()
bobClient.destroy()
}
}
fun TestScope.createRustMatrixClientFactory(
@@ -76,6 +76,7 @@ class FakeFfiClient(
private val contentScannerResult: () -> ContentScanner = { FakeFfiContentScanner() },
private val runSearchBackfillResult: (SearchBackfillStrategy) -> TaskHandle = { lambdaError() },
private val closeResult: () -> Unit = {},
private val setDelegateResult: (ClientDelegate?) -> Unit = {},
) : Client(NoHandle) {
override fun userId(): String = userId
override fun deviceId(): String = deviceId
@@ -85,7 +86,10 @@ class FakeFfiClient(
override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings
override fun encryption(): Encryption = encryption
override fun session(): Session = session
override fun setDelegate(delegate: ClientDelegate?): TaskHandle = FakeFfiTaskHandle()
override fun setDelegate(delegate: ClientDelegate?): TaskHandle {
setDelegateResult(delegate)
return FakeFfiTaskHandle()
}
override suspend fun cachedAvatarUrl(): String? = null
override suspend fun restoreSession(session: Session) = Unit
override fun syncService(): SyncServiceBuilder = FakeFfiSyncServiceBuilder()