First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.
*/
plugins {
id("io.element.android-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.libraries.oidc.api"
}
dependencies {
implementation(projects.libraries.architecture)
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.oidc.api
sealed interface OidcAction {
data class GoBack(val toUnblock: Boolean = false) : OidcAction
data class Success(val url: String) : OidcAction
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.oidc.api
import kotlinx.coroutines.flow.FlowCollector
interface OidcActionFlow {
fun post(oidcAction: OidcAction)
suspend fun collect(collector: FlowCollector<OidcAction?>)
fun reset()
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.oidc.api
import android.content.Intent
interface OidcIntentResolver {
fun resolve(intent: Intent): OidcAction?
}
+47
View File
@@ -0,0 +1,47 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024 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.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "io.element.android.libraries.oidc.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
implementation(projects.appconfig)
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(libs.androidx.browser)
implementation(platform(libs.network.retrofit.bom))
implementation(libs.network.retrofit)
implementation(libs.serialization.json)
api(projects.libraries.oidc.api)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.permissions.test)
}
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.oidc.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultOidcActionFlow : OidcActionFlow {
private val mutableStateFlow = MutableStateFlow<OidcAction?>(null)
override fun post(oidcAction: OidcAction) {
mutableStateFlow.value = oidcAction
}
override suspend fun collect(collector: FlowCollector<OidcAction?>) {
mutableStateFlow.collect(collector)
}
override fun reset() {
mutableStateFlow.value = null
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.oidc.impl
import android.content.Intent
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcIntentResolver
@ContributesBinding(AppScope::class)
class DefaultOidcIntentResolver(
private val oidcUrlParser: OidcUrlParser,
) : OidcIntentResolver {
override fun resolve(intent: Intent): OidcAction? {
return oidcUrlParser.parse(intent.dataString.orEmpty())
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.oidc.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import io.element.android.libraries.oidc.api.OidcAction
fun interface OidcUrlParser {
fun parse(url: String): OidcAction?
}
/**
* Simple parser for oidc url interception.
* TODO Find documentation about the format.
*/
@ContributesBinding(AppScope::class)
class DefaultOidcUrlParser(
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.android:/?error=access_denied&state=IFF1UETGye2ZA8pO`
* On success, we get:
* `io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB`
*/
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)
// Other case not supported, let's crash the app for now
error("Not supported: $url")
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.oidc.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.oidc.api.OidcAction
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultOidcActionFlowTest {
@Test
fun `collect gets all the posted events`() = runTest {
val data = mutableListOf<OidcAction?>()
val sut = DefaultOidcActionFlow()
backgroundScope.launch {
sut.collect { action ->
data.add(action)
}
}
sut.post(OidcAction.GoBack())
delay(1)
sut.reset()
delay(1)
assertThat(data).containsExactly(OidcAction.GoBack(), null)
}
}
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.oidc.impl
import android.app.Activity
import android.content.Intent
import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
import io.element.android.libraries.oidc.api.OidcAction
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class DefaultOidcIntentResolverTest {
@Test
fun `test resolve oidc go back`() {
val sut = createDefaultOidcIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(OidcAction.GoBack())
}
@Test
fun `test resolve oidc success`() {
val sut = createDefaultOidcIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
OidcAction.Success(
url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
)
)
}
@Test
fun `test resolve oidc invalid`() {
val sut = createDefaultOidcIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element.android:/invalid".toUri()
}
assertThrows(IllegalStateException::class.java) {
sut.resolve(intent)
}
}
private fun createDefaultOidcIntentResolver(): DefaultOidcIntentResolver {
return DefaultOidcIntentResolver(
oidcUrlParser = DefaultOidcUrlParser(
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
),
)
}
}
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.oidc.impl
import com.google.common.truth.Truth.assertThat
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 DefaultOidcUrlParserTest {
@Test
fun `test empty url`() {
val sut = createDefaultOidcUrlParser()
assertThat(sut.parse("")).isNull()
}
@Test
fun `test regular url`() {
val sut = createDefaultOidcUrlParser()
assertThat(sut.parse("https://matrix.org")).isNull()
}
@Test
fun `test cancel url`() {
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 = 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 = 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(),
)
}
}
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.oidc.test"
}
dependencies {
implementation(libs.coroutines.core)
api(projects.libraries.oidc.api)
implementation(projects.tests.testutils)
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.oidc.test
import android.content.Intent
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcIntentResolver
import io.element.android.tests.testutils.lambda.lambdaError
class FakeOidcIntentResolver(
private val resolveResult: (Intent) -> OidcAction? = { lambdaError() }
) : OidcIntentResolver {
override fun resolve(intent: Intent): OidcAction? {
return resolveResult(intent)
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.oidc.test.customtab
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
/**
* This is actually a copy of DefaultOidcActionFlow.
*/
class FakeOidcActionFlow : OidcActionFlow {
private val mutableStateFlow = MutableStateFlow<OidcAction?>(null)
override fun post(oidcAction: OidcAction) {
mutableStateFlow.value = oidcAction
}
override suspend fun collect(collector: FlowCollector<OidcAction?>) {
mutableStateFlow.collect(collector)
}
override fun reset() {
mutableStateFlow.value = null
}
}