diff --git a/changelog.d/9102.misc b/changelog.d/9102.misc
new file mode 100644
index 00000000000..705a7a57fbd
--- /dev/null
+++ b/changelog.d/9102.misc
@@ -0,0 +1 @@
+Update OAuth-awareness to support the stable version of MSC3824.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/SSOAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/SSOAction.kt
index db2dd870d52..bd5dfb662cc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/SSOAction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/SSOAction.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.auth
/**
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3824
*/
-enum class SSOAction {
- LOGIN,
- REGISTER;
+enum class SSOAction(val value: String) {
+ LOGIN("login"),
+ REGISTER("register");
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/AuthMetadata.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/AuthMetadata.kt
new file mode 100644
index 00000000000..62a2382842a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/AuthMetadata.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2026 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.auth.data
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * This is a subset of the server metadata discovery for the OAuth 2.0 API
+ * https://spec.matrix.org/v1.16/client-server-api/#get_matrixclientv1auth_metadata
+ *
+ * Includes the values from MSC4191: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
+ *
+ *
+ * {
+ * "issuer": "https://id.server.org",
+ * "account_management_uri": "https://id.server.org/my-account",
+ * "account_management_actions_supported": ["org.matrix.profile", "org.matrix.devices_list"],
+ * }
+ *
+ * .
+ */
+
+@JsonClass(generateAdapter = true)
+data class AuthMetadata(
+ @Json(name = "issuer")
+ val issuer: String,
+
+ @Json(name = "account_management_uri")
+ val accountManagementUri: String?,
+
+ @Json(name = "account_management_actions_supported")
+ val accountManagementActionsSupported: List?,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index 101947f9d9b..5404eecc496 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -82,12 +82,17 @@ data class HomeServerCapabilities(
var canRedactRelatedEvents: Boolean = false,
/**
- * External account management url for use with MSC3824 delegated OIDC, provided in Wellknown.
+ * External account management url for use with OAuth API, provided by MSC4191 /auth_metadata discovery or in unstable Wellknown.
*/
val externalAccountManagementUrl: String? = null,
/**
- * Authentication issuer for use with MSC3824 delegated OIDC, provided in Wellknown.
+ * External account management supported actions for use with OAuth API, provided by MSC4191 /auth_metadata discovery.
+ */
+ val externalAccountManagementSupportedActions: List? = null,
+
+ /**
+ * Authentication issuer for use with MSC3824 delegated OIDC, provided by /auth_metadata discovery or in unstable Wellknown.
*/
val authenticationIssuer: String? = null,
@@ -162,4 +167,26 @@ data class HomeServerCapabilities(
const val ROOM_CAP_KNOCK = "knock"
const val ROOM_CAP_RESTRICTED = "restricted"
}
+
+ fun getLogoutDeviceURL(deviceId: String): String? {
+ if (externalAccountManagementUrl == null) {
+ return null
+ }
+
+ // default to the stable value:
+ var action = "org.matrix.device_delete"
+ externalAccountManagementSupportedActions?.also { actions ->
+ if (actions.contains("org.matrix.device_delete")) {
+ // server supports stable version so use it
+ } else if (actions.contains("org.matrix.session_end")) {
+ // earlier version of MSC4191:
+ action = "org.matrix.session_end"
+ } else if (actions.contains("session_end")) {
+ // previous unspecified version
+ action = "session_end"
+ }
+ }
+
+ return externalAccountManagementUrl.removeSuffix("/") + "?action=${action}&device_id=${deviceId}"
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
index e852c61185b..6b9a06a6046 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
@@ -106,7 +106,11 @@ internal class DefaultAuthenticationService @Inject constructor(
}
// unstable MSC3824 action param
- appendParamToUrl("org.matrix.msc3824.action", action.toString())
+ // This can be removed once servers have been updated to support the stable one.
+ appendParamToUrl("org.matrix.msc3824.action", action.value)
+
+ // stable param:
+ appendParamToUrl("action", action.value)
}
}
@@ -297,8 +301,8 @@ internal class DefaultAuthenticationService @Inject constructor(
authAPI.getLoginFlows()
}
- // If an m.login.sso flow is present that is flagged as being for MSC3824 OIDC compatibility then we only return that flow
- val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibility == true }
+ // If an m.login.sso flow is present that is flagged as being for MSC3824 OAuth compatibility then we only return that flow
+ val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibility }
val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows
val supportsGetLoginTokenFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.token" && it.getLoginToken == true } != null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt
index 2e52740ed48..6d8d02fabb2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
+import org.matrix.android.sdk.api.extensions.orFalse
@JsonClass(generateAdapter = true)
internal data class LoginFlowResponse(
@@ -46,12 +47,20 @@ internal data class LoginFlow(
val ssoIdentityProvider: List? = null,
/**
- * Whether this login flow is preferred for OIDC-aware clients.
+ * Whether this login flow is preferred for OAuth 2.0-aware clients like we are.
*
* See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
*/
+ @Json(name = "oauth_aware_preferred")
+ val oauthAwarePreferred: Boolean? = null,
+
+ /**
+ * Unstable name from MSC3824.
+ *
+ */
+ @Deprecated("Use oauthAwarePreferred instead")
@Json(name = "org.matrix.msc3824.delegated_oidc_compatibility")
- val delegatedOidcCompatibility: Boolean? = null,
+ val unstableDelegatedOidcCompatibility: Boolean? = null,
/**
* Whether a login flow of type m.login.token could accept a token issued using /login/get_token.
@@ -60,4 +69,7 @@ internal data class LoginFlow(
*/
@Json(name = "get_login_token")
val getLoginToken: Boolean? = null
-)
+) {
+ @Suppress("DEPRECATION") val delegatedOidcCompatibility: Boolean
+ get() = this.oauthAwarePreferred.orFalse() || this.unstableDelegatedOidcCompatibility.orFalse()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 8cdabaf1504..1c6ba850d74 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -49,6 +49,7 @@ internal object HomeServerCapabilitiesMapper {
canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
canRedactRelatedEvents = entity.canRedactEventWithRelations,
externalAccountManagementUrl = entity.externalAccountManagementUrl,
+ externalAccountManagementSupportedActions = entity.externalAccountManagementSupportedActions?.split(","),
authenticationIssuer = entity.authenticationIssuer,
disableNetworkConstraint = entity.disableNetworkConstraint,
canUseAuthenticatedMedia = entity.canUseAuthenticatedMedia,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index 0096071d547..d77d9025953 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -36,6 +36,7 @@ internal open class HomeServerCapabilitiesEntity(
var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
var canRedactEventWithRelations: Boolean = false,
var externalAccountManagementUrl: String? = null,
+ var externalAccountManagementSupportedActions: String? = null,
var authenticationIssuer: String? = null,
var disableNetworkConstraint: Boolean? = null,
var canUseAuthenticatedMedia: Boolean = false,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/AuthMetadataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/AuthMetadataAPI.kt
new file mode 100644
index 00000000000..cf6cab289dc
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/AuthMetadataAPI.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2026 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.homeserver
+
+import org.matrix.android.sdk.api.auth.data.AuthMetadata
+import org.matrix.android.sdk.internal.network.NetworkConstants
+import retrofit2.http.GET
+
+internal interface AuthMetadataAPI {
+ /**
+ * Request the homeserver OAuth 2.0 auth metadata.
+ */
+ @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "auth_metadata")
+ suspend fun getAuthMetadata(): AuthMetadata
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index 5314237c3c2..31d71286c91 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
+import org.matrix.android.sdk.api.auth.data.AuthMetadata
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.extensions.orFalse
@@ -66,7 +67,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val configExtractor: IntegrationManagerConfigExtractor,
private val homeServerConnectionConfig: HomeServerConnectionConfig,
@UserId
- private val userId: String
+ private val userId: String,
+ private val authMetadataAPI: AuthMetadataAPI,
) : GetHomeServerCapabilitiesTask {
override suspend fun execute(params: GetHomeServerCapabilitiesTask.Params) {
@@ -104,6 +106,12 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
}
}.getOrNull()
+ val authMetadata = runCatching {
+ executeRequest(globalErrorReceiver = null) {
+ authMetadataAPI.getAuthMetadata()
+ }
+ }.getOrNull()
+
// Domain may include a port (eg, matrix.org:8080)
// Per https://spec.matrix.org/latest/client-server-api/#well-known-uri we should extract the hostname from the server name
// So we take everything before the last : as the domain for the well-known task.
@@ -117,14 +125,15 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
)
}.getOrNull()
- insertInDb(capabilities, mediaConfig, versions, wellknownResult)
+ insertInDb(capabilities, mediaConfig, versions, wellknownResult, authMetadata)
}
private suspend fun insertInDb(
getCapabilitiesResult: GetCapabilitiesResult?,
getMediaConfigResult: GetMediaConfigResult?,
getVersionResult: Versions?,
- getWellknownResult: WellknownResult?
+ getWellknownResult: WellknownResult?,
+ authMetadata: AuthMetadata?,
) {
monarchy.awaitTransaction { realm ->
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
@@ -174,11 +183,19 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
Timber.v("Extracted integration config : $config")
realm.insertOrUpdate(config)
}
+ // Getting the OAuth 2.0 metadata from well-known was in unstable MSC:
homeServerCapabilitiesEntity.authenticationIssuer = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.issuer
homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl
homeServerCapabilitiesEntity.disableNetworkConstraint = getWellknownResult.wellKnown.disableNetworkConstraint
}
+ // If the server returns OAuth 2.0 metadata then prefer that over the well-known values:
+ if (authMetadata != null) {
+ homeServerCapabilitiesEntity.authenticationIssuer = authMetadata.issuer
+ homeServerCapabilitiesEntity.externalAccountManagementUrl = authMetadata.accountManagementUri
+ homeServerCapabilitiesEntity.externalAccountManagementSupportedActions = authMetadata.accountManagementActionsSupported?.joinToString(",")
+ }
+
homeServerCapabilitiesEntity.canLoginWithQrCode = canLoginWithQrCode(getCapabilitiesResult, getVersionResult)
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt
index 2a1573dd98f..88988addcea 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesModule.kt
@@ -34,6 +34,13 @@ internal abstract class HomeServerCapabilitiesModule {
fun providesCapabilitiesAPI(retrofit: Retrofit): CapabilitiesAPI {
return retrofit.create(CapabilitiesAPI::class.java)
}
+
+ @Provides
+ @JvmStatic
+ @SessionScope
+ fun providesAuthMetadataAPI(retrofit: Retrofit): AuthMetadataAPI {
+ return retrofit.create(AuthMetadataAPI::class.java)
+ }
}
@Binds
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesTest.kt
new file mode 100644
index 00000000000..75a828fccec
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2026 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.homeserver
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class HomeServerCapabilitiesTest {
+
+ private val deviceId = "TEST_DEVICE_ID"
+
+ @Test
+ fun `given externalAccountManagementUrl is null, when getLogoutDeviceURL, then return null`() {
+ val capabilities = HomeServerCapabilities(
+ externalAccountManagementUrl = null
+ )
+ assertNull(capabilities.getLogoutDeviceURL(deviceId))
+ }
+
+ @Test
+ fun `given supported actions is null, when getLogoutDeviceURL, then uses default stable action`() {
+ val capabilities = HomeServerCapabilities(
+ externalAccountManagementUrl = "https://example.com",
+ externalAccountManagementSupportedActions = null
+ )
+ val expectedUrl = "https://example.com?action=org.matrix.device_delete&device_id=$deviceId"
+ assertEquals(expectedUrl, capabilities.getLogoutDeviceURL(deviceId))
+ }
+
+ @Test
+ fun `given supported actions contains stable action, when getLogoutDeviceURL, then uses stable action`() {
+ val capabilities = HomeServerCapabilities(
+ externalAccountManagementUrl = "https://example.com",
+ externalAccountManagementSupportedActions = listOf("org.matrix.device_delete", "org.matrix.session_end", "session_end")
+ )
+ val expectedUrl = "https://example.com?action=org.matrix.device_delete&device_id=$deviceId"
+ assertEquals(expectedUrl, capabilities.getLogoutDeviceURL(deviceId))
+ }
+
+ @Test
+ fun `given supported actions contains unstable action, when getLogoutDeviceURL, then uses unstable action`() {
+ val capabilities = HomeServerCapabilities(
+ externalAccountManagementUrl = "https://example.com",
+ externalAccountManagementSupportedActions = listOf("org.matrix.session_end", "session_end")
+ )
+ val expectedUrl = "https://example.com?action=org.matrix.session_end&device_id=$deviceId"
+ assertEquals(expectedUrl, capabilities.getLogoutDeviceURL(deviceId))
+ }
+
+ @Test
+ fun `given supported actions contains legacy action, when getLogoutDeviceURL, then uses legacy action`() {
+ val capabilities = HomeServerCapabilities(
+ externalAccountManagementUrl = "https://example.com",
+ externalAccountManagementSupportedActions = listOf("session_end")
+ )
+ val expectedUrl = "https://example.com?action=session_end&device_id=$deviceId"
+ assertEquals(expectedUrl, capabilities.getLogoutDeviceURL(deviceId))
+ }
+
+ @Test
+ fun `given url with trailing slash, when getLogoutDeviceURL, then slash is removed`() {
+ val capabilities = HomeServerCapabilities(
+ externalAccountManagementUrl = "https://example.com/account/"
+ )
+ val expectedUrl = "https://example.com/account?action=org.matrix.device_delete&device_id=$deviceId"
+ assertEquals(expectedUrl, capabilities.getLogoutDeviceURL(deviceId))
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTaskTest.kt
new file mode 100644
index 00000000000..fd228578c7f
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTaskTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2026 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.homeserver
+
+import io.mockk.coEvery
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.api.auth.data.AuthMetadata
+import org.matrix.android.sdk.api.auth.data.DelegatedAuthConfig
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.api.auth.data.WellKnown
+import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor
+import org.matrix.android.sdk.internal.session.media.AuthenticatedMediaAPI
+import org.matrix.android.sdk.internal.session.media.UnauthenticatedMediaAPI
+import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
+import org.matrix.android.sdk.test.fakes.FakeMonarchy
+
+class DefaultGetHomeServerCapabilitiesTaskTest {
+
+ private val capabilitiesAPI: CapabilitiesAPI = mockk()
+ private val unauthenticatedMediaAPI: UnauthenticatedMediaAPI = mockk()
+ private val authenticatedMediaAPI: AuthenticatedMediaAPI = mockk()
+ private val monarchy = FakeMonarchy()
+ private val globalErrorReceiver: GlobalErrorReceiver = mockk(relaxed = true)
+ private val getWellknownTask: GetWellknownTask = mockk()
+ private val configExtractor: IntegrationManagerConfigExtractor = mockk()
+ private val homeServerConnectionConfig: HomeServerConnectionConfig = mockk()
+ private val authMetadataAPI: AuthMetadataAPI = mockk()
+
+ private val task = DefaultGetHomeServerCapabilitiesTask(
+ capabilitiesAPI,
+ unauthenticatedMediaAPI,
+ authenticatedMediaAPI,
+ monarchy.instance,
+ globalErrorReceiver,
+ getWellknownTask,
+ configExtractor,
+ homeServerConnectionConfig,
+ "userId",
+ authMetadataAPI
+ )
+
+ private val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity()
+
+ @Before
+ fun setUp() {
+ coEvery { configExtractor.extract(any()) } returns null
+ monarchy.givenWhereReturns(result = homeServerCapabilitiesEntity)
+ }
+
+ private val wellKnownWithoutDelegation = WellknownResult.Prompt(
+ homeServerUrl = "https://test",
+ identityServerUrl = null,
+ wellKnown = WellKnown(
+ homeServer = mockk(),
+ identityServer = mockk(),
+ unstableDelegatedAuthConfig = null,
+ )
+ )
+
+ private val wellKnownWithDelegationToTest1 = WellknownResult.Prompt(
+ homeServerUrl = "https://test",
+ identityServerUrl = null,
+ wellKnown = WellKnown(
+ homeServer = mockk(),
+ identityServer = mockk(),
+ unstableDelegatedAuthConfig = DelegatedAuthConfig(
+ issuer = "https://test1",
+ accountManagementUrl = "https://test1/account"
+ )
+ )
+ )
+
+ private val authMetadataForTest2 = AuthMetadata(
+ issuer = "https://test2",
+ accountManagementUri = "https://test2/account",
+ accountManagementActionsSupported = listOf("org.matrix.device_delete", "org.matrix.profile")
+ )
+
+ private val authMetadataForTest2WithoutAccountManagement = AuthMetadata(
+ issuer = "https://test2",
+ accountManagementUri = null,
+ accountManagementActionsSupported = null
+ )
+
+ @Test
+ fun `given no OAuth config, when execute, then fields are null`() = runTest {
+ // Given
+ coEvery { getWellknownTask.execute(any()) } throws Exception()
+ coEvery { authMetadataAPI.getAuthMetadata() } throws Exception()
+
+ // When
+ task.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
+
+ // Then
+ homeServerCapabilitiesEntity.authenticationIssuer shouldBeEqualTo null
+ homeServerCapabilitiesEntity.externalAccountManagementUrl shouldBeEqualTo null
+ homeServerCapabilitiesEntity.externalAccountManagementSupportedActions shouldBeEqualTo null
+ }
+
+ @Test
+ fun `given well-known, when execute, populates fields`() = runTest {
+ // Given
+ coEvery { getWellknownTask.execute(any()) } returns wellKnownWithDelegationToTest1
+ coEvery { authMetadataAPI.getAuthMetadata() } throws Exception()
+
+ // When
+ task.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
+
+ // Then
+ homeServerCapabilitiesEntity.authenticationIssuer shouldBeEqualTo "https://test1"
+ homeServerCapabilitiesEntity.externalAccountManagementUrl shouldBeEqualTo "https://test1/account"
+ homeServerCapabilitiesEntity.externalAccountManagementSupportedActions shouldBeEqualTo null
+ }
+
+ @Test
+ fun `given auth metadata, when execute, populates fields`() = runTest {
+ // Given
+ coEvery { getWellknownTask.execute(any()) } returns wellKnownWithoutDelegation
+ coEvery { authMetadataAPI.getAuthMetadata() } returns authMetadataForTest2
+
+ // When
+ task.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
+
+ // Then
+ homeServerCapabilitiesEntity.authenticationIssuer shouldBeEqualTo "https://test2"
+ homeServerCapabilitiesEntity.externalAccountManagementUrl shouldBeEqualTo "https://test2/account"
+ homeServerCapabilitiesEntity.externalAccountManagementSupportedActions shouldBeEqualTo "org.matrix.device_delete,org.matrix.profile"
+ }
+
+ @Test
+ fun `given well-known and auth metadata, when execute, treats auth metadata as authoritative`() = runTest {
+ // Given
+ coEvery { getWellknownTask.execute(any()) } returns wellKnownWithDelegationToTest1
+ coEvery { authMetadataAPI.getAuthMetadata() } returns authMetadataForTest2WithoutAccountManagement
+
+ // When
+ task.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
+
+ // Then
+ homeServerCapabilitiesEntity.authenticationIssuer shouldBeEqualTo "https://test2"
+ homeServerCapabilitiesEntity.externalAccountManagementUrl shouldBeEqualTo null
+ homeServerCapabilitiesEntity.externalAccountManagementSupportedActions shouldBeEqualTo null
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
index 1c9fb718585..3ec7cd64aee 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
@@ -337,12 +337,12 @@ class DevicesViewModel @AssistedInject constructor(
private fun handleDelete(action: DevicesAction.Delete) {
val deviceId = action.deviceId
- val accountManagementUrl = session.homeServerCapabilitiesService().getHomeServerCapabilities().externalAccountManagementUrl
- if (accountManagementUrl != null) {
+ val externalLogoutDeviceUrl = session.homeServerCapabilitiesService().getHomeServerCapabilities().getLogoutDeviceURL(deviceId)
+ if (externalLogoutDeviceUrl != null) {
// Open external browser to delete this session
_viewEvents.post(
DevicesViewEvents.OpenBrowser(
- url = accountManagementUrl.removeSuffix("/") + "?action=session_end&device_id=$deviceId"
+ url = externalLogoutDeviceUrl
)
)
} else {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
index 0d90f4f2c98..56a13a77c0f 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
@@ -129,12 +129,12 @@ class SessionOverviewFragment :
}
private fun confirmSignoutOtherSession() = withState(viewModel) { state ->
- if (state.externalAccountManagementUrl != null) {
+ if (state.externalDeleteDeviceUrl != null) {
// Manage in external account manager
openUrlInChromeCustomTab(
requireContext(),
null,
- state.externalAccountManagementUrl.removeSuffix("/") + "?action=session_end&device_id=${state.deviceId}"
+ state.externalDeleteDeviceUrl,
)
} else {
activity?.let {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
index 8a34f6ce6ed..54cdde22e15 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
@@ -66,16 +66,16 @@ class SessionOverviewViewModel @AssistedInject constructor(
observeNotificationsStatus(initialState.deviceId)
refreshIpAddressVisibility()
observePreferences()
- initExternalAccountManagementUrl()
+ initExternalDeleteDeviceUrl()
}
- private fun initExternalAccountManagementUrl() {
+ private fun initExternalDeleteDeviceUrl() {
setState {
copy(
- externalAccountManagementUrl = activeSessionHolder.getSafeActiveSession()
+ externalDeleteDeviceUrl = activeSessionHolder.getSafeActiveSession()
?.homeServerCapabilitiesService()
?.getHomeServerCapabilities()
- ?.externalAccountManagementUrl
+ ?.getLogoutDeviceURL(deviceId)
)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt
index f795d29969f..da1847a397b 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt
@@ -20,7 +20,7 @@ data class SessionOverviewViewState(
val isLoading: Boolean = false,
val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED,
val isShowingIpAddress: Boolean = false,
- val externalAccountManagementUrl: String? = null,
+ val externalDeleteDeviceUrl: String? = null,
) : MavericksState {
constructor(args: SessionOverviewArgs) : this(
deviceId = args.deviceId
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt
index a66fc201c70..ff30a37895f 100644
--- a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt
+++ b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt
@@ -21,6 +21,7 @@ fun aHomeServerCapabilities(
roomVersions: RoomVersionCapabilities? = null,
canRemotelyTogglePushNotificationsOfDevices: Boolean = true,
externalAccountManagementUrl: String? = null,
+ externalAccountManagementSupportedActions: List? = null,
) = HomeServerCapabilities(
canChangePassword = canChangePassword,
canChangeDisplayName = canChangeDisplayName,
@@ -32,4 +33,5 @@ fun aHomeServerCapabilities(
roomVersions = roomVersions,
canRemotelyTogglePushNotificationsOfDevices = canRemotelyTogglePushNotificationsOfDevices,
externalAccountManagementUrl = externalAccountManagementUrl,
+ externalAccountManagementSupportedActions = externalAccountManagementSupportedActions,
)