diff --git a/docs/oidc.md b/docs/oidc.md new file mode 100644 index 0000000000..7feae3ce83 --- /dev/null +++ b/docs/oidc.md @@ -0,0 +1,45 @@ +[ios implementation](https://github.com/vector-im/element-x-ios/compare/develop...doug/oidc-temp) + +Rust sdk branch: https://github.com/matrix-org/matrix-rust-sdk/tree/oidc-ffi + +Figma https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?node-id=133-5426&t=yQXKeANatk6keoZF-0 + +Server list: https://github.com/vector-im/oidc-playground + +Metadata iOS: (from https://github.com/vector-im/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28) + +clientName: InfoPlistReader.main.bundleDisplayName, +redirectUri: "io.element:/callback", +clientUri: "https://element.io", +tosUri: "https://element.io/user-terms-of-service", +policyUri: "https://element.io/privacy" + + +Android: +clientName = "Element", +redirectUri = "io.element:/callback", +clientUri = "https://element.io", +tosUri = "https://element.io/user-terms-of-service", +policyUri = "https://element.io/privacy" + + +Example of OidcData (from presentUrl callback): +url: https://auth-oidc.lab.element.dev/authorize?response_type=code&client_id=01GYCAGG3PA70CJ97ZVP0WFJY3&redirect_uri=io.element%3A%2Fcallback&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AYAgcPW4mcG&state=ex6mNJVFZ5jn9wL8&nonce=NZ93DOyIGQd9exPQ&code_challenge_method=S256&code_challenge=FFRcPALNSPCh-ZgpyTRFu_h8NZJVncfvihbfT9CyX8U&prompt=consent + +Formatted url: +https://auth-oidc.lab.element.dev/authorize? + response_type=code& + client_id=01GYCAGG3PA70CJ97ZVP0WFJY3& + redirect_uri=io.element%3A%2Fcallback& + scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AYAgcPW4mcG& + state=ex6mNJVFZ5jn9wL8& + nonce=NZ93DOyIGQd9exPQ& + code_challenge_method=S256& + code_challenge=FFRcPALNSPCh-ZgpyTRFu_h8NZJVncfvihbfT9CyX8U& + prompt=consent + +state: ex6mNJVFZ5jn9wL8 + + +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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index dcdaf1a347..37cfec229b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -29,11 +29,13 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.login.impl.changeserver.ChangeServerNode +import io.element.android.features.login.impl.oidc.OidcNode import io.element.android.features.login.impl.root.LoginRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) @@ -55,6 +57,9 @@ class LoginFlowNode @AssistedInject constructor( @Parcelize object ChangeServer : NavTarget + + @Parcelize + data class OidcView(val oidcDetails: OidcDetails) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -64,10 +69,19 @@ class LoginFlowNode @AssistedInject constructor( override fun onChangeHomeServer() { backstack.push(NavTarget.ChangeServer) } + + override fun onOidcDetails(oidcDetails: OidcDetails) { + backstack.push(NavTarget.OidcView(oidcDetails)) + } } createNode(buildContext, plugins = listOf(callback)) } + NavTarget.ChangeServer -> createNode(buildContext) + is NavTarget.OidcView -> { + val input = OidcNode.Inputs(navTarget.oidcDetails) + createNode(buildContext, plugins = listOf(input)) + } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt new file mode 100644 index 0000000000..0c40f457bf --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +sealed interface OidcEvents { + object Cancel : OidcEvents + data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents + object ClearError : OidcEvents +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt new file mode 100644 index 0000000000..0122ead10c --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails + +/** + * TODO Transmit back press to the webview + */ +@ContributesNode(AppScope::class) +class OidcNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: OidcPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val oidcDetails: OidcDetails, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.oidcDetails) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + OidcView( + state = state, + modifier = modifier, + onNavigateBack = ::navigateUp, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt new file mode 100644 index 0000000000..76faa090d1 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.auth.OidcDetails +import kotlinx.coroutines.launch + +class OidcPresenter @AssistedInject constructor( + @Assisted private val oidcDetails: OidcDetails, + private val authenticationService: MatrixAuthenticationService, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(oidcDetails: OidcDetails): OidcPresenter + } + + @Composable + override fun present(): OidcState { + var requestState: Async by remember { + mutableStateOf(Async.Uninitialized) + } + val localCoroutineScope = rememberCoroutineScope() + + fun handleCancel() { + requestState = Async.Loading() + localCoroutineScope.launch { + requestState = try { + authenticationService.cancelOidcLogin() + // Then go back + Async.Success(Unit) + } catch (throwable: Throwable) { + Async.Failure(throwable) + } + } + } + + fun handleSuccess(url: String) { + requestState = Async.Loading() + localCoroutineScope.launch { + try { + authenticationService.loginWithOidc(url) + // Then the node tree will be updated, there is nothing to do + } catch (throwable: Throwable) { + requestState = Async.Failure(throwable) + } + } + } + + fun handleAction(action: OidcAction) { + when (action) { + OidcAction.GoBack -> handleCancel() + is OidcAction.Success -> handleSuccess(action.url) + } + } + + fun handleEvents(event: OidcEvents) { + when (event) { + OidcEvents.Cancel -> handleCancel() + is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction) + OidcEvents.ClearError -> requestState = Async.Uninitialized + } + } + + return OidcState( + oidcDetails = oidcDetails, + requestState = requestState, + eventSink = ::handleEvents + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcState.kt new file mode 100644 index 0000000000..e9b2ac2355 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.auth.OidcDetails + +data class OidcState( + val oidcDetails: OidcDetails, + val requestState: Async, + val eventSink: (OidcEvents) -> Unit +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcStateProvider.kt new file mode 100644 index 0000000000..7a5552e719 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcStateProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.auth.OidcDetails + +open class OidcStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aOidcState(), + aOidcState().copy(requestState = Async.Loading()), + ) +} + +fun aOidcState() = OidcState( + oidcDetails = aOidcDetails(), + requestState = Async.Uninitialized, + eventSink = {} +) + +fun aOidcDetails() = OidcDetails( + url = "aUrl", +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt new file mode 100644 index 0000000000..eb55368f47 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +import io.element.android.libraries.matrix.api.auth.OidcConfig + +/** + * Simple parser for oidc url interception. + * TODO Find documentation about the format. + */ +class OidcUrlParser { + + // When user press button "Cancel", we get the url: + // `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO` + // On success, we get: + // `io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB` + /** + * Return a OidcAction, or null if the url is not a OidcUrl + */ + fun parse(url: String): OidcAction? { + if (!url.startsWith(OidcConfig.redirectUri)) return null + if (url.contains("error=access_denied")) return OidcAction.GoBack + if (url.contains("code=")) return OidcAction.Success(url) + + // Other case not supported, let's crash the app for now + error("Not supported: $url") + } +} + +sealed interface OidcAction { + object GoBack : OidcAction + data class Success(val url: String) : OidcAction +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt new file mode 100644 index 0000000000..06591409c9 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +import android.webkit.WebView +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.viewinterop.AndroidView +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.bool.orTrue +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator + +@Composable +fun OidcView( + state: OidcState, + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, +) { + val oidcUrlParser = remember { OidcUrlParser() } + var webView: WebView? = null + fun shouldOverrideUrl(url: String): Boolean { + val action = oidcUrlParser.parse(url) + if (action != null) { + state.eventSink.invoke(OidcEvents.OidcActionEvent(action)) + return true + } + return false + } + + val oidcWebViewClient = remember { + OidcWebViewClient(eventListener = object : WebViewEventListener { + override fun shouldOverrideUrlLoading(url: String): Boolean { + return shouldOverrideUrl(url) + } + }) + } + + BackHandler { + if (webView?.canGoBack().orFalse()) { + webView?.goBack() + } else { + // To properly cancel Oidc login + state.eventSink.invoke(OidcEvents.Cancel) + } + } + + Box(modifier = modifier.statusBarsPadding()) { + AndroidView( + modifier = modifier + .statusBarsPadding(), + factory = { context -> + WebView(context).apply { + webViewClient = oidcWebViewClient + loadUrl(state.oidcDetails.url) + }.also { + webView = it + } + } + ) + + when (state.requestState) { + Async.Uninitialized -> Unit + is Async.Failure -> { + ErrorDialog( + content = state.requestState.error.toString(), + onDismiss = { state.eventSink(OidcEvents.ClearError) } + ) + } + is Async.Loading -> { + // Indeterminate indicator, to avoid the freeze effect if the connection takes time to initialize. + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + is Async.Success -> onNavigateBack() + } + } +} + +@Preview +@Composable +fun OidcViewLightPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun OidcViewDarkPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: OidcState) { + OidcView( + state = state, + onNavigateBack = { }, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt new file mode 100644 index 0000000000..bd7ab99ad6 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +import android.annotation.TargetApi +import android.os.Build +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import timber.log.Timber + +// TODO Move to a dedicated module +class OidcWebViewClient(private val eventListener: WebViewEventListener) : WebViewClient() { + @TargetApi(Build.VERSION_CODES.N) + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + return shouldOverrideUrl(request.url.toString()) + } + + @Deprecated("Deprecated in Java") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + return shouldOverrideUrl(url) + } + + private fun shouldOverrideUrl(url: String): Boolean { + Timber.d("shouldOverrideUrl: $url") + return eventListener.shouldOverrideUrlLoading(url) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt new file mode 100644 index 0000000000..91fde6c311 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.features.login.impl.oidc + +interface WebViewEventListener { + /** + * Triggered when a Webview loads an url. + * + * @param url The url about to be rendered. + * @return true if the method needs to manage some custom handling + */ + fun shouldOverrideUrlLoading(url: String): Boolean { + return false + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt index fce214f2e4..787f5d0b48 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt @@ -26,6 +26,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) class LoginRootNode @AssistedInject constructor( @@ -36,20 +37,26 @@ class LoginRootNode @AssistedInject constructor( interface Callback : Plugin { fun onChangeHomeServer() + fun onOidcDetails(oidcDetails: OidcDetails) } private fun onChangeHomeServer() { plugins().forEach { it.onChangeHomeServer() } } + private fun onOidcDetails(oidcDetails: OidcDetails) { + plugins().forEach { it.onOidcDetails(oidcDetails) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() LoginRootView( state = state, modifier = modifier, - onChangeServer = this::onChangeHomeServer, - onBackPressed = this::navigateUp + onChangeServer = ::onChangeHomeServer, + onOidcDetails = ::onOidcDetails, + onBackPressed = ::navigateUp ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt index c9a75365f1..fd3d293870 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt @@ -33,7 +33,11 @@ import javax.inject.Inject class LoginRootPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter { - private val defaultHomeserver = MatrixHomeServerDetails(LoginConstants.DEFAULT_HOMESERVER_URL, true, null) + private val defaultHomeserver = MatrixHomeServerDetails( + url = LoginConstants.DEFAULT_HOMESERVER_URL, + supportsPasswordLogin = true, + supportsOidc = false, + ) @Composable override fun present(): LoginRootState { @@ -54,7 +58,12 @@ class LoginRootPresenter @Inject constructor(private val authenticationService: is LoginRootEvents.SetPassword -> updateFormState(formState) { copy(password = event.password) } - LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState) + LoginRootEvents.Submit -> { + when { + homeserver.supportsOidc -> localCoroutineScope.submitOidc(homeserver.url, loggedInState) + homeserver.supportsPasswordLogin -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState) + } + } LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn } } @@ -67,9 +76,22 @@ class LoginRootPresenter @Inject constructor(private val authenticationService: ) } + private fun CoroutineScope.submitOidc(homeserver: String, loggedInState: MutableState) = launch { + loggedInState.value = LoggedInState.LoggingIn + // TODO rework the setHomeserver flow + authenticationService.setHomeserver(homeserver) + authenticationService.getOidcUrl() + .onSuccess { + loggedInState.value = LoggedInState.OidcStarted(it) + } + .onFailure { failure -> + loggedInState.value = LoggedInState.ErrorLoggingIn(failure) + } + } + private fun CoroutineScope.submit(homeserver: String, formState: LoginFormState, loggedInState: MutableState) = launch { loggedInState.value = LoggedInState.LoggingIn - //TODO rework the setHomeserver flow + // TODO rework the setHomeserver flow authenticationService.setHomeserver(homeserver) authenticationService.login(formState.login.trim(), formState.password) .onSuccess { sessionId -> diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt index 34e903dd0e..884a3037df 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt @@ -18,6 +18,7 @@ package io.element.android.features.login.impl.root import android.os.Parcelable import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.parcelize.Parcelize @@ -27,13 +28,17 @@ data class LoginRootState( val formState: LoginFormState, val eventSink: (LoginRootEvents) -> Unit ) { - val submitEnabled: Boolean get() = - formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState !is LoggedInState.ErrorLoggingIn + val supportPasswordLogin = homeserverDetails.supportsPasswordLogin + val supportOidcLogin = homeserverDetails.supportsOidc + val submitEnabled: Boolean + get() = loggedInState !is LoggedInState.ErrorLoggingIn && + ((formState.login.isNotEmpty() && formState.password.isNotEmpty()) || supportOidcLogin) } sealed interface LoggedInState { object NotLoggedIn : LoggedInState object LoggingIn : LoggedInState + data class OidcStarted(val oidcDetail: OidcDetails) : LoggedInState data class ErrorLoggingIn(val failure: Throwable) : LoggedInState data class LoggedIn(val sessionId: SessionId) : LoggedInState } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt index 9864bf2380..cdb92bcfaf 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt @@ -24,16 +24,20 @@ open class LoginRootStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLoginRootState(), - aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", true, null)), + aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", supportsPasswordLogin = true, supportsOidc = false)), aLoginRootState().copy(formState = LoginFormState("user", "pass")), aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggingIn), aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.ErrorLoggingIn(Throwable())), aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggedIn(SessionId("@user:domain"))), + // Oidc + aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("server-with-oidc.org", supportsPasswordLogin = false, supportsOidc = true)), + // No password, no oidc support + aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("wrong.org", supportsPasswordLogin = false, supportsOidc = false)), ) } fun aLoginRootState() = LoginRootState( - homeserverDetails = MatrixHomeServerDetails("matrix.org", true, null), + homeserverDetails = MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidc = false), loggedInState = LoggedInState.NotLoggedIn, formState = LoginFormState.Default, eventSink = {} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt index b72122c914..c8c85ed50f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt @@ -83,7 +83,7 @@ import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.autofill import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.R as StringR @@ -94,7 +94,7 @@ fun LoginRootView( state: LoginRootState, modifier: Modifier = Modifier, onChangeServer: () -> Unit = {}, - onLoginWithSuccess: (SessionId) -> Unit = {}, + onOidcDetails: (OidcDetails) -> Unit = {}, onBackPressed: () -> Unit, ) { val isLoading by remember(state.loggedInState) { @@ -102,6 +102,15 @@ fun LoginRootView( state.loggedInState == LoggedInState.LoggingIn } } + val focusManager = LocalFocusManager.current + + fun submit() { + // Clear focus to prevent keyboard issues with textfields + focusManager.clearFocus(force = true) + + state.eventSink(LoginRootEvents.Submit) + } + Scaffold( topBar = { TopAppBar( @@ -143,13 +152,37 @@ fun LoginRootView( Spacer(Modifier.height(32.dp)) - LoginForm(state = state, isLoading = isLoading) + when { + state.supportOidcLogin -> { + // Oidc, in this case, just display a Spacer and the submit button + Spacer(Modifier.height(28.dp)) + } + state.supportPasswordLogin -> { + LoginForm(state = state, isLoading = isLoading, onSubmit = ::submit) + } + else -> { + Text(text = "No supported login flow") + } + } - Spacer(modifier = Modifier.height(32.dp)) + Spacer(Modifier.height(28.dp)) + if (state.supportOidcLogin || state.supportPasswordLogin) { + // Submit + ButtonWithProgress( + text = stringResource(R.string.screen_login_submit), + showProgress = isLoading, + onClick = ::submit, + enabled = state.submitEnabled, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginContinue) + ) + Spacer(modifier = Modifier.height(32.dp)) + } } when (val loggedInState = state.loggedInState) { - is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId) + is LoggedInState.OidcStarted -> onOidcDetails(loggedInState.oidcDetail) else -> Unit } } @@ -217,6 +250,7 @@ internal fun ChangeServerSection( internal fun LoginForm( state: LoginRootState, isLoading: Boolean, + onSubmit: () -> Unit, modifier: Modifier = Modifier ) { var loginFieldState by textFieldState(stateValue = state.formState.login) @@ -225,13 +259,6 @@ internal fun LoginForm( val focusManager = LocalFocusManager.current val eventSink = state.eventSink - fun submit() { - // Clear focus to prevent keyboard issues with textfields - focusManager.clearFocus(force = true) - - eventSink(LoginRootEvents.Submit) - } - Column(modifier) { Text( text = stringResource(R.string.screen_login_form_header), @@ -318,23 +345,11 @@ internal fun LoginForm( imeAction = ImeAction.Done, ), keyboardActions = KeyboardActions( - onDone = { submit() } + onDone = { onSubmit() } ), singleLine = true, maxLines = 1, ) - Spacer(Modifier.height(28.dp)) - - // Submit - ButtonWithProgress( - text = stringResource(R.string.screen_login_submit), - showProgress = isLoading, - onClick = ::submit, - enabled = state.submitEnabled, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.loginContinue) - ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt index 8e2c37904f..c481fdf927 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt @@ -18,7 +18,6 @@ package io.element.android.features.login.impl.util object LoginConstants { - const val DEFAULT_HOMESERVER_URL = "matrix.org" + const val DEFAULT_HOMESERVER_URL = "synapse-oidc.lab.element.dev" // "matrix.org" const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" - } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt index 4c943db0d6..e670e02f11 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt @@ -22,4 +22,5 @@ sealed class AuthenticationException(message: String) : Exception(message) { class SlidingSyncNotAvailable(message: String) : AuthenticationException(message) class SessionMissing(message: String) : AuthenticationException(message) class Generic(message: String) : AuthenticationException(message) + class OidcError(type: String, message: String) : AuthenticationException(message) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt index 3414c4fb6e..c15153876c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt @@ -28,4 +28,23 @@ interface MatrixAuthenticationService { fun getHomeserverDetails(): StateFlow suspend fun setHomeserver(homeserver: String): Result suspend fun login(username: String, password: String): Result + + /* + * OIDC part. + */ + + /** + * Get the Oidc url to display to the user. + */ + suspend fun getOidcUrl(): Result + + /** + * Cancel Oidc login sequence. + */ + suspend fun cancelOidcLogin(): Result + + /** + * Attempt to login using the [callbackUrl] provided by the Oidc page. + */ + suspend fun loginWithOidc(callbackUrl: String): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt index 7f2b074c67..90c54cfe2e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt @@ -23,5 +23,5 @@ import kotlinx.parcelize.Parcelize data class MatrixHomeServerDetails( val url: String, val supportsPasswordLogin: Boolean, - val authenticationIssuer: String? + val supportsOidc: Boolean, ): Parcelable diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt new file mode 100644 index 0000000000..ae473885da --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.libraries.matrix.api.auth + +object OidcConfig { + const val redirectUri = "io.element:/callback" +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt new file mode 100644 index 0000000000..34c926c8e6 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.libraries.matrix.api.auth + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class OidcDetails( + val url: String, +) : Parcelable diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt index 96e3a367cb..b98ecc193f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt @@ -26,6 +26,13 @@ fun Throwable.mapAuthenticationException(): Throwable { is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!) is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!) is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!) + + is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!) + is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!) + is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!) + is RustAuthenticationException.OidcNotStarted -> AuthenticationException.OidcError("OidcNotStarted", message!!) + is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!) + else -> this } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt index e2f3cfe676..9ca3d78456 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt @@ -23,6 +23,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use { MatrixHomeServerDetails( url = url(), supportsPasswordLogin = supportsPasswordLogin(), - authenticationIssuer = authenticationIssuer() + supportsOidc = supportsOidcLogin(), ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt new file mode 100644 index 0000000000..774c22781b --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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 io.element.android.libraries.matrix.impl.auth + +import io.element.android.libraries.matrix.api.auth.OidcConfig +import org.matrix.rustcomponents.sdk.OidcClientMetadata + +val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata( + clientName = "Element", + redirectUri = OidcConfig.redirectUri, + clientUri = "https://element.io", + tosUri = "https://element.io/user-terms-of-service", + policyUri = "https://element.io/privacy" +) + diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index aff83c3b5a..b0409cfba3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -24,8 +24,8 @@ import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.impl.RustMatrixClient import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder +import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.use import java.io.File @@ -51,7 +52,12 @@ class RustMatrixAuthenticationService @Inject constructor( private val sessionStore: SessionStore, ) : MatrixAuthenticationService { - private val authService: RustAuthenticationService = RustAuthenticationService(baseDirectory.absolutePath, null, null) + private val authService: RustAuthenticationService = RustAuthenticationService( + basePath = baseDirectory.absolutePath, + passphrase = null, + oidcClientMetadata = oidcClientMetadata, + customSlidingSyncProxy = null + ) private var currentHomeserver = MutableStateFlow(null) override fun isLoggedIn(): Flow { @@ -91,9 +97,9 @@ class RustMatrixAuthenticationService @Inject constructor( if (homeServerDetails != null) { currentHomeserver.value = homeServerDetails.copy(url = homeserver) } + }.mapFailure { failure -> + failure.mapAuthenticationException() } - }.mapFailure { failure -> - failure.mapAuthenticationException() } override suspend fun login(username: String, password: String): Result = @@ -103,11 +109,55 @@ class RustMatrixAuthenticationService @Inject constructor( val sessionData = client.use { it.session().toSessionData() } sessionStore.storeData(sessionData) SessionId(sessionData.userId) + }.mapFailure { failure -> + failure.mapAuthenticationException() } - }.mapFailure { failure -> - failure.mapAuthenticationException() } + private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null + + override suspend fun getOidcUrl(): Result { + return withContext(coroutineDispatchers.io) { + runCatching { + val urlForOidcLogin = authService.urlForOidcLogin() + val url = urlForOidcLogin.loginUrl() + pendingUrlForOidcLogin = urlForOidcLogin + OidcDetails(url) + }.mapFailure { failure -> + failure.mapAuthenticationException() + } + } + } + + override suspend fun cancelOidcLogin(): Result { + return withContext(coroutineDispatchers.io) { + runCatching { + pendingUrlForOidcLogin?.close() + pendingUrlForOidcLogin = null + }.mapFailure { failure -> + failure.mapAuthenticationException() + } + } + } + + /** + * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters) + */ + override suspend fun loginWithOidc(callbackUrl: String): Result { + return withContext(coroutineDispatchers.io) { + runCatching { + val urlForOidcLogin = pendingUrlForOidcLogin ?: error("You need to call `getOidcUrl()` first") + val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl) + val sessionData = client.use { it.session().toSessionData() } + pendingUrlForOidcLogin = null + sessionStore.storeData(sessionData) + SessionId(sessionData.userId) + }.mapFailure { failure -> + failure.mapAuthenticationException() + } + } + } + private fun createMatrixClient(client: Client): MatrixClient { return RustMatrixClient( client = client,