Merge pull request #7920 from vector-im/hughns/msc3824-oidc-aware

Implementation of MSC3824 to make the client OIDC-aware
This commit is contained in:
Benoit Marty 2023-02-09 16:57:54 +01:00 committed by GitHub
commit 4cc2daa5ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 303 additions and 64 deletions

1
changelog.d/6367.feature Normal file
View file

@ -0,0 +1 @@
Adds MSC3824 OIDC-awareness when talking to an OIDC-enabled homeservers

View file

@ -1063,6 +1063,9 @@
<string name="settings_discovery_category">Discovery</string> <string name="settings_discovery_category">Discovery</string>
<string name="settings_discovery_manage">Manage your discovery settings.</string> <string name="settings_discovery_manage">Manage your discovery settings.</string>
<string name="settings_external_account_management_title">Account</string>
<string name="settings_external_account_management">Your account details are managed separately at %1$s.</string>
<!-- analytics --> <!-- analytics -->
<string name="settings_analytics">Analytics</string> <string name="settings_analytics">Analytics</string>
<string name="settings_opt_in_of_analytics">Send analytics data</string> <string name="settings_opt_in_of_analytics">Send analytics data</string>

View file

@ -44,7 +44,7 @@ interface AuthenticationService {
/** /**
* Get a SSO url. * 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. * Get the sign in or sign up fallback URL.

View file

@ -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;
}

View file

@ -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
* <pre>
* {
* "issuer": "https://id.server.org",
* "account": "https://id.server.org/my-account",
* }
* </pre>
* .
*/
@JsonClass(generateAdapter = true)
data class DelegatedAuthConfig(
@Json(name = "issuer")
val issuer: String,
@Json(name = "account")
val accountManagementUrl: String,
)

View file

@ -22,6 +22,7 @@ data class LoginFlowResult(
val isLoginAndRegistrationSupported: Boolean, val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String, val homeServerUrl: String,
val isOutdatedHomeserver: Boolean, val isOutdatedHomeserver: Boolean,
val hasOidcCompatibilityFlow: Boolean,
val isLogoutDevicesSupported: Boolean, val isLogoutDevicesSupported: Boolean,
val isLoginWithQrSupported: Boolean, val isLoginWithQrSupported: Boolean,
) )

View file

@ -54,5 +54,11 @@ data class WellKnown(
val identityServer: WellKnownBaseConfig? = null, val identityServer: WellKnownBaseConfig? = null,
@Json(name = "m.integrations") @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,
) )

View file

@ -80,6 +80,11 @@ data class HomeServerCapabilities(
* True if the home server supports event redaction with relations. * True if the home server supports event redaction with relations.
*/ */
var canRedactEventWithRelations: Boolean = false, var canRedactEventWithRelations: Boolean = false,
/**
* External account management url for use with MSC3824 delegated OIDC, provided in Wellknown.
*/
val externalAccountManagementUrl: String? = null,
) { ) {
enum class RoomCapabilitySupport { enum class RoomCapabilitySupport {

View file

@ -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.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.LoginType 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.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig 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.data.LoginFlowResult
@ -88,7 +89,7 @@ internal class DefaultAuthenticationService @Inject constructor(
return getLoginFlow(homeServerConnectionConfig) 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 val homeServerUrlBase = getHomeServerUrlBase() ?: return null
return buildString { return buildString {
@ -103,6 +104,9 @@ internal class DefaultAuthenticationService @Inject constructor(
// But https://github.com/matrix-org/synapse/issues/5755 // But https://github.com/matrix-org/synapse/issues/5755
appendParamToUrl("device_id", it) 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) { val loginFlowResponse = executeRequest(null) {
authAPI.getLoginFlows() 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( return LoginFlowResult(
supportedLoginTypes = loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, supportedLoginTypes = flows.orEmpty().mapNotNull { it.type },
ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, ssoIdentityProviders = flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(), isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl = homeServerUrl, homeServerUrl = homeServerUrl,
isOutdatedHomeserver = !versions.isSupportedBySdk(), isOutdatedHomeserver = !versions.isSupportedBySdk(),
hasOidcCompatibilityFlow = oidcCompatibilityFlow != null,
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(), isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(),
isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(), isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(),
) )

View file

@ -43,6 +43,13 @@ internal data class LoginFlow(
* See MSC #2858 * See MSC #2858
*/ */
@Json(name = "identity_providers") @Json(name = "identity_providers")
val ssoIdentityProvider: List<SsoIdentityProvider>? = null val ssoIdentityProvider: List<SsoIdentityProvider>? = 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
) )

View file

@ -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.MigrateSessionTo048
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049 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.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.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -75,7 +76,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 50L, schemaVersion = 51L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -135,5 +136,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 48) MigrateSessionTo048(realm).perform() if (oldVersion < 48) MigrateSessionTo048(realm).perform()
if (oldVersion < 49) MigrateSessionTo049(realm).perform() if (oldVersion < 49) MigrateSessionTo049(realm).perform()
if (oldVersion < 50) MigrateSessionTo050(realm).perform() if (oldVersion < 50) MigrateSessionTo050(realm).perform()
if (oldVersion < 51) MigrateSessionTo051(realm).perform()
} }
} }

View file

@ -48,6 +48,7 @@ internal object HomeServerCapabilitiesMapper {
canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications,
canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
canRedactEventWithRelations = entity.canRedactEventWithRelations, canRedactEventWithRelations = entity.canRedactEventWithRelations,
externalAccountManagementUrl = entity.externalAccountManagementUrl,
) )
} }

