From ae7a52cecf1adb6a36cea27efd04e1dc4eccc1ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 4 Jun 2020 16:55:27 +0200 Subject: [PATCH] Correctly handle SSO login redirection --- CHANGES.md | 1 + .../android/api/auth/login/LoginWizard.kt | 6 ++++ .../matrix/android/internal/auth/AuthAPI.kt | 6 ++++ .../internal/auth/data/TokenLoginParams.kt | 30 ++++++++++++++++ .../internal/auth/login/DefaultLoginWizard.kt | 17 +++++++++ .../riotx/features/login/LoginAction.kt | 1 + .../riotx/features/login/LoginViewModel.kt | 36 +++++++++++++++++++ .../riotx/features/login/LoginWebFragment.kt | 29 +++++++++++---- 8 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt diff --git a/CHANGES.md b/CHANGES.md index 33d749a7e9..7c3ea5865a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Improvements 🙌: - New wording for notice when current user is the sender - Hide "X made no changes" event by default in timeline (#1430) - Hide left rooms in breadcrumbs (#766) + - Correctly handle SSO login redirection Bugfix 🐛: - Switch theme is not fully taken into account without restarting the app diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt index d7b2f5d960..9c296d5ddb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt @@ -34,6 +34,12 @@ interface LoginWizard { deviceName: String, callback: MatrixCallback): Cancelable + /** + * Exchange a login token to an access token + */ + fun loginWithToken(loginToken: String, + callback: MatrixCallback): Cancelable + /** * Reset user password */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index 2f03c99421..5363e15d6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.RiotConfig +import im.vector.matrix.android.internal.auth.data.TokenLoginParams import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse @@ -91,6 +92,11 @@ internal interface AuthAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") fun login(@Body loginParams: PasswordLoginParams): Call + // Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun login(@Body loginParams: TokenLoginParams): Call + /** * Ask the homeserver to reset the password associated with the provided email. */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt new file mode 100644 index 0000000000..cf9c6a8e5b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt @@ -0,0 +1,30 @@ +/* + * 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.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.UUID + +@JsonClass(generateAdapter = true) +internal data class TokenLoginParams( + @Json(name = "type") override val type: String = LoginFlowTypes.TOKEN, + @Json(name = "token") val token: String, + // client generated nonce + @Json(name = "txn_id") val txId: String = UUID.randomUUID().toString() + // Param session is not useful in this case? +) : LoginParams diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt index 132073b340..2ce9372903 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.auth.PendingSessionStore import im.vector.matrix.android.internal.auth.SessionCreator import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.ThreePidMedium +import im.vector.matrix.android.internal.auth.data.TokenLoginParams import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse @@ -65,6 +66,22 @@ internal class DefaultLoginWizard( } } + /** + * Ref: https://matrix.org/docs/spec/client_server/latest#handling-the-authentication-endpoint + */ + override fun loginWithToken(loginToken: String, callback: MatrixCallback): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + val loginParams = TokenLoginParams( + token = loginToken + ) + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + } + } + private suspend fun loginInternal(login: String, password: String, deviceName: String) = withContext(coroutineDispatchers.computation) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 3403760136..afd27f04a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -24,6 +24,7 @@ sealed class LoginAction : VectorViewModelAction { data class UpdateServerType(val serverType: ServerType) : LoginAction() data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class UpdateSignMode(val signMode: SignMode) : LoginAction() + data class LoginWithToken(val loginToken: String) : LoginAction() data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() data class ResetPassword(val email: String, val newPassword: String) : LoginAction() 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 81dcfcea9f..12dfd06b37 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 @@ -110,6 +110,7 @@ class LoginViewModel @AssistedInject constructor( is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.LoginOrRegister -> handleLoginOrRegister(action) + is LoginAction.LoginWithToken -> handleLoginWithToken(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() @@ -120,6 +121,41 @@ class LoginViewModel @AssistedInject constructor( }.exhaustive } + private fun handleLoginWithToken(action: LoginAction.LoginWithToken) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + setState { + copy( + asyncLoginAction = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncLoginAction = Loading() + ) + } + + currentTask = safeLoginWizard.loginWithToken( + action.loginToken, + object : MatrixCallback { + override fun onSuccess(data: Session) { + onSessionCreated(data) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(LoginViewEvents.Failure(failure)) + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) + } + } + private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index cf3b39ebb0..7e16bb40e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -21,6 +21,7 @@ package im.vector.riotx.features.login import android.annotation.SuppressLint import android.content.DialogInterface import android.graphics.Bitmap +import android.net.Uri import android.net.http.SslError import android.os.Build import android.os.Bundle @@ -36,6 +37,7 @@ import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH import im.vector.matrix.android.api.auth.SSO_FALLBACK_PATH import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R import im.vector.riotx.core.extensions.appendParamToUrl @@ -55,6 +57,11 @@ class LoginWebFragment @Inject constructor( private val assetReader: AssetReader ) : AbstractLoginFragment() { + companion object { + // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string + private const val REDIRECT_URL = "riotx://riotx" + } + override fun getLayoutResId() = R.layout.fragment_login_web private var isWebViewLoaded = false @@ -130,12 +137,8 @@ class LoginWebFragment @Inject constructor( if (state.signMode == SignMode.SignIn) { if (state.loginMode == LoginMode.Sso) { append(SSO_FALLBACK_PATH) - // We do not want to deal with the result, so let the fallback login page to handle it for us - appendParamToUrl(SSO_REDIRECT_URL_PARAM, - buildString { - append(state.homeServerUrl?.trim { it == '/' }) - append(LOGIN_FALLBACK_PATH) - }) + // Set a redirect url we will intercept later + appendParamToUrl(SSO_REDIRECT_URL_PARAM, REDIRECT_URL) } else { append(LOGIN_FALLBACK_PATH) } @@ -226,7 +229,9 @@ class LoginWebFragment @Inject constructor( * @return */ override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { - if (null != url && url.startsWith("js:")) { + if (url == null) return super.shouldOverrideUrlLoading(view, url as String?) + + if (url.startsWith("js:")) { var json = url.substring(3) var javascriptResponse: JavascriptResponse? = null @@ -256,6 +261,8 @@ class LoginWebFragment @Inject constructor( } } return true + } else if (url.startsWith(REDIRECT_URL)) { + return handleSsoLoginSuccess(url) } return super.shouldOverrideUrlLoading(view, url) @@ -263,6 +270,14 @@ class LoginWebFragment @Inject constructor( } } + private fun handleSsoLoginSuccess(url: String): Boolean { + val uri = Uri.parse(url) + val loginToken = tryThis { uri.getQueryParameter("loginToken") } ?: return false + + loginViewModel.handle(LoginAction.LoginWithToken(loginToken)) + return true + } + private fun notifyViewModel(credentials: Credentials) { if (isForSessionRecovery) { val softLogoutViewModel: SoftLogoutViewModel by activityViewModel()