OIDC configuration (#4623)

* Login: more logs.

* Login: map Oidc error to provide more information in the error dialog.

* Oidc: use the application name.

* Oidc: move configuration from OidcConfigurationProvider to OidcConfig and add some comments.

* Oidc: limit to only 1 contact in the configuration.

* Oidc: Move configuration to BuildConfig file.

* Remove unused const.

* Add missing test on Exception mapping

* Remove contacts from OidcConfiguration.

https://github.com/matrix-org/matrix-rust-sdk/pull/4958
This commit is contained in:
Benoit Marty 2025-04-23 11:58:38 +02:00 committed by GitHub
parent 51a9a69ea0
commit 4486d5205c
12 changed files with 110 additions and 20 deletions

View file

@ -60,6 +60,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <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="io.element" />
</intent-filter> </intent-filter>
<!-- <!--

View file

@ -12,18 +12,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.ui.strings.CommonStrings
sealed class ChangeServerError : Throwable() { sealed class ChangeServerError : Throwable() {
data class Error(@StringRes val messageId: Int) : ChangeServerError() { data class Error(
@StringRes val messageId: Int? = null,
val messageStr: String? = null,
) : ChangeServerError() {
@Composable @Composable
fun message(): String = stringResource(messageId) fun message(): String = messageStr ?: stringResource(messageId ?: CommonStrings.error_unknown)
} }
data object SlidingSyncAlert : ChangeServerError() data object SlidingSyncAlert : ChangeServerError()
companion object { companion object {
fun from(error: Throwable): ChangeServerError = when (error) { fun from(error: Throwable): ChangeServerError = when (error) {
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
else -> Error(R.string.screen_change_server_error_invalid_homeserver) is AuthenticationException.Oidc -> Error(messageStr = error.message)
else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver)
} }
} }
} }

View file