View file

@ -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()
}
}

View file

@ -35,6 +35,7 @@ internal open class HomeServerCapabilitiesEntity(
var canUseThreadReadReceiptsAndNotifications: Boolean = false, var canUseThreadReadReceiptsAndNotifications: Boolean = false,
var canRemotelyTogglePushNotificationsOfDevices: Boolean = false, var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
var canRedactEventWithRelations: Boolean = false, var canRedactEventWithRelations: Boolean = false,
var externalAccountManagementUrl: String? = null,
) : RealmObject() { ) : RealmObject() {
companion object companion object

View file

@ -167,6 +167,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
Timber.v("Extracted integration config : $config") Timber.v("Extracted integration config : $config")
realm.insertOrUpdate(config) realm.insertOrUpdate(config)
} }
homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl
} }
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
} }

View file

@ -24,6 +24,7 @@ import androidx.browser.customtabs.CustomTabsSession
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.core.utils.openUrlInChromeCustomTab
import org.matrix.android.sdk.api.auth.SSOAction
abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragment<VB>() { abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragment<VB>() {
@ -90,7 +91,8 @@ abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragmen
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = null providerId = null,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
) )
?.let { prefetchUrl(it) } ?.let { prefetchUrl(it) }
} }

View file

@ -69,7 +69,8 @@ sealed class LoginAction : VectorViewModelAction {
data class SetupSsoForSessionRecovery( data class SetupSsoForSessionRecovery(
val homeServerUrl: String, val homeServerUrl: String,
val deviceId: String, val deviceId: String,
val ssoIdentityProviders: List<SsoIdentityProvider>? val ssoIdentityProviders: List<SsoIdentityProvider>?,
val hasOidcCompatibilityFlow: Boolean
) : LoginAction() ) : LoginAction()
data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction() data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction()

View file

@ -46,6 +46,7 @@ import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.onboarding.AuthenticationDescription import im.vector.app.features.onboarding.AuthenticationDescription
import im.vector.app.features.pin.UnlockedActivity import im.vector.app.features.pin.UnlockedActivity
import im.vector.lib.core.utils.compat.getParcelableExtraCompat 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.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms
@ -300,6 +301,7 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = null, providerId = null,
action = SSOAction.LOGIN
)?.let { ssoUrl -> )?.let { ssoUrl ->
openUrlInChromeCustomTab(this, null, ssoUrl) openUrlInChromeCustomTab(this, null, ssoUrl)
} }

View file

@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach 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.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidPassword
@ -200,11 +201,12 @@ class LoginFragment :
if (state.loginMode is LoginMode.SsoAndPassword) { if (state.loginMode is LoginMode.SsoAndPassword) {
views.loginSocialLoginContainer.isVisible = true views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.render(state.loginMode.ssoState, ssoMode(state)) { provider -> views.loginSocialLoginButtons.render(state.loginMode, ssoMode(state)) { provider ->
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = provider?.id providerId = provider?.id,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
) )
?.let { openInCustomTab(it) } ?.let { openInCustomTab(it) }
} }

