diff --git a/changelog.d/6367.feature b/changelog.d/6367.feature new file mode 100644 index 00000000000..5d4b46ca991 --- /dev/null +++ b/changelog.d/6367.feature @@ -0,0 +1 @@ +Adds MSC3824 OIDC-awareness when talking to an OIDC-enabled homeservers diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 17fbadd776c..2058d13d1dc 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -1063,6 +1063,9 @@ Discovery Manage your discovery settings. + Account + Your account details are managed separately at %1$s. + Analytics Send analytics data diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index e490311b916..c6fab7762f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -44,7 +44,7 @@ interface AuthenticationService { /** * Get a SSO url. */ - fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String? /** * Get the sign in or sign up fallback URL. 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 new file mode 100644 index 00000000000..db2dd870d52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/SSOAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 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 + +/** + * See https://github.com/matrix-org/matrix-spec-proposals/pull/3824 + */ +enum class SSOAction { + LOGIN, + REGISTER; +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DelegatedAuthConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DelegatedAuthConfig.kt new file mode 100644 index 00000000000..b57472ab7c9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DelegatedAuthConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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 + +/** + * https://github.com/matrix-org/matrix-spec-proposals/pull/2965 + *
+ * {
+ *     "issuer": "https://id.server.org",
+ *     "account": "https://id.server.org/my-account",
+ * }
+ * 
+ * . + */ + +@JsonClass(generateAdapter = true) +data class DelegatedAuthConfig( + @Json(name = "issuer") + val issuer: String, + + @Json(name = "account") + val accountManagementUrl: String, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index 5de83033e1c..5d737b716bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -22,6 +22,7 @@ data class LoginFlowResult( val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String, val isOutdatedHomeserver: Boolean, + val hasOidcCompatibilityFlow: Boolean, val isLogoutDevicesSupported: Boolean, val isLoginWithQrSupported: Boolean, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt index 10c7d513923..95488bd6827 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt @@ -54,5 +54,11 @@ data class WellKnown( val identityServer: WellKnownBaseConfig? = null, @Json(name = "m.integrations") - val integrations: JsonDict? = null + val integrations: JsonDict? = null, + + /** + * For delegation of auth via OIDC as per [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965). + */ + @Json(name = "org.matrix.msc2965.authentication") + val unstableDelegatedAuthConfig: DelegatedAuthConfig? = null, ) 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 96e52469c3c..4968df775ab 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 @@ -80,6 +80,11 @@ data class HomeServerCapabilities( * True if the home server supports event redaction with relations. */ var canRedactEventWithRelations: Boolean = false, + + /** + * External account management url for use with MSC3824 delegated OIDC, provided in Wellknown. + */ + val externalAccountManagementUrl: String? = null, ) { enum class RoomCapabilitySupport { 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 d9c2afcb408..d1dd0238bad 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 @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult @@ -88,7 +89,7 @@ internal class DefaultAuthenticationService @Inject constructor( return getLoginFlow(homeServerConnectionConfig) } - override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { + override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String? { val homeServerUrlBase = getHomeServerUrlBase() ?: return null return buildString { @@ -103,6 +104,9 @@ internal class DefaultAuthenticationService @Inject constructor( // But https://github.com/matrix-org/synapse/issues/5755 appendParamToUrl("device_id", it) } + + // unstable MSC3824 action param + appendParamToUrl("org.matrix.msc3824.action", action.toString()) } } @@ -292,12 +296,18 @@ internal class DefaultAuthenticationService @Inject constructor( val loginFlowResponse = executeRequest(null) { 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.delegatedOidcCompatibilty == true } + val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows + return LoginFlowResult( - supportedLoginTypes = loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, - ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, + supportedLoginTypes = flows.orEmpty().mapNotNull { it.type }, + ssoIdentityProviders = flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl = homeServerUrl, isOutdatedHomeserver = !versions.isSupportedBySdk(), + hasOidcCompatibilityFlow = oidcCompatibilityFlow != null, isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(), isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(), ) 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 df10e110d13..971407388c3 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 @@ -43,6 +43,13 @@ internal data class LoginFlow( * See MSC #2858 */ @Json(name = "identity_providers") - val ssoIdentityProvider: List? = null + val ssoIdentityProvider: List? = null, + /** + * Whether this login flow is preferred for OIDC-aware clients. + * + * See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824) + */ + @Json(name = "org.matrix.msc3824.delegated_oidc_compatibility") + val delegatedOidcCompatibilty: Boolean? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 45bcd792c2b..c5ececcddb3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -67,6 +67,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -75,7 +76,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 50L, + schemaVersion = 51L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -135,5 +136,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 48) MigrateSessionTo048(realm).perform() if (oldVersion < 49) MigrateSessionTo049(realm).perform() if (oldVersion < 50) MigrateSessionTo050(realm).perform() + if (oldVersion < 51) MigrateSessionTo051(realm).perform() } } 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 83f3e87d05c..1c7a0591a18 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 @@ -48,6 +48,7 @@ internal object HomeServerCapabilitiesMapper { canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, canRedactEventWithRelations = entity.canRedactEventWithRelations, + externalAccountManagementUrl = entity.externalAccountManagementUrl, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt new file mode 100644 index 00000000000..3bed97073d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo051(realm: DynamicRealm) : RealmMigrator(realm, 51) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.EXTERNAL_ACCOUNT_MANAGEMENT_URL, String::class.java) + ?.forceRefreshOfHomeServerCapabilities() + } +} 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 9acdcde7e53..35a5c654de8 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 @@ -35,6 +35,7 @@ internal open class HomeServerCapabilitiesEntity( var canUseThreadReadReceiptsAndNotifications: Boolean = false, var canRemotelyTogglePushNotificationsOfDevices: Boolean = false, var canRedactEventWithRelations: Boolean = false, + var externalAccountManagementUrl: String? = null, ) : RealmObject() { companion object 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 5a6107821dd..ec12695ecdc 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 @@ -167,6 +167,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( Timber.v("Extracted integration config : $config") realm.insertOrUpdate(config) } + homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl } homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time } diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt index ddab65d9810..77bcaed3fb0 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -24,6 +24,7 @@ import androidx.browser.customtabs.CustomTabsSession import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.withState import im.vector.app.core.utils.openUrlInChromeCustomTab +import org.matrix.android.sdk.api.auth.SSOAction abstract class AbstractSSOLoginFragment : AbstractLoginFragment() { @@ -90,7 +91,8 @@ abstract class AbstractSSOLoginFragment : AbstractLoginFragmen loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - providerId = null + providerId = null, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { prefetchUrl(it) } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt index 5947fa0cb5c..984c3694e88 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt @@ -69,7 +69,8 @@ sealed class LoginAction : VectorViewModelAction { data class SetupSsoForSessionRecovery( val homeServerUrl: String, val deviceId: String, - val ssoIdentityProviders: List? + val ssoIdentityProviders: List?, + val hasOidcCompatibilityFlow: Boolean ) : LoginAction() data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction() diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 4e4df5d1aab..9dfae7ff5fa 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -46,6 +46,7 @@ import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.onboarding.AuthenticationDescription import im.vector.app.features.pin.UnlockedActivity import im.vector.lib.core.utils.compat.getParcelableExtraCompat +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms @@ -300,6 +301,7 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null, + action = SSOAction.LOGIN )?.let { ssoUrl -> openUrlInChromeCustomTab(this, null, ssoUrl) } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index d61044d1015..01d15db3d18 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -200,11 +201,12 @@ class LoginFragment : if (state.loginMode is LoginMode.SsoAndPassword) { views.loginSocialLoginContainer.isVisible = true - views.loginSocialLoginButtons.render(state.loginMode.ssoState, ssoMode(state)) { provider -> + views.loginSocialLoginButtons.render(state.loginMode, ssoMode(state)) { provider -> loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - providerId = provider?.id + providerId = provider?.id, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt index 944b159441a..384108e6a89 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt @@ -23,8 +23,8 @@ sealed class LoginMode : Parcelable { // Parcelable because persist state @Parcelize object Unknown : LoginMode() @Parcelize object Password : LoginMode() - @Parcelize data class Sso(val ssoState: SsoState) : LoginMode() - @Parcelize data class SsoAndPassword(val ssoState: SsoState) : LoginMode() + @Parcelize data class Sso(val ssoState: SsoState, val hasOidcCompatibilityFlow: Boolean) : LoginMode() + @Parcelize data class SsoAndPassword(val ssoState: SsoState, val hasOidcCompatibilityFlow: Boolean) : LoginMode() @Parcelize object Unsupported : LoginMode() } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt index dbcf674847c..5ed806622f2 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt @@ -27,6 +27,7 @@ import im.vector.app.R import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding import im.vector.app.features.login.SocialLoginButtonsView.Mode +import org.matrix.android.sdk.api.auth.SSOAction /** * In this screen, the user is asked to sign up or to sign in to the homeserver. @@ -75,11 +76,12 @@ class LoginSignUpSignInSelectionFragment : when (state.loginMode) { is LoginMode.SsoAndPassword -> { views.loginSignupSigninSignInSocialLoginContainer.isVisible = true - views.loginSignupSigninSocialLoginButtons.render(state.loginMode.ssoState(), Mode.MODE_CONTINUE) { provider -> + views.loginSignupSigninSocialLoginButtons.render(state.loginMode, Mode.MODE_CONTINUE) { provider -> loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - providerId = provider?.id + providerId = provider?.id, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } @@ -111,7 +113,8 @@ class LoginSignUpSignInSelectionFragment : loginViewModel.getSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - providerId = null + providerId = null, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } else { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 8d520628f05..4da022d4bb8 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.login.LoginWizard @@ -224,7 +225,7 @@ class LoginViewModel @AssistedInject constructor( setState { copy( signMode = SignMode.SignIn, - loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState()), + loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState(), action.hasOidcCompatibilityFlow), homeServerUrlFromUser = action.homeServerUrl, homeServerUrl = action.homeServerUrl, deviceId = action.deviceId @@ -817,8 +818,11 @@ class LoginViewModel @AssistedInject constructor( val loginMode = when { // SSO login is taken first data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState()) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState()) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword( + data.ssoIdentityProviders.toSsoState(), + data.hasOidcCompatibilityFlow + ) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState(), data.hasOidcCompatibilityFlow) data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password else -> LoginMode.Unsupported } @@ -845,8 +849,8 @@ class LoginViewModel @AssistedInject constructor( return loginConfig?.homeServerUrl } - fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { - return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String? { + return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId, action) } fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index 816050420e3..4ac98d6f2dc 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -56,6 +56,14 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: } } + var hasOidcCompatibilityFlow: Boolean = false + set(value) { + if (value != hasOidcCompatibilityFlow) { + field = value + update() + } + } + var listener: InteractionListener? = null private fun update() { @@ -70,7 +78,8 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: transformationMethod = null textAlignment = View.TEXT_ALIGNMENT_CENTER }.let { - it.text = getButtonTitle(context.getString(R.string.login_social_sso)) + it.text = if (hasOidcCompatibilityFlow) context.getString(R.string.login_continue) + else getButtonTitle(context.getString(R.string.login_social_sso)) it.textAlignment = View.TEXT_ALIGNMENT_CENTER it.setOnClickListener { listener?.onProviderSelected(null) @@ -160,11 +169,14 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: } } -fun SocialLoginButtonsView.render(state: SsoState, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) { +fun SocialLoginButtonsView.render(loginMode: LoginMode, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) { this.mode = mode + val state = loginMode.ssoState() this.ssoIdentityProviders = when (state) { SsoState.Fallback -> null is SsoState.IdentityProviders -> state.providers.sorted() } + this.hasOidcCompatibilityFlow = (loginMode is LoginMode.Sso && loginMode.hasOidcCompatibilityFlow) || + (loginMode is LoginMode.SsoAndPassword && loginMode.hasOidcCompatibilityFlow) this.listener = SocialLoginButtonsView.InteractionListener { listener(it) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 04487c6198f..9c64b5ed872 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -55,6 +55,7 @@ import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.login.LoginWizard @@ -841,12 +842,12 @@ class OnboardingViewModel @AssistedInject constructor( fun getDefaultHomeserverUrl() = defaultHomeserverUrl - fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?): String? { + fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?, action: SSOAction): String? { setState { val authDescription = AuthenticationDescription.Register(provider.toAuthenticationType()) copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription)) } - return authenticationService.getSsoUrl(redirectUrl, deviceId, provider?.id) + return authenticationService.getSsoUrl(redirectUrl, deviceId, provider?.id, action) } fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt index ea0d940952f..58b28ac4e41 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -75,6 +75,7 @@ data class SelectedHomeserverState( val upstreamUrl: String? = null, val preferredLoginMode: LoginMode = LoginMode.Unknown, val supportedLoginTypes: List = emptyList(), + val hasOidcCompatibilityFlow: Boolean = false, val isLogoutDevicesSupported: Boolean = false, val isLoginWithQrSupported: Boolean = false, ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt index 9b8f0a1cc44..14a3a9bfd07 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt @@ -47,13 +47,17 @@ class StartAuthenticationFlowUseCase @Inject constructor( upstreamUrl = authFlow.homeServerUrl, preferredLoginMode = preferredLoginMode, supportedLoginTypes = authFlow.supportedLoginTypes, + hasOidcCompatibilityFlow = authFlow.hasOidcCompatibilityFlow, isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported, - isLoginWithQrSupported = authFlow.isLoginWithQrSupported, + isLoginWithQrSupported = authFlow.isLoginWithQrSupported ) private fun LoginFlowResult.findPreferredLoginMode() = when { - supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders.toSsoState()) - supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState()) + supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword( + ssoIdentityProviders.toSsoState(), + hasOidcCompatibilityFlow + ) + supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState(), hasOidcCompatibilityFlow) supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password else -> LoginMode.Unsupported } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt index b1352db0cc2..211c6303200 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/AbstractSSOFtueAuthFragment.kt @@ -27,6 +27,8 @@ import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.hasSso import im.vector.app.features.login.ssoState +import im.vector.app.features.onboarding.OnboardingFlow +import org.matrix.android.sdk.api.auth.SSOAction abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthFragment() { @@ -93,7 +95,8 @@ abstract class AbstractSSOFtueAuthFragment : AbstractFtueAuthF viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - provider = null + provider = null, + action = if (state.onboardingFlow == OnboardingFlow.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { prefetchUrl(it) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index 2c016f7077c..69090172ea7 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -41,7 +41,6 @@ import im.vector.app.features.VectorFeatures import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView -import im.vector.app.features.login.SsoState import im.vector.app.features.login.qr.QrCodeLoginArgs import im.vector.app.features.login.qr.QrCodeLoginType import im.vector.app.features.login.render @@ -50,6 +49,7 @@ import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn +import org.matrix.android.sdk.api.auth.SSOAction import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject @@ -153,11 +153,11 @@ class FtueAuthCombinedLoginFragment : when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.SsoAndPassword -> { showUsernamePassword() - renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) + renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode) } is LoginMode.Sso -> { hideUsernamePassword() - renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) + renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode) } else -> { showUsernamePassword() @@ -166,14 +166,15 @@ class FtueAuthCombinedLoginFragment : } } - private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) { + private fun renderSsoProviders(deviceId: String?, loginMode: LoginMode) { views.ssoGroup.isVisible = true views.ssoButtonsHeader.isVisible = isUsernameAndPasswordVisible() - views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> + views.ssoButtons.render(loginMode, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = deviceId, - provider = id + provider = id, + action = SSOAction.LOGIN )?.let { openInCustomTab(it) } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index 66668f53039..83a9a9c00bb 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -45,7 +45,6 @@ import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding import im.vector.app.features.login.LoginMode import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView -import im.vector.app.features.login.SsoState import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction @@ -53,6 +52,7 @@ import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername @@ -207,18 +207,19 @@ class FtueAuthCombinedRegisterFragment : } when (state.selectedHomeserver.preferredLoginMode) { - is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) + is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode) else -> hideSsoProviders() } } - private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) { + private fun renderSsoProviders(deviceId: String?, loginMode: LoginMode) { views.ssoGroup.isVisible = true - views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider -> + views.ssoButtons.render(loginMode, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = deviceId, - provider = provider + provider = provider, + action = SSOAction.REGISTER )?.let { openInCustomTab(it) } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt index 3fd8df6bb9d..8cf8dffaf38 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLoginFragment.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidUsername import org.matrix.android.sdk.api.failure.isLoginEmailUnknown @@ -215,11 +216,12 @@ class FtueAuthLoginFragment : if (state.selectedHomeserver.preferredLoginMode is LoginMode.SsoAndPassword) { views.loginSocialLoginContainer.isVisible = true - views.loginSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, ssoMode(state)) { provider -> + views.loginSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode, ssoMode(state)) { provider -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - provider = provider + provider = provider, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt index b2f2eeb1675..cd387f5f6b6 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSignUpSignInSelectionFragment.kt @@ -34,7 +34,9 @@ import im.vector.app.features.login.SignMode import im.vector.app.features.login.SocialLoginButtonsView.Mode import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingFlow import im.vector.app.features.onboarding.OnboardingViewState +import org.matrix.android.sdk.api.auth.SSOAction /** * In this screen, the user is asked to sign up or to sign in to the homeserver. @@ -81,11 +83,12 @@ class FtueAuthSignUpSignInSelectionFragment : when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.SsoAndPassword -> { views.loginSignupSigninSignInSocialLoginContainer.isVisible = true - views.loginSignupSigninSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, Mode.MODE_CONTINUE) { provider -> + views.loginSignupSigninSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode, Mode.MODE_CONTINUE) { provider -> viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - provider = provider + provider = provider, + action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } @@ -110,7 +113,8 @@ class FtueAuthSignUpSignInSelectionFragment : when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.Sso -> { // change to only one button that is sign in with sso - views.loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) + views.loginSignupSigninSubmit.text = + if (state.selectedHomeserver.hasOidcCompatibilityFlow) getString(R.string.login_continue) else getString(R.string.login_signin_sso) views.loginSignupSigninSignIn.isVisible = false } else -> { @@ -125,7 +129,8 @@ class FtueAuthSignUpSignInSelectionFragment : viewModel.fetchSsoUrl( redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, - provider = null + provider = null, + action = if (state.onboardingFlow == OnboardingFlow.SignUp) SSOAction.REGISTER else SSOAction.LOGIN ) ?.let { openInCustomTab(it) } } else { @@ -144,5 +149,7 @@ class FtueAuthSignUpSignInSelectionFragment : override fun updateWithState(state: OnboardingViewState) { render(state) setupButtons(state) + // if talking to OIDC enabled homeserver in compatibility mode then immediately start SSO + if (state.selectedHomeserver.hasOidcCompatibilityFlow) submit() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index d405d458c08..f513a5ef012 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -58,6 +58,7 @@ class VectorPreferences @Inject constructor( const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" const val SETTINGS_DISCOVERY_PREFERENCE_KEY = "SETTINGS_DISCOVERY_PREFERENCE_KEY" const val SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY = "SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY" + const val SETTINGS_EXTERNAL_ACCOUNT_MANAGEMENT_KEY = "SETTINGS_EXTERNAL_ACCOUNT_MANAGEMENT_KEY" const val SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY" const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY" diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index c90f55d22a7..1fa07329f30 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -48,6 +48,7 @@ import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.getSizeOfFiles +import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.core.utils.toast import im.vector.app.databinding.DialogChangePasswordBinding import im.vector.app.features.MainActivity @@ -71,6 +72,7 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerS import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap import java.io.File +import java.net.URL import java.util.UUID import javax.inject.Inject @@ -101,6 +103,9 @@ class VectorSettingsGeneralFragment : private val mIdentityServerPreference by lazy { findPreference(VectorPreferences.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY)!! } + private val mExternalAccountManagementPreference by lazy { + findPreference(VectorPreferences.SETTINGS_EXTERNAL_ACCOUNT_MANAGEMENT_KEY)!! + } // Local contacts private val mContactSettingsCategory by lazy { @@ -204,6 +209,24 @@ class VectorSettingsGeneralFragment : mIdentityServerPreference.onPreferenceClickListener = openDiscoveryScreenPreferenceClickListener + // External account management URL for delegated OIDC auth + // Hide the preference if no URL is given by server + if (homeServerCapabilities.externalAccountManagementUrl != null) { + mExternalAccountManagementPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + openUrlInExternalBrowser(it.context, homeServerCapabilities.externalAccountManagementUrl) + true + } + + val hostname = URL(homeServerCapabilities.externalAccountManagementUrl).host + + mExternalAccountManagementPreference.summary = requireContext().getString( + R.string.settings_external_account_management, + hostname + ) + } else { + mExternalAccountManagementPreference.isVisible = false + } + // Advanced settings // user account diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index 47670b486a5..af337b5be2f 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -66,7 +66,8 @@ class SoftLogoutFragment : LoginAction.SetupSsoForSessionRecovery( softLogoutViewState.homeServerUrl, softLogoutViewState.deviceId, - mode.ssoState.providersOrNull() + mode.ssoState.providersOrNull(), + mode.hasOidcCompatibilityFlow ) ) } @@ -75,7 +76,8 @@ class SoftLogoutFragment : LoginAction.SetupSsoForSessionRecovery( softLogoutViewState.homeServerUrl, softLogoutViewState.deviceId, - mode.ssoState.providersOrNull() + mode.ssoState.providersOrNull(), + mode.hasOidcCompatibilityFlow ) ) } @@ -85,7 +87,8 @@ class SoftLogoutFragment : LoginAction.SetupSsoForSessionRecovery( softLogoutViewState.homeServerUrl, softLogoutViewState.deviceId, - null + null, + false ) ) } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index 117c298878a..af84231af89 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -118,8 +118,11 @@ class SoftLogoutViewModel @AssistedInject constructor( val loginMode = when { // SSO login is taken first data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState()) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState()) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword( + data.ssoIdentityProviders.toSsoState(), + data.hasOidcCompatibilityFlow + ) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState(), data.hasOidcCompatibilityFlow) data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password else -> LoginMode.Unsupported } diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index 30ef4337dc8..8600dbc1bee 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -33,6 +33,12 @@ android:summary="@string/settings_discovery_manage" android:title="@string/settings_discovery_category" /> + + - \ No newline at end of file + diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index c570a75d99e..bbca6e2aa67 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -57,6 +57,7 @@ import org.amshove.kluent.shouldBeEqualTo import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.registration.Stage @@ -1088,9 +1089,9 @@ class OnboardingViewModelTest { fun `given returns Sso url, when fetching Sso url, then updates authentication state and returns supplied Sso url`() = runTest { val test = viewModel.test() val provider = SsoIdentityProvider(id = "provider_id", null, null, null) - fakeAuthenticationService.givenSsoUrl(A_REDIRECT_URI, A_DEVICE_ID, provider.id, result = A_SSO_URL) + fakeAuthenticationService.givenSsoUrl(A_REDIRECT_URI, A_DEVICE_ID, provider.id, SSOAction.LOGIN, result = A_SSO_URL) - val result = viewModel.fetchSsoUrl(A_REDIRECT_URI, A_DEVICE_ID, provider) + val result = viewModel.fetchSsoUrl(A_REDIRECT_URI, A_DEVICE_ID, provider, SSOAction.LOGIN) result shouldBeEqualTo A_SSO_URL test diff --git a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt index 9be22d7ea99..93bfc045ded 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt @@ -70,7 +70,7 @@ class StartAuthenticationFlowUseCaseTest { result shouldBeEqualTo expectedResult( supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, - preferredLoginMode = LoginMode.SsoAndPassword(SsoState.Fallback), + preferredLoginMode = LoginMode.SsoAndPassword(SsoState.Fallback, false), ) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) } @@ -84,7 +84,7 @@ class StartAuthenticationFlowUseCaseTest { result shouldBeEqualTo expectedResult( supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, - preferredLoginMode = LoginMode.SsoAndPassword(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS)), + preferredLoginMode = LoginMode.SsoAndPassword(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS), false), ) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) } @@ -98,7 +98,7 @@ class StartAuthenticationFlowUseCaseTest { result shouldBeEqualTo expectedResult( supportedLoginTypes = SSO_LOGIN_TYPE, - preferredLoginMode = LoginMode.Sso(SsoState.Fallback), + preferredLoginMode = LoginMode.Sso(SsoState.Fallback, false), ) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) } @@ -112,7 +112,7 @@ class StartAuthenticationFlowUseCaseTest { result shouldBeEqualTo expectedResult( supportedLoginTypes = SSO_LOGIN_TYPE, - preferredLoginMode = LoginMode.Sso(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS)), + preferredLoginMode = LoginMode.Sso(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS), false), ) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) } @@ -131,31 +131,50 @@ class StartAuthenticationFlowUseCaseTest { verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) } + @Test + fun `given identity providers and login supports SSO with OIDC compatibility then prefers Sso for compatibility`() = runTest { + val loginResult = aLoginResult(supportedLoginTypes = SSO_LOGIN_TYPE, ssoProviders = SSO_IDENTITY_PROVIDERS, hasOidcCompatibilityFlow = true) + fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult) + + val result = useCase.execute(A_HOMESERVER_CONFIG) + + result shouldBeEqualTo expectedResult( + supportedLoginTypes = SSO_LOGIN_TYPE, + preferredLoginMode = LoginMode.Sso(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS), hasOidcCompatibilityFlow = true), + hasOidcCompatibilityFlow = true + ) + verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) + } + private fun aLoginResult( supportedLoginTypes: List, - ssoProviders: List = FALLBACK_SSO_IDENTITY_PROVIDERS + ssoProviders: List = FALLBACK_SSO_IDENTITY_PROVIDERS, + hasOidcCompatibilityFlow: Boolean = false ) = LoginFlowResult( supportedLoginTypes = supportedLoginTypes, ssoIdentityProviders = ssoProviders, isLoginAndRegistrationSupported = true, homeServerUrl = A_DECLARED_HOMESERVER_URL, isOutdatedHomeserver = false, + hasOidcCompatibilityFlow = hasOidcCompatibilityFlow, isLogoutDevicesSupported = false, - isLoginWithQrSupported = false + isLoginWithQrSupported = false, ) private fun expectedResult( isHomeserverOutdated: Boolean = false, preferredLoginMode: LoginMode = LoginMode.Unsupported, supportedLoginTypes: List = emptyList(), - homeserverSourceUrl: String = A_HOMESERVER_CONFIG.homeServerUri.toString() + homeserverSourceUrl: String = A_HOMESERVER_CONFIG.homeServerUri.toString(), + hasOidcCompatibilityFlow: Boolean = false ) = StartAuthenticationResult( isHomeserverOutdated, SelectedHomeserverState( userFacingUrl = homeserverSourceUrl, upstreamUrl = A_DECLARED_HOMESERVER_URL, preferredLoginMode = preferredLoginMode, - supportedLoginTypes = supportedLoginTypes + supportedLoginTypes = supportedLoginTypes, + hasOidcCompatibilityFlow = hasOidcCompatibilityFlow ) ) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index af539131693..fa446537c88 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -22,6 +22,7 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.SSOAction import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult import org.matrix.android.sdk.api.auth.login.LoginWizard @@ -78,7 +79,7 @@ class FakeAuthenticationService : AuthenticationService by mockk() { coVerify { cancelPendingLoginOrRegistration() } } - fun givenSsoUrl(redirectUri: String, deviceId: String, providerId: String, result: String) { - coEvery { getSsoUrl(redirectUri, deviceId, providerId) } returns result + fun givenSsoUrl(redirectUri: String, deviceId: String, providerId: String, action: SSOAction, result: String) { + coEvery { getSsoUrl(redirectUri, deviceId, providerId, action) } returns result } } 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 c9f32c2cf20..f8e8a03706c 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 @@ -29,6 +29,7 @@ fun aHomeServerCapabilities( defaultIdentityServerUrl: String? = null, roomVersions: RoomVersionCapabilities? = null, canRemotelyTogglePushNotificationsOfDevices: Boolean = true, + externalAccountManagementUrl: String? = null, ) = HomeServerCapabilities( canChangePassword = canChangePassword, canChangeDisplayName = canChangeDisplayName, @@ -39,4 +40,5 @@ fun aHomeServerCapabilities( defaultIdentityServerUrl = defaultIdentityServerUrl, roomVersions = roomVersions, canRemotelyTogglePushNotificationsOfDevices = canRemotelyTogglePushNotificationsOfDevices, + externalAccountManagementUrl = externalAccountManagementUrl, )