Merge pull request #1451 from vector-im/feature/sso_redirect

Correctly handle SSO login redirection
This commit is contained in:
Benoit Marty 2020-06-08 18:06:00 +02:00 committed by GitHub
commit dffe096c59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 418 additions and 79 deletions

View file

@ -9,6 +9,8 @@ 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
- SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400)
Bugfix 🐛:
- Switch theme is not fully taken into account without restarting the app

View file

@ -2,7 +2,7 @@
This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver.
## Sign up flows
## Sign in flows
### Get the flow
@ -58,7 +58,7 @@ We get credential (200)
```json
{
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lr",
"home_server": "matrix.org",
"device_id": "GTVREDALBF",
"well_known": {
@ -117,7 +117,7 @@ We get the credentials (200)
```json
{
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3Jnfrfdegfszsefddvf",
"home_server": "matrix.org",
"device_id": "WBSREDASND",
"well_known": {
@ -145,12 +145,59 @@ Not supported yet in RiotX
"flows": [
{
"type": "m.login.sso"
},
{
"type": "m.login.token"
}
]
}
```
In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge
In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx
The parameter `redirectUrl` is set to `riotx://riotx`.
ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs)
The browser will then take care of the SSO login, which may include creating a third party account, entering an email, settings a display name, or any other possibilities.
During the process, user may be asked to validate an email by clicking on a link it contains. The link has to be opened in the browser which initiates the authentication. This is why we cannot use WebView anymore.
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
```json
{
"type": "m.login.token",
"token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"
}
```
We get the credentials (200)
```json
{
"user_id": "@alice:homeserver.with.sso",
"access_token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVyIGtleQowMDEwY2lkIGdlbiA9IDEKMDAyY2NpZCB1c2",
"home_server": "homeserver.with.sso",
"device_id": "DETBTVAHCH",
"well_known": {
"m.homeserver": {
"base_url": "https:\/\/homeserver.with.sso\/"
},
"m.identity_server": {
"base_url": "https:\/\/vector.im"
}
}
}
```
## Reset password

View file

@ -32,6 +32,6 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/"
* Path to use when the client want to connect using SSO
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
*/
const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect"
const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
const val SSO_REDIRECT_URL_PARAM = "redirectUrl"

View file

@ -18,10 +18,10 @@ package im.vector.matrix.android.api.auth.data
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
// Either a LoginFlowResponse, or an error if the homeserver is outdated
// Either a list of supported login types, or an error if the homeserver is outdated
sealed class LoginFlowResult {
data class Success(
val loginFlowResponse: LoginFlowResponse,
val supportedLoginTypes: List<String>,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String
) : LoginFlowResult()

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.data
package im.vector.matrix.android.api.auth.data
object LoginFlowTypes {
const val PASSWORD = "m.login.password"

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,15 +21,12 @@ sealed class Stage(open val mandatory: Boolean) {
// m.login.recaptcha
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
// m.login.oauth2
// m.login.email.identity
data class Email(override val mandatory: Boolean) : Stage(mandatory)
// m.login.msisdn
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
// m.login.token
// m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
// and a password, the dummy stage has to be done
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)

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
@ -54,7 +55,7 @@ internal interface AuthAPI {
fun versions(): Call<Versions>
/**
* Register to the homeserver
* Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete
* Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
@ -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

@ -236,7 +236,7 @@ internal class DefaultAuthenticationService @Inject constructor(
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
} else {
// Not supported
LoginFlowResult.OutdatedHomeserver

View file

@ -20,7 +20,19 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LoginFlowResponse(
internal data class LoginFlowResponse(
/**
* The homeserver's supported login types
*/
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>
val flows: List<LoginFlow>?
)
@JsonClass(generateAdapter = true)
internal data class LoginFlow(
/**
* The login type. This is supplied as the type when logging in.
*/
@Json(name = "type")
val type: String?
)

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* Ref:

View file

@ -0,0 +1,27 @@
/*
* 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 im.vector.matrix.android.api.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
internal data class TokenLoginParams(
@Json(name = "type") override val type: String = LoginFlowTypes.TOKEN,
@Json(name = "token") val token: String
) : 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

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* Open class, parent to all possible authentication parameters

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.registration
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.auth.registration.RegistrationResult
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
@ -28,7 +29,6 @@ import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.task.launchToCallback

View file

@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.auth.registration.TermPolicies
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
data class RegistrationFlowResponse(

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* This class provides the authentication data by using user and password

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth

View file

@ -332,6 +332,9 @@ dependencies {
implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version"
// Custom Tab
implementation 'androidx.browser:browser:1.2.0'
// Passphrase strength helper
implementation 'com.nulab-inc:zxcvbn:1.2.7'

View file

@ -48,7 +48,19 @@
<activity android:name=".features.home.HomeActivity" />
<activity
android:name=".features.login.LoginActivity"
android:windowSoftInputMode="adjustResize" />
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="riotx" />
<data android:host="riotx" />
</intent-filter>
</activity>
<activity android:name=".features.media.ImageMediaViewerActivity" />
<activity android:name=".features.media.BigImageViewerActivity" />
<activity

View file

@ -59,6 +59,7 @@ import im.vector.riotx.features.login.LoginResetPasswordSuccessFragment
import im.vector.riotx.features.login.LoginServerSelectionFragment
import im.vector.riotx.features.login.LoginServerUrlFormFragment
import im.vector.riotx.features.login.LoginSignUpSignInSelectionFragment
import im.vector.riotx.features.login.LoginSignUpSignInSsoFragment
import im.vector.riotx.features.login.LoginSplashFragment
import im.vector.riotx.features.login.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment
@ -217,6 +218,11 @@ interface FragmentModule {
@FragmentKey(LoginSignUpSignInSelectionFragment::class)
fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSignUpSignInSsoFragment::class)
fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginSplashFragment::class)

View file

@ -21,10 +21,14 @@ import android.content.ActivityNotFoundException
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.provider.Browser
import android.provider.MediaStore
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsSession
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import im.vector.riotx.BuildConfig
@ -64,6 +68,34 @@ fun openUrlInExternalBrowser(context: Context, uri: Uri?) {
}
}
/**
* Open url in custom tab or, if not available, in the default browser
* If several compatible browsers are installed, the user will be proposed to choose one.
* Ref: https://developer.chrome.com/multidevice/android/customtabs
*/
fun openUrlInChromeCustomTab(context: Context, session: CustomTabsSession?, url: String) {
try {
CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(context, R.color.riotx_background_light))
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_header_panel_background_light))
}
}
.setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_background_light))
.setColorScheme(CustomTabsIntent.COLOR_SCHEME_LIGHT)
// Note: setting close button icon does not work
.setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp))
.setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
.setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
.apply { session?.let { setSession(it) } }
.build()
.launchUrl(context, Uri.parse(url))
} catch (activityNotFoundException: ActivityNotFoundException) {
context.toast(R.string.error_no_external_application_found)
}
}
/**
* Open sound recorder external application
*/

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.crypto.recover
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
@ -28,7 +29,6 @@ import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion

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

@ -33,6 +33,7 @@ import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE
@ -155,7 +156,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected()
is LoginViewEvents.OnLoginFlowRetrieved ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginSignUpSignInSelectionFragment::class.java,
if (loginViewEvents.isSso) {
LoginSignUpSignInSsoFragment::class.java
} else {
LoginSignUpSignInSelectionFragment::class.java
},
option = commonOption)
is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents)
is LoginViewEvents.OnForgetPasswordClicked ->
@ -239,16 +244,14 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
SignMode.SignIn -> {
// It depends on the LoginMode
when (state.loginMode) {
LoginMode.Unknown -> error("Developer error")
LoginMode.Unknown,
LoginMode.Sso -> error("Developer error")
LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java,
tag = FRAGMENT_LOGIN_TAG,
option = commonOption)
LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginWebFragment::class.java,
option = commonOption)
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
}
}.exhaustive
}
SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java,
@ -257,6 +260,17 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
}.exhaustive
}
/**
* Handle the SSO redirection here
*/
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.data
?.let { tryThis { it.getQueryParameter("loginToken") } }
?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) }
}
private fun onRegistrationStageNotSupported() {
AlertDialog.Builder(this)
.setTitle(R.string.app_name)

View file

@ -55,7 +55,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
}
@OnClick(R.id.loginServerChoiceModularLearnMore)
fun learMore() {
fun learnMore() {
openUrlInExternalBrowser(requireActivity(), MODULAR_LINK)
}
@ -113,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
if (state.loginMode != LoginMode.Unknown) {
// LoginFlow for matrix.org has been retrieved
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso)))
}
}
}

View file

@ -124,7 +124,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
if (state.loginMode != LoginMode.Unknown) {
// The home server url is valid
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso)))
}
}
}

View file

@ -26,13 +26,11 @@ import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in to the homeserver
*/
class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
private var isSsoSignIn: Boolean = false
private fun setupUi(state: LoginViewState) {
protected fun setupUi(state: LoginViewState) {
when (state.serverType) {
ServerType.MatrixOrg -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
@ -54,25 +52,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
}
}
private fun setupButtons(state: LoginViewState) {
isSsoSignIn = state.loginMode == LoginMode.Sso
if (isSsoSignIn) {
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
loginSignupSigninSignIn.isVisible = false
} else {
loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
}
private fun setupButtons() {
loginSignupSigninSubmit.text = getString(R.string.login_signup)
loginSignupSigninSignIn.isVisible = true
}
@OnClick(R.id.loginSignupSigninSubmit)
fun signUp() {
if (isSsoSignIn) {
signIn()
} else {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
}
open fun submit() {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
}
@OnClick(R.id.loginSignupSigninSignIn)
@ -86,6 +73,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupButtons(state)
setupButtons()
}
}

View file

@ -0,0 +1,99 @@
/*
* 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.riotx.features.login
import android.content.ComponentName
import android.net.Uri
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import androidx.core.view.isVisible
import im.vector.riotx.R
import im.vector.riotx.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in using SSO
* This Fragment binds a CustomTabsServiceConnection if available, then prefetch the SSO url, as it will be likely to be opened.
*/
open class LoginSignUpSignInSsoFragment @Inject constructor() : LoginSignUpSignInSelectionFragment() {
private var ssoUrl: String? = null
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
private var customTabsClient: CustomTabsClient? = null
private var customTabsSession: CustomTabsSession? = null
override fun onStart() {
super.onStart()
val packageName = CustomTabsClient.getPackageName(requireContext(), null)
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
if (packageName != null) {
customTabsServiceConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
customTabsClient = client
.also { it.warmup(0L) }
}
override fun onServiceDisconnected(name: ComponentName?) {
}
}
.also {
CustomTabsClient.bindCustomTabsService(
requireContext(),
// Despite the API, packageName cannot be null
packageName,
it
)
}
}
}
private fun prefetchUrl(url: String) {
if (ssoUrl != null) return
ssoUrl = url
if (customTabsSession == null) {
customTabsSession = customTabsClient?.newSession(null)
}
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
}
override fun onStop() {
super.onStop()
customTabsServiceConnection?.let { requireContext().unbindService(it) }
customTabsServiceConnection = null
}
private fun setupButtons() {
loginSignupSigninSubmit.text = getString(R.string.login_signin_sso)
loginSignupSigninSignIn.isVisible = false
}
override fun submit() {
ssoUrl?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) }
}
override fun updateWithState(state: LoginViewState) {
setupUi(state)
setupButtons()
prefetchUrl(state.getSsoUrl())
}
}

View file

@ -34,7 +34,7 @@ sealed class LoginViewEvents : VectorViewEvents {
object OpenServerSelection : LoginViewEvents()
object OnServerSelectionDone : LoginViewEvents()
object OnLoginFlowRetrieved : LoginViewEvents()
data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents()
object OnSignModeSelected : LoginViewEvents()
object OnForgetPasswordClicked : LoginViewEvents()
object OnResetPasswordSendThreePidDone : LoginViewEvents()

View file

@ -32,6 +32,7 @@ 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.data.LoginFlowTypes
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
@ -40,7 +41,6 @@ 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
@ -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(
@ -635,9 +671,9 @@ class LoginViewModel @AssistedInject constructor(
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
else -> LoginMode.Unsupported
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
@ -648,7 +684,7 @@ class LoginViewModel @AssistedInject constructor(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode,
loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList()
loginModeSupportedTypes = data.supportedLoginTypes.toList()
)
}
}

View file

@ -22,6 +22,9 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.PersistState
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.auth.SSO_REDIRECT_PATH
import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM
import im.vector.riotx.core.extensions.appendParamToUrl
data class LoginViewState(
val asyncLoginAction: Async<Unit> = Uninitialized,
@ -64,4 +67,22 @@ data class LoginViewState(
fun isUserLogged(): Boolean {
return asyncLoginAction is Success
}
fun getSsoUrl(): String {
return buildString {
append(homeServerUrl?.trim { it == '/' })
append(SSO_REDIRECT_PATH)
// Set a redirect url we will intercept later
appendParamToUrl(SSO_REDIRECT_URL_PARAM, RIOTX_REDIRECT_URL)
deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755
appendParamToUrl("device_id", it)
}
}
}
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 RIOTX_REDIRECT_URL = "riotx://riotx"
}
}

View file

@ -33,8 +33,6 @@ import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH
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.internal.di.MoshiProvider
import im.vector.riotx.R
@ -48,7 +46,7 @@ import java.net.URLDecoder
import javax.inject.Inject
/**
* This screen is displayed for SSO login and also when the application does not support login flow or registration flow
* This screen is displayed when the application does not support login flow or registration flow
* of the homeserver, as a fallback to login or to create an account
*/
class LoginWebFragment @Inject constructor(
@ -128,17 +126,7 @@ class LoginWebFragment @Inject constructor(
val url = buildString {
append(state.homeServerUrl?.trim { it == '/' })
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)
})
} else {
append(LOGIN_FALLBACK_PATH)
}
append(LOGIN_FALLBACK_PATH)
state.deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755
appendParamToUrl("device_id", it)
@ -226,7 +214,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

View file

@ -22,10 +22,10 @@ import com.airbnb.mvrx.ViewModelContext
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.data.LoginFlowTypes
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.rx.rx

View file

@ -30,13 +30,13 @@ 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.NoOpMatrixCallback
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo

View file

@ -28,9 +28,9 @@ 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.LoginFlowResult
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
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.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.hasUnsavedKeys
import im.vector.riotx.core.platform.VectorViewModel
@ -105,9 +105,9 @@ class SoftLogoutViewModel @AssistedInject constructor(
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
else -> LoginMode.Unsupported
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {

View file

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M20,12H4"
android:strokeWidth="2"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"
android:strokeLineJoin="round"
tools:strokeColor="#00F000" />
<path
android:fillColor="#00000000"
android:pathData="M10,18L4,12L10,6"
android:strokeWidth="2"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"
android:strokeLineJoin="round"
tools:strokeColor="#00F000" />
</vector>