View file

@ -23,8 +23,8 @@ sealed class LoginMode : Parcelable { // Parcelable because persist state
@Parcelize object Unknown : LoginMode() @Parcelize object Unknown : LoginMode()
@Parcelize object Password : LoginMode() @Parcelize object Password : LoginMode()
@Parcelize data class Sso(val ssoState: SsoState) : LoginMode() @Parcelize data class Sso(val ssoState: SsoState, val hasOidcCompatibilityFlow: Boolean) : LoginMode()
@Parcelize data class SsoAndPassword(val ssoState: SsoState) : LoginMode() @Parcelize data class SsoAndPassword(val ssoState: SsoState, val hasOidcCompatibilityFlow: Boolean) : LoginMode()
@Parcelize object Unsupported : LoginMode() @Parcelize object Unsupported : LoginMode()
} }

View file

@ -27,6 +27,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding
import im.vector.app.features.login.SocialLoginButtonsView.Mode 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. * 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) { when (state.loginMode) {
is LoginMode.SsoAndPassword -> { is LoginMode.SsoAndPassword -> {
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true 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( loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = provider?.id providerId = provider?.id,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
) )
?.let { openInCustomTab(it) } ?.let { openInCustomTab(it) }
} }
@ -111,7 +113,8 @@ class LoginSignUpSignInSelectionFragment :
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = null providerId = null,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
) )
?.let { openInCustomTab(it) } ?.let { openInCustomTab(it) }
} else { } else {

View file

@ -39,6 +39,7 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService 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.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.login.LoginWizard
@ -224,7 +225,7 @@ class LoginViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
signMode = SignMode.SignIn, signMode = SignMode.SignIn,
loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState()), loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState(), action.hasOidcCompatibilityFlow),
homeServerUrlFromUser = action.homeServerUrl, homeServerUrlFromUser = action.homeServerUrl,
homeServerUrl = action.homeServerUrl, homeServerUrl = action.homeServerUrl,
deviceId = action.deviceId deviceId = action.deviceId
@ -817,8 +818,11 @@ class LoginViewModel @AssistedInject constructor(
val loginMode = when { val loginMode = when {
// SSO login is taken first // SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && data.supportedLoginTypes.contains(LoginFlowTypes.SSO) &&
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState()) data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState()) data.ssoIdentityProviders.toSsoState(),
data.hasOidcCompatibilityFlow
)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState(), data.hasOidcCompatibilityFlow)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported else -> LoginMode.Unsupported
} }
@ -845,8 +849,8 @@ class LoginViewModel @AssistedInject constructor(
return loginConfig?.homeServerUrl return loginConfig?.homeServerUrl
} }
fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String? {
return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId, action)
} }
fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? {

View file

@ -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 var listener: InteractionListener? = null
private fun update() { private fun update() {
@ -70,7 +78,8 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
transformationMethod = null transformationMethod = null
textAlignment = View.TEXT_ALIGNMENT_CENTER textAlignment = View.TEXT_ALIGNMENT_CENTER
}.let { }.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.textAlignment = View.TEXT_ALIGNMENT_CENTER
it.setOnClickListener { it.setOnClickListener {
listener?.onProviderSelected(null) 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 this.mode = mode
val state = loginMode.ssoState()
this.ssoIdentityProviders = when (state) { this.ssoIdentityProviders = when (state) {
SsoState.Fallback -> null SsoState.Fallback -> null
is SsoState.IdentityProviders -> state.providers.sorted() 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) } this.listener = SocialLoginButtonsView.InteractionListener { listener(it) }
} }

View file

@ -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.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService 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.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.login.LoginWizard
@ -841,12 +842,12 @@ class OnboardingViewModel @AssistedInject constructor(
fun getDefaultHomeserverUrl() = defaultHomeserverUrl fun getDefaultHomeserverUrl() = defaultHomeserverUrl
fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?): String? { fun fetchSsoUrl(redirectUrl: String, deviceId: String?, provider: SsoIdentityProvider?, action: SSOAction): String? {
setState { setState {
val authDescription = AuthenticationDescription.Register(provider.toAuthenticationType()) val authDescription = AuthenticationDescription.Register(provider.toAuthenticationType())
copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription)) 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? { fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? {

View file

@ -75,6 +75,7 @@ data class SelectedHomeserverState(
val upstreamUrl: String? = null, val upstreamUrl: String? = null,
val preferredLoginMode: LoginMode = LoginMode.Unknown, val preferredLoginMode: LoginMode = LoginMode.Unknown,
val supportedLoginTypes: List<String> = emptyList(), val supportedLoginTypes: List<String> = emptyList(),
val hasOidcCompatibilityFlow: Boolean = false,
val isLogoutDevicesSupported: Boolean = false, val isLogoutDevicesSupported: Boolean = false,
val isLoginWithQrSupported: Boolean = false, val isLoginWithQrSupported: Boolean = false,
) : Parcelable ) : Parcelable

View file

@ -47,13 +47,17 @@ class StartAuthenticationFlowUseCase @Inject constructor(
upstreamUrl = authFlow.homeServerUrl, upstreamUrl = authFlow.homeServerUrl,
preferredLoginMode = preferredLoginMode, preferredLoginMode = preferredLoginMode,
supportedLoginTypes = authFlow.supportedLoginTypes, supportedLoginTypes = authFlow.supportedLoginTypes,
hasOidcCompatibilityFlow = authFlow.hasOidcCompatibilityFlow,
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported, isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported,
isLoginWithQrSupported = authFlow.isLoginWithQrSupported, isLoginWithQrSupported = authFlow.isLoginWithQrSupported
) )
private fun LoginFlowResult.findPreferredLoginMode() = when { private fun LoginFlowResult.findPreferredLoginMode() = when {
supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders.toSsoState()) supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState()) ssoIdentityProviders.toSsoState(),
hasOidcCompatibilityFlow
)
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState(), hasOidcCompatibilityFlow)
supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported else -> LoginMode.Unsupported
} }

