Login: add SSO support
This commit is contained in:
parent
db8ea0f5e8
commit
5fbd271b1c
@ -17,6 +17,7 @@
|
|||||||
package im.vector.matrix.android.api.auth
|
package im.vector.matrix.android.api.auth
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
@ -62,4 +63,9 @@ interface Authenticator {
|
|||||||
* @return the associated session if any, or null
|
* @return the associated session if any, or null
|
||||||
*/
|
*/
|
||||||
fun getSession(sessionParams: SessionParams): Session?
|
fun getSession(sessionParams: SessionParams): Session?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a session after a SSO successful login
|
||||||
|
*/
|
||||||
|
fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
|
||||||
}
|
}
|
@ -112,6 +112,12 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
|
|||||||
sessionManager.getOrCreateSession(sessionParams)
|
sessionManager.getOrCreateSession(sessionParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
|
||||||
|
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
|
||||||
|
sessionParamsStore.save(sessionParams)
|
||||||
|
return sessionManager.getOrCreateSession(sessionParams)
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
||||||
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
|
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
|
||||||
return retrofit.create(AuthAPI::class.java)
|
return retrofit.create(AuthAPI::class.java)
|
||||||
|
@ -30,4 +30,12 @@ data class InteractiveAuthenticationFlow(
|
|||||||
|
|
||||||
@Json(name = "stages")
|
@Json(name = "stages")
|
||||||
val stages: List<String>? = null
|
val stages: List<String>? = null
|
||||||
)
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Possible values for type
|
||||||
|
const val TYPE_LOGIN_SSO = "m.login.sso"
|
||||||
|
const val TYPE_LOGIN_TOKEN = "m.login.token"
|
||||||
|
const val TYPE_LOGIN_PASSWORD = "m.login.password"
|
||||||
|
}
|
||||||
|
}
|
@ -48,6 +48,7 @@ import im.vector.riotx.features.home.room.list.RoomListFragment
|
|||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
import im.vector.riotx.features.login.LoginActivity
|
import im.vector.riotx.features.login.LoginActivity
|
||||||
import im.vector.riotx.features.login.LoginFragment
|
import im.vector.riotx.features.login.LoginFragment
|
||||||
|
import im.vector.riotx.features.login.LoginSsoFallbackFragment
|
||||||
import im.vector.riotx.features.media.ImageMediaViewerActivity
|
import im.vector.riotx.features.media.ImageMediaViewerActivity
|
||||||
import im.vector.riotx.features.media.VideoMediaViewerActivity
|
import im.vector.riotx.features.media.VideoMediaViewerActivity
|
||||||
import im.vector.riotx.features.navigation.Navigator
|
import im.vector.riotx.features.navigation.Navigator
|
||||||
@ -127,6 +128,8 @@ interface ScreenComponent {
|
|||||||
|
|
||||||
fun inject(loginFragment: LoginFragment)
|
fun inject(loginFragment: LoginFragment)
|
||||||
|
|
||||||
|
fun inject(loginSsoFallbackFragment: LoginSsoFallbackFragment)
|
||||||
|
|
||||||
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
||||||
|
|
||||||
fun inject(quickReactionFragment: QuickReactionFragment)
|
fun inject(quickReactionFragment: QuickReactionFragment)
|
||||||
|
@ -16,9 +16,12 @@
|
|||||||
|
|
||||||
package im.vector.riotx.features.login
|
package im.vector.riotx.features.login
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
|
||||||
sealed class LoginActions {
|
sealed class LoginActions {
|
||||||
|
|
||||||
data class UpdateHomeServer(val homeServerUrl: String) : LoginActions()
|
data class UpdateHomeServer(val homeServerUrl: String) : LoginActions()
|
||||||
data class Login(val login: String, val password: String) : LoginActions()
|
data class Login(val login: String, val password: String) : LoginActions()
|
||||||
|
data class SsoLoginSuccess(val credentials: Credentials) : LoginActions()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,16 +18,30 @@ package im.vector.riotx.features.login
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.viewModel
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.extensions.addFragment
|
import im.vector.riotx.core.extensions.addFragment
|
||||||
|
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||||
|
import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
||||||
|
import im.vector.riotx.features.home.HomeActivity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
class LoginActivity : VectorBaseActivity() {
|
class LoginActivity : VectorBaseActivity() {
|
||||||
|
|
||||||
|
// Supported navigation actions for this Activity
|
||||||
|
sealed class Navigation {
|
||||||
|
object OpenSsoLoginFallback : Navigation()
|
||||||
|
object GoBack : Navigation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val loginViewModel: LoginViewModel by viewModel()
|
||||||
|
|
||||||
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
|
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
|
||||||
|
|
||||||
|
|
||||||
@ -41,6 +55,21 @@ class LoginActivity : VectorBaseActivity() {
|
|||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
addFragment(LoginFragment(), R.id.simpleFragmentContainer)
|
addFragment(LoginFragment(), R.id.simpleFragmentContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginViewModel.navigationLiveData.observeEvent(this) {
|
||||||
|
when (it) {
|
||||||
|
is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(LoginSsoFallbackFragment(), R.id.simpleFragmentContainer)
|
||||||
|
is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) {
|
||||||
|
if (it is Success) {
|
||||||
|
val intent = HomeActivity.newIntent(this)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
import com.airbnb.mvrx.*
|
import com.airbnb.mvrx.*
|
||||||
import com.jakewharton.rxbinding3.view.focusChanges
|
import com.jakewharton.rxbinding3.view.focusChanges
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
@ -30,12 +31,11 @@ import im.vector.riotx.core.extensions.setTextWithColoredPart
|
|||||||
import im.vector.riotx.core.extensions.showPassword
|
import im.vector.riotx.core.extensions.showPassword
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.utils.openUrlInExternalBrowser
|
import im.vector.riotx.core.utils.openUrlInExternalBrowser
|
||||||
import im.vector.riotx.features.home.HomeActivity
|
|
||||||
import im.vector.riotx.features.homeserver.ServerUrlsRepository
|
import im.vector.riotx.features.homeserver.ServerUrlsRepository
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.functions.Function3
|
import io.reactivex.functions.Function3
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
import kotlinx.android.synthetic.main.activity_login.*
|
import kotlinx.android.synthetic.main.fragment_login.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ class LoginFragment : VectorBaseFragment() {
|
|||||||
|
|
||||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.activity_login
|
override fun getLayoutResId() = R.layout.fragment_login
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
injector.inject(this)
|
injector.inject(this)
|
||||||
@ -107,6 +107,12 @@ class LoginFragment : VectorBaseFragment() {
|
|||||||
.subscribeBy { authenticateButton.isEnabled = it }
|
.subscribeBy { authenticateButton.isEnabled = it }
|
||||||
.disposeOnDestroy()
|
.disposeOnDestroy()
|
||||||
authenticateButton.setOnClickListener { authenticate() }
|
authenticateButton.setOnClickListener { authenticate() }
|
||||||
|
|
||||||
|
authenticateButtonSso.setOnClickListener { openSso() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSso() {
|
||||||
|
viewModel.openSso()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPasswordReveal() {
|
private fun setupPasswordReveal() {
|
||||||
@ -128,25 +134,53 @@ class LoginFragment : VectorBaseFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
TransitionManager.beginDelayedTransition(login_fragment)
|
||||||
|
|
||||||
when (state.asyncHomeServerLoginFlowRequest) {
|
when (state.asyncHomeServerLoginFlowRequest) {
|
||||||
is Loading -> {
|
is Incomplete -> {
|
||||||
progressBar.isVisible = true
|
progressBar.isVisible = true
|
||||||
touchArea.isVisible = true
|
touchArea.isVisible = true
|
||||||
|
loginField.isVisible = false
|
||||||
|
passwordContainer.isVisible = false
|
||||||
|
authenticateButton.isVisible = false
|
||||||
|
authenticateButtonSso.isVisible = false
|
||||||
passwordShown = false
|
passwordShown = false
|
||||||
renderPasswordField()
|
renderPasswordField()
|
||||||
}
|
}
|
||||||
is Fail -> {
|
is Fail -> {
|
||||||
progressBar.isVisible = false
|
progressBar.isVisible = false
|
||||||
touchArea.isVisible = false
|
touchArea.isVisible = false
|
||||||
|
loginField.isVisible = false
|
||||||
|
passwordContainer.isVisible = false
|
||||||
|
authenticateButton.isVisible = false
|
||||||
|
authenticateButtonSso.isVisible = false
|
||||||
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show()
|
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
is Success -> {
|
is Success -> {
|
||||||
progressBar.isVisible = false
|
progressBar.isVisible = false
|
||||||
touchArea.isVisible = false
|
touchArea.isVisible = false
|
||||||
|
|
||||||
// Check login flow
|
when (state.asyncHomeServerLoginFlowRequest()) {
|
||||||
// TODO
|
LoginMode.Password -> {
|
||||||
|
loginField.isVisible = true
|
||||||
|
passwordContainer.isVisible = true
|
||||||
|
authenticateButton.isVisible = true
|
||||||
|
authenticateButtonSso.isVisible = false
|
||||||
|
}
|
||||||
|
LoginMode.Sso -> {
|
||||||
|
loginField.isVisible = false
|
||||||
|
passwordContainer.isVisible = false
|
||||||
|
authenticateButton.isVisible = false
|
||||||
|
authenticateButtonSso.isVisible = true
|
||||||
|
}
|
||||||
|
LoginMode.Unsupported -> {
|
||||||
|
loginField.isVisible = false
|
||||||
|
passwordContainer.isVisible = false
|
||||||
|
authenticateButton.isVisible = false
|
||||||
|
authenticateButtonSso.isVisible = false
|
||||||
|
Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,11 +197,8 @@ class LoginFragment : VectorBaseFragment() {
|
|||||||
touchArea.isVisible = false
|
touchArea.isVisible = false
|
||||||
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show()
|
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
is Success -> {
|
// Success is handled by the LoginActivity
|
||||||
val intent = HomeActivity.newIntent(requireActivity())
|
is Success -> Unit
|
||||||
startActivity(intent)
|
|
||||||
requireActivity().finish()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,303 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.riotx.features.login
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.http.SslError
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.webkit.SslErrorHandler
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import kotlinx.android.synthetic.main.fragment_login_sso_fallback.*
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URLDecoder
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only login is supported for the moment
|
||||||
|
*/
|
||||||
|
class LoginSsoFallbackFragment : VectorBaseFragment() {
|
||||||
|
|
||||||
|
private val viewModel: LoginViewModel by activityViewModel()
|
||||||
|
|
||||||
|
var homeServerUrl: String = ""
|
||||||
|
|
||||||
|
enum class Mode {
|
||||||
|
MODE_LOGIN,
|
||||||
|
// Not supported in RiotX for the moment
|
||||||
|
MODE_REGISTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode (MODE_LOGIN or MODE_REGISTER)
|
||||||
|
private var mMode = Mode.MODE_LOGIN
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_login_sso_fallback
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
setupToolbar(login_sso_fallback_toolbar)
|
||||||
|
requireActivity().setTitle(R.string.login)
|
||||||
|
|
||||||
|
setupWebview()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
private fun setupWebview() {
|
||||||
|
login_sso_fallback_webview.settings.javaScriptEnabled = true
|
||||||
|
|
||||||
|
// Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack
|
||||||
|
// the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK)
|
||||||
|
login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google"
|
||||||
|
|
||||||
|
homeServerUrl = viewModel.getHomeServerUrl()
|
||||||
|
|
||||||
|
if (!homeServerUrl.endsWith("/")) {
|
||||||
|
homeServerUrl += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppRTC requires third party cookies to work
|
||||||
|
val cookieManager = android.webkit.CookieManager.getInstance()
|
||||||
|
|
||||||
|
// clear the cookies must be cleared
|
||||||
|
if (cookieManager == null) {
|
||||||
|
launchWebView()
|
||||||
|
} else {
|
||||||
|
if (!cookieManager.hasCookies()) {
|
||||||
|
launchWebView()
|
||||||
|
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
try {
|
||||||
|
cookieManager.removeAllCookie()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, " cookieManager.removeAllCookie() fails")
|
||||||
|
}
|
||||||
|
|
||||||
|
launchWebView()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
cookieManager.removeAllCookies { launchWebView() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, " cookieManager.removeAllCookie() fails")
|
||||||
|
launchWebView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchWebView() {
|
||||||
|
if (mMode == Mode.MODE_LOGIN) {
|
||||||
|
login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/login/")
|
||||||
|
} else {
|
||||||
|
// MODE_REGISTER
|
||||||
|
login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/register/")
|
||||||
|
}
|
||||||
|
|
||||||
|
login_sso_fallback_webview.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler,
|
||||||
|
error: SslError) {
|
||||||
|
AlertDialog.Builder(requireActivity())
|
||||||
|
.setMessage(R.string.ssl_could_not_verify)
|
||||||
|
.setPositiveButton(R.string.ssl_trust) { dialog, which -> handler.proceed() }
|
||||||
|
.setNegativeButton(R.string.ssl_do_not_trust) { dialog, which -> handler.cancel() }
|
||||||
|
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
||||||
|
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
handler.cancel()
|
||||||
|
dialog.dismiss()
|
||||||
|
return@OnKeyListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||||
|
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||||
|
|
||||||
|
// on error case, close this activity
|
||||||
|
viewModel.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
|
||||||
|
login_sso_fallback_toolbar.subtitle = url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
|
// avoid infinite onPageFinished call
|
||||||
|
if (url.startsWith("http")) {
|
||||||
|
// Generic method to make a bridge between JS and the UIWebView
|
||||||
|
val mxcJavascriptSendObjectMessage = "javascript:window.sendObjectMessage = function(parameters) {" +
|
||||||
|
" var iframe = document.createElement('iframe');" +
|
||||||
|
" iframe.setAttribute('src', 'js:' + JSON.stringify(parameters));" +
|
||||||
|
" document.documentElement.appendChild(iframe);" +
|
||||||
|
" iframe.parentNode.removeChild(iframe); iframe = null;" +
|
||||||
|
" };"
|
||||||
|
|
||||||
|
view.loadUrl(mxcJavascriptSendObjectMessage)
|
||||||
|
|
||||||
|
if (mMode == Mode.MODE_LOGIN) {
|
||||||
|
// The function the fallback page calls when the login is complete
|
||||||
|
val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" +
|
||||||
|
" sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" +
|
||||||
|
" };"
|
||||||
|
|
||||||
|
view.loadUrl(mxcJavascriptOnRegistered)
|
||||||
|
} else {
|
||||||
|
// MODE_REGISTER
|
||||||
|
// The function the fallback page calls when the registration is complete
|
||||||
|
val mxcJavascriptOnRegistered = "javascript:window.matrixRegistration.onRegistered" +
|
||||||
|
" = function(homeserverUrl, userId, accessToken) {" +
|
||||||
|
" sendObjectMessage({ 'action': 'onRegistered'," +
|
||||||
|
" 'homeServer': homeserverUrl," +
|
||||||
|
" 'userId': userId," +
|
||||||
|
" 'accessToken': accessToken });" +
|
||||||
|
" };"
|
||||||
|
|
||||||
|
view.loadUrl(mxcJavascriptOnRegistered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of (formatted) url for MODE_LOGIN:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* js:{
|
||||||
|
* "action":"onLogin",
|
||||||
|
* "credentials":{
|
||||||
|
* "user_id":"@user:matrix.org",
|
||||||
|
* "access_token":"[ACCESS_TOKEN]",
|
||||||
|
* "home_server":"matrix.org",
|
||||||
|
* "device_id":"[DEVICE_ID]",
|
||||||
|
* "well_known":{
|
||||||
|
* "m.homeserver":{
|
||||||
|
* "base_url":"https://matrix.org/"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* @param view
|
||||||
|
* @param url
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
|
||||||
|
if (null != url && url.startsWith("js:")) {
|
||||||
|
var json = url.substring(3)
|
||||||
|
var parameters: Map<String, Any>? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// URL decode
|
||||||
|
json = URLDecoder.decode(json, "UTF-8")
|
||||||
|
|
||||||
|
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
|
||||||
|
|
||||||
|
parameters = adapter.fromJson(json) as Map<String, Any>?
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// succeeds to parse parameters
|
||||||
|
if (parameters != null) {
|
||||||
|
val action = parameters["action"] as String
|
||||||
|
|
||||||
|
if (mMode == Mode.MODE_LOGIN) {
|
||||||
|
try {
|
||||||
|
if (action == "onLogin") {
|
||||||
|
val credentials = parameters["credentials"] as Map<String, String>
|
||||||
|
|
||||||
|
val userId = credentials["user_id"]
|
||||||
|
val accessToken = credentials["access_token"]
|
||||||
|
val homeServer = credentials["home_server"]
|
||||||
|
val deviceId = credentials["device_id"]
|
||||||
|
|
||||||
|
// check if the parameters are defined
|
||||||
|
if (null != homeServer && null != userId && null != accessToken) {
|
||||||
|
val credentials = Credentials(
|
||||||
|
userId = userId,
|
||||||
|
accessToken = accessToken,
|
||||||
|
homeServer = homeServer,
|
||||||
|
deviceId = deviceId,
|
||||||
|
refreshToken = null
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.handle(LoginActions.SsoLoginSuccess(credentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## shouldOverrideUrlLoading() : failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// MODE_REGISTER
|
||||||
|
// check the required parameters
|
||||||
|
if (action == "onRegistered") {
|
||||||
|
// TODO The keys are very strange, this code comes from Riot-Android...
|
||||||
|
if (parameters.containsKey("homeServer")
|
||||||
|
&& parameters.containsKey("userId")
|
||||||
|
&& parameters.containsKey("accessToken")) {
|
||||||
|
|
||||||
|
// We cannot parse Credentials here because of https://github.com/matrix-org/synapse/issues/4756
|
||||||
|
// Build on object manually
|
||||||
|
val credentials = Credentials(
|
||||||
|
userId = parameters["userId"] as String,
|
||||||
|
accessToken = parameters["accessToken"] as String,
|
||||||
|
homeServer = parameters["homeServer"] as String,
|
||||||
|
// TODO We need deviceId on RiotX...
|
||||||
|
deviceId = "TODO",
|
||||||
|
refreshToken = null
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.handle(LoginActions.SsoLoginSuccess(credentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.shouldOverrideUrlLoading(view, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return if (login_sso_fallback_webview.canGoBack()) {
|
||||||
|
login_sso_fallback_webview.goBack()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package im.vector.riotx.features.login
|
package im.vector.riotx.features.login
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.airbnb.mvrx.*
|
import com.airbnb.mvrx.*
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
@ -25,10 +27,12 @@ import im.vector.matrix.android.api.auth.Authenticator
|
|||||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.extensions.configureAndStart
|
import im.vector.riotx.core.extensions.configureAndStart
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.core.utils.LiveEvent
|
||||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||||
|
|
||||||
class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState,
|
class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState,
|
||||||
@ -50,9 +54,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
private val _navigationLiveData = MutableLiveData<LiveEvent<LoginActivity.Navigation>>()
|
||||||
|
val navigationLiveData: LiveData<LiveEvent<LoginActivity.Navigation>>
|
||||||
}
|
get() = _navigationLiveData
|
||||||
|
|
||||||
var homeServerConnectionConfig: HomeServerConnectionConfig? = null
|
var homeServerConnectionConfig: HomeServerConnectionConfig? = null
|
||||||
private var currentTask: Cancelable? = null
|
private var currentTask: Cancelable? = null
|
||||||
@ -62,6 +66,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
|
|||||||
when (action) {
|
when (action) {
|
||||||
is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action)
|
is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action)
|
||||||
is LoginActions.Login -> handleLogin(action)
|
is LoginActions.Login -> handleLogin(action)
|
||||||
|
is LoginActions.SsoLoginSuccess -> handleSsoLoginSuccess(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,14 +88,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
|
|||||||
|
|
||||||
authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback<Session> {
|
authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback<Session> {
|
||||||
override fun onSuccess(data: Session) {
|
override fun onSuccess(data: Session) {
|
||||||
activeSessionHolder.setActiveSession(data)
|
onSessionCreated(data)
|
||||||
data.configureAndStart(pushRuleTriggerListener)
|
|
||||||
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
asyncLoginAction = Success(Unit)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
@ -104,6 +102,24 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onSessionCreated(session: Session) {
|
||||||
|
activeSessionHolder.setActiveSession(session)
|
||||||
|
session.configureAndStart(pushRuleTriggerListener)
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncLoginAction = Success(Unit)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSsoLoginSuccess(action: LoginActions.SsoLoginSuccess) {
|
||||||
|
val session = authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfig!!)
|
||||||
|
|
||||||
|
onSessionCreated(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) {
|
private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) {
|
||||||
currentTask?.cancel()
|
currentTask?.cancel()
|
||||||
|
|
||||||
@ -120,18 +136,16 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
|
|||||||
// This is invalid
|
// This is invalid
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
asyncHomeServerLoginFlowRequest = Fail(Throwable("Baf format"))
|
asyncHomeServerLoginFlowRequest = Fail(Throwable("Bad format"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
asyncHomeServerLoginFlowRequest = Loading()
|
asyncHomeServerLoginFlowRequest = Loading()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
currentTask = authenticator.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback<LoginFlowResponse> {
|
currentTask = authenticator.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback<LoginFlowResponse> {
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
setState {
|
setState {
|
||||||
@ -142,28 +156,41 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSuccess(data: LoginFlowResponse) {
|
override fun onSuccess(data: LoginFlowResponse) {
|
||||||
setState {
|
val loginMode = when {
|
||||||
copy(
|
// SSO login is taken first
|
||||||
asyncHomeServerLoginFlowRequest = Success(data)
|
data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_SSO } -> LoginMode.Sso
|
||||||
)
|
data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_PASSWORD } -> LoginMode.Password
|
||||||
|
else -> LoginMode.Unsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoginFlowResponse(data)
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncHomeServerLoginFlowRequest = Success(loginMode)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoginFlowResponse(loginFlowResponse: LoginFlowResponse) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
|
||||||
currentTask?.cancel()
|
currentTask?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openSso() {
|
||||||
|
// Navigate to SSO
|
||||||
|
_navigationLiveData.postValue(LiveEvent(LoginActivity.Navigation.OpenSsoLoginFallback))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goBack() {
|
||||||
|
// Navigate back
|
||||||
|
_navigationLiveData.postValue(LiveEvent(LoginActivity.Navigation.GoBack))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHomeServerUrl(): String {
|
||||||
|
return homeServerConnectionConfig?.homeServerUri?.toString() ?: ""
|
||||||
|
}
|
||||||
}
|
}
|
@ -19,9 +19,15 @@ package im.vector.riotx.features.login
|
|||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
|
||||||
|
|
||||||
data class LoginViewState(
|
data class LoginViewState(
|
||||||
val asyncLoginAction: Async<Unit> = Uninitialized,
|
val asyncLoginAction: Async<Unit> = Uninitialized,
|
||||||
val asyncHomeServerLoginFlowRequest: Async<LoginFlowResponse> = Uninitialized
|
val asyncHomeServerLoginFlowRequest: Async<LoginMode> = Uninitialized
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
|
|
||||||
|
enum class LoginMode {
|
||||||
|
Password,
|
||||||
|
Sso,
|
||||||
|
Unsupported
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/login_fragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
@ -54,6 +55,7 @@
|
|||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
android:id="@+id/passwordContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp">
|
android:layout_marginTop="16dp">
|
||||||
@ -114,6 +116,16 @@
|
|||||||
android:layout_marginTop="22dp"
|
android:layout_marginTop="22dp"
|
||||||
android:text="@string/auth_login" />
|
android:text="@string/auth_login" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/authenticateButtonSso"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="22dp"
|
||||||
|
android:text="@string/auth_login_sso"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
22
vector/src/main/res/layout/fragment_login_sso_fallback.xml
Normal file
22
vector/src/main/res/layout/fragment_login_sso_fallback.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/login_sso_fallback_toolbar"
|
||||||
|
style="@style/VectorToolbarStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:subtitle="https://www.example.org"
|
||||||
|
tools:title="@string/auth_login" />
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/login_sso_fallback_webview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user