Add WebAuthCallbackActivity to handle hCaptcha callbacks (#705)

This commit is contained in:
Brian Yencho 2024-01-22 09:08:28 -06:00 committed by Álison Fernandes
parent 49ff8a761d
commit e3547f4e13
7 changed files with 172 additions and 71 deletions

View file

@ -237,6 +237,7 @@ koverReport {
// OS-level components
"com.x8bit.bitwarden.BitwardenApplication",
"com.x8bit.bitwarden.MainActivity*",
"com.x8bit.bitwarden.WebAuthCallbackActivity*",
"com.x8bit.bitwarden.data.autofill.BitwardenAutofillService*",
"com.x8bit.bitwarden.data.push.BitwardenFirebaseMessagingService*",
// Empty Composables

View file

@ -34,6 +34,23 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="text/*" />
</intent-filter>
</activity>
<activity
android:name=".WebAuthCallbackActivity"
android:exported="true"
android:launchMode="singleTop"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -44,15 +61,6 @@
android:host="captcha-callback"
android:scheme="bitwarden" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="text/*" />
</intent-filter>
</activity>
<provider

View file

@ -5,7 +5,6 @@ import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
@ -50,39 +49,34 @@ class MainViewModel @Inject constructor(
}
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
val shareData = intentManager.getShareDataFromIntent(action.intent)
when {
shareData != null -> {
authRepository.specialCircumstance =
UserState.SpecialCircumstance.ShareNewSend(
data = shareData,
shouldFinishWhenComplete = true,
)
}
}
handleIntent(
intent = action.intent,
isFirstIntent = true,
)
}
private fun handleNewIntentReceived(action: MainAction.ReceiveNewIntent) {
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
val shareData = intentManager.getShareDataFromIntent(action.intent)
when {
captchaCallbackTokenResult != null -> {
authRepository.setCaptchaCallbackTokenResult(
tokenResult = captchaCallbackTokenResult,
)
}
handleIntent(
intent = action.intent,
isFirstIntent = false,
)
}
private fun handleIntent(
intent: Intent,
isFirstIntent: Boolean,
) {
val shareData = intentManager.getShareDataFromIntent(intent)
when {
shareData != null -> {
authRepository.specialCircumstance =
UserState.SpecialCircumstance.ShareNewSend(
data = shareData,
// Allow users back into the already-running app when completing the
// Send task.
shouldFinishWhenComplete = false,
// Send task when this is not the first intent.
shouldFinishWhenComplete = isFirstIntent,
)
}
else -> Unit
}
}
}

View file

@ -0,0 +1,32 @@
package com.x8bit.bitwarden
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
/**
* An activity to receive callbacks from Custom Chrome tabs or other web-auth related flows such
* the current state of the task holding the [MainActivity] can remain undisturbed.
*/
@AndroidEntryPoint
class WebAuthCallbackActivity : AppCompatActivity() {
private val webAuthCallbackViewModel: WebAuthCallbackViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webAuthCallbackViewModel.trySendAction(
WebAuthCallbackAction.IntentReceive(intent = intent),
)
val intent = Intent(this, MainActivity::class.java)
.apply {
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
}
startActivity(intent)
finishAffinity()
}
}

View file

@ -0,0 +1,45 @@
package com.x8bit.bitwarden
import android.content.Intent
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* A view model that handles logic for the [WebAuthCallbackActivity].
*/
@HiltViewModel
class WebAuthCallbackViewModel @Inject constructor(
private val authRepository: AuthRepository,
) : BaseViewModel<Unit, Unit, WebAuthCallbackAction>(Unit) {
override fun handleAction(action: WebAuthCallbackAction) {
when (action) {
is WebAuthCallbackAction.IntentReceive -> handleIntentReceived(action)
}
}
private fun handleIntentReceived(action: WebAuthCallbackAction.IntentReceive) {
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
when {
captchaCallbackTokenResult != null -> {
authRepository.setCaptchaCallbackTokenResult(
tokenResult = captchaCallbackTokenResult,
)
}
else -> Unit
}
}
}
/**
* Actions for the [WebAuthCallbackViewModel].
*/
sealed class WebAuthCallbackAction {
/**
* Receive Intent by the application.
*/
data class IntentReceive(val intent: Intent) : WebAuthCallbackAction()
}

View file

@ -3,7 +3,6 @@ package com.x8bit.bitwarden
import android.content.Intent
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
@ -13,14 +12,10 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class MainViewModelTest : BaseViewModelTest() {
@ -32,7 +27,6 @@ class MainViewModelTest : BaseViewModelTest() {
every { activeUserId } returns USER_ID
every { specialCircumstance } returns null
every { specialCircumstance = any() } just runs
every { setCaptchaCallbackTokenResult(any()) } just runs
}
private val settingsRepository = mockk<SettingsRepository> {
every { appTheme } returns AppTheme.DEFAULT
@ -42,16 +36,6 @@ class MainViewModelTest : BaseViewModelTest() {
every { getShareDataFromIntent(any()) } returns null
}
@BeforeEach
fun setUp() {
mockkStatic(Intent::getCaptchaCallbackTokenResult)
}
@AfterEach
fun tearDown() {
unmockkStatic(Intent::getCaptchaCallbackTokenResult)
}
@Test
fun `on AppThemeChanged should update state`() {
val viewModel = createViewModel()
@ -102,29 +86,6 @@ class MainViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `on ReceiveNewIntent with captcha host should call setCaptchaCallbackToken`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
every {
mockIntent.getCaptchaCallbackTokenResult()
} returns CaptchaCallbackTokenResult.Success(
token = "mockk_token",
)
viewModel.trySendAction(
MainAction.ReceiveNewIntent(
intent = mockIntent,
),
)
verify {
authRepository.setCaptchaCallbackTokenResult(
tokenResult = CaptchaCallbackTokenResult.Success(
token = "mockk_token",
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() {

View file

@ -0,0 +1,60 @@
package com.x8bit.bitwarden
import android.content.Intent
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class WebAuthCallbackViewModelTest : BaseViewModelTest() {
private val authRepository = mockk<AuthRepository> {
every { setCaptchaCallbackTokenResult(any()) } just runs
}
@BeforeEach
fun setUp() {
mockkStatic(Intent::getCaptchaCallbackTokenResult)
}
@AfterEach
fun tearDown() {
unmockkStatic(Intent::getCaptchaCallbackTokenResult)
}
@Test
fun `on ReceiveNewIntent with captcha host should call setCaptchaCallbackToken`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
every {
mockIntent.getCaptchaCallbackTokenResult()
} returns CaptchaCallbackTokenResult.Success(
token = "mockk_token",
)
viewModel.trySendAction(
WebAuthCallbackAction.IntentReceive(
intent = mockIntent,
),
)
verify {
authRepository.setCaptchaCallbackTokenResult(
tokenResult = CaptchaCallbackTokenResult.Success(
token = "mockk_token",
),
)
}
}
private fun createViewModel() = WebAuthCallbackViewModel(
authRepository = authRepository,
)
}