View file

@ -27,6 +27,8 @@ import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.hasSso import im.vector.app.features.login.hasSso
import im.vector.app.features.login.ssoState 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<VB : ViewBinding> : AbstractFtueAuthFragment<VB>() { abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : AbstractFtueAuthFragment<VB>() {
@ -93,7 +95,8 @@ abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : AbstractFtueAuthF
viewModel.fetchSsoUrl( viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
provider = null provider = null,
action = if (state.onboardingFlow == OnboardingFlow.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
) )
?.let { prefetchUrl(it) } ?.let { prefetchUrl(it) }
} }

View file

@ -41,7 +41,6 @@ import im.vector.app.features.VectorFeatures
import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView 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.QrCodeLoginArgs
import im.vector.app.features.login.qr.QrCodeLoginType import im.vector.app.features.login.qr.QrCodeLoginType
import im.vector.app.features.login.render 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 im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import org.matrix.android.sdk.api.auth.SSOAction
import reactivecircus.flowbinding.android.widget.textChanges import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject import javax.inject.Inject
@ -153,11 +153,11 @@ class FtueAuthCombinedLoginFragment :
when (state.selectedHomeserver.preferredLoginMode) { when (state.selectedHomeserver.preferredLoginMode) {
is LoginMode.SsoAndPassword -> { is LoginMode.SsoAndPassword -> {
showUsernamePassword() showUsernamePassword()
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode)
} }
is LoginMode.Sso -> { is LoginMode.Sso -> {
hideUsernamePassword() hideUsernamePassword()
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState) renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode)
} }
else -> { else -> {
showUsernamePassword() 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.ssoGroup.isVisible = true
views.ssoButtonsHeader.isVisible = isUsernameAndPasswordVisible() views.ssoButtonsHeader.isVisible = isUsernameAndPasswordVisible()
views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id -> views.ssoButtons.render(loginMode, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
viewModel.fetchSsoUrl( viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId, deviceId = deviceId,
provider = id provider = id,
action = SSOAction.LOGIN
)?.let { openInCustomTab(it) } )?.let { openInCustomTab(it) }
} }
} }

View file

