[PM-24642] Remove captcha connector code (#5677)

This commit is contained in:
aj-rosado 2025-08-12 21:56:18 +01:00 committed by GitHub
parent 29243c8f44
commit 694865c213
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 71 additions and 1569 deletions

View File

@ -133,16 +133,6 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data
android:host="captcha-callback"
android:scheme="bitwarden" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="duo-callback" android:host="duo-callback"
android:scheme="bitwarden" /> android:scheme="bitwarden" />

View File

@ -3,7 +3,6 @@ package com.x8bit.bitwarden
import android.content.Intent import android.content.Intent
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
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.getDuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
@ -27,7 +26,6 @@ class AuthCallbackViewModel @Inject constructor(
private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) { private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) {
val yubiKeyResult = action.intent.getYubiKeyResultOrNull() val yubiKeyResult = action.intent.getYubiKeyResultOrNull()
val webAuthResult = action.intent.getWebAuthResultOrNull() val webAuthResult = action.intent.getWebAuthResultOrNull()
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult() val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
val ssoCallbackResult = action.intent.getSsoCallbackResult() val ssoCallbackResult = action.intent.getSsoCallbackResult()
when { when {
@ -35,12 +33,6 @@ class AuthCallbackViewModel @Inject constructor(
authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult) authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult)
} }
captchaCallbackTokenResult != null -> {
authRepository.setCaptchaCallbackTokenResult(
tokenResult = captchaCallbackTokenResult,
)
}
duoCallbackTokenResult != null -> { duoCallbackTokenResult != null -> {
authRepository.setDuoCallbackTokenResult( authRepository.setDuoCallbackTokenResult(
tokenResult = duoCallbackTokenResult, tokenResult = duoCallbackTokenResult,

View File

@ -32,7 +32,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult 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.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult 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.WebAuthResult
@ -56,12 +55,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/ */
val userStateFlow: StateFlow<UserState?> val userStateFlow: StateFlow<UserState?>
/**
* Flow of the current [CaptchaCallbackTokenResult]. Subscribers should listen to the flow
* in order to receive updates whenever [setCaptchaCallbackTokenResult] is called.
*/
val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult>
/** /**
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow * Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
* in order to receive updates whenever [setDuoCallbackTokenResult] is called. * in order to receive updates whenever [setDuoCallbackTokenResult] is called.
@ -186,7 +179,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
suspend fun login( suspend fun login(
email: String, email: String,
password: String, password: String,
captchaToken: String?,
): LoginResult ): LoginResult
/** /**
@ -201,7 +193,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
asymmetricalKey: String, asymmetricalKey: String,
requestPrivateKey: String, requestPrivateKey: String,
masterPasswordHash: String?, masterPasswordHash: String?,
captchaToken: String?,
): LoginResult ): LoginResult
/** /**
@ -213,7 +204,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String, email: String,
password: String?, password: String?,
twoFactorData: TwoFactorDataModel, twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?, orgIdentifier: String?,
): LoginResult ): LoginResult
@ -226,7 +216,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
ssoCode: String, ssoCode: String,
ssoCodeVerifier: String, ssoCodeVerifier: String,
ssoRedirectUri: String, ssoRedirectUri: String,
captchaToken: String?,
organizationIdentifier: String, organizationIdentifier: String,
): LoginResult ): LoginResult
@ -239,7 +228,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String, email: String,
password: String?, password: String?,
newDeviceOtp: String, newDeviceOtp: String,
captchaToken: String?,
orgIdentifier: String?, orgIdentifier: String?,
): LoginResult ): LoginResult
@ -294,7 +282,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
masterPassword: String, masterPassword: String,
masterPasswordHint: String?, masterPasswordHint: String?,
emailVerificationToken: String? = null, emailVerificationToken: String? = null,
captchaToken: String?,
shouldCheckDataBreaches: Boolean, shouldCheckDataBreaches: Boolean,
isMasterPasswordStrong: Boolean, isMasterPasswordStrong: Boolean,
): RegisterResult ): RegisterResult
@ -332,11 +319,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
passwordHint: String?, passwordHint: String?,
): SetPasswordResult ): SetPasswordResult
/**
* Set the value of [captchaTokenResultFlow].
*/
fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult)
/** /**
* Set the value of [duoTokenResultFlow]. * Set the value of [duoTokenResultFlow].
*/ */

View File

@ -87,7 +87,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
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.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult 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.WebAuthResult
@ -331,10 +330,6 @@ class AuthRepositoryImpl(
), ),
) )
private val captchaTokenChannel = Channel<CaptchaCallbackTokenResult>(capacity = Int.MAX_VALUE)
override val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult> =
captchaTokenChannel.receiveAsFlow()
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE) private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow() override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
@ -619,7 +614,6 @@ class AuthRepositoryImpl(
override suspend fun login( override suspend fun login(
email: String, email: String,
password: String, password: String,
captchaToken: String?,
): LoginResult = identityService ): LoginResult = identityService
.preLogin(email = email) .preLogin(email = email)
.flatMap { .flatMap {
@ -638,7 +632,6 @@ class AuthRepositoryImpl(
username = email, username = email,
password = passwordHash, password = passwordHash,
), ),
captchaToken = captchaToken,
) )
} }
.fold( .fold(
@ -658,7 +651,6 @@ class AuthRepositoryImpl(
asymmetricalKey: String, asymmetricalKey: String,
requestPrivateKey: String, requestPrivateKey: String,
masterPasswordHash: String?, masterPasswordHash: String?,
captchaToken: String?,
): LoginResult = ): LoginResult =
loginCommon( loginCommon(
email = email, email = email,
@ -673,14 +665,12 @@ class AuthRepositoryImpl(
asymmetricalKey = asymmetricalKey, asymmetricalKey = asymmetricalKey,
privateKey = requestPrivateKey, privateKey = requestPrivateKey,
), ),
captchaToken = captchaToken,
) )
override suspend fun login( override suspend fun login(
email: String, email: String,
password: String?, password: String?,
twoFactorData: TwoFactorDataModel, twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?, orgIdentifier: String?,
): LoginResult = identityTokenAuthModel ): LoginResult = identityTokenAuthModel
?.let { ?.let {
@ -689,7 +679,6 @@ class AuthRepositoryImpl(
password = password, password = password,
authModel = it, authModel = it,
twoFactorData = twoFactorData, twoFactorData = twoFactorData,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData, deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier, orgIdentifier = orgIdentifier,
) )
@ -703,7 +692,6 @@ class AuthRepositoryImpl(
email: String, email: String,
password: String?, password: String?,
newDeviceOtp: String, newDeviceOtp: String,
captchaToken: String?,
orgIdentifier: String?, orgIdentifier: String?,
): LoginResult = identityTokenAuthModel ): LoginResult = identityTokenAuthModel
?.let { ?.let {
@ -712,7 +700,6 @@ class AuthRepositoryImpl(
password = password, password = password,
authModel = it, authModel = it,
newDeviceOtp = newDeviceOtp, newDeviceOtp = newDeviceOtp,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData, deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier, orgIdentifier = orgIdentifier,
) )
@ -746,7 +733,6 @@ class AuthRepositoryImpl(
ssoCode: String, ssoCode: String,
ssoCodeVerifier: String, ssoCodeVerifier: String,
ssoRedirectUri: String, ssoRedirectUri: String,
captchaToken: String?,
organizationIdentifier: String, organizationIdentifier: String,
): LoginResult = loginCommon( ): LoginResult = loginCommon(
email = email, email = email,
@ -755,7 +741,6 @@ class AuthRepositoryImpl(
ssoCodeVerifier = ssoCodeVerifier, ssoCodeVerifier = ssoCodeVerifier,
ssoRedirectUri = ssoRedirectUri, ssoRedirectUri = ssoRedirectUri,
), ),
captchaToken = captchaToken,
orgIdentifier = organizationIdentifier, orgIdentifier = organizationIdentifier,
) )
@ -905,7 +890,6 @@ class AuthRepositoryImpl(
masterPassword: String, masterPassword: String,
masterPasswordHint: String?, masterPasswordHint: String?,
emailVerificationToken: String?, emailVerificationToken: String?,
captchaToken: String?,
shouldCheckDataBreaches: Boolean, shouldCheckDataBreaches: Boolean,
isMasterPasswordStrong: Boolean, isMasterPasswordStrong: Boolean,
): RegisterResult { ): RegisterResult {
@ -940,7 +924,6 @@ class AuthRepositoryImpl(
email = email, email = email,
masterPasswordHash = registerKeyResponse.masterPasswordHash, masterPasswordHash = registerKeyResponse.masterPasswordHash,
masterPasswordHint = masterPasswordHint, masterPasswordHint = masterPasswordHint,
captchaResponse = captchaToken,
key = registerKeyResponse.encryptedUserKey, key = registerKeyResponse.encryptedUserKey,
keys = RegisterRequestJson.Keys( keys = RegisterRequestJson.Keys(
publicKey = registerKeyResponse.keys.public, publicKey = registerKeyResponse.keys.public,
@ -957,7 +940,6 @@ class AuthRepositoryImpl(
masterPasswordHash = registerKeyResponse.masterPasswordHash, masterPasswordHash = registerKeyResponse.masterPasswordHash,
masterPasswordHint = masterPasswordHint, masterPasswordHint = masterPasswordHint,
emailVerificationToken = emailVerificationToken, emailVerificationToken = emailVerificationToken,
captchaResponse = captchaToken,
userSymmetricKey = registerKeyResponse.encryptedUserKey, userSymmetricKey = registerKeyResponse.encryptedUserKey,
userAsymmetricKeys = RegisterFinishRequestJson.Keys( userAsymmetricKeys = RegisterFinishRequestJson.Keys(
publicKey = registerKeyResponse.keys.public, publicKey = registerKeyResponse.keys.public,
@ -972,18 +954,9 @@ class AuthRepositoryImpl(
.fold( .fold(
onSuccess = { onSuccess = {
when (it) { when (it) {
is RegisterResponseJson.CaptchaRequired -> {
it.validationErrors.captchaKeys.firstOrNull()
?.let { key -> RegisterResult.CaptchaRequired(captchaId = key) }
?: RegisterResult.Error(
errorMessage = null,
error = MissingPropertyException("Captcha ID"),
)
}
is RegisterResponseJson.Success -> { is RegisterResponseJson.Success -> {
settingsRepository.hasUserLoggedInOrCreatedAccount = true settingsRepository.hasUserLoggedInOrCreatedAccount = true
RegisterResult.Success(captchaToken = it.captchaBypassToken) RegisterResult.Success
} }
is RegisterResponseJson.Invalid -> { is RegisterResponseJson.Invalid -> {
@ -1229,10 +1202,6 @@ class AuthRepositoryImpl(
) )
} }
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
captchaTokenChannel.trySend(tokenResult)
}
override fun setDuoCallbackTokenResult(tokenResult: DuoCallbackTokenResult) { override fun setDuoCallbackTokenResult(tokenResult: DuoCallbackTokenResult) {
duoTokenChannel.trySend(tokenResult) duoTokenChannel.trySend(tokenResult)
} }
@ -1624,7 +1593,6 @@ class AuthRepositoryImpl(
twoFactorData: TwoFactorDataModel? = null, twoFactorData: TwoFactorDataModel? = null,
deviceData: DeviceDataModel? = null, deviceData: DeviceDataModel? = null,
orgIdentifier: String? = null, orgIdentifier: String? = null,
captchaToken: String?,
newDeviceOtp: String? = null, newDeviceOtp: String? = null,
): LoginResult = identityService ): LoginResult = identityService
.getToken( .getToken(
@ -1632,7 +1600,6 @@ class AuthRepositoryImpl(
email = email, email = email,
authModel = authModel, authModel = authModel,
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email), twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
captchaToken = captchaToken,
newDeviceOtp = newDeviceOtp, newDeviceOtp = newDeviceOtp,
) )
.fold( .fold(
@ -1651,10 +1618,6 @@ class AuthRepositoryImpl(
}, },
onSuccess = { loginResponse -> onSuccess = { loginResponse ->
when (loginResponse) { when (loginResponse) {
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
captchaId = loginResponse.captchaKey,
)
is GetTokenResponseJson.TwoFactorRequired -> handleLoginCommonTwoFactorRequired( is GetTokenResponseJson.TwoFactorRequired -> handleLoginCommonTwoFactorRequired(
loginResponse = loginResponse, loginResponse = loginResponse,
email = email, email = email,

View File

@ -9,11 +9,6 @@ sealed class LoginResult {
*/ */
data object Success : LoginResult() data object Success : LoginResult()
/**
* Captcha verification is required.
*/
data class CaptchaRequired(val captchaId: String) : LoginResult()
/** /**
* Encryption key migration is required. * Encryption key migration is required.
*/ */

View File

@ -7,16 +7,8 @@ sealed class RegisterResult {
/** /**
* Register succeeded. * Register succeeded.
* *
* @param captchaToken the captcha bypass token to bypass future captcha verifications.
*/ */
data class Success(val captchaToken: String?) : RegisterResult() data object Success : RegisterResult()
/**
* Captcha verification is required.
*
* @param captchaId the captcha id for performing the captcha verification.
*/
data class CaptchaRequired(val captchaId: String) : RegisterResult()
/** /**
* There was an error logging in. * There was an error logging in.

View File

@ -1,74 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.util
import android.content.Intent
import android.net.Uri
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.net.URLEncoder
import java.util.Base64
import java.util.Locale
private const val CAPTCHA_HOST: String = "captcha-callback"
private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST"
/**
* Generates a [Uri] to display a CAPTCHA challenge for Bitwarden authentication.
*/
fun generateUriForCaptcha(captchaId: String): Uri {
val json = buildJsonObject {
put(key = "siteKey", value = captchaId)
put(key = "locale", value = Locale.getDefault().toString())
put(key = "callbackUri", value = CALLBACK_URI)
put(key = "captchaRequiredText", value = "Captcha required")
}
val base64Data = Base64
.getEncoder()
.encodeToString(
json
.toString()
.toByteArray(Charsets.UTF_8),
)
val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
"?data=$base64Data&parent=$parentParam&v=1"
return Uri.parse(url)
}
/**
* Retrieves a [CaptchaCallbackTokenResult] from an Intent. There are three possible cases.
*
* - `null`: Intent is not a captcha callback, or data is null.
*
* - [CaptchaCallbackTokenResult.MissingToken]:
* Intent is the captcha callback, but its missing a token value.
*
* - [CaptchaCallbackTokenResult.Success]:
* Intent is the captcha callback, and it has a token.
*/
fun Intent.getCaptchaCallbackTokenResult(): CaptchaCallbackTokenResult? {
val localData = data
return if (
action == Intent.ACTION_VIEW && localData != null && localData.host == CAPTCHA_HOST
) {
localData.getQueryParameter("token")?.let {
CaptchaCallbackTokenResult.Success(token = it)
} ?: CaptchaCallbackTokenResult.MissingToken
} else {
null
}
}
/**
* Sealed class representing the result of captcha callback token extraction.
*/
sealed class CaptchaCallbackTokenResult {
/**
* Represents a missing token in the captcha callback.
*/
data object MissingToken : CaptchaCallbackTokenResult()
/**
* Represents a token present in the captcha callback.
*/
data class Success(val token: String) : CaptchaCallbackTokenResult()
}

View File

@ -84,10 +84,9 @@ fun NavGraphBuilder.authGraph(
onNavigateToPreventAccountLockout = { onNavigateToPreventAccountLockout = {
navController.navigateToPreventAccountLockout() navController.navigateToPreventAccountLockout()
}, },
onNavigateToLogin = { emailAddress, captchaToken -> onNavigateToLogin = { emailAddress ->
navController.navigateToLogin( navController.navigateToLogin(
emailAddress = emailAddress, emailAddress = emailAddress,
captchaToken = captchaToken,
navOptions = navOptions { navOptions = navOptions {
popUpTo(route = LandingRoute) popUpTo(route = LandingRoute)
}, },
@ -110,7 +109,6 @@ fun NavGraphBuilder.authGraph(
onNavigateToLogin = { emailAddress -> onNavigateToLogin = { emailAddress ->
navController.navigateToLogin( navController.navigateToLogin(
emailAddress = emailAddress, emailAddress = emailAddress,
captchaToken = null,
) )
}, },
onNavigateToEnvironment = { onNavigateToEnvironment = {

View File

@ -65,7 +65,7 @@ fun NavGraphBuilder.completeRegistrationDestination(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToPasswordGuidance: () -> Unit, onNavigateToPasswordGuidance: () -> Unit,
onNavigateToPreventAccountLockout: () -> Unit, onNavigateToPreventAccountLockout: () -> Unit,
onNavigateToLogin: (email: String, token: String?) -> Unit, onNavigateToLogin: (email: String) -> Unit,
) { ) {
composableWithSlideTransitions<CompleteRegistrationRoute> { composableWithSlideTransitions<CompleteRegistrationRoute> {
CompleteRegistrationScreen( CompleteRegistrationScreen(

View File

@ -69,7 +69,7 @@ fun CompleteRegistrationScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToPasswordGuidance: () -> Unit, onNavigateToPasswordGuidance: () -> Unit,
onNavigateToPreventAccountLockout: () -> Unit, onNavigateToPreventAccountLockout: () -> Unit,
onNavigateToLogin: (email: String, token: String?) -> Unit, onNavigateToLogin: (email: String) -> Unit,
viewModel: CompleteRegistrationViewModel = hiltViewModel(), viewModel: CompleteRegistrationViewModel = hiltViewModel(),
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -94,7 +94,6 @@ fun CompleteRegistrationScreen(
is CompleteRegistrationEvent.NavigateToLogin -> { is CompleteRegistrationEvent.NavigateToLogin -> {
onNavigateToLogin( onNavigateToLogin(
event.email, event.email,
event.captchaToken,
) )
} }
} }

View File

@ -177,13 +177,6 @@ class CompleteRegistrationViewModel @Inject constructor(
action: Internal.ReceiveRegisterResult, action: Internal.ReceiveRegisterResult,
) { ) {
when (val registerAccountResult = action.registerResult) { when (val registerAccountResult = action.registerResult) {
// TODO PM-6675: Remove captcha from RegisterResult when old flow gets removed
is RegisterResult.CaptchaRequired -> {
throw IllegalStateException(
"Captcha should not be required for the new registration flow",
)
}
is RegisterResult.Error -> { is RegisterResult.Error -> {
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
@ -202,12 +195,10 @@ class CompleteRegistrationViewModel @Inject constructor(
val loginResult = authRepository.login( val loginResult = authRepository.login(
email = state.userEmail, email = state.userEmail,
password = state.passwordInput, password = state.passwordInput,
captchaToken = registerAccountResult.captchaToken,
) )
sendAction( sendAction(
Internal.ReceiveLoginResult( Internal.ReceiveLoginResult(
loginResult = loginResult, loginResult = loginResult,
captchaToken = registerAccountResult.captchaToken,
), ),
) )
} }
@ -266,7 +257,6 @@ class CompleteRegistrationViewModel @Inject constructor(
sendEvent( sendEvent(
CompleteRegistrationEvent.NavigateToLogin( CompleteRegistrationEvent.NavigateToLogin(
email = state.userEmail, email = state.userEmail,
captchaToken = action.captchaToken,
), ),
) )
} }
@ -390,7 +380,6 @@ class CompleteRegistrationViewModel @Inject constructor(
email = state.userEmail, email = state.userEmail,
masterPassword = state.passwordInput, masterPassword = state.passwordInput,
masterPasswordHint = state.passwordHintInput.ifBlank { null }, masterPasswordHint = state.passwordHintInput.ifBlank { null },
captchaToken = null,
) )
sendAction( sendAction(
Internal.ReceiveRegisterResult( Internal.ReceiveRegisterResult(
@ -527,11 +516,10 @@ sealed class CompleteRegistrationEvent {
data object NavigateToMakePasswordStrong : CompleteRegistrationEvent() data object NavigateToMakePasswordStrong : CompleteRegistrationEvent()
/** /**
* Navigates to the captcha verification screen. * Navigates to the Login screen.
*/ */
data class NavigateToLogin( data class NavigateToLogin(
val email: String, val email: String,
val captchaToken: String?,
) : CompleteRegistrationEvent() ) : CompleteRegistrationEvent()
} }
@ -615,14 +603,11 @@ sealed class CompleteRegistrationAction {
/** /**
* Indicates registration was successful and will now attempt to login and unlock the vault. * Indicates registration was successful and will now attempt to login and unlock the vault.
* @property captchaToken The captcha token to use for login. With the login function this
* is possible to be negative.
* *
* @see [AuthRepository.login] * @see [AuthRepository.login]
*/ */
data class ReceiveLoginResult( data class ReceiveLoginResult(
val loginResult: LoginResult, val loginResult: LoginResult,
val captchaToken: String?,
) : Internal() ) : Internal()
} }
} }

View File

@ -64,10 +64,6 @@ fun EnterpriseSignOnScreen(
intentManager.startCustomTabsActivity(event.uri) intentManager.startCustomTabsActivity(event.uri)
} }
is EnterpriseSignOnEvent.NavigateToCaptcha -> {
intentManager.startCustomTabsActivity(event.uri)
}
is EnterpriseSignOnEvent.NavigateToSetPassword -> { is EnterpriseSignOnEvent.NavigateToSetPassword -> {
onNavigateToSetPassword() onNavigateToSetPassword()
} }

View File

@ -14,10 +14,8 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SSO_URI import com.x8bit.bitwarden.data.auth.repository.util.SSO_URI
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@ -53,7 +51,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
?: EnterpriseSignOnState( ?: EnterpriseSignOnState(
dialogState = null, dialogState = null,
orgIdentifierInput = "", orgIdentifierInput = "",
captchaToken = null,
), ),
) { ) {
@ -80,16 +77,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
// Automatically attempt to login again if a captcha token is received.
authRepository
.captchaTokenResultFlow
.onEach {
sendAction(
EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken(it),
)
}
.launchIn(viewModelScope)
checkOrganizationDomainSsoDetails() checkOrganizationDomainSsoDetails()
} }
@ -118,10 +105,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
handleOnLoginResult(action) handleOnLoginResult(action)
} }
is EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken -> {
handleOnReceiveCaptchaToken(action)
}
is EnterpriseSignOnAction.Internal.OnVerifiedOrganizationDomainSsoDetailsReceive -> { is EnterpriseSignOnAction.Internal.OnVerifiedOrganizationDomainSsoDetailsReceive -> {
handleOnVerifiedOrganizationDomainSsoDetailsReceive(action) handleOnVerifiedOrganizationDomainSsoDetailsReceive(action)
} }
@ -151,15 +134,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
@Suppress("LongMethod") @Suppress("LongMethod")
private fun handleOnLoginResult(action: EnterpriseSignOnAction.Internal.OnLoginResult) { private fun handleOnLoginResult(action: EnterpriseSignOnAction.Internal.OnLoginResult) {
when (val loginResult = action.loginResult) { when (val loginResult = action.loginResult) {
is LoginResult.CaptchaRequired -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(
event = EnterpriseSignOnEvent.NavigateToCaptcha(
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
),
)
}
is LoginResult.Error -> { is LoginResult.Error -> {
showError( showError(
message = loginResult.errorMessage?.asText() message = loginResult.errorMessage?.asText()
@ -304,30 +278,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
attemptLogin() attemptLogin()
} }
private fun handleOnReceiveCaptchaToken(
action: EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken,
) {
when (val tokenResult = action.tokenResult) {
CaptchaCallbackTokenResult.MissingToken -> {
mutableStateFlow.update {
it.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
title = BitwardenString.log_in_denied.asText(),
message = BitwardenString.captcha_failed.asText(),
),
)
}
}
is CaptchaCallbackTokenResult.Success -> {
mutableStateFlow.update {
it.copy(captchaToken = tokenResult.token)
}
attemptLogin()
}
}
}
private fun prevalidateSso() { private fun prevalidateSso() {
if (!networkConnectionManager.isNetworkConnected) { if (!networkConnectionManager.isNetworkConnected) {
mutableStateFlow.update { mutableStateFlow.update {
@ -393,7 +343,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
ssoCode = ssoCallbackResult.code, ssoCode = ssoCallbackResult.code,
ssoCodeVerifier = ssoData.codeVerifier, ssoCodeVerifier = ssoData.codeVerifier,
ssoRedirectUri = SSO_URI, ssoRedirectUri = SSO_URI,
captchaToken = state.captchaToken,
organizationIdentifier = state.orgIdentifierInput, organizationIdentifier = state.orgIdentifierInput,
) )
sendAction(EnterpriseSignOnAction.Internal.OnLoginResult(result)) sendAction(EnterpriseSignOnAction.Internal.OnLoginResult(result))
@ -511,7 +460,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
data class EnterpriseSignOnState( data class EnterpriseSignOnState(
val dialogState: DialogState?, val dialogState: DialogState?,
val orgIdentifierInput: String, val orgIdentifierInput: String,
val captchaToken: String?,
) : Parcelable { ) : Parcelable {
/** /**
* Represents the current state of any dialogs on the screen. * Represents the current state of any dialogs on the screen.
@ -560,11 +508,6 @@ sealed class EnterpriseSignOnEvent {
*/ */
data class NavigateToSsoLogin(val uri: Uri) : EnterpriseSignOnEvent() data class NavigateToSsoLogin(val uri: Uri) : EnterpriseSignOnEvent()
/**
* Navigates to the captcha verification screen.
*/
data class NavigateToCaptcha(val uri: Uri) : EnterpriseSignOnEvent()
/** /**
* Navigates to the set master password screen. * Navigates to the set master password screen.
*/ */
@ -644,11 +587,6 @@ sealed class EnterpriseSignOnAction {
val error: Throwable?, val error: Throwable?,
) : Internal() ) : Internal()
/**
* A captcha callback result has been received
*/
data class OnReceiveCaptchaToken(val tokenResult: CaptchaCallbackTokenResult) : Internal()
/** /**
* A result was received when requesting an [VerifiedOrganizationDomainSsoDetailsResult]. * A result was received when requesting an [VerifiedOrganizationDomainSsoDetailsResult].
*/ */

View File

@ -14,13 +14,12 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class LoginRoute( data class LoginRoute(
val emailAddress: String, val emailAddress: String,
val captchaToken: String?,
) )
/** /**
* Class to retrieve login arguments from the [SavedStateHandle]. * Class to retrieve login arguments from the [SavedStateHandle].
*/ */
data class LoginArgs(val emailAddress: String, val captchaToken: String?) data class LoginArgs(val emailAddress: String)
/** /**
* Constructs a [LoginArgs] from the [SavedStateHandle] and internal route data. * Constructs a [LoginArgs] from the [SavedStateHandle] and internal route data.
@ -29,7 +28,6 @@ fun SavedStateHandle.toLoginArgs(): LoginArgs {
val route = this.toRoute<LoginRoute>() val route = this.toRoute<LoginRoute>()
return LoginArgs( return LoginArgs(
emailAddress = route.emailAddress, emailAddress = route.emailAddress,
captchaToken = route.captchaToken,
) )
} }
@ -38,11 +36,10 @@ fun SavedStateHandle.toLoginArgs(): LoginArgs {
*/ */
fun NavController.navigateToLogin( fun NavController.navigateToLogin(
emailAddress: String, emailAddress: String,
captchaToken: String?,
navOptions: NavOptions? = null, navOptions: NavOptions? = null,
) { ) {
this.navigate( this.navigate(
route = LoginRoute(emailAddress = emailAddress, captchaToken = captchaToken), route = LoginRoute(emailAddress = emailAddress),
navOptions = navOptions, navOptions = navOptions,
) )
} }

View File

@ -50,8 +50,6 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -68,7 +66,6 @@ fun LoginScreen(
onNavigateToLoginWithDevice: (emailAddress: String) -> Unit, onNavigateToLoginWithDevice: (emailAddress: String) -> Unit,
onNavigateToTwoFactorLogin: (String, String?, Boolean) -> Unit, onNavigateToTwoFactorLogin: (String, String?, Boolean) -> Unit,
viewModel: LoginViewModel = hiltViewModel(), viewModel: LoginViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current, keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -79,10 +76,6 @@ fun LoginScreen(
onNavigateToMasterPasswordHint(event.emailAddress) onNavigateToMasterPasswordHint(event.emailAddress)
} }
is LoginEvent.NavigateToCaptcha -> {
intentManager.startCustomTabsActivity(uri = event.uri)
}
is LoginEvent.NavigateToEnterpriseSignOn -> { is LoginEvent.NavigateToEnterpriseSignOn -> {
onNavigateToEnterpriseSignOn(event.emailAddress) onNavigateToEnterpriseSignOn(event.emailAddress)
} }

View File

@ -2,7 +2,6 @@
package com.x8bit.bitwarden.ui.auth.feature.login package com.x8bit.bitwarden.ui.auth.feature.login
import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -15,16 +14,12 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.util.toUriOrNull import com.x8bit.bitwarden.data.platform.util.toUriOrNull
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
@ -53,7 +48,6 @@ class LoginViewModel @Inject constructor(
passwordInput = "", passwordInput = "",
environmentLabel = environmentRepository.environment.label, environmentLabel = environmentRepository.environment.label,
dialogState = LoginState.DialogState.Loading(BitwardenString.loading.asText()), dialogState = LoginState.DialogState.Loading(BitwardenString.loading.asText()),
captchaToken = args.captchaToken,
accountSummaries = authRepository accountSummaries = authRepository
.userStateFlow .userStateFlow
.value .value
@ -65,16 +59,6 @@ class LoginViewModel @Inject constructor(
) { ) {
init { init {
authRepository.captchaTokenResultFlow
.onEach {
sendAction(
LoginAction.Internal.ReceiveCaptchaToken(
tokenResult = it,
),
)
}
.launchIn(viewModelScope)
viewModelScope.launch { viewModelScope.launch {
trySendAction( trySendAction(
LoginAction.Internal.ReceiveKnownDeviceResult( LoginAction.Internal.ReceiveKnownDeviceResult(
@ -98,9 +82,6 @@ class LoginViewModel @Inject constructor(
LoginAction.SingleSignOnClick -> handleSingleSignOnClicked() LoginAction.SingleSignOnClick -> handleSingleSignOnClicked()
is LoginAction.PasswordInputChanged -> handlePasswordInputChanged(action) is LoginAction.PasswordInputChanged -> handlePasswordInputChanged(action)
is LoginAction.ErrorDialogDismiss -> handleErrorDialogDismiss() is LoginAction.ErrorDialogDismiss -> handleErrorDialogDismiss()
is LoginAction.Internal.ReceiveCaptchaToken -> {
handleCaptchaTokenReceived(action.tokenResult)
}
is LoginAction.Internal.ReceiveLoginResult -> { is LoginAction.Internal.ReceiveLoginResult -> {
handleReceiveLoginResult(action = action) handleReceiveLoginResult(action = action)
@ -159,15 +140,6 @@ class LoginViewModel @Inject constructor(
@Suppress("MaxLineLength", "LongMethod") @Suppress("MaxLineLength", "LongMethod")
private fun handleReceiveLoginResult(action: LoginAction.Internal.ReceiveLoginResult) { private fun handleReceiveLoginResult(action: LoginAction.Internal.ReceiveLoginResult) {
when (val loginResult = action.loginResult) { when (val loginResult = action.loginResult) {
is LoginResult.CaptchaRequired -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(
event = LoginEvent.NavigateToCaptcha(
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
),
)
}
is LoginResult.EncryptionKeyMigrationRequired -> { is LoginResult.EncryptionKeyMigrationRequired -> {
val vaultUrl = val vaultUrl =
environmentRepository environmentRepository
@ -256,28 +228,6 @@ class LoginViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialogState = null) } mutableStateFlow.update { it.copy(dialogState = null) }
} }
private fun handleCaptchaTokenReceived(tokenResult: CaptchaCallbackTokenResult) {
when (tokenResult) {
CaptchaCallbackTokenResult.MissingToken -> {
mutableStateFlow.update {
it.copy(
dialogState = LoginState.DialogState.Error(
title = BitwardenString.log_in_denied.asText(),
message = BitwardenString.captcha_failed.asText(),
),
)
}
}
is CaptchaCallbackTokenResult.Success -> {
mutableStateFlow.update {
it.copy(captchaToken = tokenResult.token)
}
attemptLogin()
}
}
}
private fun handleCloseButtonClicked() { private fun handleCloseButtonClicked() {
sendEvent(LoginEvent.NavigateBack) sendEvent(LoginEvent.NavigateBack)
} }
@ -302,7 +252,6 @@ class LoginViewModel @Inject constructor(
val result = authRepository.login( val result = authRepository.login(
email = state.emailAddress, email = state.emailAddress,
password = state.passwordInput, password = state.passwordInput,
captchaToken = state.captchaToken,
) )
sendAction( sendAction(
LoginAction.Internal.ReceiveLoginResult( LoginAction.Internal.ReceiveLoginResult(
@ -344,7 +293,6 @@ data class LoginState(
// We never want this saved since the input is sensitive data. // We never want this saved since the input is sensitive data.
@IgnoredOnParcel val passwordInput: String = "", @IgnoredOnParcel val passwordInput: String = "",
val emailAddress: String, val emailAddress: String,
val captchaToken: String?,
val environmentLabel: String, val environmentLabel: String,
val isLoginButtonEnabled: Boolean, val isLoginButtonEnabled: Boolean,
val dialogState: DialogState?, val dialogState: DialogState?,
@ -391,11 +339,6 @@ sealed class LoginEvent {
val emailAddress: String, val emailAddress: String,
) : LoginEvent() ) : LoginEvent()
/**
* Navigates to the captcha verification screen.
*/
data class NavigateToCaptcha(val uri: Uri) : LoginEvent()
/** /**
* Navigates to the enterprise single sign on screen. * Navigates to the enterprise single sign on screen.
*/ */
@ -496,12 +439,6 @@ sealed class LoginAction {
* Models actions that the [LoginViewModel] itself might send. * Models actions that the [LoginViewModel] itself might send.
*/ */
sealed class Internal : LoginAction() { sealed class Internal : LoginAction() {
/**
* Indicates a captcha callback token has been received.
*/
data class ReceiveCaptchaToken(
val tokenResult: CaptchaCallbackTokenResult,
) : Internal()
/** /**
* Indicates that a [KnownDeviceResult] has been received and state should be updated. * Indicates that a [KnownDeviceResult] has been received and state should be updated.

View File

@ -41,8 +41,6 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/** /**
* The top level composable for the Login with Device screen. * The top level composable for the Login with Device screen.
@ -53,15 +51,11 @@ fun LoginWithDeviceScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToTwoFactorLogin: (emailAddress: String) -> Unit, onNavigateToTwoFactorLogin: (emailAddress: String) -> Unit,
viewModel: LoginWithDeviceViewModel = hiltViewModel(), viewModel: LoginWithDeviceViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
LoginWithDeviceEvent.NavigateBack -> onNavigateBack() LoginWithDeviceEvent.NavigateBack -> onNavigateBack()
is LoginWithDeviceEvent.NavigateToCaptcha -> {
intentManager.startCustomTabsActivity(uri = event.uri)
}
is LoginWithDeviceEvent.NavigateToTwoFactorLogin -> { is LoginWithDeviceEvent.NavigateToTwoFactorLogin -> {
onNavigateToTwoFactorLogin(event.emailAddress) onNavigateToTwoFactorLogin(event.emailAddress)

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -11,8 +10,6 @@ import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.util.toAuthRequestType import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.util.toAuthRequestType
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
@ -56,11 +53,6 @@ class LoginWithDeviceViewModel @Inject constructor(
init { init {
sendNewAuthRequest(isResend = false) sendNewAuthRequest(isResend = false)
authRepository
.captchaTokenResultFlow
.map { LoginWithDeviceAction.Internal.ReceiveCaptchaToken(tokenResult = it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
} }
override fun handleAction(action: LoginWithDeviceAction) { override fun handleAction(action: LoginWithDeviceAction) {
@ -95,10 +87,6 @@ class LoginWithDeviceViewModel @Inject constructor(
handleNewAuthRequestResultReceived(action) handleNewAuthRequestResultReceived(action)
} }
is LoginWithDeviceAction.Internal.ReceiveCaptchaToken -> {
handleReceiveCaptchaToken(action)
}
is LoginWithDeviceAction.Internal.ReceiveLoginResult -> { is LoginWithDeviceAction.Internal.ReceiveLoginResult -> {
handleReceiveLoginResult(action) handleReceiveLoginResult(action)
} }
@ -125,7 +113,6 @@ class LoginWithDeviceViewModel @Inject constructor(
masterPasswordHash = result.authRequest.masterPasswordHash, masterPasswordHash = result.authRequest.masterPasswordHash,
asymmetricalKey = requireNotNull(result.authRequest.key), asymmetricalKey = requireNotNull(result.authRequest.key),
privateKey = result.privateKey, privateKey = result.privateKey,
captchaToken = null,
), ),
) )
} }
@ -183,44 +170,11 @@ class LoginWithDeviceViewModel @Inject constructor(
} }
} }
private fun handleReceiveCaptchaToken(
action: LoginWithDeviceAction.Internal.ReceiveCaptchaToken,
) {
when (val tokenResult = action.tokenResult) {
CaptchaCallbackTokenResult.MissingToken -> {
mutableStateFlow.update {
it.copy(
dialogState = LoginWithDeviceState.DialogState.Error(
title = BitwardenString.log_in_denied.asText(),
message = BitwardenString.captcha_failed.asText(),
),
)
}
}
is CaptchaCallbackTokenResult.Success -> {
mutableStateFlow.update {
it.copy(loginData = it.loginData?.copy(captchaToken = tokenResult.token))
}
attemptLogin()
}
}
}
@Suppress("MaxLineLength", "LongMethod") @Suppress("MaxLineLength", "LongMethod")
private fun handleReceiveLoginResult( private fun handleReceiveLoginResult(
action: LoginWithDeviceAction.Internal.ReceiveLoginResult, action: LoginWithDeviceAction.Internal.ReceiveLoginResult,
) { ) {
when (val loginResult = action.loginResult) { when (val loginResult = action.loginResult) {
is LoginResult.CaptchaRequired -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(
event = LoginWithDeviceEvent.NavigateToCaptcha(
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
),
)
}
is LoginResult.TwoFactorRequired -> { is LoginResult.TwoFactorRequired -> {
mutableStateFlow.update { it.copy(dialogState = null) } mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent( sendEvent(
@ -315,7 +269,6 @@ class LoginWithDeviceViewModel @Inject constructor(
asymmetricalKey = loginData.asymmetricalKey, asymmetricalKey = loginData.asymmetricalKey,
requestPrivateKey = loginData.privateKey, requestPrivateKey = loginData.privateKey,
masterPasswordHash = loginData.masterPasswordHash, masterPasswordHash = loginData.masterPasswordHash,
captchaToken = loginData.captchaToken,
) )
} }
@ -502,7 +455,6 @@ data class LoginWithDeviceState(
data class LoginData( data class LoginData(
val accessCode: String, val accessCode: String,
val requestId: String, val requestId: String,
val captchaToken: String?,
val masterPasswordHash: String?, val masterPasswordHash: String?,
val asymmetricalKey: String, val asymmetricalKey: String,
val privateKey: String, val privateKey: String,
@ -518,11 +470,6 @@ sealed class LoginWithDeviceEvent {
*/ */
data object NavigateBack : LoginWithDeviceEvent() data object NavigateBack : LoginWithDeviceEvent()
/**
* Navigates to the captcha verification screen.
*/
data class NavigateToCaptcha(val uri: Uri) : LoginWithDeviceEvent()
/** /**
* Navigates to the two-factor login screen. * Navigates to the two-factor login screen.
*/ */
@ -566,13 +513,6 @@ sealed class LoginWithDeviceAction {
val result: CreateAuthRequestResult, val result: CreateAuthRequestResult,
) : Internal() ) : Internal()
/**
* Indicates a captcha callback token has been received.
*/
data class ReceiveCaptchaToken(
val tokenResult: CaptchaCallbackTokenResult,
) : Internal()
/** /**
* Indicates a login result has been received. * Indicates a login result has been received.
*/ */

View File

@ -104,10 +104,6 @@ fun TwoFactorLoginScreen(
intentManager.launchUri(uri = event.uri) intentManager.launchUri(uri = event.uri)
} }
is TwoFactorLoginEvent.NavigateToCaptcha -> {
intentManager.startCustomTabsActivity(uri = event.uri)
}
is TwoFactorLoginEvent.NavigateToDuo -> { is TwoFactorLoginEvent.NavigateToDuo -> {
intentManager.startCustomTabsActivity(uri = event.uri) intentManager.startCustomTabsActivity(uri = event.uri)
} }
@ -352,7 +348,6 @@ private fun TwoFactorLoginScreenContentPreview() {
displayEmail = "email@dot.com", displayEmail = "email@dot.com",
isContinueButtonEnabled = true, isContinueButtonEnabled = true,
isRememberEnabled = true, isRememberEnabled = true,
captchaToken = null,
email = "", email = "",
password = "", password = "",
orgIdentifier = null, orgIdentifier = null,

View File

@ -20,10 +20,8 @@ import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult 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.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.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult 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.repository.util.generateUriForWebAuth
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@ -71,7 +69,6 @@ class TwoFactorLoginViewModel @Inject constructor(
.preferredAuthMethod .preferredAuthMethod
.isContinueButtonEnabled, .isContinueButtonEnabled,
isRememberEnabled = false, isRememberEnabled = false,
captchaToken = null,
email = args.emailAddress, email = args.emailAddress,
password = args.password, password = args.password,
orgIdentifier = args.orgIdentifier, orgIdentifier = args.orgIdentifier,
@ -95,13 +92,6 @@ class TwoFactorLoginViewModel @Inject constructor(
.onEach { savedStateHandle[KEY_STATE] = it } .onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope) .launchIn(viewModelScope)
// Automatically attempt to login again if a captcha token is received.
authRepository
.captchaTokenResultFlow
.map { TwoFactorLoginAction.Internal.ReceiveCaptchaToken(tokenResult = it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
// Process the Duo result when it is received. // Process the Duo result when it is received.
authRepository authRepository
.duoTokenResultFlow .duoTokenResultFlow
@ -147,9 +137,6 @@ class TwoFactorLoginViewModel @Inject constructor(
private fun handleInternalAction(action: TwoFactorLoginAction.Internal) { private fun handleInternalAction(action: TwoFactorLoginAction.Internal) {
when (action) { when (action) {
is TwoFactorLoginAction.Internal.ReceiveLoginResult -> handleReceiveLoginResult(action) is TwoFactorLoginAction.Internal.ReceiveLoginResult -> handleReceiveLoginResult(action)
is TwoFactorLoginAction.Internal.ReceiveCaptchaToken -> {
handleCaptchaTokenReceived(action)
}
is TwoFactorLoginAction.Internal.ReceiveDuoResult -> { is TwoFactorLoginAction.Internal.ReceiveDuoResult -> {
handleReceiveDuoResult(action) handleReceiveDuoResult(action)
@ -173,30 +160,6 @@ class TwoFactorLoginViewModel @Inject constructor(
} }
} }
private fun handleCaptchaTokenReceived(
action: TwoFactorLoginAction.Internal.ReceiveCaptchaToken,
) {
when (val tokenResult = action.tokenResult) {
CaptchaCallbackTokenResult.MissingToken -> {
mutableStateFlow.update {
it.copy(
dialogState = TwoFactorLoginState.DialogState.Error(
title = BitwardenString.log_in_denied.asText(),
message = BitwardenString.captcha_failed.asText(),
),
)
}
}
is CaptchaCallbackTokenResult.Success -> {
mutableStateFlow.update {
it.copy(captchaToken = tokenResult.token)
}
initiateLogin()
}
}
}
/** /**
* Update the state with the new text and enable or disable the continue button. * Update the state with the new text and enable or disable the continue button.
*/ */
@ -298,14 +261,6 @@ class TwoFactorLoginViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialogState = null) } mutableStateFlow.update { it.copy(dialogState = null) }
when (val loginResult = action.loginResult) { when (val loginResult = action.loginResult) {
// Launch the captcha flow if necessary.
is LoginResult.CaptchaRequired -> {
sendEvent(
event = TwoFactorLoginEvent.NavigateToCaptcha(
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
),
)
}
// NO-OP: This error shouldn't be possible at this stage. // NO-OP: This error shouldn't be possible at this stage.
is LoginResult.TwoFactorRequired -> Unit is LoginResult.TwoFactorRequired -> Unit
@ -611,7 +566,6 @@ class TwoFactorLoginViewModel @Inject constructor(
email = state.email, email = state.email,
password = state.password, password = state.password,
newDeviceOtp = code, newDeviceOtp = code,
captchaToken = state.captchaToken,
orgIdentifier = state.orgIdentifier, orgIdentifier = state.orgIdentifier,
) )
} else { } else {
@ -623,7 +577,6 @@ class TwoFactorLoginViewModel @Inject constructor(
method = state.authMethod.value.toString(), method = state.authMethod.value.toString(),
remember = state.isRememberEnabled, remember = state.isRememberEnabled,
), ),
captchaToken = state.captchaToken,
orgIdentifier = state.orgIdentifier, orgIdentifier = state.orgIdentifier,
) )
} }
@ -650,7 +603,6 @@ data class TwoFactorLoginState(
val isRememberEnabled: Boolean, val isRememberEnabled: Boolean,
val isNewDeviceVerification: Boolean, val isNewDeviceVerification: Boolean,
// Internal // Internal
val captchaToken: String?,
val email: String, val email: String,
val password: String?, val password: String?,
val orgIdentifier: String?, val orgIdentifier: String?,
@ -711,11 +663,6 @@ sealed class TwoFactorLoginEvent {
*/ */
data object NavigateBack : TwoFactorLoginEvent() data object NavigateBack : TwoFactorLoginEvent()
/**
* Navigates to the captcha verification screen.
*/
data class NavigateToCaptcha(val uri: Uri) : TwoFactorLoginEvent()
/** /**
* Navigates to the Duo 2-factor authentication screen. * Navigates to the Duo 2-factor authentication screen.
*/ */
@ -804,13 +751,6 @@ sealed class TwoFactorLoginAction {
* Models actions that the [TwoFactorLoginViewModel] itself might send. * Models actions that the [TwoFactorLoginViewModel] itself might send.
*/ */
sealed class Internal : TwoFactorLoginAction() { sealed class Internal : TwoFactorLoginAction() {
/**
* Indicates a captcha callback token has been received.
*/
data class ReceiveCaptchaToken(
val tokenResult: CaptchaCallbackTokenResult,
) : Internal()
/** /**
* Indicates that a Dup callback token has been received. * Indicates that a Dup callback token has been received.
*/ */

View File

@ -3,11 +3,9 @@ package com.x8bit.bitwarden
import android.content.Intent import android.content.Intent
import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.data.auth.repository.AuthRepository 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.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult 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.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.getDuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
@ -26,7 +24,6 @@ import org.junit.jupiter.api.Test
class AuthCallbackViewModelTest : BaseViewModelTest() { class AuthCallbackViewModelTest : BaseViewModelTest() {
private val authRepository = mockk<AuthRepository> { private val authRepository = mockk<AuthRepository> {
every { setCaptchaCallbackTokenResult(any()) } just runs
every { setSsoCallbackResult(any()) } just runs every { setSsoCallbackResult(any()) } just runs
every { setDuoCallbackTokenResult(any()) } just runs every { setDuoCallbackTokenResult(any()) } just runs
every { setYubiKeyResult(any()) } just runs every { setYubiKeyResult(any()) } just runs
@ -38,7 +35,6 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
mockkStatic( mockkStatic(
Intent::getYubiKeyResultOrNull, Intent::getYubiKeyResultOrNull,
Intent::getWebAuthResultOrNull, Intent::getWebAuthResultOrNull,
Intent::getCaptchaCallbackTokenResult,
Intent::getDuoCallbackTokenResult, Intent::getDuoCallbackTokenResult,
Intent::getSsoCallbackResult, Intent::getSsoCallbackResult,
) )
@ -49,35 +45,16 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
unmockkStatic( unmockkStatic(
Intent::getYubiKeyResultOrNull, Intent::getYubiKeyResultOrNull,
Intent::getWebAuthResultOrNull, Intent::getWebAuthResultOrNull,
Intent::getCaptchaCallbackTokenResult,
Intent::getDuoCallbackTokenResult, Intent::getDuoCallbackTokenResult,
Intent::getSsoCallbackResult, Intent::getSsoCallbackResult,
) )
} }
@Test
fun `on IntentReceive with captcha host should call setCaptchaCallbackToken`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val captchaCallbackTokenResult = CaptchaCallbackTokenResult.Success(token = "mockk_token")
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))
verify(exactly = 1) {
authRepository.setCaptchaCallbackTokenResult(tokenResult = captchaCallbackTokenResult)
}
}
@Test @Test
fun `on IntentReceive with duo host should call setDuoCallbackToken`() { fun `on IntentReceive with duo host should call setDuoCallbackToken`() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent>() val mockIntent = mockk<Intent>()
val duoCallbackTokenResult = DuoCallbackTokenResult.Success(token = "mockk_token") val duoCallbackTokenResult = DuoCallbackTokenResult.Success(token = "mockk_token")
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { mockIntent.getDuoCallbackTokenResult() } returns duoCallbackTokenResult every { mockIntent.getDuoCallbackTokenResult() } returns duoCallbackTokenResult
every { mockIntent.getYubiKeyResultOrNull() } returns null every { mockIntent.getYubiKeyResultOrNull() } returns null
every { mockIntent.getWebAuthResultOrNull() } returns null every { mockIntent.getWebAuthResultOrNull() } returns null
@ -100,7 +77,6 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
every { mockIntent.getSsoCallbackResult() } returns sseCallbackResult every { mockIntent.getSsoCallbackResult() } returns sseCallbackResult
every { mockIntent.getYubiKeyResultOrNull() } returns null every { mockIntent.getYubiKeyResultOrNull() } returns null
every { mockIntent.getWebAuthResultOrNull() } returns null every { mockIntent.getWebAuthResultOrNull() } returns null
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { mockIntent.getDuoCallbackTokenResult() } returns null every { mockIntent.getDuoCallbackTokenResult() } returns null
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent)) viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
@ -116,7 +92,6 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
val yubiKeyResult = mockk<YubiKeyResult>() val yubiKeyResult = mockk<YubiKeyResult>()
every { mockIntent.getYubiKeyResultOrNull() } returns yubiKeyResult every { mockIntent.getYubiKeyResultOrNull() } returns yubiKeyResult
every { mockIntent.getWebAuthResultOrNull() } returns null every { mockIntent.getWebAuthResultOrNull() } returns null
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
every { mockIntent.getDuoCallbackTokenResult() } returns null every { mockIntent.getDuoCallbackTokenResult() } returns null
every { mockIntent.getSsoCallbackResult() } returns null every { mockIntent.getSsoCallbackResult() } returns null
@ -133,7 +108,6 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getWebAuthResultOrNull() } returns webAuthResult every { getWebAuthResultOrNull() } returns webAuthResult
every { getYubiKeyResultOrNull() } returns null every { getYubiKeyResultOrNull() } returns null
every { getCaptchaCallbackTokenResult() } returns null
every { getDuoCallbackTokenResult() } returns null every { getDuoCallbackTokenResult() } returns null
every { getSsoCallbackResult() } returns null every { getSsoCallbackResult() } returns null
} }

View File

@ -1,66 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.util
import android.content.Intent
import android.net.Uri
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Test
class CaptchaUtilsTest : BitwardenComposeTest() {
@Test
fun `generateIntentForCaptcha should return valid Uri`() {
val actualUri = generateUriForCaptcha(captchaId = "testCaptchaId")
val expectedUrl = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
"?data=eyJzaXRlS2V5IjoidGVzdENhcHRjaGFJZCIsImxvY2FsZSI6ImVuX1VTIiwiY2Fsb" +
"GJhY2tVcmkiOiJiaXR3YXJkZW46Ly9jYXB0Y2hhLWNhbGxiYWNrIiwiY2FwdGNoYVJlcXVp" +
"cmVkVGV4dCI6IkNhcHRjaGEgcmVxdWlyZWQifQ==&parent=bitwarden%3A%2F%2F" +
"captcha-callback&v=1"
val expectedUri = Uri.parse(expectedUrl)
assertEquals(expectedUri, actualUri)
}
@Test
fun `getCaptchaCallbackToken should return null when data is null`() {
val intent = mockk<Intent> {
every { data } returns null
every { action } returns Intent.ACTION_VIEW
}
val result = intent.getCaptchaCallbackTokenResult()
assertEquals(null, result)
}
@Test
fun `getCaptchaCallbackToken should return null when action is not Intent ACTION_VIEW`() {
val intent = mockk<Intent> {
every { data } returns null
every { action } returns Intent.ACTION_ANSWER
}
val result = intent.getCaptchaCallbackTokenResult()
assertEquals(null, result)
}
@Test
fun `getCaptchaCallbackToken should return MissingToken with missing token parameter`() {
val intent = mockk<Intent> {
every { data?.getQueryParameter("token") } returns null
every { action } returns Intent.ACTION_VIEW
every { data?.host } returns "captcha-callback"
}
val result = intent.getCaptchaCallbackTokenResult()
assertEquals(CaptchaCallbackTokenResult.MissingToken, result)
}
@Test
fun `getCaptchaCallbackToken should return Success when token query parameter is present`() {
val intent = mockk<Intent> {
every { data?.getQueryParameter("token") } returns "myToken"
every { action } returns Intent.ACTION_VIEW
every { data?.host } returns "captcha-callback"
}
val result = intent.getCaptchaCallbackTokenResult()
assertEquals(CaptchaCallbackTokenResult.Success("myToken"), result)
}
}

View File

@ -61,10 +61,9 @@ class CompleteRegistrationScreenTest : BitwardenComposeTest() {
onNavigateToPreventAccountLockout = { onNavigateToPreventAccountLockout = {
onNavigateToPreventAccountLockoutCalled = true onNavigateToPreventAccountLockoutCalled = true
}, },
onNavigateToLogin = { email, captchaToken -> onNavigateToLogin = { email ->
onNavigateToLoginCalled = true onNavigateToLoginCalled = true
assertTrue(email == EMAIL) assertTrue(email == EMAIL)
assertTrue(captchaToken == TOKEN)
}, },
viewModel = viewModel, viewModel = viewModel,
) )
@ -261,7 +260,6 @@ class CompleteRegistrationScreenTest : BitwardenComposeTest() {
mutableEventFlow.tryEmit( mutableEventFlow.tryEmit(
CompleteRegistrationEvent.NavigateToLogin( CompleteRegistrationEvent.NavigateToLogin(
email = EMAIL, email = EMAIL,
captchaToken = TOKEN,
), ),
) )

View File

@ -18,7 +18,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance.RegistrationEvent import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance.RegistrationEvent
@ -63,7 +62,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
login( login(
email = any(), email = any(),
password = any(), password = any(),
captchaToken = any(),
) )
} returns LoginResult.Success } returns LoginResult.Success
@ -73,11 +71,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
masterPassword = any(), masterPassword = any(),
masterPasswordHint = any(), masterPasswordHint = any(),
emailVerificationToken = any(), emailVerificationToken = any(),
captchaToken = any(),
shouldCheckDataBreaches = any(), shouldCheckDataBreaches = any(),
isMasterPasswordStrong = any(), isMasterPasswordStrong = any(),
) )
} returns RegisterResult.Success(captchaToken = CAPTCHA_BYPASS_TOKEN) } returns RegisterResult.Success
coEvery { coEvery {
setOnboardingStatus(OnboardingStatus.NOT_STARTED) setOnboardingStatus(OnboardingStatus.NOT_STARTED)
@ -103,7 +100,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
specialCircumstanceManager.specialCircumstance = mockCompleteRegistrationCircumstance specialCircumstanceManager.specialCircumstance = mockCompleteRegistrationCircumstance
mockkStatic( mockkStatic(
SavedStateHandle::toCompleteRegistrationArgs, SavedStateHandle::toCompleteRegistrationArgs,
::generateUriForCaptcha,
) )
} }
@ -111,7 +107,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
fun tearDown() { fun tearDown() {
unmockkStatic( unmockkStatic(
SavedStateHandle::toCompleteRegistrationArgs, SavedStateHandle::toCompleteRegistrationArgs,
::generateUriForCaptcha,
) )
} }
@ -153,11 +148,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
masterPassword = PASSWORD, masterPassword = PASSWORD,
masterPasswordHint = null, masterPasswordHint = null,
emailVerificationToken = TOKEN, emailVerificationToken = TOKEN,
captchaToken = null,
shouldCheckDataBreaches = false, shouldCheckDataBreaches = false,
isMasterPasswordStrong = true, isMasterPasswordStrong = true,
) )
} returns RegisterResult.Success(captchaToken = CAPTCHA_BYPASS_TOKEN) } returns RegisterResult.Success
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE) val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE)
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow -> viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem()) assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem())
@ -186,7 +180,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
masterPassword = PASSWORD, masterPassword = PASSWORD,
masterPasswordHint = null, masterPasswordHint = null,
emailVerificationToken = TOKEN, emailVerificationToken = TOKEN,
captchaToken = null,
shouldCheckDataBreaches = false, shouldCheckDataBreaches = false,
isMasterPasswordStrong = true, isMasterPasswordStrong = true,
) )
@ -220,7 +213,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
mockAuthRepository.login( mockAuthRepository.login(
email = EMAIL, email = EMAIL,
password = PASSWORD, password = PASSWORD,
captchaToken = CAPTCHA_BYPASS_TOKEN,
) )
} }
} }
@ -246,7 +238,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
mockAuthRepository.login( mockAuthRepository.login(
email = EMAIL, email = EMAIL,
password = PASSWORD, password = PASSWORD,
captchaToken = CAPTCHA_BYPASS_TOKEN,
) )
} returns LoginResult.TwoFactorRequired } returns LoginResult.TwoFactorRequired
@ -255,7 +246,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertTrue(awaitItem() is CompleteRegistrationEvent.ShowToast) assertTrue(awaitItem() is CompleteRegistrationEvent.ShowToast)
assertEquals( assertEquals(
CompleteRegistrationEvent.NavigateToLogin(EMAIL, CAPTCHA_BYPASS_TOKEN), CompleteRegistrationEvent.NavigateToLogin(EMAIL),
awaitItem(), awaitItem(),
) )
} }
@ -272,7 +263,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
masterPassword = PASSWORD, masterPassword = PASSWORD,
masterPasswordHint = null, masterPasswordHint = null,
emailVerificationToken = TOKEN, emailVerificationToken = TOKEN,
captchaToken = null,
shouldCheckDataBreaches = false, shouldCheckDataBreaches = false,
isMasterPasswordStrong = true, isMasterPasswordStrong = true,
) )
@ -285,7 +275,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
masterPassword = PASSWORD, masterPassword = PASSWORD,
masterPasswordHint = null, masterPasswordHint = null,
emailVerificationToken = TOKEN, emailVerificationToken = TOKEN,
captchaToken = null,
shouldCheckDataBreaches = false, shouldCheckDataBreaches = false,
isMasterPasswordStrong = true, isMasterPasswordStrong = true,
) )
@ -302,7 +291,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
masterPassword = PASSWORD, masterPassword = PASSWORD,
masterPasswordHint = null, masterPasswordHint = null,
emailVerificationToken = TOKEN, emailVerificationToken = TOKEN,
captchaToken = null,
shouldCheckDataBreaches = true, shouldCheckDataBreaches = true,
isMasterPasswordStrong = true, isMasterPasswordStrong = true,
) )
@ -341,7 +329,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
masterPassword = PASSWORD, masterPassword = PASSWORD,
masterPasswordHint = null, masterPasswordHint = null,
emailVerificationToken = TOKEN, emailVerificationToken = TOKEN,
captchaToken = null,
shouldCheckDataBreaches = true, shouldCheckDataBreaches = true,
isMasterPasswordStrong = false, isMasterPasswordStrong = false,
) )
@ -374,7 +361,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
masterPassword = PASSWORD, masterPassword = PASSWORD,
masterPasswordHint = null, masterPasswordHint = null,
emailVerificationToken = TOKEN, emailVerificationToken = TOKEN,
captchaToken = null,
shouldCheckDataBreaches = true, shouldCheckDataBreaches = true,
isMasterPasswordStrong = false, isMasterPasswordStrong = false,
) )
@ -681,7 +667,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
private const val PASSWORD = "longenoughtpassword" private const val PASSWORD = "longenoughtpassword"
private const val EMAIL = "test@test.com" private const val EMAIL = "test@test.com"
private const val TOKEN = "token" private const val TOKEN = "token"
private const val CAPTCHA_BYPASS_TOKEN = "captcha_bypass"
private val DEFAULT_STATE = CompleteRegistrationState( private val DEFAULT_STATE = CompleteRegistrationState(
userEmail = EMAIL, userEmail = EMAIL,
emailVerificationToken = TOKEN, emailVerificationToken = TOKEN,

View File

@ -111,15 +111,6 @@ class EnterpriseSignOnScreenTest : BitwardenComposeTest() {
} }
} }
@Test
fun `NavigateToCaptcha should call startCustomTabsActivity`() {
val captchaUri = Uri.parse("https://captcha.com")
mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToCaptcha(captchaUri))
verify(exactly = 1) {
intentManager.startCustomTabsActivity(captchaUri)
}
}
@Test @Test
fun `NavigateToSetPassword should call onNavigateToSetPassword`() { fun `NavigateToSetPassword should call onNavigateToSetPassword`() {
mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToSetPassword) mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToSetPassword)
@ -279,7 +270,6 @@ class EnterpriseSignOnScreenTest : BitwardenComposeTest() {
private val DEFAULT_STATE = EnterpriseSignOnState( private val DEFAULT_STATE = EnterpriseSignOnState(
dialogState = null, dialogState = null,
orgIdentifierInput = "", orgIdentifierInput = "",
captchaToken = null,
) )
} }
} }

View File

@ -14,9 +14,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
import com.x8bit.bitwarden.data.platform.manager.model.NetworkConnection import com.x8bit.bitwarden.data.platform.manager.model.NetworkConnection
import com.x8bit.bitwarden.data.platform.manager.util.FakeNetworkConnectionManager import com.x8bit.bitwarden.data.platform.manager.util.FakeNetworkConnectionManager
@ -43,11 +41,8 @@ import org.junit.jupiter.api.Test
class EnterpriseSignOnViewModelTest : BaseViewModelTest() { class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
private val mutableSsoCallbackResultFlow = bufferedMutableSharedFlow<SsoCallbackResult>() private val mutableSsoCallbackResultFlow = bufferedMutableSharedFlow<SsoCallbackResult>()
private val mutableCaptchaTokenResultFlow =
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
private val authRepository: AuthRepository = mockk { private val authRepository: AuthRepository = mockk {
every { ssoCallbackResultFlow } returns mutableSsoCallbackResultFlow every { ssoCallbackResultFlow } returns mutableSsoCallbackResultFlow
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
every { rememberedOrgIdentifier } returns null every { rememberedOrgIdentifier } returns null
every { rememberedOrgIdentifier = "Bitwarden" } just runs every { rememberedOrgIdentifier = "Bitwarden" } just runs
coEvery { coEvery {
@ -341,7 +336,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
val orgIdentifier = "Bitwarden" val orgIdentifier = "Bitwarden"
val error = Throwable("Fail!") val error = Throwable("Fail!")
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.Error(errorMessage = null, error = error) } returns LoginResult.Error(errorMessage = null, error = error)
val viewModel = createViewModel( val viewModel = createViewModel(
@ -397,7 +392,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = orgIdentifier, organizationIdentifier = orgIdentifier,
) )
} }
@ -409,7 +403,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
runTest { runTest {
val orgIdentifier = "Bitwarden" val orgIdentifier = "Bitwarden"
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required") } returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required")
val viewModel = createViewModel( val viewModel = createViewModel(
@ -464,7 +458,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = orgIdentifier, organizationIdentifier = orgIdentifier,
) )
} }
@ -476,7 +469,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
runTest { runTest {
val orgIdentifier = "Bitwarden" val orgIdentifier = "Bitwarden"
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.EncryptionKeyMigrationRequired } returns LoginResult.EncryptionKeyMigrationRequired
environmentRepository.environment = Environment.SelfHosted( environmentRepository.environment = Environment.SelfHosted(
@ -539,7 +532,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = orgIdentifier, organizationIdentifier = orgIdentifier,
) )
} }
@ -551,7 +543,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
runTest { runTest {
val orgIdentifier = "Bitwarden" val orgIdentifier = "Bitwarden"
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.EncryptionKeyMigrationRequired } returns LoginResult.EncryptionKeyMigrationRequired
environmentRepository.environment = Environment.SelfHosted( environmentRepository.environment = Environment.SelfHosted(
@ -614,7 +606,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = orgIdentifier, organizationIdentifier = orgIdentifier,
) )
} }
@ -626,7 +617,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
runTest { runTest {
val orgIdentifier = "Bitwarden" val orgIdentifier = "Bitwarden"
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.EncryptionKeyMigrationRequired } returns LoginResult.EncryptionKeyMigrationRequired
environmentRepository.environment = Environment.SelfHosted( environmentRepository.environment = Environment.SelfHosted(
@ -689,7 +680,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = orgIdentifier, organizationIdentifier = orgIdentifier,
) )
} }
@ -701,7 +691,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
runTest { runTest {
val orgIdentifier = "Bitwarden" val orgIdentifier = "Bitwarden"
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.CertificateError } returns LoginResult.CertificateError
val viewModel = createViewModel( val viewModel = createViewModel(
@ -756,7 +746,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = orgIdentifier, organizationIdentifier = orgIdentifier,
) )
} }
@ -767,8 +756,9 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
fun `ssoCallbackResultFlow Success with same state with login Success should show loading dialog, hide it, and save org identifier`() = fun `ssoCallbackResultFlow Success with same state with login Success should show loading dialog, hide it, and save org identifier`() =
runTest { runTest {
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.Success } returns LoginResult.Success
every { every {
authRepository.rememberedOrgIdentifier authRepository.rememberedOrgIdentifier
} returns "Bitwarden" } returns "Bitwarden"
@ -809,7 +799,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = "Bitwarden", organizationIdentifier = "Bitwarden",
) )
} }
@ -818,71 +807,12 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `ssoCallbackResultFlow Success with same state with login CaptchaRequired should show loading dialog, hide it, and send NavigateToCaptcha event`() =
runTest {
coEvery {
authRepository.login(any(), any(), any(), any(), any(), any())
} returns LoginResult.CaptchaRequired("captcha")
every {
authRepository.rememberedOrgIdentifier
} returns "Bitwarden"
val uri: Uri = mockk()
every {
generateUriForCaptcha(captchaId = "captcha")
} returns uri
val initialState = DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden")
val viewModel = createViewModel(
initialState = initialState,
ssoData = DEFAULT_SSO_DATA,
)
val ssoCallbackResult = SsoCallbackResult.Success(state = "abc", code = "lmn")
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
assertEquals(initialState, stateFlow.awaitItem())
mutableSsoCallbackResultFlow.tryEmit(ssoCallbackResult)
assertEquals(
initialState.copy(
dialogState = EnterpriseSignOnState.DialogState.Loading(
BitwardenString.logging_in.asText(),
),
),
stateFlow.awaitItem(),
)
assertEquals(
initialState,
stateFlow.awaitItem(),
)
assertEquals(
EnterpriseSignOnEvent.NavigateToCaptcha(uri),
eventFlow.awaitItem(),
)
}
coVerify(exactly = 1) {
authRepository.login(
email = "test@gmail.com",
ssoCode = "lmn",
ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = "Bitwarden",
)
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `ssoCallbackResultFlow Success with same state with login TwoFactorRequired should show loading dialog, hide it, and send NavigateToTwoFactorLogin event`() = fun `ssoCallbackResultFlow Success with same state with login TwoFactorRequired should show loading dialog, hide it, and send NavigateToTwoFactorLogin event`() =
runTest { runTest {
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.TwoFactorRequired } returns LoginResult.TwoFactorRequired
every { every {
authRepository.rememberedOrgIdentifier authRepository.rememberedOrgIdentifier
@ -925,7 +855,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = "Bitwarden", organizationIdentifier = "Bitwarden",
) )
} }
@ -936,7 +865,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
runTest { runTest {
val orgIdentifier = "Bitwarden" val orgIdentifier = "Bitwarden"
coEvery { coEvery {
authRepository.login(any(), any(), any(), any(), any(), any()) authRepository.login(any(), any(), any(), any(), any())
} returns LoginResult.ConfirmKeyConnectorDomain("bitwarden.com") } returns LoginResult.ConfirmKeyConnectorDomain("bitwarden.com")
val viewModel = createViewModel( val viewModel = createViewModel(
@ -990,75 +919,11 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
ssoCode = "lmn", ssoCode = "lmn",
ssoCodeVerifier = "def", ssoCodeVerifier = "def",
ssoRedirectUri = "bitwarden://sso-callback", ssoRedirectUri = "bitwarden://sso-callback",
captchaToken = null,
organizationIdentifier = orgIdentifier, organizationIdentifier = orgIdentifier,
) )
} }
} }
@Test
fun `captchaTokenResultFlow MissingToken should show error dialog`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.MissingToken)
assertEquals(
DEFAULT_STATE.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
title = BitwardenString.log_in_denied.asText(),
message = BitwardenString.captcha_failed.asText(),
),
),
awaitItem(),
)
}
}
@Test
fun `captchaTokenResultFlow Success should update the state and attempt to login`() = runTest {
coEvery {
authRepository.login(any(), any(), any(), any(), any(), any())
} returns LoginResult.Success
every {
authRepository.rememberedOrgIdentifier
} returns "Bitwarden"
val initialState = DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden")
val viewModel = createViewModel(
initialState = initialState,
ssoData = DEFAULT_SSO_DATA,
ssoCallbackResult = SsoCallbackResult.Success(
state = "abc",
code = "lmn",
),
)
viewModel.stateFlow.test {
assertEquals(
initialState,
awaitItem(),
)
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
assertEquals(
initialState.copy(
captchaToken = "token",
dialogState = EnterpriseSignOnState.DialogState.Loading(
BitwardenString.logging_in.asText(),
),
),
awaitItem(),
)
assertEquals(
initialState.copy(captchaToken = "token"),
awaitItem(),
)
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `OrganizationDomainSsoDetails failure should make a request, hide the dialog, and update the org input based on the remembered org`() = fun `OrganizationDomainSsoDetails failure should make a request, hide the dialog, and update the org input based on the remembered org`() =
@ -1415,7 +1280,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
private val DEFAULT_STATE = EnterpriseSignOnState( private val DEFAULT_STATE = EnterpriseSignOnState(
dialogState = null, dialogState = null,
orgIdentifierInput = "", orgIdentifierInput = "",
captchaToken = null,
) )
private val DEFAULT_SSO_DATA = SsoResponseData( private val DEFAULT_SSO_DATA = SsoResponseData(
state = "abc", state = "abc",

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.auth.feature.login package com.x8bit.bitwarden.ui.auth.feature.login
import android.net.Uri
import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
@ -318,13 +317,6 @@ class LoginScreenTest : BitwardenComposeTest() {
assertTrue(onNavigateBackCalled) assertTrue(onNavigateBackCalled)
} }
@Test
fun `NavigateToCaptcha should call intentManager startCustomTabsActivity`() {
val mockUri = mockk<Uri>()
mutableEventFlow.tryEmit(LoginEvent.NavigateToCaptcha(mockUri))
verify { intentManager.startCustomTabsActivity(mockUri) }
}
@Test @Test
fun `NavigateToMasterPasswordHint should call onNavigateToMasterPasswordHint`() { fun `NavigateToMasterPasswordHint should call onNavigateToMasterPasswordHint`() {
mutableEventFlow.tryEmit(LoginEvent.NavigateToMasterPasswordHint("email")) mutableEventFlow.tryEmit(LoginEvent.NavigateToMasterPasswordHint("email"))
@ -359,7 +351,6 @@ private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
private val DEFAULT_STATE = private val DEFAULT_STATE =
LoginState( LoginState(
emailAddress = EMAIL, emailAddress = EMAIL,
captchaToken = null,
isLoginButtonEnabled = false, isLoginButtonEnabled = false,
passwordInput = "", passwordInput = "",
environmentLabel = "", environmentLabel = "",

View File

@ -1,9 +1,7 @@
package com.x8bit.bitwarden.ui.auth.feature.login package com.x8bit.bitwarden.ui.auth.feature.login
import android.net.Uri
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
import com.bitwarden.data.repository.model.Environment import com.bitwarden.data.repository.model.Environment
import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.base.BaseViewModelTest
@ -15,8 +13,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.model.UserState 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.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@ -41,14 +37,11 @@ import org.junit.jupiter.api.Test
@Suppress("LargeClass") @Suppress("LargeClass")
class LoginViewModelTest : BaseViewModelTest() { class LoginViewModelTest : BaseViewModelTest() {
private val mutableCaptchaTokenResultFlow =
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null) private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
private val authRepository: AuthRepository = mockk(relaxed = true) { private val authRepository: AuthRepository = mockk(relaxed = true) {
coEvery { coEvery {
getIsKnownDevice(EMAIL) getIsKnownDevice(EMAIL)
} returns KnownDeviceResult.Success(false) } returns KnownDeviceResult.Success(false)
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
every { userStateFlow } returns mutableUserStateFlow every { userStateFlow } returns mutableUserStateFlow
every { logout(any()) } just runs every { logout(any()) } just runs
} }
@ -60,7 +53,6 @@ class LoginViewModelTest : BaseViewModelTest() {
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
mockkStatic( mockkStatic(
::generateUriForCaptcha,
SavedStateHandle::toLoginArgs, SavedStateHandle::toLoginArgs,
) )
} }
@ -68,7 +60,6 @@ class LoginViewModelTest : BaseViewModelTest() {
@AfterEach @AfterEach
fun tearDown() { fun tearDown() {
unmockkStatic( unmockkStatic(
::generateUriForCaptcha,
SavedStateHandle::toLoginArgs, SavedStateHandle::toLoginArgs,
) )
} }
@ -265,7 +256,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = "", password = "",
captchaToken = null,
) )
} returns LoginResult.Error(errorMessage = "mock_error", error = error) } returns LoginResult.Error(errorMessage = "mock_error", error = error)
val viewModel = createViewModel() val viewModel = createViewModel()
@ -292,7 +282,7 @@ class LoginViewModelTest : BaseViewModelTest() {
) )
} }
coVerify { coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = null) authRepository.login(email = EMAIL, password = "")
} }
} }
@ -304,7 +294,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = "", password = "",
captchaToken = null,
) )
} returns LoginResult.UnofficialServerError } returns LoginResult.UnofficialServerError
val viewModel = createViewModel() val viewModel = createViewModel()
@ -330,7 +319,7 @@ class LoginViewModelTest : BaseViewModelTest() {
) )
} }
coVerify { coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = null) authRepository.login(email = EMAIL, password = "")
} }
} }
@ -342,7 +331,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = "", password = "",
captchaToken = null,
) )
} returns LoginResult.EncryptionKeyMigrationRequired } returns LoginResult.EncryptionKeyMigrationRequired
fakeEnvironmentRepository.environment = Environment.SelfHosted( fakeEnvironmentRepository.environment = Environment.SelfHosted(
@ -378,7 +366,7 @@ class LoginViewModelTest : BaseViewModelTest() {
) )
} }
coVerify { coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = null) authRepository.login(email = EMAIL, password = "")
} }
} }
@ -390,7 +378,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = "", password = "",
captchaToken = null,
) )
} returns LoginResult.EncryptionKeyMigrationRequired } returns LoginResult.EncryptionKeyMigrationRequired
fakeEnvironmentRepository.environment = Environment.SelfHosted( fakeEnvironmentRepository.environment = Environment.SelfHosted(
@ -424,7 +411,7 @@ class LoginViewModelTest : BaseViewModelTest() {
) )
} }
coVerify { coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = null) authRepository.login(email = EMAIL, password = "")
} }
} }
@ -436,7 +423,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = "", password = "",
captchaToken = null,
) )
} returns LoginResult.EncryptionKeyMigrationRequired } returns LoginResult.EncryptionKeyMigrationRequired
fakeEnvironmentRepository.environment = Environment.SelfHosted( fakeEnvironmentRepository.environment = Environment.SelfHosted(
@ -470,7 +456,7 @@ class LoginViewModelTest : BaseViewModelTest() {
) )
} }
coVerify { coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = null) authRepository.login(email = EMAIL, password = "")
} }
} }
@ -482,7 +468,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = "", password = "",
captchaToken = null,
) )
} returns LoginResult.CertificateError } returns LoginResult.CertificateError
val viewModel = createViewModel() val viewModel = createViewModel()
@ -508,7 +493,7 @@ class LoginViewModelTest : BaseViewModelTest() {
) )
} }
coVerify { coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = null) authRepository.login(email = EMAIL, password = "")
} }
} }
@ -518,7 +503,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = "", password = "",
captchaToken = null,
) )
} returns LoginResult.Success } returns LoginResult.Success
val viewModel = createViewModel() val viewModel = createViewModel()
@ -539,35 +523,10 @@ class LoginViewModelTest : BaseViewModelTest() {
) )
} }
coVerify { coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = null) authRepository.login(email = EMAIL, password = "")
} }
} }
@Test
fun `LoginButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
runTest {
val mockkUri = mockk<Uri>()
every {
generateUriForCaptcha(captchaId = "mock_captcha_id")
} returns mockkUri
coEvery {
authRepository.login(
email = EMAIL,
password = "",
captchaToken = null,
)
} returns LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(LoginAction.LoginButtonClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
assertEquals(LoginEvent.NavigateToCaptcha(uri = mockkUri), awaitItem())
}
coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = null)
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `LoginButtonClick login returns TwoFactorRequired should base64 URL encode password and emit NavigateToTwoFactorLogin`() = fun `LoginButtonClick login returns TwoFactorRequired should base64 URL encode password and emit NavigateToTwoFactorLogin`() =
@ -577,7 +536,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = password, password = password,
captchaToken = null,
) )
} returns LoginResult.TwoFactorRequired } returns LoginResult.TwoFactorRequired
@ -604,7 +562,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = password, password = password,
captchaToken = null,
) )
} }
} }
@ -618,7 +575,6 @@ class LoginViewModelTest : BaseViewModelTest() {
authRepository.login( authRepository.login(
email = EMAIL, email = EMAIL,
password = password, password = password,
captchaToken = null,
) )
} returns LoginResult.NewDeviceVerification(errorMessage = "new device verification needed") } returns LoginResult.NewDeviceVerification(errorMessage = "new device verification needed")
@ -643,7 +599,7 @@ class LoginViewModelTest : BaseViewModelTest() {
) )
} }
coVerify { coVerify {
authRepository.login(email = EMAIL, password = password, captchaToken = null) authRepository.login(email = EMAIL, password = password)
} }
} }
@ -743,22 +699,6 @@ class LoginViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `captchaTokenFlow success update should trigger a login`() = runTest {
coEvery {
authRepository.login(
email = EMAIL,
password = "",
captchaToken = "token",
)
} returns LoginResult.Success
createViewModel()
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
coVerify {
authRepository.login(email = EMAIL, password = "", captchaToken = "token")
}
}
private fun createViewModel(state: LoginState? = null): LoginViewModel = private fun createViewModel(state: LoginState? = null): LoginViewModel =
LoginViewModel( LoginViewModel(
authRepository = authRepository, authRepository = authRepository,
@ -766,7 +706,7 @@ class LoginViewModelTest : BaseViewModelTest() {
vaultRepository = vaultRepository, vaultRepository = vaultRepository,
savedStateHandle = SavedStateHandle().apply { savedStateHandle = SavedStateHandle().apply {
set(key = "state", value = state) set(key = "state", value = state)
every { toLoginArgs() } returns LoginArgs(emailAddress = EMAIL, captchaToken = null) every { toLoginArgs() } returns LoginArgs(emailAddress = EMAIL)
}, },
) )
@ -778,7 +718,6 @@ class LoginViewModelTest : BaseViewModelTest() {
isLoginButtonEnabled = false, isLoginButtonEnabled = false,
environmentLabel = Environment.Us.label, environmentLabel = Environment.Us.label,
dialogState = null, dialogState = null,
captchaToken = null,
accountSummaries = emptyList(), accountSummaries = emptyList(),
shouldShowLoginWithDevice = false, shouldShowLoginWithDevice = false,
) )

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import android.net.Uri
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
@ -113,15 +112,6 @@ class LoginWithDeviceScreenTest : BitwardenComposeTest() {
assertEquals(email, onNavigateToTwoFactorLoginEmail) assertEquals(email, onNavigateToTwoFactorLoginEmail)
} }
@Test
fun `NavigateToCaptcha should call launchUri on intentManager`() {
val uri = mockk<Uri>()
mutableEventFlow.tryEmit(LoginWithDeviceEvent.NavigateToCaptcha(uri))
verify(exactly = 1) {
intentManager.startCustomTabsActivity(uri)
}
}
@Test @Test
fun `progress bar should be displayed according to state`() { fun `progress bar should be displayed according to state`() {
mutableStateFlow.update { mutableStateFlow.update {

View File

@ -11,12 +11,10 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import io.mockk.awaits
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
@ -38,13 +36,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
private val mutableCreateAuthRequestWithUpdatesFlow = private val mutableCreateAuthRequestWithUpdatesFlow =
bufferedMutableSharedFlow<CreateAuthRequestResult>() bufferedMutableSharedFlow<CreateAuthRequestResult>()
private val mutableCaptchaTokenResultFlow =
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
private val authRepository = mockk<AuthRepository> { private val authRepository = mockk<AuthRepository> {
coEvery { coEvery {
createAuthRequestWithUpdates(email = EMAIL, authRequestType = any()) createAuthRequestWithUpdates(email = EMAIL, authRequestType = any())
} returns mutableCreateAuthRequestWithUpdatesFlow } returns mutableCreateAuthRequestWithUpdatesFlow
coEvery { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
} }
private val snackbarRelayManager: SnackbarRelayManager = mockk { private val snackbarRelayManager: SnackbarRelayManager = mockk {
every { sendSnackbarData(data = any(), relay = any()) } just runs every { sendSnackbarData(data = any(), relay = any()) } just runs
@ -181,7 +176,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey, asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey, requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash, masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
captchaToken = null,
) )
} returns LoginResult.Success } returns LoginResult.Success
val viewModel = createViewModel() val viewModel = createViewModel()
@ -232,7 +226,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = requireNotNull(AUTH_REQUEST.key), asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash, masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
) )
} }
} }
@ -315,7 +308,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey, asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey, requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash, masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
captchaToken = null,
) )
} returns LoginResult.TwoFactorRequired } returns LoginResult.TwoFactorRequired
val viewModel = createViewModel() val viewModel = createViewModel()
@ -341,7 +333,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = requireNotNull(AUTH_REQUEST.key), asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash, masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
) )
} }
} }
@ -358,7 +349,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey, asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey, requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash, masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
captchaToken = null,
) )
} returns LoginResult.Error(errorMessage = null, error = error) } returns LoginResult.Error(errorMessage = null, error = error)
val viewModel = createViewModel() val viewModel = createViewModel()
@ -409,7 +399,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = requireNotNull(AUTH_REQUEST.key), asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash, masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
) )
} }
} }
@ -426,7 +415,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey, asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey, requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash, masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
captchaToken = null,
) )
} returns LoginResult.UnofficialServerError } returns LoginResult.UnofficialServerError
val viewModel = createViewModel() val viewModel = createViewModel()
@ -476,7 +464,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = requireNotNull(AUTH_REQUEST.key), asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash, masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
) )
} }
} }
@ -493,7 +480,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey, asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey, requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash, masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
captchaToken = null,
) )
} returns LoginResult.CertificateError } returns LoginResult.CertificateError
val viewModel = createViewModel() val viewModel = createViewModel()
@ -543,7 +529,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = requireNotNull(AUTH_REQUEST.key), asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash, masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
) )
} }
} }
@ -560,7 +545,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey, asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey, requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash, masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
captchaToken = null,
) )
} returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required") } returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required")
val viewModel = createViewModel() val viewModel = createViewModel()
@ -610,69 +594,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
asymmetricalKey = requireNotNull(AUTH_REQUEST.key), asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash, masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
) )
} }
} }
@Test
fun `on captchaTokenResultFlow missing token should should display error dialog`() = runTest {
val viewModel = createViewModel()
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.MissingToken)
assertEquals(
DEFAULT_STATE.copy(
dialogState = LoginWithDeviceState.DialogState.Error(
title = BitwardenString.log_in_denied.asText(),
message = BitwardenString.captcha_failed.asText(),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `on captchaTokenResultFlow success should update the token`() = runTest {
val captchaToken = "captchaToken"
val initialState = DEFAULT_STATE.copy(loginData = DEFAULT_LOGIN_DATA)
coEvery {
authRepository.login(
email = EMAIL,
requestId = DEFAULT_LOGIN_DATA.requestId,
accessCode = DEFAULT_LOGIN_DATA.accessCode,
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
captchaToken = captchaToken,
)
} just awaits
val viewModel = createViewModel(initialState)
viewModel.stateFlow.test {
assertEquals(initialState, awaitItem())
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success(captchaToken))
assertEquals(
initialState.copy(
loginData = DEFAULT_LOGIN_DATA.copy(captchaToken = captchaToken),
dialogState = LoginWithDeviceState.DialogState.Loading(
message = BitwardenString.logging_in.asText(),
),
),
awaitItem(),
)
}
coVerify(exactly = 1) {
authRepository.login(
email = EMAIL,
requestId = AUTH_REQUEST.id,
accessCode = AUTH_REQUEST_ACCESS_CODE,
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = captchaToken,
)
}
}
@Test @Test
fun `on createAuthRequestWithUpdates Error received should show content with error dialog`() { fun `on createAuthRequestWithUpdates Error received should show content with error dialog`() {
val error = Throwable("Fail!") val error = Throwable("Fail!")
@ -803,5 +728,4 @@ private val DEFAULT_LOGIN_DATA = LoginWithDeviceState.LoginData(
masterPasswordHash = "verySecureHash", masterPasswordHash = "verySecureHash",
asymmetricalKey = "public", asymmetricalKey = "public",
privateKey = "private_key", privateKey = "private_key",
captchaToken = null,
) )

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.auth.feature.startregistration package com.x8bit.bitwarden.ui.auth.feature.startregistration
import android.net.Uri
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
@ -10,7 +9,6 @@ import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ContinueClick import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ContinueClick
@ -33,20 +31,13 @@ import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@Suppress("LargeClass") @Suppress("LargeClass")
class StartRegistrationViewModelTest : BaseViewModelTest() { class StartRegistrationViewModelTest : BaseViewModelTest() {
private val mockAuthRepository = mockk<AuthRepository> { private val mockAuthRepository = mockk<AuthRepository>()
every { captchaTokenResultFlow } returns flowOf()
}
private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>() private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>()
private val snackbarRelayManager = mockk<SnackbarRelayManager> { private val snackbarRelayManager = mockk<SnackbarRelayManager> {
every { every {
@ -55,16 +46,6 @@ class StartRegistrationViewModelTest : BaseViewModelTest() {
} }
private val fakeEnvironmentRepository = FakeEnvironmentRepository() private val fakeEnvironmentRepository = FakeEnvironmentRepository()
@BeforeEach
fun setUp() {
mockkStatic(::generateUriForCaptcha)
}
@AfterEach
fun tearDown() {
unmockkStatic(::generateUriForCaptcha)
}
@Test @Test
fun `initial state should be correct`() { fun `initial state should be correct`() {
val viewModel = createViewModel() val viewModel = createViewModel()
@ -186,10 +167,6 @@ class StartRegistrationViewModelTest : BaseViewModelTest() {
@Test @Test
fun `ContinueClick register returns Success with emailVerificationToken should emit NavigateToCompleteRegistration`() = fun `ContinueClick register returns Success with emailVerificationToken should emit NavigateToCompleteRegistration`() =
runTest { runTest {
val mockkUri = mockk<Uri>()
every {
generateUriForCaptcha(captchaId = "mock_captcha_id")
} returns mockkUri
coEvery { coEvery {
mockAuthRepository.sendVerificationEmail( mockAuthRepository.sendVerificationEmail(
email = EMAIL, email = EMAIL,
@ -216,10 +193,6 @@ class StartRegistrationViewModelTest : BaseViewModelTest() {
@Test @Test
fun `ContinueClick register returns Success without emailVerificationToken should emit NavigateToCheckEmail`() = fun `ContinueClick register returns Success without emailVerificationToken should emit NavigateToCheckEmail`() =
runTest { runTest {
val mockkUri = mockk<Uri>()
every {
generateUriForCaptcha(captchaId = "mock_captcha_id")
} returns mockkUri
coEvery { coEvery {
mockAuthRepository.sendVerificationEmail( mockAuthRepository.sendVerificationEmail(
email = EMAIL, email = EMAIL,

View File

@ -270,13 +270,6 @@ class TwoFactorLoginScreenTest : BitwardenComposeTest() {
TestCase.assertTrue(onNavigateBackCalled) TestCase.assertTrue(onNavigateBackCalled)
} }
@Test
fun `NavigateToCaptcha should call intentManager startCustomTabsActivity`() {
val mockUri = mockk<Uri>()
mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToCaptcha(mockUri))
verify { intentManager.startCustomTabsActivity(mockUri) }
}
@Test @Test
fun `NavigateToDuo should call intentManager startCustomTabsActivity`() { fun `NavigateToDuo should call intentManager startCustomTabsActivity`() {
val mockUri = mockk<Uri>() val mockUri = mockk<Uri>()
@ -351,7 +344,6 @@ private val DEFAULT_STATE = TwoFactorLoginState(
isContinueButtonEnabled = false, isContinueButtonEnabled = false,
isRememberEnabled = false, isRememberEnabled = false,
isNewDeviceVerification = false, isNewDeviceVerification = false,
captchaToken = null,
email = "example@email.com", email = "example@email.com",
password = "password123", password = "password123",
orgIdentifier = "orgIdentifier", orgIdentifier = "orgIdentifier",

View File

@ -16,10 +16,8 @@ import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult 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.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.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult 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.repository.util.generateUriForWebAuth
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@ -42,14 +40,11 @@ import org.junit.jupiter.api.Test
@Suppress("LargeClass") @Suppress("LargeClass")
class TwoFactorLoginViewModelTest : BaseViewModelTest() { class TwoFactorLoginViewModelTest : BaseViewModelTest() {
private val mutableCaptchaTokenResultFlow =
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
private val mutableDuoTokenResultFlow = bufferedMutableSharedFlow<DuoCallbackTokenResult>() private val mutableDuoTokenResultFlow = bufferedMutableSharedFlow<DuoCallbackTokenResult>()
private val mutableYubiKeyResultFlow = bufferedMutableSharedFlow<YubiKeyResult>() private val mutableYubiKeyResultFlow = bufferedMutableSharedFlow<YubiKeyResult>()
private val mutableWebAuthResultFlow = bufferedMutableSharedFlow<WebAuthResult>() private val mutableWebAuthResultFlow = bufferedMutableSharedFlow<WebAuthResult>()
private val authRepository: AuthRepository = mockk { private val authRepository: AuthRepository = mockk {
every { twoFactorResponse } returns TWO_FACTOR_RESPONSE every { twoFactorResponse } returns TWO_FACTOR_RESPONSE
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
every { duoTokenResultFlow } returns mutableDuoTokenResultFlow every { duoTokenResultFlow } returns mutableDuoTokenResultFlow
every { yubiKeyResultFlow } returns mutableYubiKeyResultFlow every { yubiKeyResultFlow } returns mutableYubiKeyResultFlow
every { webAuthResultFlow } returns mutableWebAuthResultFlow every { webAuthResultFlow } returns mutableWebAuthResultFlow
@ -59,7 +54,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
email = any(), email = any(),
password = any(), password = any(),
twoFactorData = any(), twoFactorData = any(),
captchaToken = any(),
orgIdentifier = any(), orgIdentifier = any(),
) )
} returns LoginResult.Success } returns LoginResult.Success
@ -72,7 +66,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
mockkStatic( mockkStatic(
::generateUriForCaptcha,
::generateUriForWebAuth, ::generateUriForWebAuth,
SavedStateHandle::toTwoFactorLoginArgs, SavedStateHandle::toTwoFactorLoginArgs,
) )
@ -82,7 +75,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
@AfterEach @AfterEach
fun tearDown() { fun tearDown() {
unmockkStatic( unmockkStatic(
::generateUriForCaptcha,
::generateUriForWebAuth, ::generateUriForWebAuth,
SavedStateHandle::toTwoFactorLoginArgs, SavedStateHandle::toTwoFactorLoginArgs,
) )
@ -177,7 +169,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.YUBI_KEY.value.toString(), method = TwoFactorAuthMethod.YUBI_KEY.value.toString(),
remember = DEFAULT_STATE.isRememberEnabled, remember = DEFAULT_STATE.isRememberEnabled,
), ),
captchaToken = DEFAULT_STATE.captchaToken,
orgIdentifier = DEFAULT_STATE.orgIdentifier, orgIdentifier = DEFAULT_STATE.orgIdentifier,
) )
} }
@ -196,7 +187,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.WEB_AUTH.value.toString(), method = TwoFactorAuthMethod.WEB_AUTH.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} returns LoginResult.Success } returns LoginResult.Success
@ -217,7 +207,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.WEB_AUTH.value.toString(), method = TwoFactorAuthMethod.WEB_AUTH.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} }
@ -254,38 +243,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
) )
} }
@Test
fun `captchaTokenFlow success update should trigger a login`() = runTest {
coEvery {
authRepository.login(
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false,
),
captchaToken = "token",
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.Success
createViewModel()
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
coVerify {
authRepository.login(
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false,
),
captchaToken = "token",
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@Test @Test
fun `duoTokenResultFlow success update should trigger a login`() = runTest { fun `duoTokenResultFlow success update should trigger a login`() = runTest {
coEvery { coEvery {
@ -297,7 +254,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.DUO.value.toString(), method = TwoFactorAuthMethod.DUO.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} returns LoginResult.Success } returns LoginResult.Success
@ -316,7 +272,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.DUO.value.toString(), method = TwoFactorAuthMethod.DUO.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} }
@ -411,7 +366,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} returns LoginResult.Success } returns LoginResult.Success
@ -443,7 +397,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} }
@ -460,7 +413,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
) )
val response = GetTokenResponseJson.TwoFactorRequired( val response = GetTokenResponseJson.TwoFactorRequired(
authMethodsData = authMethodsData, authMethodsData = authMethodsData,
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -495,7 +447,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
) )
val response = GetTokenResponseJson.TwoFactorRequired( val response = GetTokenResponseJson.TwoFactorRequired(
authMethodsData = authMethodsData, authMethodsData = authMethodsData,
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -531,7 +482,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
val data = JsonObject(mapOf("AuthUrl" to JsonPrimitive("bitwarden.com"))) val data = JsonObject(mapOf("AuthUrl" to JsonPrimitive("bitwarden.com")))
val response = GetTokenResponseJson.TwoFactorRequired( val response = GetTokenResponseJson.TwoFactorRequired(
authMethodsData = mapOf(TwoFactorAuthMethod.WEB_AUTH to data), authMethodsData = mapOf(TwoFactorAuthMethod.WEB_AUTH to data),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -574,7 +524,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
runTest { runTest {
val response = GetTokenResponseJson.TwoFactorRequired( val response = GetTokenResponseJson.TwoFactorRequired(
authMethodsData = emptyMap(), authMethodsData = emptyMap(),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -595,55 +544,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `ContinueButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
runTest {
val mockkUri = mockk<Uri>()
every {
generateUriForCaptcha(captchaId = "mock_captcha_id")
} returns mockkUri
coEvery {
authRepository.login(
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick)
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
assertEquals(
TwoFactorLoginEvent.NavigateToCaptcha(uri = mockkUri),
awaitItem(),
)
}
coVerify {
authRepository.login(
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@Test @Test
fun `ContinueButtonClick login returns Error should update dialogState`() = runTest { fun `ContinueButtonClick login returns Error should update dialogState`() = runTest {
val error = Throwable("Fail!") val error = Throwable("Fail!")
@ -656,7 +556,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} returns LoginResult.Error(errorMessage = null, error = error) } returns LoginResult.Error(errorMessage = null, error = error)
@ -698,7 +597,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} }
@ -717,7 +615,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} returns LoginResult.Error(errorMessage = "Mock error message", error = error) } returns LoginResult.Error(errorMessage = "Mock error message", error = error)
@ -759,7 +656,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} }
@ -778,7 +674,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} returns LoginResult.UnofficialServerError } returns LoginResult.UnofficialServerError
@ -819,7 +714,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} }
@ -838,7 +732,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} returns LoginResult.CertificateError } returns LoginResult.CertificateError
@ -879,7 +772,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} }
@ -898,7 +790,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required") } returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required")
@ -939,7 +830,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(), method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
remember = false, remember = false,
), ),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,
) )
} }
@ -1204,7 +1094,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
) )
val localAuthRepository: AuthRepository = mockk { val localAuthRepository: AuthRepository = mockk {
every { twoFactorResponse } returns TWO_FACTOR_RESPONSE every { twoFactorResponse } returns TWO_FACTOR_RESPONSE
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
every { duoTokenResultFlow } returns mutableDuoTokenResultFlow every { duoTokenResultFlow } returns mutableDuoTokenResultFlow
every { yubiKeyResultFlow } returns mutableYubiKeyResultFlow every { yubiKeyResultFlow } returns mutableYubiKeyResultFlow
every { webAuthResultFlow } returns mutableWebAuthResultFlow every { webAuthResultFlow } returns mutableWebAuthResultFlow
@ -1213,7 +1102,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
email = any(), email = any(),
password = any(), password = any(),
newDeviceOtp = any(), newDeviceOtp = any(),
captchaToken = any(),
orgIdentifier = any(), orgIdentifier = any(),
) )
} returns LoginResult.Success } returns LoginResult.Success
@ -1243,7 +1131,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
email = DEFAULT_STATE.email, email = DEFAULT_STATE.email,
password = DEFAULT_STATE.password, password = DEFAULT_STATE.password,
newDeviceOtp = code.trim(), newDeviceOtp = code.trim(),
captchaToken = DEFAULT_STATE.captchaToken,
orgIdentifier = DEFAULT_STATE.orgIdentifier, orgIdentifier = DEFAULT_STATE.orgIdentifier,
) )
} }
@ -1325,7 +1212,6 @@ private val TWO_FACTOR_AUTH_METHODS_DATA = mapOf(
private val TWO_FACTOR_RESPONSE = GetTokenResponseJson.TwoFactorRequired( private val TWO_FACTOR_RESPONSE = GetTokenResponseJson.TwoFactorRequired(
authMethodsData = TWO_FACTOR_AUTH_METHODS_DATA, authMethodsData = TWO_FACTOR_AUTH_METHODS_DATA,
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -1345,7 +1231,6 @@ private val DEFAULT_STATE = TwoFactorLoginState(
dialogState = null, dialogState = null,
isContinueButtonEnabled = false, isContinueButtonEnabled = false,
isRememberEnabled = false, isRememberEnabled = false,
captchaToken = null,
email = DEFAULT_EMAIL_ADDRESS, email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD, password = DEFAULT_PASSWORD,
orgIdentifier = DEFAULT_ORG_IDENTIFIER, orgIdentifier = DEFAULT_ORG_IDENTIFIER,

View File

@ -39,7 +39,6 @@ internal interface UnauthenticatedIdentityApi {
@Field(value = "deviceName") deviceName: String, @Field(value = "deviceName") deviceName: String,
@Field(value = "deviceType") deviceType: String, @Field(value = "deviceType") deviceType: String,
@Field(value = "grant_type") grantType: String, @Field(value = "grant_type") grantType: String,
@Field(value = "captchaResponse") captchaResponse: String?,
@Field(value = "code") ssoCode: String?, @Field(value = "code") ssoCode: String?,
@Field(value = "code_verifier") ssoCodeVerifier: String?, @Field(value = "code_verifier") ssoCodeVerifier: String?,
@Field(value = "redirect_uri") ssoRedirectUri: String?, @Field(value = "redirect_uri") ssoRedirectUri: String?,

View File

@ -83,15 +83,6 @@ sealed class GetTokenResponseJson {
val keyConnectorUrl: String?, val keyConnectorUrl: String?,
) : GetTokenResponseJson() ) : GetTokenResponseJson()
/**
* Models json body of a captcha error.
*/
@Serializable
data class CaptchaRequired(
@SerialName("HCaptcha_SiteKey")
val captchaKey: String,
) : GetTokenResponseJson()
/** /**
* Models json body of an invalid request. * Models json body of an invalid request.
* *
@ -134,11 +125,12 @@ sealed class GetTokenResponseJson {
get() = if (errorMessage?.lowercase() == "new device verification required") { get() = if (errorMessage?.lowercase() == "new device verification required") {
InvalidType.NewDeviceVerification InvalidType.NewDeviceVerification
} else if (errorMessage } else if (errorMessage
?.lowercase() ?.lowercase()
?.contains( ?.contains(
"encryption key migration is required. please log in to the web vault at", "encryption key migration is required. please log in to the web vault at",
) == true) { ) == true
InvalidType.EncryptionKeyMigrationRequired ) {
InvalidType.EncryptionKeyMigrationRequired
} else { } else {
InvalidType.GenericInvalid InvalidType.GenericInvalid
} }
@ -164,9 +156,6 @@ sealed class GetTokenResponseJson {
* `{"1":{"Email":"sh*****@example.com"},"0":{"Email":null}}` * `{"1":{"Email":"sh*****@example.com"},"0":{"Email":null}}`
* The keys are the raw values of the [TwoFactorAuthMethod], * The keys are the raw values of the [TwoFactorAuthMethod],
* and the map is any extra information for the method. * and the map is any extra information for the method.
* @property captchaToken The captcha token used in the second
* login attempt if the user has already passed a captcha
* authentication in the first attempt.
* @property ssoToken If the user is logging on via Single * @property ssoToken If the user is logging on via Single
* Sign On, they'll need this value to complete authentication * Sign On, they'll need this value to complete authentication
* after entering their two-factor code. * after entering their two-factor code.
@ -179,9 +168,6 @@ sealed class GetTokenResponseJson {
@SerialName("TwoFactorProviders") @SerialName("TwoFactorProviders")
val twoFactorProviders: List<String>?, val twoFactorProviders: List<String>?,
@SerialName("CaptchaBypassToken")
val captchaToken: String?,
@SerialName("SsoEmail2faSessionToken") @SerialName("SsoEmail2faSessionToken")
val ssoToken: String?, val ssoToken: String?,
) : GetTokenResponseJson() ) : GetTokenResponseJson()

View File

@ -10,7 +10,6 @@ import kotlinx.serialization.Serializable
* @param emailVerificationToken token used to finish the registration process. * @param emailVerificationToken token used to finish the registration process.
* @param masterPasswordHash the master password (encrypted). * @param masterPasswordHash the master password (encrypted).
* @param masterPasswordHint the hint for the master password (nullable). * @param masterPasswordHint the hint for the master password (nullable).
* @param captchaResponse the captcha bypass token.
* @param userSymmetricKey the user key for the request (encrypted). * @param userSymmetricKey the user key for the request (encrypted).
* @param userAsymmetricKeys a [Keys] object containing public and private keys. * @param userAsymmetricKeys a [Keys] object containing public and private keys.
* @param kdfType the kdf type represented as an [Int]. * @param kdfType the kdf type represented as an [Int].
@ -30,9 +29,6 @@ data class RegisterFinishRequestJson(
@SerialName("masterPasswordHint") @SerialName("masterPasswordHint")
val masterPasswordHint: String?, val masterPasswordHint: String?,
@SerialName("captchaResponse")
val captchaResponse: String?,
@SerialName("userSymmetricKey") @SerialName("userSymmetricKey")
val userSymmetricKey: String, val userSymmetricKey: String,

View File

@ -9,7 +9,6 @@ import kotlinx.serialization.Serializable
* @param email the email to be registered. * @param email the email to be registered.
* @param masterPasswordHash the master password (encrypted). * @param masterPasswordHash the master password (encrypted).
* @param masterPasswordHint the hint for the master password (nullable). * @param masterPasswordHint the hint for the master password (nullable).
* @param captchaResponse the captcha bypass token.
* @param key the user key for the request (encrypted). * @param key the user key for the request (encrypted).
* @param keys a [Keys] object containing public and private keys. * @param keys a [Keys] object containing public and private keys.
* @param kdfType the kdf type represented as an [Int]. * @param kdfType the kdf type represented as an [Int].
@ -26,9 +25,6 @@ data class RegisterRequestJson(
@SerialName("masterPasswordHint") @SerialName("masterPasswordHint")
val masterPasswordHint: String?, val masterPasswordHint: String?,
@SerialName("captchaResponse")
val captchaResponse: String?,
@SerialName("key") @SerialName("key")
val key: String, val key: String,

View File

@ -13,37 +13,9 @@ sealed class RegisterResponseJson {
/** /**
* Models a successful json response of the register request. * Models a successful json response of the register request.
*
* @param captchaBypassToken the bypass token.
*/ */
@Serializable @Serializable
data class Success( data object Success : RegisterResponseJson()
@SerialName("captchaBypassToken")
val captchaBypassToken: String?,
) : RegisterResponseJson()
/**
* Models a json body of a captcha error.
*
* @param validationErrors object containing error validations of the response.
*/
@Serializable
data class CaptchaRequired(
@SerialName("validationErrors")
val validationErrors: ValidationErrors,
) : RegisterResponseJson() {
/**
* Error validations containing a HCaptcha Site Key.
*
* @param captchaKeys keys for attempting captcha verification.
*/
@Serializable
data class ValidationErrors(
@SerialName("HCaptcha_SiteKey")
val captchaKeys: List<String>,
)
}
/** /**
* Represents the json body of an invalid register request. * Represents the json body of an invalid register request.

View File

@ -36,7 +36,6 @@ interface IdentityService {
* @param email user's email address. * @param email user's email address.
* @param authModel information necessary to authenticate with any * @param authModel information necessary to authenticate with any
* of the available login methods. * of the available login methods.
* @param captchaToken captcha token to be passed to the API (nullable).
* @param twoFactorData the two-factor data, if applicable. * @param twoFactorData the two-factor data, if applicable.
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -44,7 +43,6 @@ interface IdentityService {
uniqueAppId: String, uniqueAppId: String,
email: String, email: String,
authModel: IdentityTokenAuthModel, authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel? = null, twoFactorData: TwoFactorDataModel? = null,
newDeviceOtp: String? = null, newDeviceOtp: String? = null,
): Result<GetTokenResponseJson> ): Result<GetTokenResponseJson>

View File

@ -43,11 +43,7 @@ internal class IdentityServiceImpl(
.recoverCatching { throwable -> .recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError() val bitwardenError = throwable.toBitwardenError()
bitwardenError bitwardenError
.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>( .parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
codes = listOf( codes = listOf(
NetworkErrorCode.BAD_REQUEST, NetworkErrorCode.BAD_REQUEST,
NetworkErrorCode.TOO_MANY_REQUESTS, NetworkErrorCode.TOO_MANY_REQUESTS,
@ -61,7 +57,6 @@ internal class IdentityServiceImpl(
uniqueAppId: String, uniqueAppId: String,
email: String, email: String,
authModel: IdentityTokenAuthModel, authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel?, twoFactorData: TwoFactorDataModel?,
newDeviceOtp: String?, newDeviceOtp: String?,
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi ): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
@ -81,7 +76,6 @@ internal class IdentityServiceImpl(
twoFactorCode = twoFactorData?.code, twoFactorCode = twoFactorData?.code,
twoFactorMethod = twoFactorData?.method, twoFactorMethod = twoFactorData?.method,
twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " }, twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " },
captchaResponse = captchaToken,
authRequestId = authModel.authRequestId, authRequestId = authModel.authRequestId,
newDeviceOtp = newDeviceOtp, newDeviceOtp = newDeviceOtp,
) )
@ -89,11 +83,7 @@ internal class IdentityServiceImpl(
.recoverCatching { throwable -> .recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError() val bitwardenError = throwable.toBitwardenError()
bitwardenError bitwardenError
.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>( .parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
code = NetworkErrorCode.BAD_REQUEST, code = NetworkErrorCode.BAD_REQUEST,
json = json, json = json,
) )

View File

@ -134,10 +134,8 @@ class IdentityServiceTest : BaseServiceTest() {
@Test @Test
fun `register success json should be Success`() = runTest { fun `register success json should be Success`() = runTest {
val expectedResponse = RegisterResponseJson.Success( val expectedResponse = RegisterResponseJson.Success
captchaBypassToken = "mock_token", val response = MockResponse().setBody(LOGIN_SUCCESS_JSON)
)
val response = MockResponse().setBody(CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON)
server.enqueue(response) server.enqueue(response)
assertEquals( assertEquals(
expectedResponse.asSuccess(), expectedResponse.asSuccess(),
@ -175,30 +173,6 @@ class IdentityServiceTest : BaseServiceTest() {
) )
} }
@Test
fun `register captcha json should be CaptchaRequired`() = runTest {
val json = """
{
"validationErrors": {
"HCaptcha_SiteKey": [
"mock_token"
]
}
}
"""
val expectedResponse = RegisterResponseJson.CaptchaRequired(
validationErrors = RegisterResponseJson.CaptchaRequired.ValidationErrors(
captchaKeys = listOf("mock_token"),
),
)
val response = MockResponse().setResponseCode(400).setBody(json)
server.enqueue(response)
assertEquals(
expectedResponse.asSuccess(),
identityService.register(registerRequestBody),
)
}
@Test @Test
fun `getToken when request response is Success should return Success`() = runTest { fun `getToken when request response is Success should return Success`() = runTest {
server.enqueue(MockResponse().setBody(LOGIN_SUCCESS_JSON)) server.enqueue(MockResponse().setBody(LOGIN_SUCCESS_JSON))
@ -208,7 +182,6 @@ class IdentityServiceTest : BaseServiceTest() {
username = EMAIL, username = EMAIL,
password = PASSWORD_HASH, password = PASSWORD_HASH,
), ),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
assertEquals(LOGIN_SUCCESS.asSuccess(), result) assertEquals(LOGIN_SUCCESS.asSuccess(), result)
@ -223,27 +196,11 @@ class IdentityServiceTest : BaseServiceTest() {
username = EMAIL, username = EMAIL,
password = PASSWORD_HASH, password = PASSWORD_HASH,
), ),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
assertTrue(result.isFailure) assertTrue(result.isFailure)
} }
@Test
fun `getToken when response is CaptchaRequired should return CaptchaRequired`() = runTest {
server.enqueue(MockResponse().setResponseCode(400).setBody(CAPTCHA_BODY_JSON))
val result = identityService.getToken(
email = EMAIL,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
assertEquals(CAPTCHA_BODY.asSuccess(), result)
}
@Test @Test
fun `getToken when response is TwoFactorRequired should return TwoFactorRequired`() = runTest { fun `getToken when response is TwoFactorRequired should return TwoFactorRequired`() = runTest {
server.enqueue(MockResponse().setResponseCode(400).setBody(TWO_FACTOR_BODY_JSON)) server.enqueue(MockResponse().setResponseCode(400).setBody(TWO_FACTOR_BODY_JSON))
@ -253,7 +210,6 @@ class IdentityServiceTest : BaseServiceTest() {
username = EMAIL, username = EMAIL,
password = PASSWORD_HASH, password = PASSWORD_HASH,
), ),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
assertEquals(TWO_FACTOR_BODY.asSuccess(), result) assertEquals(TWO_FACTOR_BODY.asSuccess(), result)
@ -268,7 +224,6 @@ class IdentityServiceTest : BaseServiceTest() {
username = EMAIL, username = EMAIL,
password = PASSWORD_HASH, password = PASSWORD_HASH,
), ),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
assertEquals(INVALID_LOGIN.asSuccess(), result) assertEquals(INVALID_LOGIN.asSuccess(), result)
@ -284,7 +239,6 @@ class IdentityServiceTest : BaseServiceTest() {
username = EMAIL, username = EMAIL,
password = PASSWORD_HASH, password = PASSWORD_HASH,
), ),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
assertEquals(INVALID_LOGIN.asSuccess(), result) assertEquals(INVALID_LOGIN.asSuccess(), result)
@ -349,10 +303,8 @@ class IdentityServiceTest : BaseServiceTest() {
@Test @Test
fun `registerFinish success json should be Success`() = runTest { fun `registerFinish success json should be Success`() = runTest {
val expectedResponse = RegisterResponseJson.Success( val expectedResponse = RegisterResponseJson.Success
captchaBypassToken = "mock_token", val response = MockResponse().setBody(LOGIN_SUCCESS_JSON)
)
val response = MockResponse().setBody(CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON)
server.enqueue(response) server.enqueue(response)
assertEquals( assertEquals(
expectedResponse.asSuccess(), expectedResponse.asSuccess(),
@ -494,7 +446,6 @@ class IdentityServiceTest : BaseServiceTest() {
email = EMAIL, email = EMAIL,
masterPasswordHash = "mockk_masterPasswordHash", masterPasswordHash = "mockk_masterPasswordHash",
masterPasswordHint = "mockk_masterPasswordHint", masterPasswordHint = "mockk_masterPasswordHint",
captchaResponse = "mockk_captchaResponse",
key = "mockk_key", key = "mockk_key",
keys = RegisterRequestJson.Keys( keys = RegisterRequestJson.Keys(
publicKey = "mockk_publicKey", publicKey = "mockk_publicKey",
@ -508,7 +459,6 @@ class IdentityServiceTest : BaseServiceTest() {
masterPasswordHash = "mockk_masterPasswordHash", masterPasswordHash = "mockk_masterPasswordHash",
masterPasswordHint = "mockk_masterPasswordHint", masterPasswordHint = "mockk_masterPasswordHint",
emailVerificationToken = "mock_emailVerificationToken", emailVerificationToken = "mock_emailVerificationToken",
captchaResponse = "mockk_captchaResponse",
userSymmetricKey = "mockk_key", userSymmetricKey = "mockk_key",
userAsymmetricKeys = RegisterFinishRequestJson.Keys( userAsymmetricKeys = RegisterFinishRequestJson.Keys(
publicKey = "mockk_publicKey", publicKey = "mockk_publicKey",
@ -566,18 +516,10 @@ private val REFRESH_TOKEN_SUCCESS_BODY = RefreshTokenResponseJson.Success(
tokenType = "Bearer", tokenType = "Bearer",
) )
private const val CAPTCHA_BODY_JSON = """
{
"HCaptcha_SiteKey": "123"
}
"""
private val CAPTCHA_BODY = GetTokenResponseJson.CaptchaRequired("123")
private const val TWO_FACTOR_BODY_JSON = """ private const val TWO_FACTOR_BODY_JSON = """
{ {
"TwoFactorProviders2": {"1": {"Email": "ex***@email.com"}, "0": {"Email": null}}, "TwoFactorProviders2": {"1": {"Email": "ex***@email.com"}, "0": {"Email": null}},
"SsoEmail2faSessionToken": "exampleToken", "SsoEmail2faSessionToken": "exampleToken",
"CaptchaBypassToken": "BWCaptchaBypass_ABCXYZ",
"TwoFactorProviders": ["1", "3", "0"] "TwoFactorProviders": ["1", "3", "0"]
} }
""" """
@ -587,7 +529,6 @@ private val TWO_FACTOR_BODY = GetTokenResponseJson.TwoFactorRequired(
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)), TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
), ),
ssoToken = "exampleToken", ssoToken = "exampleToken",
captchaToken = "BWCaptchaBypass_ABCXYZ",
twoFactorProviders = listOf("1", "3", "0"), twoFactorProviders = listOf("1", "3", "0"),
) )
@ -708,12 +649,6 @@ private const val INVALID_MODEL_STATE_EMAIL_TAKEN_ERROR_JSON = """
} }
""" """
private const val CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON = """
{
"captchaBypassToken": "mock_token"
}
"""
private val INVALID_LOGIN = GetTokenResponseJson.Invalid( private val INVALID_LOGIN = GetTokenResponseJson.Invalid(
errorModel = GetTokenResponseJson.Invalid.ErrorModel( errorModel = GetTokenResponseJson.Invalid.ErrorModel(
errorMessage = "123", errorMessage = "123",

View File

@ -19,7 +19,6 @@ class TwoFactorRequiredExtensionTest {
), ),
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)), TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -42,7 +41,6 @@ class TwoFactorRequiredExtensionTest {
), ),
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)), TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -58,7 +56,6 @@ class TwoFactorRequiredExtensionTest {
), ),
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)), TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -71,7 +68,6 @@ class TwoFactorRequiredExtensionTest {
authMethodsData = mapOf( authMethodsData = mapOf(
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)), TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -87,7 +83,6 @@ class TwoFactorRequiredExtensionTest {
), ),
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)), TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -100,7 +95,6 @@ class TwoFactorRequiredExtensionTest {
authMethodsData = mapOf( authMethodsData = mapOf(
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)), TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -116,7 +110,6 @@ class TwoFactorRequiredExtensionTest {
), ),
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)), TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -132,7 +125,6 @@ class TwoFactorRequiredExtensionTest {
mapOf("AuthUrl" to JsonPrimitive(authUrl)), mapOf("AuthUrl" to JsonPrimitive(authUrl)),
), ),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )
@ -148,7 +140,6 @@ class TwoFactorRequiredExtensionTest {
mapOf("AuthUrl" to JsonPrimitive(authUrl)), mapOf("AuthUrl" to JsonPrimitive(authUrl)),
), ),
), ),
captchaToken = null,
ssoToken = null, ssoToken = null,
twoFactorProviders = null, twoFactorProviders = null,
) )

View File

@ -483,7 +483,6 @@ Scanning will happen automatically.</string>
<string name="password_prompt">Master password re-prompt</string> <string name="password_prompt">Master password re-prompt</string>
<string name="password_confirmation">Master password confirmation</string> <string name="password_confirmation">Master password confirmation</string>
<string name="password_confirmation_desc">This action is protected, to continue please re-enter your master password to verify your identity.</string> <string name="password_confirmation_desc">This action is protected, to continue please re-enter your master password to verify your identity.</string>
<string name="captcha_failed">Captcha failed. Please try again.</string>
<string name="update_master_password">Update master password</string> <string name="update_master_password">Update master password</string>
<string name="update_master_password_warning">Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</string> <string name="update_master_password_warning">Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</string>
<string name="updating_password">Updating password</string> <string name="updating_password">Updating password</string>