diff --git a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt
new file mode 100644
index 0000000000..54ee1d3a52
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2022 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.app.features.onboarding
+
+import android.net.Uri
+import im.vector.app.R
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister
+import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.auth.AuthenticationService
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
+import org.matrix.android.sdk.api.session.Session
+import javax.inject.Inject
+
+class DirectLoginUseCase @Inject constructor(
+        private val authenticationService: AuthenticationService,
+        private val stringProvider: StringProvider,
+) {
+
+    suspend fun execute(action: LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?): Result<Session> {
+        return fetchWellKnown(action.username, homeServerConnectionConfig)
+                .andThen { wellKnown -> createSessionFor(wellKnown, action, homeServerConnectionConfig) }
+    }
+
+    private suspend fun fetchWellKnown(matrixId: String, config: HomeServerConnectionConfig?) = runCatching {
+        authenticationService.getWellKnownData(matrixId, config)
+    }
+
+    private suspend fun createSessionFor(data: WellknownResult, action: LoginOrRegister, config: HomeServerConnectionConfig?) = when (data) {
+        is WellknownResult.Prompt     -> loginDirect(action, data, config)
+        is WellknownResult.FailPrompt -> handleFailPrompt(data, action, config)
+        else                          -> onWellKnownError()
+    }
+
+    private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginOrRegister, config: HomeServerConnectionConfig?): Result<Session> {
+        // Relax on IS discovery if homeserver is valid
+        val isMissingInformationToLogin = data.homeServerUrl == null || data.wellKnown == null
+        return when {
+            isMissingInformationToLogin -> onWellKnownError()
+            else                        -> loginDirect(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), config)
+        }
+    }
+
+    private suspend fun loginDirect(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result<Session> {
+        val alteredHomeServerConnectionConfig = config?.updateWith(wellKnownPrompt) ?: fallbackConfig(action, wellKnownPrompt)
+        return runCatching {
+            authenticationService.directAuthentication(
+                    alteredHomeServerConnectionConfig,
+                    action.username,
+                    action.password,
+                    action.initialDeviceName
+            )
+        }
+    }
+
+    private fun HomeServerConnectionConfig.updateWith(wellKnownPrompt: WellknownResult.Prompt) = copy(
+            homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
+            identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
+    )
+
+    private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
+            homeServerUri = Uri.parse("https://${action.username.getDomain()}"),
+            homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
+            identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
+    )
+
+    private fun onWellKnownError() = Result.failure<Session>(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))
+}
+
+@Suppress("UNCHECKED_CAST") // We're casting null failure results to R
+private inline fun <T, R> Result<T>.andThen(block: (T) -> Result<R>): Result<R> {
+    return when (val result = getOrNull()) {
+        null -> this as Result<R>
+        else -> block(result)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index 3fb52619da..6d959ef124 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -17,7 +17,6 @@
 package im.vector.app.features.onboarding
 
 import android.content.Context
-import android.net.Uri
 import com.airbnb.mvrx.MavericksViewModelFactory
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -45,7 +44,6 @@ import im.vector.app.features.login.SignMode
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
 import org.matrix.android.sdk.api.auth.AuthenticationService
 import org.matrix.android.sdk.api.auth.HomeServerHistoryService
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@@ -55,9 +53,6 @@ import org.matrix.android.sdk.api.auth.registration.FlowResult
 import org.matrix.android.sdk.api.auth.registration.RegistrationResult
 import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
 import org.matrix.android.sdk.api.auth.registration.Stage
-import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
-import org.matrix.android.sdk.api.failure.Failure
-import org.matrix.android.sdk.api.failure.MatrixIdFailure
 import org.matrix.android.sdk.api.session.Session
 import timber.log.Timber
 import java.util.UUID
@@ -79,6 +74,7 @@ class OnboardingViewModel @AssistedInject constructor(
         private val analyticsTracker: AnalyticsTracker,
         private val uriFilenameResolver: UriFilenameResolver,
         private val registrationActionHandler: RegistrationActionHandler,
+        private val directLoginUseCase: DirectLoginUseCase,
         private val vectorOverrides: VectorOverrides
 ) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
 
@@ -470,74 +466,14 @@ class OnboardingViewModel @AssistedInject constructor(
 
     private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
         setState { copy(isLoading = true) }
-
         currentJob = viewModelScope.launch {
-            val data = try {
-                authenticationService.getWellKnownData(action.username, homeServerConnectionConfig)
-            } catch (failure: Throwable) {
-                onDirectLoginError(failure)
-                return@launch
-            }
-            when (data) {
-                is WellknownResult.Prompt     ->
-                    directLoginOnWellknownSuccess(action, data, homeServerConnectionConfig)
-                is WellknownResult.FailPrompt ->
-                    // Relax on IS discovery if homeserver is valid
-                    if (data.homeServerUrl != null && data.wellKnown != null) {
-                        directLoginOnWellknownSuccess(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), homeServerConnectionConfig)
-                    } else {
-                        onWellKnownError()
+            directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
+                    onSuccess = { onSessionCreated(it, isAccountCreated = false) },
+                    onFailure = {
+                        setState { copy(isLoading = false) }
+                        _viewEvents.post(OnboardingViewEvents.Failure(it))
                     }
-                else                          -> {
-                    onWellKnownError()
-                }
-            }
-        }
-    }
-
-    private fun onWellKnownError() {
-        setState { copy(isLoading = false) }
-        _viewEvents.post(OnboardingViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error))))
-    }
-
-    private suspend fun directLoginOnWellknownSuccess(action: OnboardingAction.LoginOrRegister,
-                                                      wellKnownPrompt: WellknownResult.Prompt,
-                                                      homeServerConnectionConfig: HomeServerConnectionConfig?) {
-        val alteredHomeServerConnectionConfig = homeServerConnectionConfig
-                ?.copy(
-                        homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
-                        identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
-                )
-                ?: HomeServerConnectionConfig(
-                        homeServerUri = Uri.parse("https://${action.username.getDomain()}"),
-                        homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
-                        identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
-                )
-
-        val data = try {
-            authenticationService.directAuthentication(
-                    alteredHomeServerConnectionConfig,
-                    action.username,
-                    action.password,
-                    action.initialDeviceName)
-        } catch (failure: Throwable) {
-            onDirectLoginError(failure)
-            return
-        }
-        onSessionCreated(data, isAccountCreated = false)
-    }
-
-    private fun onDirectLoginError(failure: Throwable) {
-        when (failure) {
-            is MatrixIdFailure.InvalidMatrixId,
-            is Failure.UnrecognizedCertificateFailure -> {
-                setState { copy(isLoading = false) }
-                // Display this error in a dialog
-                _viewEvents.post(OnboardingViewEvents.Failure(failure))
-            }
-            else                                      -> {
-                setState { copy(isLoading = false) }
-            }
+            )
         }
     }
 
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index df4e0de65e..8d4ee50f2f 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -24,6 +24,7 @@ import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakeAnalyticsTracker
 import im.vector.app.test.fakes.FakeAuthenticationService
 import im.vector.app.test.fakes.FakeContext
+import im.vector.app.test.fakes.FakeDirectLoginUseCase
 import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory
 import im.vector.app.test.fakes.FakeHomeServerHistoryService
 import im.vector.app.test.fakes.FakeRegisterActionHandler
@@ -44,6 +45,7 @@ import org.matrix.android.sdk.api.auth.registration.FlowResult
 import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
 import org.matrix.android.sdk.api.auth.registration.RegistrationResult
 import org.matrix.android.sdk.api.auth.registration.Stage
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 
 private const val A_DISPLAY_NAME = "a display name"
@@ -55,6 +57,7 @@ private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.AddThreePid(Regist
 private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
 private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList())
 private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT)
+private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name")
 
 class OnboardingViewModelTest {
 
@@ -69,6 +72,7 @@ class OnboardingViewModelTest {
     private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
     private val fakeAuthenticationService = FakeAuthenticationService()
     private val fakeRegisterActionHandler = FakeRegisterActionHandler()
+    private val fakeDirectLoginUseCase = FakeDirectLoginUseCase()
 
     lateinit var viewModel: OnboardingViewModel
 
@@ -114,6 +118,26 @@ class OnboardingViewModelTest {
                 .finish()
     }
 
+    @Test
+    fun `given has sign in with matrix id sign mode, when handling login or register action, then logs in directly`() = runTest {
+        val initialState = initialState.copy(signMode = SignMode.SignInWithMatrixId)
+        viewModel = createViewModel(initialState)
+        fakeDirectLoginUseCase.givenSuccessResult(A_LOGIN_OR_REGISTER_ACTION, config = null, result = fakeSession)
+        givenInitialisesSession(fakeSession)
+        val test = viewModel.test()
+
+        viewModel.handle(A_LOGIN_OR_REGISTER_ACTION)
+
+        test
+                .assertStatesChanges(
+                        initialState,
+                        { copy(isLoading = true) },
+                        { copy(isLoading = false) }
+                )
+                .assertEvents(OnboardingViewEvents.OnAccountSignedIn)
+                .finish()
+    }
+
     @Test
     fun `when handling SignUp then sets sign mode to sign up and starts registration`() = runTest {
         givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT)
@@ -344,6 +368,7 @@ class OnboardingViewModelTest {
                 FakeAnalyticsTracker(),
                 fakeUriFilenameResolver.instance,
                 fakeRegisterActionHandler.instance,
+                fakeDirectLoginUseCase.instance,
                 FakeVectorOverrides()
         )
     }
@@ -384,7 +409,11 @@ class OnboardingViewModelTest {
 
     private fun givenSuccessfullyCreatesAccount(homeServerCapabilities: HomeServerCapabilities) {
         fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(homeServerCapabilities)
-        fakeActiveSessionHolder.expectSetsActiveSession(fakeSession)
+        givenInitialisesSession(fakeSession)
+    }
+
+    private fun givenInitialisesSession(session: Session) {
+        fakeActiveSessionHolder.expectSetsActiveSession(session)
         fakeAuthenticationService.expectReset()
         fakeSession.expectStartsSyncing()
     }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt
new file mode 100644
index 0000000000..b3fba51354
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 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.app.test.fakes
+
+import im.vector.app.features.onboarding.DirectLoginUseCase
+import im.vector.app.features.onboarding.OnboardingAction
+import io.mockk.coEvery
+import io.mockk.mockk
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+
+class FakeDirectLoginUseCase {
+    val instance = mockk<DirectLoginUseCase>()
+
+    fun givenSuccessResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, result: FakeSession) {
+        coEvery { instance.execute(action, config) } returns Result.success(result)
+    }
+}