@ -45,7 +45,6 @@ import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView 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.login.render
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction 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 im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn 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.isHomeserverUnavailable
import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isInvalidUsername import org.matrix.android.sdk.api.failure.isInvalidUsername
@ -207,18 +207,19 @@ class FtueAuthCombinedRegisterFragment :
} }
when (state.selectedHomeserver.preferredLoginMode) { 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() else -> hideSsoProviders()
} }
} }
private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) { private fun renderSsoProviders(deviceId: String?, loginMode: LoginMode) {
views.ssoGroup.isVisible = true views.ssoGroup.isVisible = true
views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider -> views.ssoButtons.render(loginMode, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider ->
viewModel.fetchSsoUrl( viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId, deviceId = deviceId,
provider = provider provider = provider,
action = SSOAction.REGISTER
)?.let { openInCustomTab(it) } )?.let { openInCustomTab(it) }
} }
} }

View file

@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach 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.isInvalidPassword
import org.matrix.android.sdk.api.failure.isInvalidUsername import org.matrix.android.sdk.api.failure.isInvalidUsername
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
@ -215,11 +216,12 @@ class FtueAuthLoginFragment :
if (state.selectedHomeserver.preferredLoginMode is LoginMode.SsoAndPassword) { if (state.selectedHomeserver.preferredLoginMode is LoginMode.SsoAndPassword) {
views.loginSocialLoginContainer.isVisible = true 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( viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
provider = provider provider = provider,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
) )
?.let { openInCustomTab(it) } ?.let { openInCustomTab(it) }
} }

View file

@ -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.SocialLoginButtonsView.Mode
import im.vector.app.features.login.render import im.vector.app.features.login.render
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingFlow
import im.vector.app.features.onboarding.OnboardingViewState 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. * 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) { when (state.selectedHomeserver.preferredLoginMode) {
is LoginMode.SsoAndPassword -> { is LoginMode.SsoAndPassword -> {
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true 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( viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
provider = provider provider = provider,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
) )
?.let { openInCustomTab(it) } ?.let { openInCustomTab(it) }
} }
@ -110,7 +113,8 @@ class FtueAuthSignUpSignInSelectionFragment :
when (state.selectedHomeserver.preferredLoginMode) { when (state.selectedHomeserver.preferredLoginMode) {
is LoginMode.Sso -> { is LoginMode.Sso -> {
// change to only one button that is sign in with 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 views.loginSignupSigninSignIn.isVisible = false
} }
else -> { else -> {
@ -125,7 +129,8 @@ class FtueAuthSignUpSignInSelectionFragment :
viewModel.fetchSsoUrl( viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
provider = null provider = null,
action = if (state.onboardingFlow == OnboardingFlow.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
) )
?.let { openInCustomTab(it) } ?.let { openInCustomTab(it) }
} else { } else {
@ -144,5 +149,7 @@ class FtueAuthSignUpSignInSelectionFragment :
override fun updateWithState(state: OnboardingViewState) { override fun updateWithState(state: OnboardingViewState) {
render(state) render(state)
setupButtons(state) setupButtons(state)
// if talking to OIDC enabled homeserver in compatibility mode then immediately start SSO
if (state.selectedHomeserver.hasOidcCompatibilityFlow) submit()
} }
} }

View file

@ -58,6 +58,7 @@ class VectorPreferences @Inject constructor(
const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" 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_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_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_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY"
const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY" const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY"

View file

@ -48,6 +48,7 @@ import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.TextUtils
import im.vector.app.core.utils.getSizeOfFiles 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.core.utils.toast
import im.vector.app.databinding.DialogChangePasswordBinding import im.vector.app.databinding.DialogChangePasswordBinding
import im.vector.app.features.MainActivity 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.flow
import org.matrix.android.sdk.flow.unwrap import org.matrix.android.sdk.flow.unwrap
import java.io.File import java.io.File
import java.net.URL
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -101,6 +103,9 @@ class VectorSettingsGeneralFragment :
private val mIdentityServerPreference by lazy { private val mIdentityServerPreference by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY)!!
} }
private val mExternalAccountManagementPreference by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_EXTERNAL_ACCOUNT_MANAGEMENT_KEY)!!
}
// Local contacts // Local contacts
private val mContactSettingsCategory by lazy { private val mContactSettingsCategory by lazy {
@ -204,6 +209,24 @@ class VectorSettingsGeneralFragment :
mIdentityServerPreference.onPreferenceClickListener = openDiscoveryScreenPreferenceClickListener 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 // Advanced settings
// user account // user account

View file

@ -66,7 +66,8 @@ class SoftLogoutFragment :
LoginAction.SetupSsoForSessionRecovery( LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl, softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId, softLogoutViewState.deviceId,
mode.ssoState.providersOrNull() mode.ssoState.providersOrNull(),
mode.hasOidcCompatibilityFlow
) )
) )
} }
@ -75,7 +76,8 @@ class SoftLogoutFragment :
LoginAction.SetupSsoForSessionRecovery( LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl, softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId, softLogoutViewState.deviceId,
mode.ssoState.providersOrNull() mode.ssoState.providersOrNull(),
mode.hasOidcCompatibilityFlow
) )
) )
} }
@ -85,7 +87,8 @@ class SoftLogoutFragment :
LoginAction.SetupSsoForSessionRecovery( LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl, softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId, softLogoutViewState.deviceId,
null null,
false
) )
) )
} }

