1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Compare commits

...

21 Commits

Author SHA1 Message Date
David Langley
1d5b19e963 fix copyright 2022-02-22 16:29:39 +00:00
David Langley
de24d73b95 Merge branch 'develop' of github.com:vector-im/element-android into feature/dla/refresh_tokens 2022-02-22 16:09:18 +00:00
David Langley
a34a7be78d lint 2022-02-22 16:01:46 +00:00
David Langley
dc43479699 Merge branch 'develop' of github.com:vector-im/element-android into feature/dla/refresh_tokens 2022-02-21 12:03:00 +00:00
David Langley
ed4606b113 Add refreshTokenAuth api to public interface. Cleanup. 2022-02-18 16:19:29 +00:00
David Langley
87201cc972 Specify user is logged out when clearing data from SoftLgoutActivity. 2022-02-09 15:55:12 +00:00
David Langley
883174852d Remove error testing code. 2022-02-09 14:04:15 +00:00
David Langley
b03d1a6270 Add explicit conversion to Long. 2022-02-09 09:43:51 +00:00
David Langley
4b1a3dc42e Cleanup and only use interceptor retry logic for homeserver auth not identity. 2022-02-09 09:39:14 +00:00
David Langley
b3ccbdf66c lint 2022-02-08 17:46:02 +00:00
David Langley
d29a0dcbba lint 2022-02-08 16:53:58 +00:00
David Langley
9fa5722158 Rename refreshToken boolean to enableRefreshTokenAuth 2022-02-08 16:47:39 +00:00
David Langley
e6f6883e8e Add analytics for UnauthenticatedError and BuildConfig 2022-02-08 14:53:11 +00:00
David Langley
d086a32959 Peek error body in interceptor to not consume it. Fix refresh catch case. enable refresh tokens. 2022-02-07 23:41:10 +00:00
David Langley
74669da5e5 Merge branch 'develop' of github.com:vector-im/element-android into feature/dla/refresh_tokens 2022-02-07 12:21:55 +00:00
David Langley
c0e4edfb63 Address comments, add MatrixConfig, add global error handling. 2022-02-07 12:20:45 +00:00
David Langley
66511200ee Merge branch 'develop' of github.com:vector-im/element-android into feature/dla/refresh_tokens 2022-02-03 12:13:29 +00:00
David Langley
cb31c2c83a Moar lint. Local lint not working so well :/ 2022-01-21 14:01:59 +00:00
David Langley
e3ad5ff308 lint 2022-01-21 13:46:36 +00:00
David Langley
c8d2682e0d Fix copyright and add changelog 2022-01-21 13:30:50 +00:00
David Langley
3d25d72f2a Add happy path implementation of refresh
- Hardcodes refresh_tokens on for the moment
- Refresh tokens requested during login
- Refresh tokens and access token expiry parsed in login response
- Access Token provider synchronises refresh so that we have a leader refresh requests while other requests are blocked and recieve the new access token when the leader succeeds.
- Adds refresh wizard to authentication service for unauthed refresh api.
2022-01-21 13:01:31 +00:00
38 changed files with 477 additions and 72 deletions

1
changelog.d/4943.feature Normal file
View File

@@ -0,0 +1 @@
Support for refresh token auth.

View File

@@ -53,15 +53,19 @@ interface AuthenticationService {
* Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
*
* See [LoginWizard] for more details
* @param enableRefreshTokenAuth whether to enable refresh token based auth (in beta, hence default is false for now)
* instead of long lived access tokens.
*/
fun getLoginWizard(): LoginWizard
fun getLoginWizard(enableRefreshTokenAuth: Boolean = false): LoginWizard
/**
* Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first.
*
* See [RegistrationWizard] for more details.
* @param enableRefreshTokenAuth whether to enable refresh token based auth (in beta, hence default is false for now)
* instead of long lived access tokens.
*/
fun getRegistrationWizard(): RegistrationWizard
fun getRegistrationWizard(enableRefreshTokenAuth: Boolean = false): RegistrationWizard
/**
* True when login and password has been sent with success to the homeserver
@@ -109,11 +113,16 @@ interface AuthenticationService {
* @param matrixId the matrixId of the user
* @param password the password of the account
* @param initialDeviceName the initial device name
* @param enableRefreshTokenAuth whether to enable refresh token based auth (in beta, hence default is false for now)
* instead of long lived access tokens.
* @param deviceId the device id, optional. If not provided or null, the server will generate one.
*/
suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
matrixId: String,
password: String,
initialDeviceName: String,
deviceId: String? = null): Session
suspend fun directAuthentication(
homeServerConnectionConfig: HomeServerConnectionConfig,
matrixId: String,
password: String,
initialDeviceName: String,
enableRefreshTokenAuth: Boolean = false,
deviceId: String? = null,
): Session
}

