Improve the callback uri format and customization. (#4664)

* Remove unused SUPPORT_EMAIL_ADDRESS

* Improve the callback uri format and customization.

Use io.element.android for the scheme of Oidc redirection for Element X.
For nightly the scheme will be io.element.android.nightly
For debug the scheme will be  io.element.android.debug

Element Pro is using `io.element`
This commit is contained in:
Benoit Marty 2025-05-05 17:46:17 +02:00 committed by GitHub
parent 1904c98c9a
commit c61ee59528
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 166 additions and 49 deletions

View file

@ -7,25 +7,34 @@
package io.element.android.libraries.oidc.impl
import io.element.android.libraries.matrix.api.auth.OidcConfig
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import io.element.android.libraries.oidc.api.OidcAction
import javax.inject.Inject
fun interface OidcUrlParser {
fun parse(url: String): OidcAction?
}
/**
* Simple parser for oidc url interception.
* TODO Find documentation about the format.
*/
class OidcUrlParser @Inject constructor() {
@ContributesBinding(AppScope::class)
class DefaultOidcUrlParser @Inject constructor(
private val oidcRedirectUrlProvider: OidcRedirectUrlProvider,
) : OidcUrlParser {
/**
* Return a OidcAction, or null if the url is not a OidcUrl.
* Note:
* When user press button "Cancel", we get the url:
* `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO`
* `io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO`
* On success, we get:
* `io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB`
* `io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB`
*/
fun parse(url: String): OidcAction? {
if (url.startsWith(OidcConfig.REDIRECT_URI).not()) return null
override fun parse(url: String): OidcAction? {
if (url.startsWith(oidcRedirectUrlProvider.provide()).not()) return null
if (url.contains("error=access_denied")) return OidcAction.GoBack
if (url.contains("code=")) return OidcAction.Success(url)

View file

@ -19,12 +19,14 @@ 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
import io.element.android.libraries.oidc.impl.OidcUrlParser
@ContributesNode(AppScope::class)
class OidcNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: OidcPresenter.Factory,
private val oidcUrlParser: OidcUrlParser,
) : Node(buildContext, plugins = plugins) {
data class Inputs(
val oidcDetails: OidcDetails,
@ -38,6 +40,7 @@ class OidcNode @AssistedInject constructor(
val state = presenter.present()
OidcView(
state = state,
oidcUrlParser = oidcUrlParser,
modifier = modifier,
onNavigateBack = ::navigateUp,
)

View file

@ -34,11 +34,11 @@ import io.element.android.libraries.oidc.impl.OidcUrlParser
@Composable
fun OidcView(
state: OidcState,
oidcUrlParser: OidcUrlParser,
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val isPreview = LocalInspectionMode.current
val oidcUrlParser = remember { OidcUrlParser() }
var webView by remember { mutableStateOf<WebView?>(null) }
fun shouldOverrideUrl(url: String): Boolean {
val action = oidcUrlParser.parse(url)
@ -111,6 +111,7 @@ fun OidcView(
internal fun OidcViewPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) = ElementPreview {
OidcView(
state = state,
oidcUrlParser = { null },
onNavigateBack = {},
)
}

View file

@ -8,44 +8,51 @@
package io.element.android.libraries.oidc.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.OidcConfig
import io.element.android.libraries.matrix.test.auth.FAKE_REDIRECT_URL
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
import io.element.android.libraries.oidc.api.OidcAction
import org.junit.Assert
import org.junit.Test
class OidcUrlParserTest {
class DefaultOidcUrlParserTest {
@Test
fun `test empty url`() {
val sut = OidcUrlParser()
val sut = createDefaultOidcUrlParser()
assertThat(sut.parse("")).isNull()
}
@Test
fun `test regular url`() {
val sut = OidcUrlParser()
val sut = createDefaultOidcUrlParser()
assertThat(sut.parse("https://matrix.org")).isNull()
}
@Test
fun `test cancel url`() {
val sut = OidcUrlParser()
val aCancelUrl = OidcConfig.REDIRECT_URI + "?error=access_denied&state=IFF1UETGye2ZA8pO"
val sut = createDefaultOidcUrlParser()
val aCancelUrl = "$FAKE_REDIRECT_URL?error=access_denied&state=IFF1UETGye2ZA8pO"
assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack)
}
@Test
fun `test success url`() {
val sut = OidcUrlParser()
val aSuccessUrl = OidcConfig.REDIRECT_URI + "?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
val sut = createDefaultOidcUrlParser()
val aSuccessUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
assertThat(sut.parse(aSuccessUrl)).isEqualTo(OidcAction.Success(aSuccessUrl))
}
@Test
fun `test unknown url`() {
val sut = OidcUrlParser()
val anUnknownUrl = OidcConfig.REDIRECT_URI + "?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
val sut = createDefaultOidcUrlParser()
val anUnknownUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
Assert.assertThrows(IllegalStateException::class.java) {
assertThat(sut.parse(anUnknownUrl))
}
}
private fun createDefaultOidcUrlParser(): DefaultOidcUrlParser {
return DefaultOidcUrlParser(
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
)
}
}