Identity: refresh pepper, logout feature and other improvements

This commit is contained in:
Benoit Marty 2020-05-07 22:51:18 +02:00
parent 426171508e
commit 6e43e9b51c
7 changed files with 127 additions and 25 deletions

View file

@ -39,7 +39,10 @@ data class MatrixError(
// For M_LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
// For M_UNKNOWN_TOKEN
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
@Json(name = "soft_logout") val isSoftLogout: Boolean = false,
// For M_INVALID_PEPPER
// {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
@Json(name = "lookup_pepper") val newLookupPepper: String? = null
) {
companion object {
@ -131,6 +134,9 @@ data class MatrixError(
const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED"
// For identity service
const val M_INVALID_PEPPER = "M_INVALID_PEPPER"
// Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user"
}

View file

@ -16,6 +16,8 @@
package im.vector.matrix.android.internal.session.identity
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.session.identity.FoundThreePid
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.ThreePid
@ -26,6 +28,7 @@ import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStor
import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse
import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Params
import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Response
import im.vector.matrix.android.internal.session.profile.ThirdPartyIdentifier
import im.vector.matrix.android.internal.task.Task
import java.util.Locale
import javax.inject.Inject
@ -47,16 +50,14 @@ internal class DefaultBulkLookupTask @Inject constructor(
val pepper = entity.hashLookupPepper
val hashDetailResponse = if (pepper == null) {
// We need to fetch the hash details first
executeRequest<IdentityHashDetailResponse>(null) {
apiCall = identityAPI.hashDetails()
}
.also { identityServiceStore.setHashDetails(it) }
fetchAndStoreHashDetails(identityAPI)
} else {
IdentityHashDetailResponse(pepper, entity.hashLookupAlgorithm.toList())
}
if (hashDetailResponse.algorithms.contains("sha256").not()) {
// TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment do not do it
// TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
// Also, what we have in cache could be outdated, the identity server maybe now supports sha256
throw IdentityServiceError.BulkLookupSha256NotSupported
}
@ -69,20 +70,59 @@ internal class DefaultBulkLookupTask @Inject constructor(
}
}
val identityLookUpV2Response = executeRequest<IdentityLookUpV2Response>(null) {
apiCall = identityAPI.bulkLookupV2(IdentityLookUpV2Params(
hashedAddresses,
"sha256",
hashDetailResponse.pepper
))
}
// TODO Catch invalid hash pepper and retry
val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true)
// Convert back to List<FoundThreePid>
return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response)
}
private suspend fun lookUpInternal(identityAPI: IdentityAPI,
hashedAddresses: List<String>,
hashDetailResponse: IdentityHashDetailResponse,
canRetry: Boolean): IdentityLookUpV2Response {
return try {
executeRequest(null) {
apiCall = identityAPI.bulkLookupV2(IdentityLookUpV2Params(
hashedAddresses,
"sha256",
hashDetailResponse.pepper
))
}
} catch (failure: Throwable) {
// Catch invalid hash pepper and retry
if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) {
// This is not documented, by the error can contain the new pepper!
if (!failure.error.newLookupPepper.isNullOrEmpty()) {
// Store it and use it right now
hashDetailResponse.copy(pepper = failure.error.newLookupPepper)
.also { identityServiceStore.setHashDetails(it) }
.let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) }
} else {
// Retrieve the new hash details
val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI)
if (hashDetailResponse.algorithms.contains("sha256").not()) {
// TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
// Also, what we have in cache is maybe outdated, the identity server maybe now support sha256
throw IdentityServiceError.BulkLookupSha256NotSupported
}
lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */)
}
} else {
// Other error
throw failure
}
}
}
private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse {
return executeRequest<IdentityHashDetailResponse>(null) {
apiCall = identityAPI.hashDetails()
}
.also { identityServiceStore.setHashDetails(it) }
}
private fun handleSuccess(threePids: List<ThreePid>, hashedAddresses: List<String>, identityLookUpV2Response: IdentityLookUpV2Response): List<FoundThreePid> {
return identityLookUpV2Response.mappings.keys.map { hashedAddress ->
FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpV2Response.mappings[hashedAddress] ?: error(""))
@ -91,8 +131,8 @@ internal class DefaultBulkLookupTask @Inject constructor(
private fun ThreePid.toMedium(): String {
return when (this) {
is ThreePid.Email -> "email"
is ThreePid.Msisdn -> "msisdn"
is ThreePid.Email -> ThirdPartyIdentifier.MEDIUM_EMAIL
is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN
}
}
}

View file

@ -42,7 +42,6 @@ import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
@ -57,7 +56,7 @@ internal class DefaultIdentityService @Inject constructor(
private val getOpenIdTokenTask: GetOpenIdTokenTask,
private val bulkLookupTask: BulkLookupTask,
private val identityRegisterTask: IdentityRegisterTask,
private val taskExecutor: TaskExecutor,
private val identityDisconnectTask: IdentityDisconnectTask,
@Unauthenticated
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
@AuthenticatedIdentity
@ -147,12 +146,19 @@ internal class DefaultIdentityService @Inject constructor(
// Nothing to do
Timber.d("Same URL, nothing to do")
null -> {
// TODO Disconnect previous one if any
// Disconnect previous one if any
identityServiceStore.get()?.let {
if (it.identityServerUrl != null && it.token != null) {
// Disconnect, ignoring any error
runCatching {
identityDisconnectTask.execute(Unit)
}
}
}
identityServiceStore.setUrl(null)
updateAccountData(null)
}
else -> {
// TODO: check first that it is a valid identity server
// Try to get a token
getIdentityServerToken(urlCandidate)
@ -179,7 +185,7 @@ internal class DefaultIdentityService @Inject constructor(
}
}
private suspend fun lookUpInternal(firstTime: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
ensureToken()
return try {
@ -187,12 +193,12 @@ internal class DefaultIdentityService @Inject constructor(
} catch (throwable: Throwable) {
// Refresh token?
when {
throwable.isInvalidToken() && firstTime -> {
throwable.isInvalidToken() && canRetry -> {
identityServiceStore.setToken(null)
lookUpInternal(false, threePids)
}
throwable.isTermsNotSigned() -> throw IdentityServiceError.TermsNotSignedException
else -> throw throwable
throwable.isTermsNotSigned() -> throw IdentityServiceError.TermsNotSignedException
else -> throw throwable
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.identity
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface IdentityDisconnectTask : Task<Unit, Unit>
internal class DefaultIdentityDisconnectTask @Inject constructor(
private val identityApiProvider: IdentityApiProvider
) : IdentityDisconnectTask {
override suspend fun execute(params: Unit) {
val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured
executeRequest<Unit>(null) {
apiCall = identityAPI.logout()
}
}
}

View file

@ -70,4 +70,7 @@ internal abstract class IdentityModule {
@Binds
abstract fun bindBulkLookupTask(task: DefaultBulkLookupTask): BulkLookupTask
@Binds
abstract fun bindIdentityDisconnectTask(task: DefaultIdentityDisconnectTask): IdentityDisconnectTask
}

View file

@ -130,6 +130,10 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
// Unfortunately, this is not supported in lib v7
// it.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
it.setOnPreferenceClickListener {
notImplemented()
true
}
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
notImplemented()

View file

@ -28,6 +28,7 @@ import im.vector.riotx.core.epoxy.onClick
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import kotlinx.android.synthetic.main.fragment_review_terms.*
@ -76,6 +77,11 @@ class ReviewTermsFragment @Inject constructor(
super.onDestroyView()
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.terms_of_service)
}
override fun invalidate() = withState(reviewTermsViewModel) { state ->
when (state.termsList) {
is Loading -> {