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