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:
commit
4cc2daa5ef
40 changed files with 303 additions and 64 deletions
1
changelog.d/6367.feature
Normal file
1
changelog.d/6367.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Adds MSC3824 OIDC-awareness when talking to an OIDC-enabled homeservers
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue