From c9bc6f4a9e38b75057c3aa8e85017fee3d6508af Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 2 Mar 2020 17:34:31 +0100 Subject: [PATCH] Support homeserver discovery from MXID - Wellknown (#476) --- CHANGES.md | 1 + .../android/api/auth/AuthenticationService.kt | 18 +- .../matrix/android/api/auth/data/WellKnown.kt | 3 +- .../api/auth/data/WellKnownManagerConfig.kt | 2 +- .../api/auth/wellknown/WellknownResult.kt | 55 +++++ .../android/internal/auth/AuthModule.kt | 18 +- .../auth/DefaultAuthenticationService.kt | 30 ++- .../auth/wellknown/DirectLoginTask.kt | 61 ++++++ .../auth/wellknown/GetWellknownTask.kt | 199 ++++++++++++++++++ .../internal/auth/wellknown/WellKnownAPI.kt | 26 +++ .../internal/identity/IdentityPingApi.kt | 34 +++ .../internal/network/NetworkConstants.kt | 6 + .../internal/session/user/SearchUserAPI.kt | 4 +- .../matrix/android/internal/util/UrlUtils.kt | 28 +++ .../riotx/features/login/LoginActivity.kt | 19 +- .../riotx/features/login/LoginFragment.kt | 76 ++++--- .../login/LoginServerSelectionFragment.kt | 6 + .../riotx/features/login/LoginViewModel.kt | 93 +++++++- .../vector/riotx/features/login/SignMode.kt | 4 +- .../fragment_login_server_selection.xml | 25 ++- vector/src/main/res/values/strings_riotX.xml | 8 + 21 files changed, 653 insertions(+), 63 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt diff --git a/CHANGES.md b/CHANGES.md index 52896df63d..2b36909c33 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features ✨: Improvements 🙌: - Invite member(s) to an existing room (#1276) - Improve notification accessibility with ticker text (#1226) + - Support homeserver discovery from MXID (#476) Bugfix 🐛: - Sometimes the same device appears twice in the list of devices of a user (#1329) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt index 140d1c259f..5150420de2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.auth.data.LoginFlowResult import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -30,7 +31,6 @@ import im.vector.matrix.android.api.util.Cancelable * This interface defines methods to authenticate or to create an account to a matrix server. */ interface AuthenticationService { - /** * Request the supported login flows for this homeserver. * This is the first method to call to be able to get a wizard to login or the create an account @@ -89,4 +89,20 @@ interface AuthenticationService { fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback): Cancelable + + /** + * Perform a wellknown request, using the domain from the matrixId + */ + fun getWellKnownData(matrixId: String, + callback: MatrixCallback): Cancelable + + /** + * Authenticate with a matrixId and a password + * Usually call this after a successful call to getWellKnownData() + */ + fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt index bdad4702b7..9dd1fa2012 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict /** * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery @@ -52,7 +53,7 @@ data class WellKnown( val identityServer: WellKnownBaseConfig? = null, @Json(name = "m.integrations") - val integrations: Map? = null + val integrations: JsonDict? = null ) { /** * Returns the list of integration managers proposed diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt index 33ed412a2a..ffdea37afe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt @@ -16,6 +16,6 @@ package im.vector.matrix.android.api.auth.data data class WellKnownManagerConfig( - val apiUrl : String, + val apiUrl: String, val uiUrl: String ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt new file mode 100644 index 0000000000..974a65adbc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt @@ -0,0 +1,55 @@ +/* + * 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.api.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#well-known-uri + */ +sealed class WellknownResult { + /** + * The provided matrixId is no valid. Unable to extract a domain name. + */ + object InvalidMatrixId : WellknownResult() + + /** + * Retrieve the specific piece of information from the user in a way which fits within the existing client user experience, + * if the client is inclined to do so. Failure can take place instead if no good user experience for this is possible at this point. + */ + data class Prompt(val homerServerUrl: String, + val identityServerUrl: String?, + val wellKnown: WellKnown) : WellknownResult() + + /** + * Stop the current auto-discovery mechanism. If no more auto-discovery mechanisms are available, + * then the client may use other methods of determining the required parameters, such as prompting the user, or using default values. + */ + object Ignore : WellknownResult() + + /** + * Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter. + */ + object FailPrompt : WellknownResult() + + /** + * Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process. + * At this point, valid data was obtained, but no homeserver is available to serve the client. + * No further guess should be attempted and the user should make a conscientious decision what to do next. + */ + object FailError : WellknownResult() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 6b6321de36..232cb3f541 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -25,6 +25,10 @@ import im.vector.matrix.android.internal.auth.db.AuthRealmMigration import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore +import im.vector.matrix.android.internal.auth.wellknown.DefaultDirectLoginTask +import im.vector.matrix.android.internal.auth.wellknown.DefaultGetWellknownTask +import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask +import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.RealmConfiguration @@ -59,14 +63,20 @@ internal abstract class AuthModule { } @Binds - abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore + abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore @Binds - abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore + abstract fun bindPendingSessionStore(store: RealmPendingSessionStore): PendingSessionStore @Binds - abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService + abstract fun bindAuthenticationService(service: DefaultAuthenticationService): AuthenticationService @Binds - abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator + abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator + + @Binds + abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask + + @Binds + abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index 85c2cdbf3d..b2c2dc12a5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedByS import im.vector.matrix.android.api.auth.data.isSupportedBySdk import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -38,9 +39,13 @@ import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard +import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask +import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.toCancelable @@ -59,7 +64,10 @@ internal class DefaultAuthenticationService @Inject constructor( private val sessionParamsStore: SessionParamsStore, private val sessionManager: SessionManager, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val getWellknownTask: GetWellknownTask, + private val directLoginTask: DirectLoginTask, + private val taskExecutor: TaskExecutor ) : AuthenticationService { private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() @@ -260,6 +268,26 @@ internal class DefaultAuthenticationService @Inject constructor( } } + override fun getWellKnownData(matrixId: String, callback: MatrixCallback): Cancelable { + return getWellknownTask + .configureWith(GetWellknownTask.Params(matrixId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable { + return directLoginTask + .configureWith(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + private suspend fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { sessionCreator.createSession(credentials, homeServerConnectionConfig) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt new file mode 100644 index 0000000000..01a3ab192d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt @@ -0,0 +1,61 @@ +/* + * 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.auth.wellknown + +import dagger.Lazy +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.auth.SessionCreator +import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface DirectLoginTask : Task { + data class Params( + val homeServerConnectionConfig: HomeServerConnectionConfig, + val userId: String, + val password: String, + val deviceName: String + ) +} + +internal class DefaultDirectLoginTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val sessionCreator: SessionCreator +) : DirectLoginTask { + + override suspend fun execute(params: DirectLoginTask.Params): Session { + val authAPI = retrofitFactory.create(okHttpClient, params.homeServerConnectionConfig.homeServerUri.toString()) + .create(AuthAPI::class.java) + + val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) + + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt new file mode 100644 index 0000000000..8ed2cb3b0f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt @@ -0,0 +1,199 @@ +/* + * 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.auth.wellknown + +import android.util.MalformedJsonException +import dagger.Lazy +import im.vector.matrix.android.api.MatrixPatterns +import im.vector.matrix.android.api.auth.data.WellKnown +import im.vector.matrix.android.api.auth.wellknown.WellknownResult +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.identity.IdentityPingApi +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.isValidUrl +import okhttp3.OkHttpClient +import java.io.EOFException +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetWellknownTask : Task { + data class Params( + val matrixId: String + ) +} + +/** + * Inspired from AutoDiscovery class from legacy Matrix Android SDK + */ +internal class DefaultGetWellknownTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory +) : GetWellknownTask { + + override suspend fun execute(params: GetWellknownTask.Params): WellknownResult { + if (!MatrixPatterns.isUserId(params.matrixId)) { + return WellknownResult.InvalidMatrixId + } + + val homeServerDomain = params.matrixId.substringAfter(":") + + return findClientConfig(homeServerDomain) + } + + /** + * Find client config + * + * - Do the .well-known request + * - validate homeserver url and identity server url if provide in .well-known result + * - return action and .well-known data + * + * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + */ + private suspend fun findClientConfig(domain: String): WellknownResult { + val wellKnownAPI = retrofitFactory.create(okHttpClient, "https://dummy.org") + .create(WellKnownAPI::class.java) + + return try { + val wellKnown = executeRequest(null) { + apiCall = wellKnownAPI.getWellKnown(domain) + } + + // Success + val homeServerBaseUrl = wellKnown.homeServer?.baseURL + if (homeServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailPrompt + } else { + if (homeServerBaseUrl.isValidUrl()) { + // Check that HS is a real one + validateHomeServer(homeServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } + } catch (throwable: Throwable) { + when (throwable) { + is Failure.NetworkConnection -> { + WellknownResult.Ignore + } + is Failure.OtherServerError -> { + when (throwable.httpCode) { + HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore + else -> WellknownResult.FailPrompt + } + } + is MalformedJsonException, is EOFException -> { + WellknownResult.FailPrompt + } + else -> { + throw throwable + } + } + } + } + + /** + * Return true if home server is valid, and (if applicable) if identity server is pingable + */ + private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown): WellknownResult { + val capabilitiesAPI = retrofitFactory.create(okHttpClient, homeServerBaseUrl) + .create(CapabilitiesAPI::class.java) + + try { + executeRequest(null) { + apiCall = capabilitiesAPI.getVersions() + } + } catch (throwable: Throwable) { + return WellknownResult.FailError + } + + return if (wellKnown.identityServer == null) { + // No identity server + WellknownResult.Prompt(homeServerBaseUrl, null, wellKnown) + } else { + // if m.identity_server is present it must be valid + val identityServerBaseUrl = wellKnown.identityServer.baseURL + if (identityServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailError + } else { + if (identityServerBaseUrl.isValidUrl()) { + if (validateIdentityServer(identityServerBaseUrl)) { + // All is ok + WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } else { + WellknownResult.FailError + } + } + } + } + + /** + * Return true if identity server is pingable + */ + private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { + val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) + .create(IdentityPingApi::class.java) + + return try { + executeRequest(null) { + apiCall = identityPingApi.ping() + } + + true + } catch (throwable: Throwable) { + false + } + } + + /** + * Try to get an identity server URL from a home server URL, using a .wellknown request + */ + /* + fun getIdentityServer(homeServerUrl: String, callback: ApiCallback) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.identityServer?.baseURL) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + + fun getServerPreferredIntegrationManagers(homeServerUrl: String, callback: ApiCallback>) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.getIntegrationManagers()) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + */ +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt new file mode 100644 index 0000000000..71928123bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt @@ -0,0 +1,26 @@ +/* + * 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.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface WellKnownAPI { + @GET("https://{domain}/.well-known/matrix/client") + fun getWellKnown(@Path("domain") domain: String): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt new file mode 100644 index 0000000000..2a0e00704c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt @@ -0,0 +1,34 @@ +/* + * 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.identity + +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface IdentityPingApi { + + /** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * Simple ping call to check if server alive + * + * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check + * + * @return 200 in case of success + */ + @GET(NetworkConstants.URI_API_PREFIX_IDENTITY) + fun ping(): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index c6c10d9a8f..ab6745148f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -26,4 +26,10 @@ internal object NetworkConstants { // Media private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" + + // Identity server + const val URI_IDENTITY_PATH = "_matrix/identity/api/v1/" + const val URI_IDENTITY_PATH_V2 = "_matrix/identity/v2/" + + const val URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt index e57daed617..4adcee88aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.user -import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0 +import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.user.model.SearchUsersParams import im.vector.matrix.android.internal.session.user.model.SearchUsersResponse import retrofit2.Call @@ -30,6 +30,6 @@ internal interface SearchUserAPI { * * @param searchUsersParams the search params. */ - @POST(URI_API_PREFIX_PATH_R0 + "user_directory/search") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search") fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt new file mode 100644 index 0000000000..8ad5e89605 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt @@ -0,0 +1,28 @@ +/* + * 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.util + +import java.net.URL + +internal fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + false + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index c67e45d9e7..cd8c1df46e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -38,6 +38,7 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.HomeActivity @@ -125,9 +126,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { } private fun handleLoginNavigation(loginNavigation: LoginNavigation) { - // Assigning to dummy make sure we do not forget a case - @Suppress("UNUSED_VARIABLE") - val dummy = when (loginNavigation) { + when (loginNavigation) { is LoginNavigation.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java, @@ -177,7 +176,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginNavigation.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - } + }.exhaustive } private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { @@ -254,11 +253,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private fun onSignModeSelected() = withState(loginViewModel) { state -> when (state.signMode) { - SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> { + SignMode.Unknown -> error("Sign mode has to be set before calling this method") + SignMode.SignUp -> { // This is managed by the LoginViewEvents } - SignMode.SignIn -> { + SignMode.SignIn -> { // It depends on the LoginMode when (state.loginMode) { LoginMode.Unknown -> error("Developer error") @@ -272,7 +271,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) } } - } + SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + }.exhaustive } private fun onRegistrationStageNotSupported() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 8b89aeda2a..371f669736 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.isInvalidPassword import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.extensions.toReducedUrl @@ -73,16 +74,17 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { private fun setupAutoFill(state: LoginViewState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) } - SignMode.SignIn -> { + SignMode.SignIn, + SignMode.SignInWithMatrixId -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) } - } + }.exhaustive } } @@ -116,35 +118,44 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { } private fun setupUi(state: LoginViewState) { - val resId = when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_to - SignMode.SignIn -> R.string.login_connect_to - } - loginFieldTil.hint = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_username_hint - SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_username_hint + SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint }) - when (state.serverType) { - ServerType.MatrixOrg -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) - loginNotice.text = getString(R.string.login_server_matrix_org_text) + // Handle direct signin first + if (state.signMode == SignMode.SignInWithMatrixId) { + loginServerIcon.isVisible = false + loginTitle.text = getString(R.string.login_signin_matrix_id_title) + loginNotice.text = getString(R.string.login_signin_matrix_id_notice) + } else { + val resId = when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_to + SignMode.SignIn -> R.string.login_connect_to + SignMode.SignInWithMatrixId -> R.string.login_connect_to } - ServerType.Modular -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_modular) - loginTitle.text = getString(resId, "Modular") - loginNotice.text = getString(R.string.login_server_modular_text) - } - ServerType.Other -> { - loginServerIcon.isVisible = false - loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) - loginNotice.text = getString(R.string.login_server_other_text) + + when (state.serverType) { + ServerType.MatrixOrg -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) + loginNotice.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_modular) + loginTitle.text = getString(resId, "Modular") + loginNotice.text = getString(R.string.login_server_modular_text) + } + ServerType.Other -> { + loginServerIcon.isVisible = false + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) + loginNotice.text = getString(R.string.login_server_other_text) + } } } } @@ -153,9 +164,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn loginSubmit.text = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_submit - SignMode.SignIn -> R.string.login_signin + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_submit + SignMode.SignIn, + SignMode.SignInWithMatrixId -> R.string.login_signin }) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 9050ea2688..ddd56c314e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -99,6 +99,12 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment } } + @OnClick(R.id.loginServerIKnowMyIdSubmit) + fun loginWithMatrixId() { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignInWithMatrixId)) + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + } + override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetHomeServerType) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 80b04fe062..05f841269f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.login import android.content.Context +import android.net.Uri import androidx.fragment.app.FragmentActivity import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail @@ -29,19 +30,24 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.signout.soft.SoftLogoutActivity @@ -51,14 +57,16 @@ import java.util.concurrent.CancellationException /** * */ -class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, - private val applicationContext: Context, - private val authenticationService: AuthenticationService, - private val activeSessionHolder: ActiveSessionHolder, - private val pushRuleTriggerListener: PushRuleTriggerListener, - private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, - private val sessionListener: SessionListener, - private val reAuthHelper: ReAuthHelper) +class LoginViewModel @AssistedInject constructor( + @Assisted initialState: LoginViewState, + private val applicationContext: Context, + private val authenticationService: AuthenticationService, + private val activeSessionHolder: ActiveSessionHolder, + private val pushRuleTriggerListener: PushRuleTriggerListener, + private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, + private val sessionListener: SessionListener, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -421,10 +429,73 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state -> when (state.signMode) { - SignMode.SignIn -> handleLogin(action) - SignMode.SignUp -> handleRegisterWith(action) - else -> error("Developer error, invalid sign mode") + SignMode.Unknown -> error("Developer error, invalid sign mode") + SignMode.SignIn -> handleLogin(action) + SignMode.SignUp -> handleRegisterWith(action) + SignMode.SignInWithMatrixId -> handleDirectLogin(action) + }.exhaustive + } + + private fun handleDirectLogin(action: LoginAction.LoginOrRegister) { + setState { + copy( + asyncLoginAction = Loading() + ) } + + authenticationService.getWellKnownData(action.username, object : MatrixCallback { + override fun onSuccess(data: WellknownResult) { + when (data) { + is WellknownResult.Prompt -> + onWellknownSuccess(action, data) + is WellknownResult.InvalidMatrixId -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(LoginViewEvents.Failure(Exception(stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)))) + } + else -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(LoginViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) + } + }.exhaustive + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) + } + + private fun onWellknownSuccess(action: LoginAction.LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) { + val homeServerConnectionConfig = HomeServerConnectionConfig( + homeServerUri = Uri.parse(wellKnownPrompt.homerServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + + authenticationService.directAuthentication(homeServerConnectionConfig, action.username, action.password, action.initialDeviceName, object : MatrixCallback { + override fun onSuccess(data: Session) { + onSessionCreated(data) + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) } private fun handleLogin(action: LoginAction.LoginOrRegister) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt index b793a0fe1d..ad06f1f4a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt @@ -21,5 +21,7 @@ enum class SignMode { // Account creation SignUp, // Login - SignIn + SignIn, + // Login directly with matrix Id + SignInWithMatrixId } diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index c97b32bd21..dc04f202fa 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -184,11 +184,34 @@ android:layout_marginTop="24dp" android:text="@string/login_continue" android:transitionName="loginSubmitTransition" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/loginServerIKnowMyIdNotice" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOther" /> + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 585102d46a..dd1043819d 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -57,4 +57,12 @@ + Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method: + Sign in with my Matrix identifier + Sign in + Enter your identifier and your password + User identifier + This is not a valid user identifier. Expected format: \'@user:homeserver.org\' + Unable to find a valid homeserver. Please check your identifier +