diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e5e6d3a1e5..f8853bb846 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -92,6 +92,16 @@
android:host="sso-callback"
android:scheme="bitwarden" />
+
+
+
+
+
+
+
+
{
+ authRepository.setWebAuthResult(webAuthResult = webAuthResult)
+ }
+
else -> Unit
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/util/TwoFactorRequiredExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/util/TwoFactorRequiredExtensions.kt
index 959c0865c5..3ed674c34e 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/util/TwoFactorRequiredExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/util/TwoFactorRequiredExtensions.kt
@@ -2,11 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.network.util
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
-import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
-import kotlinx.serialization.json.jsonArray
-import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
/**
@@ -61,53 +58,3 @@ val GetTokenResponseJson.TwoFactorRequired?.twoFactorDisplayEmail: String
*/
private val Map.duo: JsonObject?
get() = get(TwoFactorAuthMethod.DUO) ?: get(TwoFactorAuthMethod.DUO_ORGANIZATION)
-
-/**
- * If it exists, return the identifier for the relying party used with Web AuthN two-factor
- * authentication.
- */
-val GetTokenResponseJson.TwoFactorRequired?.webAuthRpId: String?
- get() = this
- ?.authMethodsData
- ?.get(TwoFactorAuthMethod.WEB_AUTH)
- ?.get("rpId")
- ?.jsonPrimitive
- ?.contentOrNull
-
-/**
- * If it exists, return the type of user verification needed to complete the Web AuthN two-factor
- * authentication.
- */
-val GetTokenResponseJson.TwoFactorRequired?.webAuthUserVerification: String?
- get() = this
- ?.authMethodsData
- ?.get(TwoFactorAuthMethod.WEB_AUTH)
- ?.get("userVerification")
- ?.jsonPrimitive
- ?.contentOrNull
-
-/**
- * If it exists, return the challenge that the authenticator need to solve to complete the
- * Web AuthN two-factor authentication.
- */
-val GetTokenResponseJson.TwoFactorRequired?.webAuthChallenge: String?
- get() = this
- ?.authMethodsData
- ?.get(TwoFactorAuthMethod.WEB_AUTH)
- ?.get("challenge")
- ?.jsonPrimitive
- ?.contentOrNull
-
-/**
- * If it exists, return the credentials allowed to be used to solve the challenge to complete the
- * Web AuthN two-factor authentication.
- */
-val GetTokenResponseJson.TwoFactorRequired?.webAuthAllowCredentials: List?
- get() = this
- ?.authMethodsData
- ?.get(TwoFactorAuthMethod.WEB_AUTH)
- ?.get("allowCredentials")
- ?.jsonArray
- ?.mapNotNull {
- it.jsonObject["id"]?.jsonPrimitive?.contentOrNull?.base64UrlDecodeOrNull()
- }
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt
index 8d49e5000c..dfb09041ca 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt
@@ -27,6 +27,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
+import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.AuthenticatorProvider
import kotlinx.coroutines.flow.Flow
@@ -71,6 +72,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
val yubiKeyResultFlow: Flow
+ /**
+ * Flow of the current [WebAuthResult]. Subscribers should listen to the flow in order to
+ * receive updates whenever [setWebAuthResult] is called.
+ */
+ val webAuthResultFlow: Flow
+
/**
* The organization identifier currently associated with this user's SSO flow.
*/
@@ -285,6 +292,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
fun setYubiKeyResult(yubiKeyResult: YubiKeyResult)
+ /**
+ * Set the value of [webAuthResultFlow].
+ */
+ fun setWebAuthResult(webAuthResult: WebAuthResult)
+
/**
* Checks for a claimed domain organization for the [email] that can be used for an SSO request.
*/
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
index 984c07e31b..bb1dd85677 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
@@ -59,6 +59,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
+import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
@@ -275,6 +276,9 @@ class AuthRepositoryImpl(
private val yubiKeyResultChannel = Channel(capacity = Int.MAX_VALUE)
override val yubiKeyResultFlow: Flow = yubiKeyResultChannel.receiveAsFlow()
+ private val webAuthResultChannel = Channel(capacity = Int.MAX_VALUE)
+ override val webAuthResultFlow: Flow = webAuthResultChannel.receiveAsFlow()
+
private val mutableSsoCallbackResultFlow = bufferedMutableSharedFlow()
override val ssoCallbackResultFlow: Flow =
mutableSsoCallbackResultFlow.asSharedFlow()
@@ -934,6 +938,10 @@ class AuthRepositoryImpl(
yubiKeyResultChannel.trySend(yubiKeyResult)
}
+ override fun setWebAuthResult(webAuthResult: WebAuthResult) {
+ webAuthResultChannel.trySend(webAuthResult)
+ }
+
override suspend fun getOrganizationDomainSsoDetails(
email: String,
): OrganizationDomainSsoDetailsResult = organizationService
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtils.kt
new file mode 100644
index 0000000000..85c7f0f907
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtils.kt
@@ -0,0 +1,78 @@
+package com.x8bit.bitwarden.data.auth.repository.util
+
+import android.content.Intent
+import android.net.Uri
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import java.net.URLEncoder
+import java.util.Base64
+
+private const val WEB_AUTH_HOST: String = "webauthn-callback"
+private const val CALLBACK_URI = "bitwarden://$WEB_AUTH_HOST"
+
+/**
+ * Retrieves an [WebAuthResult] from an [Intent]. There are three possible cases.
+ *
+ * - `null`: Intent is not an web auth key callback.
+ * - [WebAuthResult.Success]: Intent is the web auth key callback with correct data.
+ * - [WebAuthResult.Failure]: Intent is the web auth key callback with incorrect data.
+ */
+fun Intent.getWebAuthResultOrNull(): WebAuthResult? {
+ val localData = data
+ return if (action == Intent.ACTION_VIEW &&
+ localData != null &&
+ localData.host == WEB_AUTH_HOST
+ ) {
+ localData
+ .getQueryParameter("data")
+ ?.let { WebAuthResult.Success(token = it) }
+ ?: WebAuthResult.Failure
+ } else {
+ null
+ }
+}
+
+/**
+ * Generates a [Uri] to display a web authn challenge for Bitwarden authentication.
+ */
+fun generateUriForWebAuth(
+ baseUrl: String,
+ data: JsonObject,
+ headerText: String,
+ buttonText: String,
+ returnButtonText: String,
+): Uri {
+ val json = buildJsonObject {
+ put(key = "callbackUri", value = CALLBACK_URI)
+ put(key = "data", value = data.toString())
+ put(key = "headerText", value = headerText)
+ put(key = "btnText", value = buttonText)
+ put(key = "btnReturnText", value = returnButtonText)
+ }
+ val base64Data = Base64
+ .getEncoder()
+ .encodeToString(json.toString().toByteArray(Charsets.UTF_8))
+ val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
+ val url = baseUrl +
+ "/webauthn-mobile-connector.html" +
+ "?data=$base64Data" +
+ "&parent=$parentParam" +
+ "&v=2"
+ return Uri.parse(url)
+}
+
+/**
+ * Sealed class representing the result of web auth callback token extraction.
+ */
+sealed class WebAuthResult {
+ /**
+ * Represents a token present in the web auth callback.
+ */
+ data class Success(val token: String) : WebAuthResult()
+
+ /**
+ * Represents a failure in the web auth callback.
+ */
+ data object Failure : WebAuthResult()
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt
index efa21ddfe8..d7b57e8557 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt
@@ -108,6 +108,10 @@ fun TwoFactorLoginScreen(
intentManager.startCustomTabsActivity(uri = event.uri)
}
+ is TwoFactorLoginEvent.NavigateToWebAuth -> {
+ intentManager.startCustomTabsActivity(uri = event.uri)
+ }
+
is TwoFactorLoginEvent.ShowToast -> {
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt
index 1781584cb8..5c569bab31 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt
@@ -18,17 +18,21 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
+import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
+import com.x8bit.bitwarden.data.auth.repository.util.generateUriForWebAuth
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.button
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.imageRes
-import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.isDuo
+import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.isContinueButtonEnabled
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.shouldUseNfc
+import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.showPasswordInput
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
+import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -48,6 +52,7 @@ private const val KEY_STATE = "state"
class TwoFactorLoginViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
+ private val resourceManager: ResourceManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel(
initialState = savedStateHandle[KEY_STATE]
@@ -57,7 +62,10 @@ class TwoFactorLoginViewModel @Inject constructor(
codeInput = "",
displayEmail = authRepository.twoFactorResponse.twoFactorDisplayEmail,
dialogState = null,
- isContinueButtonEnabled = authRepository.twoFactorResponse.preferredAuthMethod.isDuo,
+ isContinueButtonEnabled = authRepository
+ .twoFactorResponse
+ .preferredAuthMethod
+ .isContinueButtonEnabled,
isRememberMeEnabled = false,
captchaToken = null,
email = TwoFactorLoginArgs(savedStateHandle).emailAddress,
@@ -100,6 +108,13 @@ class TwoFactorLoginViewModel @Inject constructor(
.map { TwoFactorLoginAction.Internal.ReceiveYubiKeyResult(yubiKeyResult = it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
+
+ // Process the Web Authn result when it is received.
+ authRepository
+ .webAuthResultFlow
+ .map { TwoFactorLoginAction.Internal.ReceiveWebAuthResult(webAuthResult = it) }
+ .onEach(::sendAction)
+ .launchIn(viewModelScope)
}
override fun handleAction(action: TwoFactorLoginAction) {
@@ -130,6 +145,10 @@ class TwoFactorLoginViewModel @Inject constructor(
handleReceiveYubiKeyResult(action)
}
+ is TwoFactorLoginAction.Internal.ReceiveWebAuthResult -> {
+ handleReceiveWebAuthResult(action)
+ }
+
is TwoFactorLoginAction.Internal.ReceiveResendEmailResult -> {
handleReceiveResendEmailResult(action)
}
@@ -175,23 +194,59 @@ class TwoFactorLoginViewModel @Inject constructor(
/**
* Navigates to the Duo webpage if appropriate, else processes the login.
*/
+ @Suppress("MaxLineLength")
private fun handleContinueButtonClick() {
- if (state.authMethod.isDuo) {
- val authUrl = authRepository.twoFactorResponse.twoFactorDuoAuthUrl
- // The url should not be empty unless the environment is somehow not supported.
- sendEvent(
- event = authUrl
- ?.let {
- TwoFactorLoginEvent.NavigateToDuo(
- uri = Uri.parse(it),
- )
- }
- ?: TwoFactorLoginEvent.ShowToast(
- message = R.string.generic_error_message.asText(),
- ),
- )
- } else {
- initiateLogin()
+ when (state.authMethod) {
+ TwoFactorAuthMethod.DUO,
+ TwoFactorAuthMethod.DUO_ORGANIZATION,
+ -> {
+ val authUrl = authRepository.twoFactorResponse.twoFactorDuoAuthUrl
+ // The url should not be empty unless the environment is somehow not supported.
+ sendEvent(
+ event = authUrl
+ ?.let { TwoFactorLoginEvent.NavigateToDuo(uri = Uri.parse(it)) }
+ ?: TwoFactorLoginEvent.ShowToast(R.string.generic_error_message.asText()),
+ )
+ }
+
+ TwoFactorAuthMethod.WEB_AUTH -> {
+ sendEvent(
+ event = authRepository
+ .twoFactorResponse
+ ?.authMethodsData
+ ?.get(TwoFactorAuthMethod.WEB_AUTH)
+ ?.let {
+ val uri = generateUriForWebAuth(
+ baseUrl = environmentRepository
+ .environment
+ .environmentUrlData
+ .baseWebVaultUrlOrDefault,
+ data = it,
+ headerText = resourceManager.getString(
+ resId = R.string.fido2_title,
+ ),
+ buttonText = resourceManager.getString(
+ resId = R.string.fido2_authenticate_web_authn,
+ ),
+ returnButtonText = resourceManager.getString(
+ resId = R.string.fido2_return_to_app,
+ ),
+ )
+ TwoFactorLoginEvent.NavigateToWebAuth(uri = uri)
+ }
+ ?: TwoFactorLoginEvent.ShowToast(
+ message = R.string.there_was_an_error_starting_web_authn_two_factor_authentication.asText(),
+ ),
+ )
+ }
+
+ TwoFactorAuthMethod.AUTHENTICATOR_APP,
+ TwoFactorAuthMethod.EMAIL,
+ TwoFactorAuthMethod.YUBI_KEY,
+ TwoFactorAuthMethod.U2F,
+ TwoFactorAuthMethod.REMEMBER,
+ TwoFactorAuthMethod.RECOVERY_CODE,
+ -> initiateLogin()
}
}
@@ -289,6 +344,30 @@ class TwoFactorLoginViewModel @Inject constructor(
}
}
+ /**
+ * Handle the web auth result.
+ */
+ private fun handleReceiveWebAuthResult(
+ action: TwoFactorLoginAction.Internal.ReceiveWebAuthResult,
+ ) {
+ when (val result = action.webAuthResult) {
+ WebAuthResult.Failure -> {
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = TwoFactorLoginState.DialogState.Error(
+ message = R.string.generic_error_message.asText(),
+ ),
+ )
+ }
+ }
+
+ is WebAuthResult.Success -> {
+ mutableStateFlow.update { it.copy(codeInput = result.token) }
+ initiateLogin()
+ }
+ }
+ }
+
/**
* Handle the resend email result.
*/
@@ -481,7 +560,7 @@ data class TwoFactorLoginState(
/**
* Indicates whether the code input should be displayed.
*/
- val shouldShowCodeInput: Boolean get() = !authMethod.isDuo
+ val shouldShowCodeInput: Boolean get() = authMethod.showPasswordInput
/**
* The image to display for the given the [authMethod].
@@ -532,6 +611,11 @@ sealed class TwoFactorLoginEvent {
*/
data class NavigateToDuo(val uri: Uri) : TwoFactorLoginEvent()
+ /**
+ * Navigates to the WebAuth authentication screen.
+ */
+ data class NavigateToWebAuth(val uri: Uri) : TwoFactorLoginEvent()
+
/**
* Navigates to the recovery code help page.
*
@@ -631,5 +715,12 @@ sealed class TwoFactorLoginAction {
val resendEmailResult: ResendEmailResult,
val isUserInitiated: Boolean,
) : Internal()
+
+ /**
+ * Indicates a web auth result has been received.
+ */
+ data class ReceiveWebAuthResult(
+ val webAuthResult: WebAuthResult,
+ ) : Internal()
}
}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/util/TwoFactorAuthMethodExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/util/TwoFactorAuthMethodExtensions.kt
index 740e2732ac..4bbe2d5308 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/util/TwoFactorAuthMethodExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/util/TwoFactorAuthMethodExtensions.kt
@@ -65,15 +65,41 @@ val TwoFactorAuthMethod.button: Text
}
/**
- * Gets a boolean indicating if the given auth method uses Duo.
+ * Gets a boolean indicating if the given auth method has the continue button enabled by default.
*/
-val TwoFactorAuthMethod.isDuo: Boolean
+val TwoFactorAuthMethod.isContinueButtonEnabled: Boolean
get() = when (this) {
TwoFactorAuthMethod.DUO,
TwoFactorAuthMethod.DUO_ORGANIZATION,
+ TwoFactorAuthMethod.WEB_AUTH,
-> true
- else -> false
+ TwoFactorAuthMethod.AUTHENTICATOR_APP,
+ TwoFactorAuthMethod.EMAIL,
+ TwoFactorAuthMethod.YUBI_KEY,
+ TwoFactorAuthMethod.U2F,
+ TwoFactorAuthMethod.REMEMBER,
+ TwoFactorAuthMethod.RECOVERY_CODE,
+ -> false
+ }
+
+/**
+ * Gets a boolean indicating if the given auth method should display the password input field.
+ */
+val TwoFactorAuthMethod.showPasswordInput: Boolean
+ get() = when (this) {
+ TwoFactorAuthMethod.DUO,
+ TwoFactorAuthMethod.DUO_ORGANIZATION,
+ TwoFactorAuthMethod.WEB_AUTH,
+ -> false
+
+ TwoFactorAuthMethod.AUTHENTICATOR_APP,
+ TwoFactorAuthMethod.EMAIL,
+ TwoFactorAuthMethod.YUBI_KEY,
+ TwoFactorAuthMethod.U2F,
+ TwoFactorAuthMethod.REMEMBER,
+ TwoFactorAuthMethod.RECOVERY_CODE,
+ -> true
}
/**
diff --git a/app/src/main/res/values/strings_non_localized.xml b/app/src/main/res/values/strings_non_localized.xml
index 8faa49ff77..412d57200e 100644
--- a/app/src/main/res/values/strings_non_localized.xml
+++ b/app/src/main/res/values/strings_non_localized.xml
@@ -17,4 +17,5 @@
Autofill suggestion
Continue to complete WebAuthn verification.
Launch WebAuthn
+ There was an error starting WebAuthn two factor authentication
diff --git a/app/src/test/java/com/x8bit/bitwarden/AuthCallbackViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/AuthCallbackViewModelTest.kt
index df775e479f..0ecb570ad9 100644
--- a/app/src/test/java/com/x8bit/bitwarden/AuthCallbackViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/AuthCallbackViewModelTest.kt
@@ -5,9 +5,11 @@ 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.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
+import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
+import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.getYubiKeyResultOrNull
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
@@ -34,6 +36,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
fun setUp() {
mockkStatic(
Intent::getYubiKeyResultOrNull,
+ Intent::getWebAuthResultOrNull,
Intent::getCaptchaCallbackTokenResult,
Intent::getDuoCallbackTokenResult,
Intent::getSsoCallbackResult,
@@ -44,6 +47,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
fun tearDown() {
unmockkStatic(
Intent::getYubiKeyResultOrNull,
+ Intent::getWebAuthResultOrNull,
Intent::getCaptchaCallbackTokenResult,
Intent::getDuoCallbackTokenResult,
Intent::getSsoCallbackResult,
@@ -58,6 +62,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
every { mockIntent.getCaptchaCallbackTokenResult() } returns captchaCallbackTokenResult
every { mockIntent.getDuoCallbackTokenResult() } returns null
every { mockIntent.getYubiKeyResultOrNull() } returns null
+ every { mockIntent.getWebAuthResultOrNull() } returns null
every { mockIntent.getSsoCallbackResult() } returns null
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
@@ -74,6 +79,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { mockIntent.getDuoCallbackTokenResult() } returns duoCallbackTokenResult
every { mockIntent.getYubiKeyResultOrNull() } returns null
+ every { mockIntent.getWebAuthResultOrNull() } returns null
every { mockIntent.getSsoCallbackResult() } returns null
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
@@ -92,6 +98,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
)
every { mockIntent.getSsoCallbackResult() } returns sseCallbackResult
every { mockIntent.getYubiKeyResultOrNull() } returns null
+ every { mockIntent.getWebAuthResultOrNull() } returns null
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { mockIntent.getDuoCallbackTokenResult() } returns null
@@ -107,6 +114,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
val mockIntent = mockk()
val yubiKeyResult = mockk()
every { mockIntent.getYubiKeyResultOrNull() } returns yubiKeyResult
+ every { mockIntent.getWebAuthResultOrNull() } returns null
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { mockIntent.getDuoCallbackTokenResult() } returns null
every { mockIntent.getSsoCallbackResult() } returns null
@@ -117,6 +125,24 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
}
}
+ @Test
+ fun `on ReceiveNewIntent with Web Auth Result should call setWebAuthResult`() {
+ val viewModel = createViewModel()
+ val webAuthResult = mockk()
+ val mockIntent = mockk {
+ every { getWebAuthResultOrNull() } returns webAuthResult
+ every { getYubiKeyResultOrNull() } returns null
+ every { getCaptchaCallbackTokenResult() } returns null
+ every { getDuoCallbackTokenResult() } returns null
+ every { getSsoCallbackResult() } returns null
+ }
+
+ viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
+ verify(exactly = 1) {
+ authRepository.setWebAuthResult(webAuthResult)
+ }
+ }
+
private fun createViewModel() = AuthCallbackViewModel(
authRepository = authRepository,
)
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/util/TwoFactorRequiredExtensionTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/util/TwoFactorRequiredExtensionTest.kt
index 26faa0b5a7..86c885416d 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/util/TwoFactorRequiredExtensionTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/util/TwoFactorRequiredExtensionTest.kt
@@ -2,12 +2,10 @@ package com.x8bit.bitwarden.data.auth.datasource.network.util
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
-import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
@@ -156,79 +154,4 @@ class TwoFactorRequiredExtensionTest {
)
assertEquals(authUrl, subject.twoFactorDuoAuthUrl)
}
-
- @Test
- fun `webAuthRpId returns the expected value`() {
- val rpId = "vault.bitwarden.com"
- val subject = GetTokenResponseJson.TwoFactorRequired(
- authMethodsData = mapOf(
- TwoFactorAuthMethod.WEB_AUTH to JsonObject(
- mapOf("rpId" to JsonPrimitive(rpId)),
- ),
- ),
- captchaToken = null,
- ssoToken = null,
- twoFactorProviders = null,
- )
- assertEquals(rpId, subject.webAuthRpId)
- }
-
- @Test
- fun `webAuthUserVerification returns the expected value`() {
- val userVerification = "discouraged"
- val subject = GetTokenResponseJson.TwoFactorRequired(
- authMethodsData = mapOf(
- TwoFactorAuthMethod.WEB_AUTH to JsonObject(
- mapOf("userVerification" to JsonPrimitive(userVerification)),
- ),
- ),
- captchaToken = null,
- ssoToken = null,
- twoFactorProviders = null,
- )
- assertEquals(userVerification, subject.webAuthUserVerification)
- }
-
- @Test
- fun `webAuthChallenge returns the expected value`() {
- val challenge = "987t34478t9rxq7t8n"
- val subject = GetTokenResponseJson.TwoFactorRequired(
- authMethodsData = mapOf(
- TwoFactorAuthMethod.WEB_AUTH to JsonObject(
- mapOf("challenge" to JsonPrimitive(challenge)),
- ),
- ),
- captchaToken = null,
- ssoToken = null,
- twoFactorProviders = null,
- )
- assertEquals(challenge, subject.webAuthChallenge)
- }
-
- @Test
- fun `webAuthAllowCredentials returns the expected value`() {
- val credential = "98426435782"
- val subject = GetTokenResponseJson.TwoFactorRequired(
- authMethodsData = mapOf(
- TwoFactorAuthMethod.WEB_AUTH to JsonObject(
- mapOf(
- "allowCredentials" to JsonArray(
- listOf(
- JsonObject(
- mapOf(
- "type" to JsonPrimitive("public-key"),
- "id" to JsonPrimitive(credential),
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- captchaToken = null,
- ssoToken = null,
- twoFactorProviders = null,
- )
- assertNotNull(subject.webAuthAllowCredentials)
- }
}
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
index e9fc08840e..205a9fb07f 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
@@ -75,6 +75,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
+import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
@@ -4048,6 +4049,15 @@ class AuthRepositoryTest {
}
}
+ @Test
+ fun `setWebAuthResult should change the value of webAuthResultFlow`() = runTest {
+ val webAuthResult = WebAuthResult.Success("mockk")
+ repository.webAuthResultFlow.test {
+ repository.setWebAuthResult(webAuthResult)
+ assertEquals(webAuthResult, awaitItem())
+ }
+ }
+
@Test
fun `getOrganizationDomainSsoDetails Failure should return Failure `() = runTest {
val email = "test@gmail.com"
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtilsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtilsTest.kt
new file mode 100644
index 0000000000..762a14f72e
--- /dev/null
+++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtilsTest.kt
@@ -0,0 +1,76 @@
+package com.x8bit.bitwarden.data.auth.repository.util
+
+import android.content.Intent
+import android.net.Uri
+import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.serialization.json.JsonObject
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class WebAuthUtilsTest : BaseComposeTest() {
+
+ @Test
+ fun `generateUriForWebAuth should return valid Uri`() {
+ val baseUrl = "https://vault.bitwarden.com"
+ val actualUri = generateUriForWebAuth(
+ baseUrl = baseUrl,
+ data = JsonObject(emptyMap()),
+ headerText = "header",
+ buttonText = "button",
+ returnButtonText = "returnButton",
+ )
+ val expectedUrl = baseUrl +
+ "/webauthn-mobile-connector.html" +
+ "?data=eyJjYWxsYmFja1VyaSI6ImJpdHdhcmRlbjovL3dlYmF1dGhuLWNhbGxiYWNrIiwiZ" +
+ "GF0YSI6Int9IiwiaGVhZGVyVGV4dCI6ImhlYWRlciIsImJ0blRleHQiOiJidXR0b24iLCJi" +
+ "dG5SZXR1cm5UZXh0IjoicmV0dXJuQnV0dG9uIn0=" +
+ "&parent=bitwarden%3A%2F%2Fwebauthn-callback" +
+ "&v=2"
+ val expectedUri = Uri.parse(expectedUrl)
+ assertEquals(expectedUri, actualUri)
+ }
+
+ @Test
+ fun `getWebAuthResultOrNull should return null when data is null`() {
+ val intent = mockk {
+ every { data } returns null
+ every { action } returns Intent.ACTION_VIEW
+ }
+ val result = intent.getWebAuthResultOrNull()
+ assertEquals(null, result)
+ }
+
+ @Test
+ fun `getWebAuthResultOrNull should return null when action is not Intent ACTION_VIEW`() {
+ val intent = mockk {
+ every { data } returns null
+ every { action } returns Intent.ACTION_ANSWER
+ }
+ val result = intent.getWebAuthResultOrNull()
+ assertEquals(null, result)
+ }
+
+ @Test
+ fun `getWebAuthResultOrNull should return Failure with missing data parameter`() {
+ val intent = mockk {
+ every { data?.getQueryParameter("data") } returns null
+ every { action } returns Intent.ACTION_VIEW
+ every { data?.host } returns "webauthn-callback"
+ }
+ val result = intent.getWebAuthResultOrNull()
+ assertEquals(WebAuthResult.Failure, result)
+ }
+
+ @Test
+ fun `getWebAuthResultOrNull should return Success when data query parameter is present`() {
+ val intent = mockk {
+ every { data?.getQueryParameter("data") } returns "myToken"
+ every { action } returns Intent.ACTION_VIEW
+ every { data?.host } returns "webauthn-callback"
+ }
+ val result = intent.getWebAuthResultOrNull()
+ assertEquals(WebAuthResult.Success("myToken"), result)
+ }
+}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreenTest.kt
index aff2547fd9..9beaa24c9c 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreenTest.kt
@@ -200,6 +200,16 @@ class TwoFactorLoginScreenTest : BaseComposeTest() {
it.copy(authMethod = TwoFactorAuthMethod.DUO)
}
composeTestRule.onNodeWithText("Verification code").assertIsNotDisplayed()
+
+ mutableStateFlow.update {
+ it.copy(authMethod = TwoFactorAuthMethod.DUO_ORGANIZATION)
+ }
+ composeTestRule.onNodeWithText("Verification code").assertIsNotDisplayed()
+
+ mutableStateFlow.update {
+ it.copy(authMethod = TwoFactorAuthMethod.WEB_AUTH)
+ }
+ composeTestRule.onNodeWithText("Verification code").assertIsNotDisplayed()
}
@Test
@@ -253,6 +263,13 @@ class TwoFactorLoginScreenTest : BaseComposeTest() {
verify { intentManager.startCustomTabsActivity(mockUri) }
}
+ @Test
+ fun `NavigateToDuoNavigateToWebAuth should call intentManager startCustomTabsActivity`() {
+ val mockUri = mockk()
+ mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToWebAuth(mockUri))
+ verify { intentManager.startCustomTabsActivity(mockUri) }
+ }
+
@Test
fun `NavigateToRecoveryCode should launch the recovery code uri`() {
val mockUri = mockk()
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
index 2beffe8856..455f0b60e4 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
@@ -13,13 +13,17 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
+import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
+import com.x8bit.bitwarden.data.auth.repository.util.generateUriForWebAuth
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
+import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
+import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -36,33 +40,40 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
+@Suppress("LargeClass")
class TwoFactorLoginViewModelTest : BaseViewModelTest() {
private val mutableCaptchaTokenResultFlow =
bufferedMutableSharedFlow()
- private val mutableDuoTokenResultFlow =
- bufferedMutableSharedFlow()
+ private val mutableDuoTokenResultFlow = bufferedMutableSharedFlow()
private val mutableYubiKeyResultFlow = bufferedMutableSharedFlow()
+ private val mutableWebAuthResultFlow = bufferedMutableSharedFlow()
private val authRepository: AuthRepository = mockk(relaxed = true) {
every { twoFactorResponse } returns TWO_FACTOR_RESPONSE
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
every { duoTokenResultFlow } returns mutableDuoTokenResultFlow
every { yubiKeyResultFlow } returns mutableYubiKeyResultFlow
+ every { webAuthResultFlow } returns mutableWebAuthResultFlow
}
- private val environmentRepository: EnvironmentRepository = mockk(relaxed = true) {
- every {
- environment.environmentUrlData.baseWebVaultUrlOrDefault
- } returns "https://vault.bitwarden.com"
+ private val environmentRepository: EnvironmentRepository = mockk {
+ every { environment } returns Environment.Us
}
+ private val resourceManager: ResourceManager = mockk()
@BeforeEach
fun setUp() {
- mockkStatic(::generateUriForCaptcha)
+ mockkStatic(
+ ::generateUriForCaptcha,
+ ::generateUriForWebAuth,
+ )
mockkStatic(Uri::class)
}
@AfterEach
fun tearDown() {
- unmockkStatic(::generateUriForCaptcha)
+ unmockkStatic(
+ ::generateUriForCaptcha,
+ ::generateUriForWebAuth,
+ )
unmockkStatic(Uri::class)
}
@@ -89,6 +100,59 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
)
}
+ @Test
+ fun `webAuthResultFlow update with success should populate the codeInput and initial login`() {
+ val token = "token"
+ val initialState = DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.WEB_AUTH)
+ coEvery {
+ authRepository.login(
+ email = "example@email.com",
+ password = "password123",
+ twoFactorData = TwoFactorDataModel(
+ code = token,
+ method = TwoFactorAuthMethod.WEB_AUTH.value.toString(),
+ remember = false,
+ ),
+ captchaToken = null,
+ )
+ } returns LoginResult.Success
+ val viewModel = createViewModel(state = initialState)
+
+ mutableWebAuthResultFlow.tryEmit(WebAuthResult.Success(token))
+
+ assertEquals(
+ initialState.copy(codeInput = token),
+ viewModel.stateFlow.value,
+ )
+ coVerify(exactly = 1) {
+ authRepository.login(
+ email = "example@email.com",
+ password = "password123",
+ twoFactorData = TwoFactorDataModel(
+ code = token,
+ method = TwoFactorAuthMethod.WEB_AUTH.value.toString(),
+ remember = false,
+ ),
+ captchaToken = null,
+ )
+ }
+ }
+
+ @Test
+ fun `webAuthResultFlow update with failure should display error dialog`() {
+ val initialState = DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.WEB_AUTH)
+ val viewModel = createViewModel(state = initialState)
+ mutableWebAuthResultFlow.tryEmit(WebAuthResult.Failure)
+ assertEquals(
+ initialState.copy(
+ dialogState = TwoFactorLoginState.DialogState.Error(
+ message = R.string.generic_error_message.asText(),
+ ),
+ ),
+ viewModel.stateFlow.value,
+ )
+ }
+
@Test
fun `captchaTokenFlow success update should trigger a login`() = runTest {
coEvery {
@@ -317,6 +381,75 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
}
+ @Suppress("MaxLineLength")
+ @Test
+ fun `ContinueButtonClick login should emit NavigateToWebAuth when auth method is WEB_AUTH and data is non-null`() =
+ runTest {
+ val data = JsonObject(mapOf("AuthUrl" to JsonPrimitive("bitwarden.com")))
+ val response = GetTokenResponseJson.TwoFactorRequired(
+ authMethodsData = mapOf(TwoFactorAuthMethod.WEB_AUTH to data),
+ captchaToken = null,
+ ssoToken = null,
+ twoFactorProviders = null,
+ )
+ val mockkUri = mockk()
+ val headerText = "header"
+ val buttonText = "button"
+ val returnButtonText = "return"
+ every { resourceManager.getString(R.string.fido2_title) } returns headerText
+ every {
+ resourceManager.getString(R.string.fido2_authenticate_web_authn)
+ } returns buttonText
+ every {
+ resourceManager.getString(R.string.fido2_return_to_app)
+ } returns returnButtonText
+ every { authRepository.twoFactorResponse } returns response
+ every {
+ generateUriForWebAuth(
+ baseUrl = Environment.Us.environmentUrlData.baseWebVaultUrlOrDefault,
+ data = data,
+ headerText = headerText,
+ buttonText = buttonText,
+ returnButtonText = returnButtonText,
+ )
+ } returns mockkUri
+ val viewModel = createViewModel(
+ state = DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.WEB_AUTH),
+ )
+ viewModel.eventFlow.test {
+ viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick)
+ assertEquals(
+ TwoFactorLoginEvent.NavigateToWebAuth(mockkUri),
+ awaitItem(),
+ )
+ }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `ContinueButtonClick login should emit ShowToast when auth method is WEB_AUTH and data is null`() =
+ runTest {
+ val response = GetTokenResponseJson.TwoFactorRequired(
+ authMethodsData = emptyMap(),
+ captchaToken = null,
+ ssoToken = null,
+ twoFactorProviders = null,
+ )
+ every { authRepository.twoFactorResponse } returns response
+ val viewModel = createViewModel(
+ state = DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.WEB_AUTH),
+ )
+ viewModel.eventFlow.test {
+ viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick)
+ assertEquals(
+ TwoFactorLoginEvent.ShowToast(
+ message = R.string.there_was_an_error_starting_web_authn_two_factor_authentication.asText(),
+ ),
+ awaitItem(),
+ )
+ }
+ }
+
@Test
fun `ContinueButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
runTest {
@@ -627,43 +760,42 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
TwoFactorLoginViewModel(
authRepository = authRepository,
environmentRepository = environmentRepository,
+ resourceManager = resourceManager,
savedStateHandle = SavedStateHandle().also {
it["state"] = state
it["email_address"] = "example@email.com"
it["password"] = "password123"
},
)
-
- companion object {
- private val TWO_FACTOR_AUTH_METHODS_DATA = mapOf(
- TwoFactorAuthMethod.EMAIL to JsonObject(
- mapOf("Email" to JsonPrimitive("ex***@email.com")),
- ),
- TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
- )
- private val TWO_FACTOR_RESPONSE =
- GetTokenResponseJson.TwoFactorRequired(
- authMethodsData = TWO_FACTOR_AUTH_METHODS_DATA,
- captchaToken = null,
- ssoToken = null,
- twoFactorProviders = null,
- )
-
- private val DEFAULT_STATE = TwoFactorLoginState(
- authMethod = TwoFactorAuthMethod.AUTHENTICATOR_APP,
- availableAuthMethods = listOf(
- TwoFactorAuthMethod.EMAIL,
- TwoFactorAuthMethod.AUTHENTICATOR_APP,
- TwoFactorAuthMethod.RECOVERY_CODE,
- ),
- codeInput = "",
- displayEmail = "ex***@email.com",
- dialogState = null,
- isContinueButtonEnabled = false,
- isRememberMeEnabled = false,
- captchaToken = null,
- email = "example@email.com",
- password = "password123",
- )
- }
}
+
+private val TWO_FACTOR_AUTH_METHODS_DATA = mapOf(
+ TwoFactorAuthMethod.EMAIL to JsonObject(
+ mapOf("Email" to JsonPrimitive("ex***@email.com")),
+ ),
+ TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
+)
+
+private val TWO_FACTOR_RESPONSE = GetTokenResponseJson.TwoFactorRequired(
+ authMethodsData = TWO_FACTOR_AUTH_METHODS_DATA,
+ captchaToken = null,
+ ssoToken = null,
+ twoFactorProviders = null,
+)
+
+private val DEFAULT_STATE = TwoFactorLoginState(
+ authMethod = TwoFactorAuthMethod.AUTHENTICATOR_APP,
+ availableAuthMethods = listOf(
+ TwoFactorAuthMethod.EMAIL,
+ TwoFactorAuthMethod.AUTHENTICATOR_APP,
+ TwoFactorAuthMethod.RECOVERY_CODE,
+ ),
+ codeInput = "",
+ displayEmail = "ex***@email.com",
+ dialogState = null,
+ isContinueButtonEnabled = false,
+ isRememberMeEnabled = false,
+ captchaToken = null,
+ email = "example@email.com",
+ password = "password123",
+)
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/util/TwoFactorAuthMethodExtensionTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/util/TwoFactorAuthMethodExtensionTest.kt
index 60e758d260..48f17d9b6a 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/util/TwoFactorAuthMethodExtensionTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/util/TwoFactorAuthMethodExtensionTest.kt
@@ -79,7 +79,7 @@ class TwoFactorAuthMethodExtensionTest {
}
@Test
- fun `isDuo returns the expected value`() {
+ fun `isContinueButtonEnabled returns the expected value`() {
mapOf(
TwoFactorAuthMethod.AUTHENTICATOR_APP to false,
TwoFactorAuthMethod.EMAIL to false,
@@ -88,11 +88,29 @@ class TwoFactorAuthMethodExtensionTest {
TwoFactorAuthMethod.U2F to false,
TwoFactorAuthMethod.REMEMBER to false,
TwoFactorAuthMethod.DUO_ORGANIZATION to true,
- TwoFactorAuthMethod.WEB_AUTH to false,
+ TwoFactorAuthMethod.WEB_AUTH to true,
TwoFactorAuthMethod.RECOVERY_CODE to false,
)
- .forEach { (type, isDuo) ->
- assertEquals(isDuo, type.isDuo)
+ .forEach { (type, isContinueButtonEnabled) ->
+ assertEquals(isContinueButtonEnabled, type.isContinueButtonEnabled)
+ }
+ }
+
+ @Test
+ fun `showPasswordInput returns the expected value`() {
+ mapOf(
+ TwoFactorAuthMethod.AUTHENTICATOR_APP to true,
+ TwoFactorAuthMethod.EMAIL to true,
+ TwoFactorAuthMethod.DUO to false,
+ TwoFactorAuthMethod.YUBI_KEY to true,
+ TwoFactorAuthMethod.U2F to true,
+ TwoFactorAuthMethod.REMEMBER to true,
+ TwoFactorAuthMethod.DUO_ORGANIZATION to false,
+ TwoFactorAuthMethod.WEB_AUTH to false,
+ TwoFactorAuthMethod.RECOVERY_CODE to true,
+ )
+ .forEach { (type, showPasswordInput) ->
+ assertEquals(showPasswordInput, type.showPasswordInput)
}
}