@ -1,3 +1,5 @@
import config.BuildTimeConfig
import extension.buildConfigFieldStr
import extension.setupAnvil import extension.setupAnvil
/* /*
@ -19,6 +21,32 @@ android {
buildFeatures { buildFeatures {
buildConfig = true buildConfig = true
} }
defaultConfig {
buildConfigFieldStr(
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"
)
buildConfigFieldStr(
name = "TOS_URI",
value = BuildTimeConfig.URL_ACCEPTABLE_USE ?: "https://element.io/acceptable-use-policy-terms"
)
buildConfigFieldStr(
name = "POLICY_URI",
value = BuildTimeConfig.URL_POLICY ?: "https://element.io/privacy"
)
}
} }
setupAnvil() setupAnvil()

View file

@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.auth
sealed class AuthenticationException(message: String) : Exception(message) { sealed class AuthenticationException(message: String) : Exception(message) {
class InvalidServerName(message: String) : AuthenticationException(message) class InvalidServerName(message: String) : AuthenticationException(message)
class SlidingSyncVersion(message: String) : AuthenticationException(message) class SlidingSyncVersion(message: String) : AuthenticationException(message)
class Oidc(message: String) : AuthenticationException(message)
class Generic(message: String) : AuthenticationException(message) class Generic(message: String) : AuthenticationException(message)
} }

View file

@ -7,6 +7,27 @@
package io.element.android.libraries.matrix.api.auth package io.element.android.libraries.matrix.api.auth
import io.element.android.libraries.matrix.api.BuildConfig
object OidcConfig { object OidcConfig {
const val REDIRECT_URI = "io.element:/callback" 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
// Note: host must match with the host of CLIENT_URI
const val TOS_URI = BuildConfig.TOS_URI
// Note: host must match with the host of CLIENT_URI
const val POLICY_URI = BuildConfig.POLICY_URI
// Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
val STATIC_REGISTRATIONS = mapOf(
"https://id.thirdroom.io/realms/thirdroom" to "elementx",
)
} }

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.matrix.rustcomponents.sdk.ClientBuildException import org.matrix.rustcomponents.sdk.ClientBuildException
import org.matrix.rustcomponents.sdk.OidcException
fun Throwable.mapAuthenticationException(): AuthenticationException { fun Throwable.mapAuthenticationException(): AuthenticationException {
val message = this.message ?: "Unknown error" val message = this.message ?: "Unknown error"
@ -24,6 +25,13 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
is ClientBuildException.EventCache -> AuthenticationException.Generic(message) is ClientBuildException.EventCache -> AuthenticationException.Generic(message)
} }
is OidcException -> when (this) {
is OidcException.Generic -> AuthenticationException.Oidc(message)
is OidcException.CallbackUrlInvalid -> AuthenticationException.Oidc(message)
is OidcException.Cancelled -> AuthenticationException.Oidc(message)
is OidcException.MetadataInvalid -> AuthenticationException.Oidc(message)
is OidcException.NotSupported -> AuthenticationException.Oidc(message)
}
else -> AuthenticationException.Generic(message) else -> AuthenticationException.Generic(message)
} }
} }

View file

@ -7,24 +7,22 @@
package io.element.android.libraries.matrix.impl.auth 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.OidcConfig
import org.matrix.rustcomponents.sdk.OidcConfiguration import org.matrix.rustcomponents.sdk.OidcConfiguration
import javax.inject.Inject import javax.inject.Inject
class OidcConfigurationProvider @Inject constructor() { class OidcConfigurationProvider @Inject constructor(
private val buildMeta: BuildMeta,
) {
fun get(): OidcConfiguration = OidcConfiguration( fun get(): OidcConfiguration = OidcConfiguration(
clientName = "Element", clientName = buildMeta.applicationName,
redirectUri = OidcConfig.REDIRECT_URI, redirectUri = OidcConfig.REDIRECT_URI,
clientUri = "https://element.io", clientUri = OidcConfig.CLIENT_URI,
logoUri = "https://element.io/mobile-icon.png", logoUri = OidcConfig.LOGO_URI,
tosUri = "https://element.io/acceptable-use-policy-terms", tosUri = OidcConfig.TOS_URI,
policyUri = "https://element.io/privacy", policyUri = OidcConfig.POLICY_URI,
contacts = listOf( contacts = null,
"support@element.io", staticRegistrations = OidcConfig.STATIC_REGISTRATIONS,
),
// 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",
),
) )
} }

View file

@ -137,6 +137,7 @@ class RustMatrixAuthenticationService @Inject constructor(
}.onFailure { }.onFailure {
clear() clear()
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to set homeserver to $homeserver")
failure.mapAuthenticationException() failure.mapAuthenticationException()
} }
} }
@ -162,6 +163,7 @@ class RustMatrixAuthenticationService @Inject constructor(
SessionId(sessionData.userId) SessionId(sessionData.userId)
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to login")
failure.mapAuthenticationException() failure.mapAuthenticationException()
} }
} }
@ -197,6 +199,7 @@ class RustMatrixAuthenticationService @Inject constructor(
pendingOAuthAuthorizationData = oAuthAuthorizationData pendingOAuthAuthorizationData = oAuthAuthorizationData
OidcDetails(url) OidcDetails(url)
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to get OIDC URL")
failure.mapAuthenticationException() failure.mapAuthenticationException()
} }
} }
@ -210,6 +213,7 @@ class RustMatrixAuthenticationService @Inject constructor(
} }
pendingOAuthAuthorizationData = null pendingOAuthAuthorizationData = null
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to cancel OIDC login")
failure.mapAuthenticationException() failure.mapAuthenticationException()
} }
} }
@ -243,6 +247,7 @@ class RustMatrixAuthenticationService @Inject constructor(
SessionId(sessionData.userId) SessionId(sessionData.userId)
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to login with OIDC")
failure.mapAuthenticationException() failure.mapAuthenticationException()
} }
} }

View file

@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.ClientBuildException import org.matrix.rustcomponents.sdk.ClientBuildException
import org.matrix.rustcomponents.sdk.OidcException
class AuthenticationExceptionMappingTest { class AuthenticationExceptionMappingTest {
@Test @Test
@ -56,6 +57,20 @@ class AuthenticationExceptionMappingTest {
.isException<AuthenticationException.Generic>("EventCache error") .isException<AuthenticationException.Generic>("EventCache error")
} }
@Test
fun `mapping Oidc exceptions map to the Oidc Kotlin`() {
assertThat(OidcException.Generic("Generic").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("Generic")
assertThat(OidcException.CallbackUrlInvalid("CallbackUrlInvalid").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("CallbackUrlInvalid")
assertThat(OidcException.Cancelled("Cancelled").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("Cancelled")
assertThat(OidcException.MetadataInvalid("MetadataInvalid").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("MetadataInvalid")
assertThat(OidcException.NotSupported("NotSupported").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("NotSupported")
}
private inline fun <reified T> ThrowableSubject.isException(message: String) { private inline fun <reified T> ThrowableSubject.isException(message: String) {
isInstanceOf(T::class.java) isInstanceOf(T::class.java)
hasMessageThat().isEqualTo(message) hasMessageThat().isEqualTo(message)

View file

@ -9,12 +9,18 @@ package io.element.android.libraries.matrix.impl.auth
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.OidcConfig import io.element.android.libraries.matrix.api.auth.OidcConfig
import io.element.android.libraries.matrix.test.core.aBuildMeta
import org.junit.Test import org.junit.Test
class OidcConfigurationProviderTest { class OidcConfigurationProviderTest {
@Test @Test
fun get() { fun get() {
val result = OidcConfigurationProvider().get() val result = OidcConfigurationProvider(
aBuildMeta(
applicationName = "myName",
)
).get()
assertThat(result.clientName).isEqualTo("myName")
assertThat(result.redirectUri).isEqualTo(OidcConfig.REDIRECT_URI) assertThat(result.redirectUri).isEqualTo(OidcConfig.REDIRECT_URI)
} }
} }

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.api.core.SessionId
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.libraries.sessionstorage.test.aSessionData
@ -48,7 +49,7 @@ class RustMatrixAuthenticationServiceTest {
sessionStore = sessionStore, sessionStore = sessionStore,
rustMatrixClientFactory = rustMatrixClientFactory, rustMatrixClientFactory = rustMatrixClientFactory,
passphraseGenerator = FakePassphraseGenerator(), passphraseGenerator = FakePassphraseGenerator(),
oidcConfigurationProvider = OidcConfigurationProvider(), oidcConfigurationProvider = OidcConfigurationProvider(aBuildMeta()),
) )
} }
} }

View file

@ -14,7 +14,7 @@ object BuildTimeConfig {
const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c" const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c"
const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c" const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c"
val METADATA_HOST: String? = null val METADATA_HOST_REVERSED: String? = null
val URL_WEBSITE: String? = null val URL_WEBSITE: String? = null
val URL_LOGO: String? = null val URL_LOGO: String? = null
val URL_COPYRIGHT: String? = null val URL_COPYRIGHT: String? = null