Restore OIDC support.
This commit is contained in:
parent
92fe22d9d7
commit
06a9b129d0
23 changed files with 164 additions and 87 deletions
|
|
@ -45,3 +45,7 @@ state: ex6mNJVFZ5jn9wL8
|
||||||
|
|
||||||
Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
|
Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
|
||||||
Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
|
Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
|
||||||
|
|
||||||
|
|
||||||
|
Test server:
|
||||||
|
synapse-oidc.lab.element.dev
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -26,8 +27,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
|
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
|
@ -40,7 +44,9 @@ import java.net.URL
|
||||||
class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
@Assisted private val params: Params,
|
@Assisted private val params: Params,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
private val authenticationService: MatrixAuthenticationService
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
|
private val defaultOidcActionFlow: DefaultOidcActionFlow,
|
||||||
|
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||||
) : Presenter<ConfirmAccountProviderState> {
|
) : Presenter<ConfirmAccountProviderState> {
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
|
|
@ -61,6 +67,14 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
mutableStateOf(Async.Uninitialized)
|
mutableStateOf(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch {
|
||||||
|
defaultOidcActionFlow.collect {
|
||||||
|
onOidcAction(it, loginFlowAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ConfirmAccountProviderEvents.Continue -> {
|
ConfirmAccountProviderEvents.Continue -> {
|
||||||
|
|
@ -97,4 +111,33 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
|
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun onOidcAction(
|
||||||
|
oidcAction: OidcAction?,
|
||||||
|
loginFlowAction: MutableState<Async<LoginFlow>>,
|
||||||
|
) {
|
||||||
|
oidcAction ?: return
|
||||||
|
loginFlowAction.value = Async.Loading()
|
||||||
|
when (oidcAction) {
|
||||||
|
OidcAction.GoBack -> {
|
||||||
|
authenticationService.cancelOidcLogin()
|
||||||
|
.onSuccess {
|
||||||
|
loginFlowAction.value = Async.Uninitialized
|
||||||
|
}
|
||||||
|
.onFailure { failure ->
|
||||||
|
loginFlowAction.value = Async.Failure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OidcAction.Success -> {
|
||||||
|
authenticationService.loginWithOidc(oidcAction.url)
|
||||||
|
.onSuccess { _ ->
|
||||||
|
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||||
|
}
|
||||||
|
.onFailure { failure ->
|
||||||
|
loginFlowAction.value = Async.Failure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultOidcActionFlow.reset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
object LoginConstants {
|
object LoginConstants {
|
||||||
const val MATRIX_ORG_URL = "matrix.org"
|
const val MATRIX_ORG_URL = "matrix.org"
|
||||||
|
|
||||||
const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev"
|
const val DEFAULT_HOMESERVER_URL = "matrix.org"
|
||||||
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
|
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,12 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
|
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
|
||||||
import io.element.android.features.login.impl.util.defaultAccountProvider
|
import io.element.android.features.login.impl.util.defaultAccountProvider
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
|
|
@ -33,11 +36,7 @@ import org.junit.Test
|
||||||
class ConfirmAccountProviderPresenterTest {
|
class ConfirmAccountProviderPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial test`() = runTest {
|
fun `present - initial test`() = runTest {
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter()
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
|
||||||
AccountProviderDataSource(),
|
|
||||||
FakeAuthenticationService(),
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -51,13 +50,11 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - continue password login`() = runTest {
|
fun `present - continue password login`() = runTest {
|
||||||
val authServer = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
matrixAuthenticationService = authenticationService,
|
||||||
AccountProviderDataSource(),
|
|
||||||
authServer,
|
|
||||||
)
|
)
|
||||||
authServer.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -75,13 +72,11 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - continue oidc`() = runTest {
|
fun `present - continue oidc`() = runTest {
|
||||||
val authServer = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
matrixAuthenticationService = authenticationService,
|
||||||
AccountProviderDataSource(),
|
|
||||||
authServer,
|
|
||||||
)
|
)
|
||||||
authServer.givenHomeserver(A_HOMESERVER_OIDC)
|
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -99,17 +94,15 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - submit fails`() = runTest {
|
fun `present - submit fails`() = runTest {
|
||||||
val authServer = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
matrixAuthenticationService = authenticationService,
|
||||||
AccountProviderDataSource(),
|
|
||||||
authServer,
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
authServer.givenChangeServerError(Throwable())
|
authenticationService.givenChangeServerError(Throwable())
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
skipItems(1) // Loading
|
skipItems(1) // Loading
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
|
|
@ -121,10 +114,8 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - clear error`() = runTest {
|
fun `present - clear error`() = runTest {
|
||||||
val authenticationService = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
matrixAuthenticationService = authenticationService,
|
||||||
AccountProviderDataSource(),
|
|
||||||
authenticationService,
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -147,4 +138,18 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
|
assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createConfirmAccountProviderPresenter(
|
||||||
|
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
||||||
|
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(),
|
||||||
|
matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(),
|
||||||
|
defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(),
|
||||||
|
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
|
||||||
|
) = ConfirmAccountProviderPresenter(
|
||||||
|
params = params,
|
||||||
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
|
authenticationService = matrixAuthenticationService,
|
||||||
|
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||||
|
defaultLoginUserStory = defaultLoginUserStory,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,12 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
@Composable
|
@Composable
|
||||||
fun LogoutPreferenceView(
|
fun LogoutPreferenceView(
|
||||||
state: LogoutPreferenceState,
|
state: LogoutPreferenceState,
|
||||||
onSuccessLogout: () -> Unit = {}
|
onSuccessLogout: (String?) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val eventSink = state.eventSink
|
val eventSink = state.eventSink
|
||||||
if (state.logoutAction is Async.Success) {
|
if (state.logoutAction is Async.Success) {
|
||||||
LaunchedEffect(state.logoutAction) {
|
LaunchedEffect(state.logoutAction) {
|
||||||
onSuccessLogout()
|
onSuccessLogout(state.logoutAction.data)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,6 @@ package io.element.android.features.logout.api
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
data class LogoutPreferenceState(
|
data class LogoutPreferenceState(
|
||||||
val logoutAction: Async<Unit>,
|
val logoutAction: Async<String?>,
|
||||||
val eventSink: (LogoutPreferenceEvents) -> Unit,
|
val eventSink: (LogoutPreferenceEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LogoutPreferenceState {
|
override fun present(): LogoutPreferenceState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val logoutAction: MutableState<Async<Unit>> = remember {
|
val logoutAction: MutableState<Async<String?>> = remember {
|
||||||
mutableStateOf(Async.Uninitialized)
|
mutableStateOf(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
|
private fun CoroutineScope.logout(logoutAction: MutableState<Async<String?>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.logout()
|
matrixClient.logout()
|
||||||
}.runCatchingUpdatingState(logoutAction)
|
}.runCatchingUpdatingState(logoutAction)
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
|
@ -25,7 +27,9 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class PreferencesRootNode @AssistedInject constructor(
|
class PreferencesRootNode @AssistedInject constructor(
|
||||||
|
|
@ -65,6 +69,7 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
val activity = LocalContext.current as Activity
|
||||||
PreferencesRootView(
|
PreferencesRootView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -73,7 +78,15 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
onOpenAnalytics = this::onOpenAnalytics,
|
onOpenAnalytics = this::onOpenAnalytics,
|
||||||
onOpenAbout = this::onOpenAbout,
|
onOpenAbout = this::onOpenAbout,
|
||||||
onVerifyClicked = this::onVerifyClicked,
|
onVerifyClicked = this::onVerifyClicked,
|
||||||
onOpenDeveloperSettings = this::onOpenDeveloperSettings
|
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
|
||||||
|
onSuccessLogout = { onSuccessLogout(activity, it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onSuccessLogout(activity: Activity, url: String?) {
|
||||||
|
Timber.d("Success logout with result url: $url")
|
||||||
|
url?.let {
|
||||||
|
activity.openUrlInChromeCustomTab(null, false, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ fun PreferencesRootView(
|
||||||
onOpenRageShake: () -> Unit,
|
onOpenRageShake: () -> Unit,
|
||||||
onOpenAbout: () -> Unit,
|
onOpenAbout: () -> Unit,
|
||||||
onOpenDeveloperSettings: () -> Unit,
|
onOpenDeveloperSettings: () -> Unit,
|
||||||
|
onSuccessLogout: (String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
|
|
@ -98,6 +99,7 @@ fun PreferencesRootView(
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
LogoutPreferenceView(
|
LogoutPreferenceView(
|
||||||
state = state.logoutState,
|
state = state.logoutState,
|
||||||
|
onSuccessLogout = onSuccessLogout,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -140,5 +142,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||||
onOpenDeveloperSettings = {},
|
onOpenDeveloperSettings = {},
|
||||||
onOpenAbout = {},
|
onOpenAbout = {},
|
||||||
onVerifyClicked = {},
|
onVerifyClicked = {},
|
||||||
|
onSuccessLogout = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,12 @@ interface MatrixClient : Closeable {
|
||||||
* Will close the client and delete the cache data.
|
* Will close the client and delete the cache data.
|
||||||
*/
|
*/
|
||||||
suspend fun clearCache()
|
suspend fun clearCache()
|
||||||
suspend fun logout()
|
|
||||||
|
/**
|
||||||
|
* Logout the user.
|
||||||
|
* Returns an optional URL. When the URL is there, it should be presented to the user after logout for RP initiated logout on their account page.
|
||||||
|
*/
|
||||||
|
suspend fun logout(): String?
|
||||||
suspend fun loadUserDisplayName(): Result<String>
|
suspend fun loadUserDisplayName(): Result<String>
|
||||||
suspend fun loadUserAvatarURLString(): Result<String?>
|
suspend fun loadUserAvatarURLString(): Result<String?>
|
||||||
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,5 @@ sealed class AuthenticationException(message: String) : Exception(message) {
|
||||||
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
|
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
|
||||||
class SessionMissing(message: String) : AuthenticationException(message)
|
class SessionMissing(message: String) : AuthenticationException(message)
|
||||||
class Generic(message: String) : AuthenticationException(message)
|
class Generic(message: String) : AuthenticationException(message)
|
||||||
// TODO Oidc
|
data class OidcError(val type: String, override val message: String) : AuthenticationException(message)
|
||||||
// class OidcError(type: String, message: String) : AuthenticationException(message)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,11 @@ class RustMatrixClient constructor(
|
||||||
Timber.v("didReceiveAuthError -> already cleaning up")
|
Timber.v("didReceiveAuthError -> already cleaning up")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun didRefreshTokens() {
|
||||||
|
Timber.w("didRefreshTokens()")
|
||||||
|
// TODO handle refresh token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val rustRoomListService: RoomListService =
|
private val rustRoomListService: RoomListService =
|
||||||
|
|
@ -287,19 +292,23 @@ class RustMatrixClient constructor(
|
||||||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
|
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout() = doLogout(doRequest = true)
|
override suspend fun logout(): String? = doLogout(doRequest = true)
|
||||||
|
|
||||||
private suspend fun doLogout(doRequest: Boolean) = withContext(sessionDispatcher) {
|
private suspend fun doLogout(doRequest: Boolean): String? {
|
||||||
if (doRequest) {
|
var result: String? = null
|
||||||
try {
|
withContext(sessionDispatcher) {
|
||||||
client.logout()
|
if (doRequest) {
|
||||||
} catch (failure: Throwable) {
|
try {
|
||||||
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
result = client.logout()
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
close()
|
||||||
|
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
|
||||||
|
sessionStore.removeSession(sessionId.value)
|
||||||
}
|
}
|
||||||
close()
|
return result
|
||||||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
|
|
||||||
sessionStore.removeSession(sessionId.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
|
override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
|
||||||
|
|
|
||||||
|
|
@ -75,4 +75,5 @@ private fun SessionData.toSession() = Session(
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
homeserverUrl = homeserverUrl,
|
homeserverUrl = homeserverUrl,
|
||||||
slidingSyncProxy = slidingSyncProxy,
|
slidingSyncProxy = slidingSyncProxy,
|
||||||
|
oidcData = oidcData,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,12 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||||
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
|
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
|
||||||
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
|
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
|
||||||
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
|
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
|
||||||
|
|
||||||
/* TODO Oidc
|
|
||||||
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
|
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
|
||||||
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
|
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
|
||||||
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
|
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
|
||||||
is RustAuthenticationException.OidcNotStarted -> AuthenticationException.OidcError("OidcNotStarted", message!!)
|
|
||||||
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
|
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
|
||||||
*/
|
is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message!!)
|
||||||
|
is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message!!)
|
||||||
else -> AuthenticationException.Generic(this.message ?: "Unknown error")
|
else -> AuthenticationException.Generic(this.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
|
||||||
MatrixHomeServerDetails(
|
MatrixHomeServerDetails(
|
||||||
url = url(),
|
url = url(),
|
||||||
supportsPasswordLogin = supportsPasswordLogin(),
|
supportsPasswordLogin = supportsPasswordLogin(),
|
||||||
supportsOidcLogin = false // TODO Oidc supportsOidcLogin(),
|
supportsOidcLogin = supportsOidcLogin(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.auth
|
package io.element.android.libraries.matrix.impl.auth
|
||||||
|
|
||||||
// TODO Oidc
|
import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||||
// import io.element.android.libraries.matrix.api.auth.OidcConfig
|
import org.matrix.rustcomponents.sdk.OidcConfiguration
|
||||||
// import org.matrix.rustcomponents.sdk.OidcClientMetadata
|
|
||||||
|
|
||||||
/*
|
val oidcConfiguration: OidcConfiguration = OidcConfiguration(
|
||||||
val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata(
|
|
||||||
clientName = "Element",
|
clientName = "Element",
|
||||||
redirectUri = OidcConfig.redirectUri,
|
redirectUri = OidcConfig.redirectUri,
|
||||||
clientUri = "https://element.io",
|
clientUri = "https://element.io",
|
||||||
tosUri = "https://element.io/user-terms-of-service",
|
tosUri = "https://element.io/user-terms-of-service",
|
||||||
policyUri = "https://element.io/privacy"
|
policyUri = "https://element.io/privacy",
|
||||||
|
/**
|
||||||
|
* Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
|
||||||
|
*/
|
||||||
|
staticRegistrations = mapOf(
|
||||||
|
"https://id.thirdroom.io/realms/thirdroom" to "elementx",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.auth
|
package io.element.android.libraries.matrix.impl.auth
|
||||||
|
|
||||||
// TODO Oidc
|
|
||||||
// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.extensions.mapFailure
|
import io.element.android.libraries.core.extensions.mapFailure
|
||||||
|
|
@ -37,6 +35,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
|
||||||
import org.matrix.rustcomponents.sdk.Session
|
import org.matrix.rustcomponents.sdk.Session
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -57,9 +56,8 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
private val authService: RustAuthenticationService = RustAuthenticationService(
|
private val authService: RustAuthenticationService = RustAuthenticationService(
|
||||||
basePath = baseDirectory.absolutePath,
|
basePath = baseDirectory.absolutePath,
|
||||||
passphrase = null,
|
passphrase = null,
|
||||||
// TODO Oidc
|
|
||||||
// oidcClientMetadata = oidcClientMetadata,
|
|
||||||
userAgent = userAgentProvider.provide(),
|
userAgent = userAgentProvider.provide(),
|
||||||
|
oidcConfiguration = oidcConfiguration,
|
||||||
customSlidingSyncProxy = null,
|
customSlidingSyncProxy = null,
|
||||||
)
|
)
|
||||||
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||||
|
|
@ -112,60 +110,50 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Oidc
|
private var pendingOidcAuthenticationData: OidcAuthenticationData? = null
|
||||||
// private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null
|
|
||||||
|
|
||||||
override suspend fun getOidcUrl(): Result<OidcDetails> {
|
override suspend fun getOidcUrl(): Result<OidcDetails> {
|
||||||
TODO("Oidc")
|
|
||||||
/*
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val urlForOidcLogin = authService.urlForOidcLogin()
|
val oidcAuthenticationData = authService.urlForOidcLogin()
|
||||||
val url = urlForOidcLogin.loginUrl()
|
val url = oidcAuthenticationData.loginUrl()
|
||||||
pendingUrlForOidcLogin = urlForOidcLogin
|
pendingOidcAuthenticationData = oidcAuthenticationData
|
||||||
OidcDetails(url)
|
OidcDetails(url)
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun cancelOidcLogin(): Result<Unit> {
|
override suspend fun cancelOidcLogin(): Result<Unit> {
|
||||||
TODO("Oidc")
|
|
||||||
/*
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
pendingUrlForOidcLogin?.close()
|
pendingOidcAuthenticationData?.close()
|
||||||
pendingUrlForOidcLogin = null
|
pendingOidcAuthenticationData = null
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters).
|
* callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters).
|
||||||
*/
|
*/
|
||||||
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
|
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
|
||||||
TODO("Oidc")
|
|
||||||
/*
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val urlForOidcLogin = pendingUrlForOidcLogin ?: error("You need to call `getOidcUrl()` first")
|
val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first")
|
||||||
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
|
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
|
||||||
val sessionData = client.use { it.session().toSessionData() }
|
val sessionData = client.use { it.session().toSessionData() }
|
||||||
pendingUrlForOidcLogin = null
|
pendingOidcAuthenticationData?.close()
|
||||||
|
pendingOidcAuthenticationData = null
|
||||||
sessionStore.storeData(sessionData)
|
sessionStore.storeData(sessionData)
|
||||||
SessionId(sessionData.userId)
|
SessionId(sessionData.userId)
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Session.toSessionData() = SessionData(
|
private fun Session.toSessionData() = SessionData(
|
||||||
|
|
@ -174,6 +162,7 @@ private fun Session.toSessionData() = SessionData(
|
||||||
accessToken = accessToken,
|
accessToken = accessToken,
|
||||||
refreshToken = refreshToken,
|
refreshToken = refreshToken,
|
||||||
homeserverUrl = homeserverUrl,
|
homeserverUrl = homeserverUrl,
|
||||||
|
oidcData = oidcData,
|
||||||
slidingSyncProxy = slidingSyncProxy,
|
slidingSyncProxy = slidingSyncProxy,
|
||||||
loginTimestamp = Date(),
|
loginTimestamp = Date(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,10 @@ class FakeMatrixClient(
|
||||||
override suspend fun clearCache() {
|
override suspend fun clearCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout() {
|
override suspend fun logout(): String? {
|
||||||
delay(100)
|
delay(100)
|
||||||
logoutFailure?.let { throw it }
|
logoutFailure?.let { throw it }
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() = Unit
|
override fun close() = Unit
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ data class SessionData(
|
||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
val refreshToken: String?,
|
val refreshToken: String?,
|
||||||
val homeserverUrl: String,
|
val homeserverUrl: String,
|
||||||
|
val oidcData: String?,
|
||||||
val slidingSyncProxy: String?,
|
val slidingSyncProxy: String?,
|
||||||
val loginTimestamp: Date?,
|
val loginTimestamp: Date?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
||||||
accessToken = accessToken,
|
accessToken = accessToken,
|
||||||
refreshToken = refreshToken,
|
refreshToken = refreshToken,
|
||||||
homeserverUrl = homeserverUrl,
|
homeserverUrl = homeserverUrl,
|
||||||
|
oidcData = oidcData,
|
||||||
slidingSyncProxy = slidingSyncProxy,
|
slidingSyncProxy = slidingSyncProxy,
|
||||||
loginTimestamp = loginTimestamp?.time,
|
loginTimestamp = loginTimestamp?.time,
|
||||||
)
|
)
|
||||||
|
|
@ -39,6 +40,7 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
||||||
accessToken = accessToken,
|
accessToken = accessToken,
|
||||||
refreshToken = refreshToken,
|
refreshToken = refreshToken,
|
||||||
homeserverUrl = homeserverUrl,
|
homeserverUrl = homeserverUrl,
|
||||||
|
oidcData = oidcData,
|
||||||
slidingSyncProxy = slidingSyncProxy,
|
slidingSyncProxy = slidingSyncProxy,
|
||||||
loginTimestamp = loginTimestamp?.let { Date(it) }
|
loginTimestamp = loginTimestamp?.let { Date(it) }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ CREATE TABLE SessionData (
|
||||||
refreshToken TEXT,
|
refreshToken TEXT,
|
||||||
homeserverUrl TEXT NOT NULL,
|
homeserverUrl TEXT NOT NULL,
|
||||||
slidingSyncProxy TEXT,
|
slidingSyncProxy TEXT,
|
||||||
loginTimestamp INTEGER
|
loginTimestamp INTEGER,
|
||||||
|
oidcData TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE SessionData ADD COLUMN oidcData TEXT;
|
||||||
|
|
@ -37,6 +37,7 @@ class DatabaseSessionStoreTests {
|
||||||
homeserverUrl = "homeserverUrl",
|
homeserverUrl = "homeserverUrl",
|
||||||
slidingSyncProxy = null,
|
slidingSyncProxy = null,
|
||||||
loginTimestamp = null,
|
loginTimestamp = null,
|
||||||
|
oidcData = "aOidcData",
|
||||||
)
|
)
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue