Login screens: refacto: create an AuthenticationWizard

This commit is contained in:
Benoit Marty 2019-11-21 22:04:52 +01:00
parent 90027cc4d5
commit 6723a566c2
5 changed files with 246 additions and 143 deletions

View file

@ -0,0 +1,48 @@
/*
* 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.matrix.android.api.auth
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
interface AuthenticationWizard {
/**
* @param login the login field
* @param password the password field
* @param deviceName the initial device name
* @param callback the matrix callback on which you'll receive the result of authentication.
* @return return a [Cancelable]
*/
fun authenticate(login: String,
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Reset user password
*/
fun resetPassword(email: String,
newPassword: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Confirm the new password, once the user has checked his email
*/
fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable
}

View file

@ -26,7 +26,6 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
/**
* This interface defines methods to authenticate to a matrix server.
* TODO Some methods has to be moved to and authenticationWizard, has it is done for registration
*/
interface Authenticator {
@ -36,18 +35,10 @@ interface Authenticator {
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable
/**
* @param homeServerConnectionConfig this param is used to configure the Homeserver
* @param login the login field
* @param password the password field
* @param deviceName the initial device name
* @param callback the matrix callback on which you'll receive the result of authentication.
* @return return a [Cancelable]
* Return an AuthenticationWizard
* @param homeServerConnectionConfig this param is used to request the Homeserver
*/
fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable
fun createAuthenticationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): AuthenticationWizard
/**
* Check if there is an authenticated [Session].
@ -73,15 +64,7 @@ interface Authenticator {
/**
* Create a session after a SSO successful login
*/
fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<Session>): Cancelable
/**
* Reset user password
*/
fun resetPassword(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable
/**
* Confirm the new password, once the user has check his email
*/
fun resetPasswordMailConfirmed(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<Unit>): Cancelable
fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials,
callback: MatrixCallback<Session>): Cancelable
}

View file

@ -0,0 +1,153 @@
/*
* 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.matrix.android.internal.auth
import android.util.Patterns
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationWizard
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.SessionParams
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.SessionManager
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.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
import im.vector.matrix.android.internal.auth.registration.RegisterAddThreePidTask
import im.vector.matrix.android.internal.auth.signin.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import java.util.*
// Container to store the data when a reset password is in the email validation step
internal data class ResetPasswordData(
val newPassword: String,
val addThreePidRegistrationResponse: AddThreePidRegistrationResponse
)
internal class DefaultAuthenticationWizard(
private val homeServerConnectionConfig: HomeServerConnectionConfig,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager,
retrofitFactory: RetrofitFactory,
okHttpClient: Lazy<OkHttpClient>
) : AuthenticationWizard {
private var clientSecret = UUID.randomUUID().toString()
private var sendAttempt = 0
private var resetPasswordData: ResetPasswordData? = null
private val authAPI = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
.create(AuthAPI::class.java)
override fun authenticate(login: String,
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
authenticate(login, password, deviceName)
}
sessionOrFailure.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun authenticate(login: String,
password: String,
deviceName: String) = withContext(coroutineDispatchers.io) {
val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName)
} else {
PasswordLoginParams.userIdentifier(login, password, deviceName)
}
val credentials = executeRequest<Credentials> {
apiCall = authAPI.login(loginParams)
}
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
sessionManager.getOrCreateSession(sessionParams)
}
override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {
resetPasswordInternal(email, newPassword)
}
result.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun resetPasswordInternal(email: String, newPassword: String) {
val param = RegisterAddThreePidTask.Params(
RegisterThreePid.Email(email),
clientSecret,
sendAttempt++
)
val result = executeRequest<AddThreePidRegistrationResponse> {
apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param))
}
resetPasswordData = ResetPasswordData(newPassword, result)
}
override fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable {
val safeResetPasswordData = resetPasswordData ?: run {
callback.onFailure(IllegalStateException("developer error, no reset password in progress"))
return NoOpCancellable
}
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {
resetPasswordMailConfirmedInternal(safeResetPasswordData)
}
result.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun resetPasswordMailConfirmedInternal(resetPasswordData: ResetPasswordData) {
val param = ResetPasswordMailConfirmed.create(
clientSecret,
resetPasswordData.addThreePidRegistrationResponse.sid,
resetPasswordData.newPassword
)
executeRequest<Unit> {
apiCall = authAPI.resetPasswordMailConfirmed(param)
}
// Set to null?
// resetPasswordData = null
}
}

View file

@ -16,25 +16,17 @@
package im.vector.matrix.android.internal.auth
import android.util.Patterns
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationWizard
import im.vector.matrix.android.api.auth.Authenticator
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.SessionParams
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.SessionManager
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.ThreePidMedium
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
import im.vector.matrix.android.internal.auth.registration.RegisterAddThreePidTask
import im.vector.matrix.android.internal.auth.signin.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.network.RetrofitFactory
@ -45,15 +37,8 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import java.util.*
import javax.inject.Inject
// Container to store the data when a reset password is in the email validation step
internal data class ResetPasswordData(
val newPassword: String,
val addThreePidRegistrationResponse: AddThreePidRegistrationResponse
)
internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
@ -61,10 +46,6 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager
) : Authenticator {
private var clientSecret = UUID.randomUUID().toString()
private var sendAttempt = 0
private var resetPasswordData: ResetPasswordData? = null
override fun hasAuthenticatedSessions(): Boolean {
return sessionParamsStore.getLast() != null
@ -91,20 +72,6 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
return CancelableCoroutine(job)
}
override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
authenticate(homeServerConnectionConfig, login, password, deviceName)
}
sessionOrFailure.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
@ -113,26 +80,19 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
}
}
private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String,
deviceName: String) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName)
} else {
PasswordLoginParams.userIdentifier(login, password, deviceName)
}
val credentials = executeRequest<Credentials> {
apiCall = authAPI.login(loginParams)
}
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
sessionManager.getOrCreateSession(sessionParams)
override fun createAuthenticationWizard(homeServerConnectionConfig: HomeServerConnectionConfig): AuthenticationWizard {
return DefaultAuthenticationWizard(
homeServerConnectionConfig,
coroutineDispatchers,
sessionParamsStore,
sessionManager,
retrofitFactory,
okHttpClient
)
}
override fun createSessionFromSso(credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig,
override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
@ -150,63 +110,6 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
sessionManager.getOrCreateSession(sessionParams)
}
override fun resetPassword(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {
resetPasswordInternal(homeServerConnectionConfig, email, newPassword)
}
result.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun resetPasswordInternal(homeServerConnectionConfig: HomeServerConnectionConfig, email: String, newPassword: String) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
val param = RegisterAddThreePidTask.Params(
RegisterThreePid.Email(email),
clientSecret,
sendAttempt++
)
val result = executeRequest<AddThreePidRegistrationResponse> {
apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param))
}
resetPasswordData = ResetPasswordData(newPassword, result)
}
override fun resetPasswordMailConfirmed(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<Unit>): Cancelable {
val safeResetPasswordData = resetPasswordData ?: run {
callback.onFailure(IllegalStateException("developer error, no reset password in progress"))
return NoOpCancellable
}
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {
resetPasswordMailConfirmedInternal(homeServerConnectionConfig, safeResetPasswordData)
}
result.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun resetPasswordMailConfirmedInternal(homeServerConnectionConfig: HomeServerConnectionConfig, resetPasswordData: ResetPasswordData) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
val param = ResetPasswordMailConfirmed.create(
clientSecret,
resetPasswordData.addThreePidRegistrationResponse.sid,
resetPasswordData.newPassword
)
executeRequest<Unit> {
apiCall = authAPI.resetPasswordMailConfirmed(param)
}
// Set to null?
// resetPasswordData = null
}
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)

View file

@ -21,6 +21,7 @@ import com.airbnb.mvrx.*
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.AuthenticationWizard
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.registration.FlowResult
@ -74,6 +75,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
private set
private var registrationWizard: RegistrationWizard? = null
private var authenticationWizard: AuthenticationWizard? = null
var serverType: ServerType = ServerType.MatrixOrg
private set
@ -294,6 +296,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
if (signMode == SignMode.SignUp) {
startRegistrationFlow()
} else if (signMode == SignMode.SignIn) {
startAuthenticationFlow()
}
}
@ -306,9 +310,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
private fun handleResetPassword(action: LoginAction.ResetPassword) {
val homeServerConnectionConfigFinal = homeServerConnectionConfig
val safeAuthenticationWizard = authenticationWizard
if (homeServerConnectionConfigFinal == null) {
if (safeAuthenticationWizard == null) {
setState {
copy(
asyncResetPassword = Fail(Throwable("Bad configuration"))
@ -323,7 +327,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
)
}
currentTask = authenticator.resetPassword(homeServerConnectionConfigFinal, action.email, action.newPassword, object : MatrixCallback<Unit> {
currentTask = safeAuthenticationWizard.resetPassword(action.email, action.newPassword, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
setState {
copy(
@ -345,9 +349,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
private fun handleResetPasswordMailConfirmed() {
val homeServerConnectionConfigFinal = homeServerConnectionConfig
val safeAuthenticationWizard = authenticationWizard
if (homeServerConnectionConfigFinal == null) {
if (safeAuthenticationWizard == null) {
setState {
copy(
asyncResetMailConfirmed = Fail(Throwable("Bad configuration"))
@ -360,7 +364,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
)
}
currentTask = authenticator.resetPasswordMailConfirmed(homeServerConnectionConfigFinal, object : MatrixCallback<Unit> {
currentTask = safeAuthenticationWizard.resetPasswordMailConfirmed(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
setState {
copy(
@ -382,9 +386,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
private fun handleLogin(action: LoginAction.Login) {
val homeServerConnectionConfigFinal = homeServerConnectionConfig
val safeAuthenticationWizard = authenticationWizard
if (homeServerConnectionConfigFinal == null) {
if (safeAuthenticationWizard == null) {
setState {
copy(
asyncLoginAction = Fail(Throwable("Bad configuration"))
@ -397,8 +401,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
)
}
currentTask = authenticator.authenticate(
homeServerConnectionConfigFinal,
currentTask = safeAuthenticationWizard.authenticate(
action.login,
action.password,
action.initialDeviceName,
@ -443,6 +446,17 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
}
private fun startAuthenticationFlow() {
val homeServerConnectionConfigFinal = homeServerConnectionConfig
if (homeServerConnectionConfigFinal == null) {
// Notify the user
_viewEvents.post(LoginViewEvents.RegistrationError(Throwable("Bad configuration")))
} else {
authenticationWizard = authenticator.createAuthenticationWizard(homeServerConnectionConfigFinal)
}
}
private fun onFlowResponse(flowResult: FlowResult) {
// Notify the user
_viewEvents.post(LoginViewEvents.RegistrationFlowResult(flowResult))
@ -465,7 +479,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
// Should not happen
Timber.w("homeServerConnectionConfig is null")
} else {
authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfigFinal, object : MatrixCallback<Session> {
authenticator.createSessionFromSso(homeServerConnectionConfigFinal, action.credentials, object : MatrixCallback<Session> {
override fun onSuccess(data: Session) {
onSessionCreated(data)
}
@ -493,6 +507,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
currentTask?.cancel()
currentTask = null
homeServerConnectionConfig = newConfig
authenticationWizard = null
registrationWizard = null
val homeServerConnectionConfigFinal = homeServerConnectionConfig