View File

@@ -36,6 +36,15 @@ data class Credentials(
* An access token for the account. This access token can then be used to authorize other requests.
*/
@Json(name = "access_token") val accessToken: String,
/**
* The interval in milliseconds that the access token will expire in.
*/
@Json(name = "expires_in_ms") val expiresInMs: Long?,
/**
* The timestamp that indicates when the access token will expire.
* It is estimated using `expiresInMs` when the SDK retrieves it from the homeserver.
*/
@Json(name = "expiry_ts") val expiryTs: Long?,
/**
* Not documented
*/

View File

@@ -24,6 +24,11 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException
import javax.net.ssl.HttpsURLConnection
fun Throwable.isTokenUnknownError() =
this is Failure.ServerError &&
httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED &&
error.code == MatrixError.M_UNKNOWN_TOKEN
fun Throwable.is401() =
this is Failure.ServerError &&
httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */

View File

@@ -20,7 +20,7 @@ import org.matrix.android.sdk.internal.network.ssl.Fingerprint
// This class will be sent to the bus
sealed class GlobalError {
data class InvalidToken(val softLogout: Boolean) : GlobalError()
data class InvalidToken(val softLogout: Boolean, val refreshTokenAuth: Boolean, val errorCode: String, val errorReason: String) : GlobalError()
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
data class CertificateError(val fingerprint: Fingerprint) : GlobalError()
object ExpiredAccount : GlobalError()

View File

@@ -27,7 +27,7 @@ interface SignOutService {
* Ask the homeserver for a new access token.
* The same deviceId will be used
*/
suspend fun signInAgain(password: String)
suspend fun signInAgain(password: String, enableRefreshTokenAuth: Boolean)
/**
* Update the session with credentials received after SSO

View File

@@ -60,7 +60,7 @@ internal class DefaultAuthenticationService @Inject constructor(
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore,
private val getWellknownTask: GetWellknownTask,
private val directLoginTask: DirectLoginTask
private val directLoginTask: DirectLoginTask,
) : AuthenticationService {
private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData()
@@ -308,14 +308,15 @@ internal class DefaultAuthenticationService @Inject constructor(
)
}
override fun getRegistrationWizard(): RegistrationWizard {
override fun getRegistrationWizard(enableRefreshTokenAuth: Boolean): RegistrationWizard {
return currentRegistrationWizard
?: let {
pendingSessionData?.homeServerConnectionConfig?.let {
DefaultRegistrationWizard(
buildAuthAPI(it),
sessionCreator,
pendingSessionStore
pendingSessionStore,
enableRefreshTokenAuth
).also {
currentRegistrationWizard = it
}
@@ -326,14 +327,15 @@ internal class DefaultAuthenticationService @Inject constructor(
override val isRegistrationStarted: Boolean
get() = currentRegistrationWizard?.isRegistrationStarted == true
override fun getLoginWizard(): LoginWizard {
override fun getLoginWizard(enableRefreshTokenAuth: Boolean): LoginWizard {
return currentLoginWizard
?: let {
pendingSessionData?.homeServerConnectionConfig?.let {
DefaultLoginWizard(
buildAuthAPI(it),
sessionCreator,
pendingSessionStore
pendingSessionStore,
enableRefreshTokenAuth
).also {
currentLoginWizard = it
}
@@ -385,17 +387,21 @@ internal class DefaultAuthenticationService @Inject constructor(
)
}
override suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
matrixId: String,
password: String,
initialDeviceName: String,
deviceId: String?): Session {
override suspend fun directAuthentication(
homeServerConnectionConfig: HomeServerConnectionConfig,
matrixId: String,
password: String,
initialDeviceName: String,
enableRefreshTokenAuth: Boolean,
deviceId: String?,
): Session {
return directLoginTask.execute(DirectLoginTask.Params(
homeServerConnectionConfig = homeServerConnectionConfig,
userId = matrixId,
password = password,
deviceName = initialDeviceName,
deviceId = deviceId
deviceId = deviceId,
refreshToken = enableRefreshTokenAuth
))
}

View File

@@ -0,0 +1,37 @@
/*
* 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.internal.auth
import org.matrix.android.sdk.internal.auth.data.RefreshParams
import org.matrix.android.sdk.internal.auth.data.RefreshResult
import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
/**
* The refresh token REST API.
*/
internal interface RefreshTokenAPI {
/**
* Refresh the access token given a refresh token.
* @param refreshParams the refresh parameters
*/
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_V1 + "refresh")
suspend fun refreshToken(@Body refreshParams: RefreshParams): RefreshResult
}

View File

@@ -18,4 +18,5 @@ package org.matrix.android.sdk.internal.auth.data
internal interface LoginParams {
val type: String
val enableRefreshTokenAuth: Boolean
}

View File

@@ -30,6 +30,7 @@ internal data class PasswordLoginParams(
@Json(name = "identifier") val identifier: Map<String, String>,
@Json(name = "password") val password: String,
@Json(name = "type") override val type: String,
@Json(name = "refresh_token") override val enableRefreshTokenAuth: Boolean,
@Json(name = "initial_device_display_name") val deviceDisplayName: String?,
@Json(name = "device_id") val deviceId: String?) : LoginParams {
@@ -50,7 +51,8 @@ internal data class PasswordLoginParams(
fun userIdentifier(user: String,
password: String,
deviceDisplayName: String?,
deviceId: String?): PasswordLoginParams {
deviceId: String?,
enableRefreshTokenAuth: Boolean): PasswordLoginParams {
return PasswordLoginParams(
identifier = mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER,
@@ -58,6 +60,7 @@ internal data class PasswordLoginParams(
),
password = password,
type = LoginFlowTypes.PASSWORD,
enableRefreshTokenAuth = enableRefreshTokenAuth,
deviceDisplayName = deviceDisplayName,
deviceId = deviceId
)
@@ -67,7 +70,8 @@ internal data class PasswordLoginParams(
address: String,
password: String,
deviceDisplayName: String?,
deviceId: String?): PasswordLoginParams {
deviceId: String?,
enableRefreshTokenAuth: Boolean): PasswordLoginParams {
return PasswordLoginParams(
identifier = mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY,
@@ -76,6 +80,7 @@ internal data class PasswordLoginParams(
),
password = password,
type = LoginFlowTypes.PASSWORD,
enableRefreshTokenAuth = enableRefreshTokenAuth,
deviceDisplayName = deviceDisplayName,
deviceId = deviceId
)
@@ -85,7 +90,8 @@ internal data class PasswordLoginParams(
phone: String,
password: String,
deviceDisplayName: String?,
deviceId: String?): PasswordLoginParams {
deviceId: String?,
enableRefreshTokenAuth: Boolean): PasswordLoginParams {
return PasswordLoginParams(
identifier = mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE,
@@ -94,6 +100,7 @@ internal data class PasswordLoginParams(
),
password = password,
type = LoginFlowTypes.PASSWORD,
enableRefreshTokenAuth = enableRefreshTokenAuth,
deviceDisplayName = deviceDisplayName,
deviceId = deviceId
)

View File

@@ -0,0 +1,25 @@
/*
* Copyright (c) 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.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class RefreshParams(
@Json(name = "refresh_token") val refreshToken: String
)

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 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.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RefreshResult(
@Json(name = "access_token") val accessToken: String,
@Json(name = "expires_in_ms") val expiresInMs: Long,
@Json(name = "refresh_token") val refreshToken: String
)

View File

@@ -23,5 +23,6 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
internal data class TokenLoginParams(
@Json(name = "type") override val type: String = LoginFlowTypes.TOKEN,
@Json(name = "refresh_token") override val enableRefreshTokenAuth: Boolean,
@Json(name = "token") val token: String
) : LoginParams

View File

@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.auth.login
import android.util.Patterns
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
@@ -38,7 +39,8 @@ import org.matrix.android.sdk.internal.session.contentscanner.DisabledContentSca
internal class DefaultLoginWizard(
private val authAPI: AuthAPI,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
private val pendingSessionStore: PendingSessionStore,
private val enableRefreshTokenAuth: Boolean
) : LoginWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
@@ -62,14 +64,16 @@ internal class DefaultLoginWizard(
address = login,
password = password,
deviceDisplayName = initialDeviceName,
deviceId = deviceId
deviceId = deviceId,
enableRefreshTokenAuth = enableRefreshTokenAuth
)
} else {
PasswordLoginParams.userIdentifier(
user = login,
password = password,
deviceDisplayName = initialDeviceName,
deviceId = deviceId
deviceId = deviceId,
enableRefreshTokenAuth = enableRefreshTokenAuth
)
}
val credentials = executeRequest(null) {
@@ -84,7 +88,8 @@ internal class DefaultLoginWizard(
*/
override suspend fun loginWithToken(loginToken: String): Session {
val loginParams = TokenLoginParams(
token = loginToken
token = loginToken,
enableRefreshTokenAuth = enableRefreshTokenAuth
)
val credentials = executeRequest(null) {
authAPI.login(loginParams)
@@ -94,8 +99,10 @@ internal class DefaultLoginWizard(
}
override suspend fun loginCustom(data: JsonDict): Session {
val loginParams = data.toMutableMap()
loginParams["refresh_token"] = enableRefreshTokenAuth
val credentials = executeRequest(null) {
authAPI.login(data)
authAPI.login(loginParams)
}
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)

View File

@@ -38,7 +38,8 @@ internal interface DirectLoginTask : Task<DirectLoginTask.Params, Session> {
val userId: String,
val password: String,
val deviceName: String,
val deviceId: String?
val deviceId: String?,
val refreshToken: Boolean
)
}
@@ -60,7 +61,8 @@ internal class DefaultDirectLoginTask @Inject constructor(
user = params.userId,
password = params.password,
deviceDisplayName = params.deviceName,
deviceId = params.deviceId
deviceId = params.deviceId,
enableRefreshTokenAuth = params.refreshToken
)
val credentials = try {

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 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.internal.auth.refresh
import org.matrix.android.sdk.internal.auth.RefreshTokenAPI
import org.matrix.android.sdk.internal.auth.data.RefreshParams
import org.matrix.android.sdk.internal.auth.data.RefreshResult
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface RefreshTokenTask : Task<RefreshTokenTask.Params, RefreshResult> {
data class Params(
val refreshToken: String
)
}
internal class DefaultRefreshTokenTask @Inject constructor(
private val refreshTokenAPI: RefreshTokenAPI
) : RefreshTokenTask {
override suspend fun execute(params: RefreshTokenTask.Params): RefreshResult {
return executeRequest(null) {
refreshTokenAPI.refreshToken(RefreshParams(params.refreshToken))
}
}
}

View File

@@ -36,7 +36,8 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
internal class DefaultRegistrationWizard(
authAPI: AuthAPI,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
private val pendingSessionStore: PendingSessionStore,
private val enableRefreshTokenAuth: Boolean
) : RegistrationWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
@@ -72,7 +73,8 @@ internal class DefaultRegistrationWizard(
val params = RegistrationParams(
username = userName,
password = password,
initialDeviceDisplayName = initialDeviceDisplayName
initialDeviceDisplayName = initialDeviceDisplayName,
enableRefreshTokenAuth = enableRefreshTokenAuth
)
return performRegistrationRequest(params)
.also {
@@ -85,7 +87,10 @@ internal class DefaultRegistrationWizard(
val safeSession = pendingSessionData.currentSession
?: throw IllegalStateException("developer error, call createAccount() method first")
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
val params = RegistrationParams(
auth = AuthParams.createForCaptcha(safeSession, response),
enableRefreshTokenAuth = enableRefreshTokenAuth
)
return performRegistrationRequest(params)
}
@@ -93,7 +98,10 @@ internal class DefaultRegistrationWizard(
val safeSession = pendingSessionData.currentSession
?: throw IllegalStateException("developer error, call createAccount() method first")
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
val params = RegistrationParams(
auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession),
enableRefreshTokenAuth = enableRefreshTokenAuth
)
return performRegistrationRequest(params)
}
@@ -137,7 +145,8 @@ internal class DefaultRegistrationWizard(
sid = response.sid
)
)
}
},
enableRefreshTokenAuth = enableRefreshTokenAuth
)
// Store data
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))

View File

@@ -42,5 +42,9 @@ internal data class RegistrationParams(
// Temporary flag to notify the server that we support msisdn flow. Used to prevent old app
// versions to end up in fallback because the HS returns the msisdn flow which they don't support
@Json(name = "x_show_msisdn")
val xShowMsisdn: Boolean? = null
val xShowMsisdn: Boolean? = null,
// whether the session created should use refresh-token-based auth
@Json(name = "refresh_token")
val enableRefreshTokenAuth: Boolean? = null
)

View File

@@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.internal.network.parsing.CipherSuiteMoshiAdapter
import org.matrix.android.sdk.internal.network.parsing.CredentialsResponseAdapter
import org.matrix.android.sdk.internal.network.parsing.ForceToBooleanJsonAdapter
import org.matrix.android.sdk.internal.network.parsing.RuntimeJsonAdapterFactory
import org.matrix.android.sdk.internal.network.parsing.TlsVersionMoshiAdapter
@@ -44,6 +45,7 @@ object MoshiProvider {
.add(ForceToBooleanJsonAdapter())
.add(CipherSuiteMoshiAdapter())
.add(TlsVersionMoshiAdapter())
.add(CredentialsResponseAdapter())
// Use addLast here so we can inject a SplitLazyRoomSyncJsonAdapter later to override the default parsing.
.addLast(DefaultLazyRoomSyncEphemeralJsonAdapter())
.add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java)

View File

@@ -109,6 +109,8 @@ internal class DefaultLegacySessionImporter @Inject constructor(
userId = legacyConfig.credentials.userId,
accessToken = legacyConfig.credentials.accessToken,
refreshToken = legacyConfig.credentials.refreshToken,
expiresInMs = null,
expiryTs = null,
homeServer = legacyConfig.credentials.homeServer,
deviceId = legacyConfig.credentials.deviceId,
discoveryInformation = legacyConfig.credentials.wellKnown?.let { wellKnown ->

View File

@@ -16,22 +16,44 @@
package org.matrix.android.sdk.internal.network
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.isTokenUnknownError
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor {
internal class AccessTokenInterceptor(
private val accessTokenProvider: AccessTokenProvider,
private val globalErrorReceiver: GlobalErrorReceiver?,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
// Attempt to get the latest access token before the request token might be expiring soon.
val response = attemptRequestWithLatestToken(chain)
// Add the access token to all requests if it is set
accessTokenProvider.getToken()?.let { token ->
val newRequestBuilder = request.newBuilder()
newRequestBuilder.header(HttpHeaders.Authorization, "Bearer $token")
request = newRequestBuilder.build()
if (!accessTokenProvider.supportsRefreshTokens) {
return response
}
val serverError = response.peekFailure(globalErrorReceiver) as? Failure.ServerError
if (serverError == null || !serverError.isTokenUnknownError()) {
return response
}
// Server is the source of truth on token validity, if it is no longer valid we should refresh and retry the original request.
return attemptRequestWithLatestToken(chain, serverError)
}
private fun attemptRequestWithLatestToken(chain: Interceptor.Chain, serverError: Failure.ServerError? = null): Response {
var request = chain.request()
runBlocking {
accessTokenProvider.getToken(serverError)?.let { token ->
val newRequestBuilder = request.newBuilder()
newRequestBuilder.header(HttpHeaders.Authorization, "Bearer $token")
request = newRequestBuilder.build()
}
}
return chain.proceed(request)
}
}

View File

@@ -21,6 +21,7 @@ internal object NetworkConstants {
private const val URI_API_PREFIX_PATH = "_matrix/client"
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
// Media

View File

@@ -21,7 +21,6 @@ package org.matrix.android.sdk.internal.network
import com.squareup.moshi.JsonEncodingException
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ResponseBody
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.failure.MatrixError
@@ -30,7 +29,6 @@ import retrofit2.HttpException
import retrofit2.Response
import timber.log.Timber
import java.io.IOException
import java.net.HttpURLConnection
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -73,6 +71,18 @@ internal fun okhttp3.Response.toFailure(globalErrorReceiver: GlobalErrorReceiver
return toFailure(body, code, globalErrorReceiver)
}
/**
* Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError.
* This is used to check error responses in interceptors as it does not consume the body but uses `Response.peekBody` instead.
*/
internal fun okhttp3.Response.peekFailure(globalErrorReceiver: GlobalErrorReceiver?): Failure? {
if (isSuccessful) {
return null
}
val megabyteInBytes = (1024 * 1024).toLong()
return toFailure(this.peekBody(megabyteInBytes), code, globalErrorReceiver)
}
private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiver: GlobalErrorReceiver?): Failure {
if (errorBody == null) {
return Failure.Unknown(RuntimeException("errorBody should not be null"))
@@ -91,10 +101,6 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiv
matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank() -> {
globalErrorReceiver?.handleGlobalError(GlobalError.ConsentNotGivenError(matrixError.consentUri))
}
httpCode == HttpURLConnection.HTTP_UNAUTHORIZED && /* 401 */
matrixError.code == MatrixError.M_UNKNOWN_TOKEN -> {
globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse()))
}
matrixError.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT -> {
globalErrorReceiver?.handleGlobalError(GlobalError.ExpiredAccount)
}

View File

@@ -19,17 +19,21 @@ package org.matrix.android.sdk.internal.network.httpclient
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.internal.network.AccessTokenInterceptor
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor
import org.matrix.android.sdk.internal.network.ssl.CertUtil
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
import timber.log.Timber
internal fun OkHttpClient.Builder.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient.Builder {
internal fun OkHttpClient.Builder.addAccessTokenInterceptor(
accessTokenProvider: AccessTokenProvider,
globalErrorReceiver: GlobalErrorReceiver? = null
): OkHttpClient.Builder {
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
interceptors().removeAll(existingCurlInterceptors)
addInterceptor(AccessTokenInterceptor(accessTokenProvider))
addInterceptor(AccessTokenInterceptor(accessTokenProvider, globalErrorReceiver))
// Re add eventually the curl logging interceptors
existingCurlInterceptors.forEach {

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 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.internal.network.parsing
import com.squareup.moshi.FromJson
import org.matrix.android.sdk.api.auth.data.Credentials
/**
* Used when we receive a new credential from the serer that contains an access token expiry. We want to
* estimate the expiry date as soon as possible using `expiresInMs`.
*/
class CredentialsResponseAdapter {
@FromJson
fun credentialsFromResponse(responseCredentials: Credentials): Credentials {
if (responseCredentials.expiresInMs == null || responseCredentials.expiryTs != null) {
// expiryTs already estimated or doesn't apply, return existing credential.
return responseCredentials
}
// We have received a credential response from the server, estimate the expiry datetime.
return responseCredentials.copy(expiryTs = System.currentTimeMillis() + responseCredentials.expiresInMs)
}
}

View File

@@ -16,6 +16,9 @@
package org.matrix.android.sdk.internal.network.token
import org.matrix.android.sdk.api.failure.Failure
internal interface AccessTokenProvider {
fun getToken(): String?
val supportsRefreshTokens: Boolean
suspend fun getToken(serverError: Failure.ServerError? = null): String?
}

View File

@@ -16,13 +16,107 @@
package org.matrix.android.sdk.internal.network.token
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.failure.MatrixError.Companion.M_FORBIDDEN
import org.matrix.android.sdk.api.failure.MatrixError.Companion.M_UNKNOWN_TOKEN
import org.matrix.android.sdk.api.failure.isTokenUnknownError
import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.auth.data.RefreshResult
import org.matrix.android.sdk.internal.auth.refresh.RefreshTokenTask
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import timber.log.Timber
import javax.inject.Inject
internal class HomeserverAccessTokenProvider @Inject constructor(
@SessionId private val sessionId: String,
private val sessionParamsStore: SessionParamsStore
private val sessionParamsStore: SessionParamsStore,
private val refreshTokenTask: RefreshTokenTask,
private val globalErrorReceiver: GlobalErrorReceiver
) : AccessTokenProvider {
override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken
companion object {
private val mutex = Mutex()
/**
* The time interval before the access token expires that we will start trying to refresh the token.]
* This avoids us having to block other users requests while the token refreshes.
* Choosing a value larger than DEFAULT_DELAY_MILLIS + DEFAULT_LONG_POOL_TIMEOUT_SECONDS guarantees we will at least have attempted it before expiry.
*/
private const val PREEMPT_REFRESH_EXPIRATION_INTERVAL = 60000
}
override val supportsRefreshTokens = true
override suspend fun getToken(serverError: Failure.ServerError?): String? {
var accessToken: String?
// We synchronise here so that when refresh is required, a single request becomes the leader.
// On successful refresh the leader saves the credential and the new access token then becomes available to other requests when they are unblocked.
// We should never send multiple refresh requests as refresh tokens are single-use(they rotate with a new one returned in the response).
// Mishandled via race conditions and we could become unauthenticated.
mutex.withLock {
accessToken = verifyAccessTokenAndRefreshIfStale(serverError)
}
return accessToken
}
private suspend fun verifyAccessTokenAndRefreshIfStale(serverError: Failure.ServerError?): String? {
val credentials = sessionParamsStore.get(sessionId)?.credentials ?: return null
val receivedTokenUnknown = serverError?.isTokenUnknownError().orFalse()
if (credentials.refreshToken.isNullOrEmpty() && serverError != null && receivedTokenUnknown) {
Timber.d("## HomeserverAccessTokenProvider: accessToken-based auth failed, requires logout.")
globalErrorReceiver.handleGlobalError(invalidToken(serverError, false))
}
if (credentials.refreshToken.isNullOrEmpty() || (!receivedTokenUnknown && expiryIsValid(credentials.expiryTs))) {
// Existing access token is valid
return credentials.accessToken
}
Timber.d("## HomeserverAccessTokenProvider: Refreshing access token...")
var result: RefreshResult? = null
try {
result = refreshTokenTask.execute(RefreshTokenTask.Params(credentials.refreshToken))
} catch (throwable: Throwable) {
if (throwable is Failure.ServerError) {
Timber.d("## HomeserverAccessTokenProvider: Failed to refresh access token. error: $throwable")
if (throwable.error.code == M_UNKNOWN_TOKEN || throwable.error.code == M_FORBIDDEN) {
Timber.d("## HomeserverAccessTokenProvider: refreshToken-based auth failed, requires logout.")
globalErrorReceiver.handleGlobalError(invalidToken(throwable, true))
}
}
}
if (result == null) {
return null
}
val updatedCredentials = credentials.copy(
accessToken = result.accessToken,
expiresInMs = result.expiresInMs,
expiryTs = System.currentTimeMillis() + result.expiresInMs,
refreshToken = result.refreshToken
)
sessionParamsStore.updateCredentials(updatedCredentials)
Timber.d("## HomeserverAccessTokenProvider: Tokens refreshed and saved.")
return result.accessToken
}
private fun expiryIsValid(expiryTs: Long?) = expiryTs == null || System.currentTimeMillis() < (expiryTs - PREEMPT_REFRESH_EXPIRATION_INTERVAL)
private fun invalidToken(serverError: Failure.ServerError, refreshTokenAuth: Boolean) = GlobalError.InvalidToken(
softLogout = serverError.error.isSoftLogout.orFalse(),
refreshTokenAuth = refreshTokenAuth,
errorCode = serverError.error.code,
errorReason = serverError.error.message
)
}

View File

@@ -45,6 +45,9 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
import org.matrix.android.sdk.internal.auth.RefreshTokenAPI
import org.matrix.android.sdk.internal.auth.refresh.DefaultRefreshTokenTask
import org.matrix.android.sdk.internal.auth.refresh.RefreshTokenTask
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
@@ -222,10 +225,11 @@ internal abstract class SessionModule {
fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
@Authenticated accessTokenProvider: AccessTokenProvider,
@SessionId sessionId: String,
@MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient {
@MockHttpInterceptor testInterceptor: TestInterceptor?,
globalErrorReceiver: GlobalErrorReceiver): OkHttpClient {
return okHttpClient
.newBuilder()
.addAccessTokenInterceptor(accessTokenProvider)
.addAccessTokenInterceptor(accessTokenProvider, globalErrorReceiver)
.apply {
if (testInterceptor != null) {
testInterceptor.sessionId = sessionId
@@ -285,6 +289,19 @@ internal abstract class SessionModule {
fun providesMxCryptoConfig(matrixConfiguration: MatrixConfiguration): MXCryptoConfig {
return matrixConfiguration.cryptoConfig
}
@Provides
@JvmStatic
@SessionScope
fun providesRefreshTokenAPI(
@Unauthenticated okHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory,
homeServerConnectionConfig: HomeServerConnectionConfig
): RefreshTokenAPI {
val homeServerUrl = homeServerConnectionConfig.homeServerUriBase.toString()
return retrofitFactory.create(okHttpClient, homeServerUrl)
.create(RefreshTokenAPI::class.java)
}
}
@Binds
@@ -390,4 +407,7 @@ internal abstract class SessionModule {
@Binds
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
@Binds
abstract fun bindRefreshTokenTask(task: DefaultRefreshTokenTask): RefreshTokenTask
}

View File

@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.identity
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
import javax.inject.Inject
@@ -23,5 +24,6 @@ import javax.inject.Inject
internal class IdentityAccessTokenProvider @Inject constructor(
private val identityStore: IdentityStore
) : AccessTokenProvider {
override fun getToken() = identityStore.getIdentityData()?.token
override val supportsRefreshTokens = false
override suspend fun getToken(serverError: Failure.ServerError?): String? = identityStore.getIdentityData()?.token
}

View File

@@ -26,8 +26,8 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask
private val sessionParamsStore: SessionParamsStore
) : SignOutService {
override suspend fun signInAgain(password: String) {
signInAgainTask.execute(SignInAgainTask.Params(password))
override suspend fun signInAgain(password: String, enableRefreshTokenAuth: Boolean) {
signInAgainTask.execute(SignInAgainTask.Params(password, enableRefreshTokenAuth))
}
override suspend fun updateCredentials(credentials: Credentials) {

View File

@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.signout
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams
@@ -26,7 +27,8 @@ import javax.inject.Inject
internal interface SignInAgainTask : Task<SignInAgainTask.Params, Unit> {
data class Params(
val password: String
val password: String,
val enableRefreshTokenAuth: Boolean
)
}
@@ -34,7 +36,8 @@ internal class DefaultSignInAgainTask @Inject constructor(
private val signOutAPI: SignOutAPI,
private val sessionParams: SessionParams,
private val sessionParamsStore: SessionParamsStore,
private val globalErrorReceiver: GlobalErrorReceiver
private val globalErrorReceiver: GlobalErrorReceiver,
private val matrixConfiguration: MatrixConfiguration
) : SignInAgainTask {
override suspend fun execute(params: SignInAgainTask.Params) {
@@ -49,7 +52,8 @@ internal class DefaultSignInAgainTask @Inject constructor(
// but https://github.com/matrix-org/synapse/issues/6525
deviceDisplayName = null,
// Reuse the same deviceId
deviceId = sessionParams.deviceId
deviceId = sessionParams.deviceId,
enableRefreshTokenAuth = params.enableRefreshTokenAuth
)
)
}

View File

@@ -159,6 +159,8 @@ android {
buildConfigField "Boolean", "enableLocationSharing", "true"
buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\""
buildConfigField "Boolean", "enableRefreshTokenAuth", "false"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Keep abiFilter for the universalApk

View File

@@ -68,6 +68,7 @@ import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.plan.UnauthenticatedError
import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.consent.ConsentNotGivenHelper
import im.vector.app.features.navigation.Navigator
@@ -297,6 +298,11 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
return
}
val errorCode: UnauthenticatedError.ErrorCode = UnauthenticatedError.ErrorCode.values().firstOrNull { it.name == globalError.errorCode }
?: UnauthenticatedError.ErrorCode.M_UNKNOWN
val unauthenticatedError = UnauthenticatedError(errorCode, globalError.errorReason, globalError.refreshTokenAuth, globalError.softLogout)
analyticsTracker.capture(unauthenticatedError)
mainActivityStarted = true
MainActivity.restartApp(this,

View File

@@ -26,6 +26,7 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
@@ -99,10 +100,10 @@ class LoginViewModel @AssistedInject constructor(
get() = authenticationService.isRegistrationStarted
private val registrationWizard: RegistrationWizard?
get() = authenticationService.getRegistrationWizard()
get() = authenticationService.getRegistrationWizard(BuildConfig.enableRefreshTokenAuth)
private val loginWizard: LoginWizard?
get() = authenticationService.getLoginWizard()
get() = authenticationService.getLoginWizard(BuildConfig.enableRefreshTokenAuth)
private var loginConfig: LoginConfig? = null
@@ -617,7 +618,9 @@ class LoginViewModel @AssistedInject constructor(
alteredHomeServerConnectionConfig,
action.username,
action.password,
action.initialDeviceName)
action.initialDeviceName,
BuildConfig.enableRefreshTokenAuth
)
} catch (failure: Throwable) {
onDirectLoginError(failure)
return

View File

@@ -24,6 +24,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
@@ -101,10 +102,10 @@ class LoginViewModel2 @AssistedInject constructor(
get() = authenticationService.isRegistrationStarted
private val registrationWizard: RegistrationWizard?
get() = authenticationService.getRegistrationWizard()
get() = authenticationService.getRegistrationWizard(BuildConfig.enableRefreshTokenAuth)
private val loginWizard: LoginWizard?
get() = authenticationService.getLoginWizard()
get() = authenticationService.getLoginWizard(BuildConfig.enableRefreshTokenAuth)
private var loginConfig: LoginConfig? = null

View File

@@ -26,6 +26,7 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
@@ -112,10 +113,10 @@ class OnboardingViewModel @AssistedInject constructor(
get() = authenticationService.isRegistrationStarted
private val registrationWizard: RegistrationWizard?
get() = authenticationService.getRegistrationWizard()
get() = authenticationService.getRegistrationWizard(BuildConfig.enableRefreshTokenAuth)
private val loginWizard: LoginWizard?
get() = authenticationService.getLoginWizard()
get() = authenticationService.getLoginWizard(BuildConfig.enableRefreshTokenAuth)
private var loginConfig: LoginConfig? = null
@@ -656,7 +657,9 @@ class OnboardingViewModel @AssistedInject constructor(
alteredHomeServerConnectionConfig,
action.username,
action.password,
action.initialDeviceName)
action.initialDeviceName,
BuildConfig.enableRefreshTokenAuth
)
} catch (failure: Throwable) {
onDirectLoginError(failure)
return

View File

@@ -73,7 +73,10 @@ class SoftLogoutActivity : LoginActivity() {
)
}
is SoftLogoutViewEvents.ClearData -> {
MainActivity.restartApp(this, MainActivityArgs(clearCredentials = true))
MainActivity.restartApp(this, MainActivityArgs(
clearCredentials = true,
isUserLoggedOut = true
))
}
}
}

View File

@@ -26,6 +26,7 @@ import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
@@ -177,7 +178,7 @@ class SoftLogoutViewModel @AssistedInject constructor(
}
viewModelScope.launch {
try {
session.signInAgain(action.password)
session.signInAgain(action.password, BuildConfig.enableRefreshTokenAuth)
onSessionRestored()
} catch (failure: Throwable) {
setState {