Merge pull request #5692 from element-hq/feature/bma/loginFlow
Improve account provider selection during the login flow
This commit is contained in:
commit
77bc9b811a
36 changed files with 366 additions and 254 deletions
|
|
@ -7,10 +7,14 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.auth
|
||||
|
||||
sealed class AuthenticationException(message: String) : Exception(message) {
|
||||
class AccountAlreadyLoggedIn(userId: String) : AuthenticationException(userId)
|
||||
class InvalidServerName(message: String) : AuthenticationException(message)
|
||||
class SlidingSyncVersion(message: String) : AuthenticationException(message)
|
||||
class Oidc(message: String) : AuthenticationException(message)
|
||||
class Generic(message: String) : AuthenticationException(message)
|
||||
sealed class AuthenticationException(message: String?) : Exception(message) {
|
||||
data class AccountAlreadyLoggedIn(
|
||||
val userId: String,
|
||||
) : AuthenticationException(null)
|
||||
|
||||
class InvalidServerName(message: String?) : AuthenticationException(message)
|
||||
class SlidingSyncVersion(message: String?) : AuthenticationException(message)
|
||||
class ServerUnreachable(message: String?) : AuthenticationException(message)
|
||||
class Oidc(message: String?) : AuthenticationException(message)
|
||||
class Generic(message: String?) : AuthenticationException(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession
|
|||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface MatrixAuthenticationService {
|
||||
/**
|
||||
|
|
@ -22,8 +21,12 @@ interface MatrixAuthenticationService {
|
|||
* Generally this method should not be used directly, prefer using [MatrixClientProvider.getOrRestore] instead.
|
||||
*/
|
||||
suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient>
|
||||
fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?>
|
||||
suspend fun setHomeserver(homeserver: String): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set the homeserver to use for authentication, and return its details.
|
||||
*/
|
||||
suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails>
|
||||
|
||||
suspend fun login(username: String, password: String): Result<SessionId>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,12 +7,10 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.auth
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MatrixHomeServerDetails(
|
||||
val url: String,
|
||||
val supportsPasswordLogin: Boolean,
|
||||
val supportsOidcLogin: Boolean,
|
||||
) : Parcelable
|
||||
) {
|
||||
val isSupported = supportsPasswordLogin || supportsOidcLogin
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector 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.api.auth
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||
import org.junit.Test
|
||||
|
||||
class MatrixHomeServerDetailsTest {
|
||||
@Test
|
||||
fun `if homeserver supports oidc, then it is supported`() {
|
||||
val sut = aMatrixHomeServerDetails(
|
||||
supportsOidcLogin = true,
|
||||
supportsPasswordLogin = false,
|
||||
)
|
||||
assertThat(sut.isSupported).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if homeserver supports password, then it is supported`() {
|
||||
val sut = aMatrixHomeServerDetails(
|
||||
supportsOidcLogin = false,
|
||||
supportsPasswordLogin = true,
|
||||
)
|
||||
assertThat(sut.isSupported).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if homeserver supports both, then it is supported`() {
|
||||
val sut = aMatrixHomeServerDetails(
|
||||
supportsOidcLogin = true,
|
||||
supportsPasswordLogin = true,
|
||||
)
|
||||
assertThat(sut.isSupported).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if homeserver supports none, then it is not supported`() {
|
||||
val sut = aMatrixHomeServerDetails(
|
||||
supportsOidcLogin = false,
|
||||
supportsPasswordLogin = false,
|
||||
)
|
||||
assertThat(sut.isSupported).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import org.matrix.rustcomponents.sdk.ClientBuildException
|
|||
import org.matrix.rustcomponents.sdk.OidcException
|
||||
|
||||
fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||
val message = this.message ?: "Unknown error"
|
||||
return when (this) {
|
||||
is AuthenticationException -> this
|
||||
is ClientBuildException -> when (this) {
|
||||
|
|
@ -20,7 +19,7 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
|||
is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
|
||||
is ClientBuildException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message)
|
||||
is ClientBuildException.Sdk -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.ServerUnreachable -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.ServerUnreachable -> AuthenticationException.ServerUnreachable(message)
|
||||
is ClientBuildException.SlidingSync -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.WellKnownDeserializationException -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@ import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
|||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
|
|
@ -67,7 +65,6 @@ class RustMatrixAuthenticationService(
|
|||
// Ideally it would be possible to get the sessionPath from the Client to avoid doing this.
|
||||
private var sessionPaths: SessionPaths? = null
|
||||
private var currentClient: Client? = null
|
||||
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
|
||||
private val newMatrixClientObservers = mutableListOf<(MatrixClient) -> Unit>()
|
||||
override fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit) {
|
||||
|
|
@ -111,9 +108,7 @@ class RustMatrixAuthenticationService(
|
|||
return passphrase
|
||||
}
|
||||
|
||||
override fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?> = currentHomeserver
|
||||
|
||||
override suspend fun setHomeserver(homeserver: String): Result<Unit> =
|
||||
override suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails> =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
val emptySessionPath = rotateSessionPath()
|
||||
runCatchingExceptions {
|
||||
|
|
@ -122,8 +117,7 @@ class RustMatrixAuthenticationService(
|
|||
}
|
||||
|
||||
currentClient = client
|
||||
val homeServerDetails = client.homeserverLoginDetails().map()
|
||||
currentHomeserver.value = homeServerDetails.copy(url = homeserver)
|
||||
client.homeserverLoginDetails().map()
|
||||
}.onFailure {
|
||||
clear()
|
||||
}.mapFailure { failure ->
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ import org.matrix.rustcomponents.sdk.OidcException
|
|||
|
||||
class AuthenticationExceptionMappingTest {
|
||||
@Test
|
||||
fun `mapping an exception with no message returns 'Unknown error' message`() {
|
||||
fun `mapping an exception with no message returns null message`() {
|
||||
val exception = Exception()
|
||||
val mappedException = exception.mapAuthenticationException()
|
||||
assertThat(mappedException.message).isEqualTo("Unknown error")
|
||||
assertThat(mappedException.message).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -46,7 +46,7 @@ class AuthenticationExceptionMappingTest {
|
|||
assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException())
|
||||
.isException<AuthenticationException.Generic>("SDK issue")
|
||||
assertThat(ClientBuildException.ServerUnreachable("Server unreachable").mapAuthenticationException())
|
||||
.isException<AuthenticationException.Generic>("Server unreachable")
|
||||
.isException<AuthenticationException.ServerUnreachable>("Server unreachable")
|
||||
assertThat(ClientBuildException.SlidingSync("Sliding Sync").mapAuthenticationException())
|
||||
.isException<AuthenticationException.Generic>("Sliding Sync")
|
||||
assertThat(ClientBuildException.WellKnownDeserializationException("WellKnown Deserialization").mapAuthenticationException())
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
package io.element.android.libraries.matrix.test
|
||||
|
||||
import androidx.annotation.ColorInt
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
|
|
@ -79,8 +78,6 @@ const val AN_ACCOUNT_PROVIDER = "matrix.org"
|
|||
const val AN_ACCOUNT_PROVIDER_2 = "element.io"
|
||||
const val AN_ACCOUNT_PROVIDER_3 = "other.io"
|
||||
|
||||
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
|
||||
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
|
||||
val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE
|
||||
|
||||
const val AN_AVATAR_URL = "mxc://data"
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
val A_OIDC_DATA = OidcDetails(url = "a-url")
|
||||
|
||||
|
|
@ -31,13 +29,12 @@ class FakeMatrixAuthenticationService(
|
|||
var matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null,
|
||||
var loginWithQrCodeResult: (qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) -> Result<SessionId> =
|
||||
lambdaRecorder<MatrixQrCodeLoginData, (QrCodeLoginStep) -> Unit, Result<SessionId>> { _, _ -> Result.success(A_SESSION_ID) },
|
||||
private val importCreatedSessionLambda: (ExternalSession) -> Result<SessionId> = { lambdaError() }
|
||||
private val importCreatedSessionLambda: (ExternalSession) -> Result<SessionId> = { lambdaError() },
|
||||
private val setHomeserverResult: (String) -> Result<MatrixHomeServerDetails> = { lambdaError() },
|
||||
) : MatrixAuthenticationService {
|
||||
private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
private var oidcError: Throwable? = null
|
||||
private var oidcCancelError: Throwable? = null
|
||||
private var loginError: Throwable? = null
|
||||
private var changeServerError: Throwable? = null
|
||||
private var matrixClient: MatrixClient? = null
|
||||
private var onAuthenticationListener: ((MatrixClient) -> Unit)? = null
|
||||
|
||||
|
|
@ -53,16 +50,8 @@ class FakeMatrixAuthenticationService(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?> {
|
||||
return homeserver
|
||||
}
|
||||
|
||||
fun givenHomeserver(homeserver: MatrixHomeServerDetails) {
|
||||
this.homeserver.value = homeserver
|
||||
}
|
||||
|
||||
override suspend fun setHomeserver(homeserver: String): Result<Unit> = simulateLongTask {
|
||||
changeServerError?.let { Result.failure(it) } ?: Result.success(Unit)
|
||||
override suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails> = simulateLongTask {
|
||||
setHomeserverResult(homeserver)
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String): Result<SessionId> = simulateLongTask {
|
||||
|
|
@ -115,10 +104,6 @@ class FakeMatrixAuthenticationService(
|
|||
loginError = throwable
|
||||
}
|
||||
|
||||
fun givenChangeServerError(throwable: Throwable?) {
|
||||
changeServerError = throwable
|
||||
}
|
||||
|
||||
fun givenMatrixClient(matrixClient: MatrixClient) {
|
||||
this.matrixClient = matrixClient
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector 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.test.auth
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
|
||||
fun aMatrixHomeServerDetails(
|
||||
url: String = A_HOMESERVER_URL,
|
||||
supportsPasswordLogin: Boolean = false,
|
||||
supportsOidcLogin: Boolean = false,
|
||||
) = MatrixHomeServerDetails(
|
||||
url = url,
|
||||
supportsPasswordLogin = supportsPasswordLogin,
|
||||
supportsOidcLogin = supportsOidcLogin,
|
||||
)
|
||||
|
|
@ -44,7 +44,7 @@ class DefaultActiveNotificationsProviderTest {
|
|||
@Test
|
||||
fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() {
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value,),
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue