Correctly handle SSO login redirection

This commit is contained in:
Benoit Marty 2020-06-04 16:55:27 +02:00
parent 2e244dd448
commit ae7a52cecf
8 changed files with 119 additions and 7 deletions

View file

@ -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

View file

@ -34,6 +34,12 @@ interface LoginWizard {
deviceName: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Exchange a login token to an access token
*/
fun loginWithToken(loginToken: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Reset user password
*/

View file

@ -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<Credentials>
// 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<Credentials>
/**
* Ask the homeserver to reset the password associated with the provided email.
*/

View file

@ -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

View file

@ -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<Session>): Cancelable {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
val loginParams = TokenLoginParams(
token = loginToken
)
val credentials = executeRequest<Credentials>(null) {
apiCall = authAPI.login(loginParams)
}
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
}
}
private suspend fun loginInternal(login: String,
password: String,
deviceName: String) = withContext(coroutineDispatchers.computation) {

View file

@ -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()

View file

@ -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<Session> {
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(

View file

@ -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()