Skip to content

Improve the callback uri format and customization. #4664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,25 @@ android {
logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName)")

buildTypes {
val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
getByName("debug") {
resValue("string", "app_name", "$baseAppName dbg")
resValue(
"string",
"login_redirect_scheme",
"$oidcRedirectSchemeBase.debug",
)
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}

getByName("release") {
resValue("string", "app_name", baseAppName)
resValue(
"string",
"login_redirect_scheme",
oidcRedirectSchemeBase,
)
signingConfig = signingConfigs.getByName("debug")

postprocessing {
Expand All @@ -131,6 +142,11 @@ android {
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "$baseAppName nightly")
resValue(
"string",
"login_redirect_scheme",
"$oidcRedirectSchemeBase.nightly",
)
matchingFallbacks += listOf("release")
signingConfig = signingConfigs.getByName("nightly")

Expand Down Expand Up @@ -284,6 +300,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.toolbox.test)

koverDependencies()
}
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<!-- Note: the scheme must match the scheme of the value of OidcConfig.REDIRECT_URI -->
<data android:scheme="io.element" />
<data android:scheme="@string/login_redirect_scheme" />
</intent-filter>
<!--
Element web links
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.x.oidc

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.services.toolbox.api.strings.StringProvider
import io.element.android.x.R
import javax.inject.Inject

@ContributesBinding(AppScope::class)
class DefaultOidcRedirectUrlProvider @Inject constructor(
private val stringProvider: StringProvider,
) : OidcRedirectUrlProvider {
override fun provide() = buildString {
append(stringProvider.getString(R.string.login_redirect_scheme))
append(":/")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.x.oidc

import com.google.common.truth.Truth.assertThat
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.x.R
import org.junit.Test

class DefaultOidcRedirectUrlProviderTest {
@Test
fun `test provide`() {
val stringProvider = FakeStringProvider(
defaultResult = "str"
)
val sut = DefaultOidcRedirectUrlProvider(
stringProvider = stringProvider,
)
val result = sut.provide()
assertThat(result).isEqualTo("str:/")
assertThat(stringProvider.lastResIdParam).isEqualTo(R.string.login_redirect_scheme)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver
import io.element.android.libraries.oidc.impl.OidcUrlParser
import io.element.android.libraries.oidc.impl.DefaultOidcUrlParser
import io.element.android.tests.testutils.lambda.lambdaError
import org.junit.Assert.assertThrows
import org.junit.Test
Expand Down Expand Up @@ -119,7 +120,7 @@ class IntentResolverTest {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
Expand All @@ -134,13 +135,13 @@ class IntentResolverTest {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
data = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Oidc(
oidcAction = OidcAction.Success(
url = "io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
)
)
)
Expand All @@ -151,7 +152,7 @@ class IntentResolverTest {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element:/callback/invalid".toUri()
data = "io.element.android:/invalid".toUri()
}
assertThrows(IllegalStateException::class.java) {
sut.resolve(intent)
Expand Down Expand Up @@ -246,7 +247,9 @@ class IntentResolverTest {
return IntentResolver(
deeplinkParser = DeeplinkParser(),
oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = OidcUrlParser()
oidcUrlParser = DefaultOidcUrlParser(
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
)
),
permalinkParser = FakePermalinkParser(
result = permalinkParserResult
Expand Down
4 changes: 2 additions & 2 deletions docs/oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ Server list: https://github.com/element-hq/oidc-playground
Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28)

clientName: InfoPlistReader.main.bundleDisplayName,
redirectUri: "io.element:/callback",
redirectUri: "io.element.android:/",
clientUri: "https://element.io",
tosUri: "https://element.io/user-terms-of-service",
policyUri: "https://element.io/privacy"


Android:
clientName = "Element",
redirectUri = "io.element:/callback",
redirectUri = "io.element.android:/",
clientUri = "https://element.io",
tosUri = "https://element.io/user-terms-of-service",
policyUri = "https://element.io/privacy"
Expand Down
7 changes: 0 additions & 7 deletions libraries/matrix/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@
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

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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,7 +50,10 @@ class RustMatrixAuthenticationServiceTest {
sessionStore = sessionStore,
rustMatrixClientFactory = rustMatrixClientFactory,
passphraseGenerator = FakePassphraseGenerator(),
oidcConfigurationProvider = OidcConfigurationProvider(aBuildMeta()),
oidcConfigurationProvider = OidcConfigurationProvider(
buildMeta = aBuildMeta(),
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
),
)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ fun aBuildMeta(
gitRevision = gitRevision,
gitBranchName = gitBranchName,
flavorDescription = flavorDescription,
flavorShortDescription = flavorShortDescription
flavorShortDescription = flavorShortDescription,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,6 +40,7 @@ class OidcNode @AssistedInject constructor(
val state = presenter.present()
OidcView(
state = state,
oidcUrlParser = oidcUrlParser,
modifier = modifier,
onNavigateBack = ::navigateUp,
)
Expand Down
Loading
Loading