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

@ -27,13 +27,6 @@ android {
name = "CLIENT_URI",
value = BuildTimeConfig.URL_WEBSITE ?: "https://element.io"
)
buildConfigFieldStr(
name = "REDIRECT_URI",
value = buildString {
append(BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element")
append(":/callback")
}
)
buildConfigFieldStr(
name = "LOGO_URI",
value = BuildTimeConfig.URL_LOGO ?: "https://element.io/mobile-icon.png"

View file

@ -12,11 +12,6 @@ import io.element.android.libraries.matrix.api.BuildConfig
object OidcConfig {
const val CLIENT_URI = BuildConfig.CLIENT_URI
// Notes:
// 1. the scheme must match the value declared in the AndroidManifest.xml
// 2. the scheme must be the reverse of the host of CLIENT_URI
const val REDIRECT_URI = BuildConfig.REDIRECT_URI
// Note: host must match with the host of CLIENT_URI
const val LOGO_URI = BuildConfig.LOGO_URI

View file

@ -0,0 +1,12 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
interface OidcRedirectUrlProvider {
fun provide(): String
}

View file

@ -9,15 +9,17 @@ package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.OidcConfig
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import org.matrix.rustcomponents.sdk.OidcConfiguration
import javax.inject.Inject
class OidcConfigurationProvider @Inject constructor(
private val buildMeta: BuildMeta,
private val oidcRedirectUrlProvider: OidcRedirectUrlProvider,
) {
fun get(): OidcConfiguration = OidcConfiguration(
clientName = buildMeta.applicationName,
redirectUri = OidcConfig.REDIRECT_URI,
redirectUri = oidcRedirectUrlProvider.provide(),
clientUri = OidcConfig.CLIENT_URI,
logoUri = OidcConfig.LOGO_URI,
tosUri = OidcConfig.TOS_URI,

View file

@ -8,7 +8,8 @@
package io.element.android.libraries.matrix.impl.auth
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.matrix.test.core.aBuildMeta
import org.junit.Test
@ -16,11 +17,12 @@ class OidcConfigurationProviderTest {
@Test
fun get() {
val result = OidcConfigurationProvider(
aBuildMeta(
buildMeta = aBuildMeta(
applicationName = "myName",
)
),
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
).get()
assertThat(result.clientName).isEqualTo("myName")
assertThat(result.redirectUri).isEqualTo(OidcConfig.REDIRECT_URI)
assertThat(result.redirectUri).isEqualTo(FAKE_REDIRECT_URL)
}
}

View file

@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
@ -49,7 +50,10 @@ class RustMatrixAuthenticationServiceTest {
sessionStore = sessionStore,
rustMatrixClientFactory = rustMatrixClientFactory,
passphraseGenerator = FakePassphraseGenerator(),
oidcConfigurationProvider = OidcConfigurationProvider(aBuildMeta()),
oidcConfigurationProvider = OidcConfigurationProvider(
buildMeta = aBuildMeta(),
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
),
)
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.test.auth
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
const val FAKE_REDIRECT_URL = "io.element.android:/"
class FakeOidcRedirectUrlProvider(
private val provideResult: String = FAKE_REDIRECT_URL,
) : OidcRedirectUrlProvider {
override fun provide() = provideResult
}

View file

@ -39,5 +39,5 @@ fun aBuildMeta(
gitRevision = gitRevision,
gitBranchName = gitBranchName,
flavorDescription = flavorDescription,
flavorShortDescription = flavorShortDescription
flavorShortDescription = flavorShortDescription,
)

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(),
)
}
}