View file

@ -118,8 +118,11 @@ class SoftLogoutViewModel @AssistedInject constructor(
val loginMode = when { val loginMode = when {
// SSO login is taken first // SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && data.supportedLoginTypes.contains(LoginFlowTypes.SSO) &&
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState()) data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState()) data.ssoIdentityProviders.toSsoState(),
data.hasOidcCompatibilityFlow
)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState(), data.hasOidcCompatibilityFlow)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported else -> LoginMode.Unsupported
} }

View file

@ -33,6 +33,12 @@
android:summary="@string/settings_discovery_manage" android:summary="@string/settings_discovery_manage"
android:title="@string/settings_discovery_category" /> android:title="@string/settings_discovery_category" />
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_EXTERNAL_ACCOUNT_MANAGEMENT_KEY"
android:persistent="false"
android:summary="@string/settings_external_account_management"
android:title="@string/settings_external_account_management_title" />
</im.vector.app.core.preference.VectorPreferenceCategory> </im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory <im.vector.app.core.preference.VectorPreferenceCategory
@ -117,4 +123,4 @@
</im.vector.app.core.preference.VectorPreferenceCategory> </im.vector.app.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View file

@ -57,6 +57,7 @@ import org.amshove.kluent.shouldBeEqualTo
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test 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.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.auth.registration.Stage 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 { fun `given returns Sso url, when fetching Sso url, then updates authentication state and returns supplied Sso url`() = runTest {
val test = viewModel.test() val test = viewModel.test()
val provider = SsoIdentityProvider(id = "provider_id", null, null, null) 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 result shouldBeEqualTo A_SSO_URL
test test

View file

@ -70,7 +70,7 @@ class StartAuthenticationFlowUseCaseTest {
result shouldBeEqualTo expectedResult( result shouldBeEqualTo expectedResult(
supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES,
preferredLoginMode = LoginMode.SsoAndPassword(SsoState.Fallback), preferredLoginMode = LoginMode.SsoAndPassword(SsoState.Fallback, false),
) )
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
} }
@ -84,7 +84,7 @@ class StartAuthenticationFlowUseCaseTest {
result shouldBeEqualTo expectedResult( result shouldBeEqualTo expectedResult(
supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, 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) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
} }
@ -98,7 +98,7 @@ class StartAuthenticationFlowUseCaseTest {
result shouldBeEqualTo expectedResult( result shouldBeEqualTo expectedResult(
supportedLoginTypes = SSO_LOGIN_TYPE, supportedLoginTypes = SSO_LOGIN_TYPE,
preferredLoginMode = LoginMode.Sso(SsoState.Fallback), preferredLoginMode = LoginMode.Sso(SsoState.Fallback, false),
) )
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
} }
@ -112,7 +112,7 @@ class StartAuthenticationFlowUseCaseTest {
result shouldBeEqualTo expectedResult( result shouldBeEqualTo expectedResult(
supportedLoginTypes = SSO_LOGIN_TYPE, 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) verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
} }
@ -131,31 +131,50 @@ class StartAuthenticationFlowUseCaseTest {
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG) 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( private fun aLoginResult(
supportedLoginTypes: List<String>, supportedLoginTypes: List<String>,
ssoProviders: List<SsoIdentityProvider> = FALLBACK_SSO_IDENTITY_PROVIDERS ssoProviders: List<SsoIdentityProvider> = FALLBACK_SSO_IDENTITY_PROVIDERS,
hasOidcCompatibilityFlow: Boolean = false
) = LoginFlowResult( ) = LoginFlowResult(
supportedLoginTypes = supportedLoginTypes, supportedLoginTypes = supportedLoginTypes,
ssoIdentityProviders = ssoProviders, ssoIdentityProviders = ssoProviders,
isLoginAndRegistrationSupported = true, isLoginAndRegistrationSupported = true,
homeServerUrl = A_DECLARED_HOMESERVER_URL, homeServerUrl = A_DECLARED_HOMESERVER_URL,
isOutdatedHomeserver = false, isOutdatedHomeserver = false,
hasOidcCompatibilityFlow = hasOidcCompatibilityFlow,
isLogoutDevicesSupported = false, isLogoutDevicesSupported = false,
isLoginWithQrSupported = false isLoginWithQrSupported = false,
) )
private fun expectedResult( private fun expectedResult(
isHomeserverOutdated: Boolean = false, isHomeserverOutdated: Boolean = false,
preferredLoginMode: LoginMode = LoginMode.Unsupported, preferredLoginMode: LoginMode = LoginMode.Unsupported,
supportedLoginTypes: List<String> = emptyList(), supportedLoginTypes: List<String> = emptyList(),
homeserverSourceUrl: String = A_HOMESERVER_CONFIG.homeServerUri.toString() homeserverSourceUrl: String = A_HOMESERVER_CONFIG.homeServerUri.toString(),
hasOidcCompatibilityFlow: Boolean = false
) = StartAuthenticationResult( ) = StartAuthenticationResult(
isHomeserverOutdated, isHomeserverOutdated,
SelectedHomeserverState( SelectedHomeserverState(
userFacingUrl = homeserverSourceUrl, userFacingUrl = homeserverSourceUrl,
upstreamUrl = A_DECLARED_HOMESERVER_URL, upstreamUrl = A_DECLARED_HOMESERVER_URL,
preferredLoginMode = preferredLoginMode, preferredLoginMode = preferredLoginMode,
supportedLoginTypes = supportedLoginTypes supportedLoginTypes = supportedLoginTypes,
hasOidcCompatibilityFlow = hasOidcCompatibilityFlow
) )
) )

View file

@ -22,6 +22,7 @@ import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.auth.AuthenticationService 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.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.login.LoginWizard
@ -78,7 +79,7 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
coVerify { cancelPendingLoginOrRegistration() } coVerify { cancelPendingLoginOrRegistration() }
} }
fun givenSsoUrl(redirectUri: String, deviceId: String, providerId: String, result: String) { fun givenSsoUrl(redirectUri: String, deviceId: String, providerId: String, action: SSOAction, result: String) {
coEvery { getSsoUrl(redirectUri, deviceId, providerId) } returns result coEvery { getSsoUrl(redirectUri, deviceId, providerId, action) } returns result
} }
} }

View file

@ -29,6 +29,7 @@ fun aHomeServerCapabilities(
defaultIdentityServerUrl: String? = null, defaultIdentityServerUrl: String? = null,
roomVersions: RoomVersionCapabilities? = null, roomVersions: RoomVersionCapabilities? = null,
canRemotelyTogglePushNotificationsOfDevices: Boolean = true, canRemotelyTogglePushNotificationsOfDevices: Boolean = true,
externalAccountManagementUrl: String? = null,
) = HomeServerCapabilities( ) = HomeServerCapabilities(
canChangePassword = canChangePassword, canChangePassword = canChangePassword,
canChangeDisplayName = canChangeDisplayName, canChangeDisplayName = canChangeDisplayName,
@ -39,4 +40,5 @@ fun aHomeServerCapabilities(
defaultIdentityServerUrl = defaultIdentityServerUrl, defaultIdentityServerUrl = defaultIdentityServerUrl,
roomVersions = roomVersions, roomVersions = roomVersions,
canRemotelyTogglePushNotificationsOfDevices = canRemotelyTogglePushNotificationsOfDevices, canRemotelyTogglePushNotificationsOfDevices = canRemotelyTogglePushNotificationsOfDevices,
externalAccountManagementUrl = externalAccountManagementUrl,
) )