[PM-104678] (WIP) Refactor passkey part 3 - Authenticate and select passkeys on vault unlock

This commit introduces authentication and selection of passkeys when unlocking the vault. The Fido2Screen is now used to handle this process.
The Fido2CredentialStore now stores FIDO credentials and silently discovers them for use during authentication.
This commit also modifies the behavior of processGetCredentialRequest to filter and discover passkeys for autofill purposes, and introduces functionality to authenticate and select passkeys in the vault unlocking flow, and ensures that the user's email address is displayed on the UI during selection of a passkey for autofill.
This commit is contained in:
Patrick Honkonen 2024-11-15 18:12:51 -05:00
parent 8fbb4b7af4
commit 222ac2f24e
No known key found for this signature in database
GPG key ID: B63AF42A5531C877
4 changed files with 404 additions and 182 deletions

View file

@ -0,0 +1,315 @@
package com.x8bit.bitwarden.ui.autofill.fido2
import androidx.lifecycle.viewModelScope
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.util.toFido2GetCredentialsRequestOrNull
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Models logic for [Fido2Screen].
*/
@Suppress("LargeClass")
@HiltViewModel
class Fido2ViewModel @Inject constructor(
private val authRepository: AuthRepository,
vaultRepository: VaultRepository,
private val fido2CredentialManager: Fido2CredentialManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel<Fido2State, Fido2Event, Fido2Action>(
initialState = run {
val specialCircumstance = specialCircumstanceManager.specialCircumstance
val fido2GetCredentialsRequest = specialCircumstance?.toFido2GetCredentialsRequestOrNull()
val requestUserId = fido2GetCredentialsRequest?.userId
Fido2State(
requestUserId = requireNotNull(requestUserId),
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
dialog = null,
)
},
) {
init {
vaultRepository.vaultDataStateFlow
.map { Fido2Action.Internal.VaultDataStateChangeReceive(it) }
.onEach(::trySendAction)
.launchIn(viewModelScope)
authRepository.userStateFlow
.map { Fido2Action.Internal.UserStateChangeReceive(it) }
.onEach(::trySendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: Fido2Action) {
when (action) {
is Fido2Action.DismissErrorDialogClick -> {
clearDialogState()
sendEvent(
Fido2Event.CompleteFido2GetCredentialsRequest(
result = action.result,
),
)
}
is Fido2Action.Internal -> {
handleInternalAction(action)
}
}
}
private fun handleInternalAction(action: Fido2Action.Internal) {
when (action) {
is Fido2Action.Internal.VaultDataStateChangeReceive -> {
handleVaultDataStateChangeReceive(action.vaultData)
}
is Fido2Action.Internal.UserStateChangeReceive -> {
handleUserStateChangeReceive(action)
}
}
}
private fun handleUserStateChangeReceive(action: Fido2Action.Internal.UserStateChangeReceive) {
val activeUserId = action.userState?.activeUserId ?: return
val requestUserId = state.requestUserId
if (requestUserId != activeUserId) {
authRepository.switchAccount(requestUserId)
}
}
private fun handleVaultDataStateChangeReceive(vaultDataState: DataState<VaultData>) {
when (vaultDataState) {
is DataState.Error -> mutableStateFlow.update {
it.copy(
dialog = Fido2State.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
),
)
}
is DataState.Loaded -> handleVaultDataLoaded()
DataState.Loading -> handleVaultDataLoading()
is DataState.NoNetwork -> handleNoNetwork()
is DataState.Pending -> clearDialogState()
}
}
private fun handleVaultDataLoaded() {
clearDialogState()
if (authRepository.activeUserId != state.requestUserId) {
// Ignore vault data when we are waiting for the account to switch
return
}
viewModelScope.launch {
state
.fido2GetCredentialsRequest
?.let { getCredentialsRequest ->
getFido2CredentialAutofillViewsForSelection(getCredentialsRequest)
}
}
}
private suspend fun getFido2CredentialAutofillViewsForSelection(
fido2GetCredentialsRequest: Fido2GetCredentialsRequest,
) {
val getCredentialsResult = fido2CredentialManager
.getFido2CredentialsForRelyingParty(fido2GetCredentialsRequest)
when (getCredentialsResult) {
is Fido2GetCredentialsResult.Error -> {
showFido2ErrorDialog(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_passkey_does_not_exist
.asText(),
)
}
Fido2GetCredentialsResult.Cancelled,
is Fido2GetCredentialsResult.Success,
-> {
sendEvent(
Fido2Event.CompleteFido2GetCredentialsRequest(
result = getCredentialsResult,
),
)
}
}
sendEvent(
Fido2Event.CompleteFido2GetCredentialsRequest(
result = getCredentialsResult,
),
)
}
private fun handleVaultDataLoading() {
mutableStateFlow.update { it.copy(dialog = Fido2State.DialogState.Loading) }
}
private fun handleNoNetwork() {
mutableStateFlow.update {
it.copy(
dialog = Fido2State.DialogState.Error(
R.string.internet_connection_required_title.asText(),
R.string.internet_connection_required_message.asText(),
),
)
}
}
private fun showFido2ErrorDialog(title: Text, message: Text) {
mutableStateFlow.update {
it.copy(
dialog = Fido2State.DialogState.Error(title, message),
)
}
}
private fun clearDialogState() {
mutableStateFlow.update { it.copy(dialog = null) }
}
}
/**
* Represents the UI state for [Fido2Screen].
*
* @property requestUserId User ID contained within the FIDO 2 request.
* @property fido2GetCredentialsRequest Data required to discover FIDO 2 credential.
*/
data class Fido2State(
val requestUserId: String,
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest?,
val dialog: DialogState?,
) {
/**
* Represents the dialog UI state for [Fido2Screen].
*/
sealed class DialogState {
/**
* Displays a loading dialog.
*/
data object Loading : DialogState()
/**
* Displays a generic error dialog with a [title] and [message].
*/
data class Error(val title: Text, val message: Text) : DialogState()
/**
* Displays a PIN entry dialog to verify the user.
*/
data class Fido2PinPrompt(val selectedCipherId: String) : DialogState()
/**
* Displays a master password entry dialog to verify the user.
*/
data class Fido2MasterPasswordPrompt(val selectedCipherId: String) : DialogState()
/**
* Displays a PIN creation dialog for user verification.
*/
data class Fido2PinSetUpPrompt(val selectedCipherId: String) : DialogState()
/**
* Displays a master password validation error dialog.
*/
data class Fido2MasterPasswordError(
val title: Text?,
val message: Text,
val selectedCipherId: String,
) : DialogState()
/**
* Displays a PIN set up error dialog.
*/
data class Fido2PinSetUpError(
val title: Text?,
val message: Text,
val selectedCipherId: String,
) : DialogState()
/**
* Displays a PIN validation error dialog.
*/
data class Fido2PinError(
val title: Text?,
val message: Text,
val selectedCipherId: String,
) : DialogState()
}
}
/**
* Models events for [Fido2Screen].
*/
sealed class Fido2Event {
/**
* Completes FIDO 2 credential discovery with the given [result].
*/
data class CompleteFido2GetCredentialsRequest(
val result: Fido2GetCredentialsResult,
) : BackgroundEvent, Fido2Event()
/**
* Performs device based user verification.
*/
data class Fido2UserVerification(
val required: Boolean,
val selectedCipher: CipherView,
) : BackgroundEvent, Fido2Event()
}
/**
* Models actions for [Fido2Screen].
*/
sealed class Fido2Action {
/**
* Indicates the user dismissed the error dialog.
*/
data class DismissErrorDialogClick(val result: Fido2GetCredentialsResult) : Fido2Action()
/**
* Models actions [Fido2ViewModel] may itself send.
*/
sealed class Internal : Fido2Action() {
/**
* Indicates the [userState] has changed.
*/
data class UserStateChangeReceive(
val userState: UserState?,
) : Internal()
/**
* Indicates the [vaultData] has changed.
*/
data class VaultDataStateChangeReceive(
val vaultData: DataState<VaultData>,
) : Internal()
}
}

View file

@ -5,7 +5,6 @@ import androidx.annotation.DrawableRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.vault.CipherRepromptType
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@ -170,7 +169,6 @@ class VaultItemListingViewModel @Inject constructor(
it
.filterForAutofillIfNecessary()
.filterForFido2CreationIfNecessary()
.filterForFidoGetCredentialsIfNecessary()
.filterForTotpIfNecessary(),
)
}
@ -518,6 +516,7 @@ class VaultItemListingViewModel @Inject constructor(
VaultItemListingEvent.NavigateToAddVaultItem(
vaultItemCipherType = itemListingType.toVaultItemCipherType(),
selectedFolderId = itemListingType.folderId,
selectedCollectionId = null,
)
}
@ -688,16 +687,6 @@ class VaultItemListingViewModel @Inject constructor(
}
}
private fun authenticateFido2Credential(request: Fido2CredentialAssertionRequest) {
viewModelScope.launch {
sendAction(
VaultItemListingsAction.Internal.Fido2AssertionResultReceive(
result = fido2CredentialManager.authenticateFido2Credential(request),
),
)
}
}
private fun handleMasterPasswordRepromptSubmit(
action: VaultItemListingsAction.MasterPasswordRepromptSubmit,
) {
@ -940,14 +929,6 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> {
handleFido2RegisterCredentialResultReceive(action)
}
is VaultItemListingsAction.Internal.Fido2AssertionDataReceive -> {
handleFido2AssertionDataReceive(action)
}
is VaultItemListingsAction.Internal.Fido2AssertionResultReceive -> {
handleFido2AssertionResultReceive(action)
}
}
}
@ -1212,10 +1193,6 @@ class VaultItemListingViewModel @Inject constructor(
cipherView = cipherView,
)
}
?: specialCircumstanceManager
.specialCircumstance
?.toFido2AssertionRequestOrNull()
?.let { authenticateFido2Credential(request = it) }
?: showFido2ErrorDialog()
}
//endregion VaultItemListing Handlers
@ -1238,31 +1215,7 @@ class VaultItemListingViewModel @Inject constructor(
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
state
// TODO: Move FIDO 2 get credentials to vault unlock screen
.fido2GetCredentialsRequest
?.let { fido2GetCredentialsRequest ->
viewModelScope.launch {
sendEvent(
VaultItemListingEvent.CompleteFido2GetCredentialsRequest(
fido2CredentialManager.getFido2CredentialsForRelyingParty(
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
),
),
)
}
}
?: state
// TODO: Move FIDO 2 authentication to vault unlock screen
.fido2CredentialAssertionRequest
?.let { fido2AssertionRequest ->
trySendAction(
VaultItemListingsAction.Internal.Fido2AssertionDataReceive(
fido2AssertionRequest,
),
)
}
?: mutableStateFlow.update { it.copy(isRefreshing = false) }
mutableStateFlow.update { it.copy(isRefreshing = false) }
}
private fun vaultLoadingReceive() {
@ -1324,43 +1277,6 @@ class VaultItemListingViewModel @Inject constructor(
sendEvent(VaultItemListingEvent.CompleteFido2Registration(action.result))
}
private fun handleFido2AssertionDataReceive(
action: VaultItemListingsAction.Internal.Fido2AssertionDataReceive,
) {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Loading(
message = R.string.loading.asText(),
),
)
}
val request = action.data
val ciphers = vaultRepository
.ciphersStateFlow
.value
.data
.orEmpty()
.filter { it.isActiveWithFido2Credentials }
if (request.cipherId.isNullOrEmpty()) {
showFido2ErrorDialog()
} else {
val selectedCipher = ciphers
.find { it.id == request.cipherId }
?: run {
showFido2ErrorDialog()
return
}
if (state.hasMasterPassword &&
selectedCipher.reprompt == CipherRepromptType.PASSWORD
) {
repromptMasterPasswordForFido2Assertion(request.cipherId)
} else {
verifyUserAndAuthenticateCredential(request, selectedCipher)
}
}
}
private fun repromptMasterPasswordForFido2Assertion(cipherId: String) {
mutableStateFlow.update {
it.copy(
@ -1371,43 +1287,6 @@ class VaultItemListingViewModel @Inject constructor(
}
}
private fun verifyUserAndAuthenticateCredential(
request: Fido2CredentialAssertionRequest,
selectedCipher: CipherView,
) {
if (fido2CredentialManager.isUserVerified) {
authenticateFido2Credential(request)
return
}
val userVerificationRequirement: UserVerificationRequirement = fido2CredentialManager
.getUserVerificationRequirementForAssertion(request)
when (userVerificationRequirement) {
UserVerificationRequirement.DISCOURAGED -> {
authenticateFido2Credential(request)
}
UserVerificationRequirement.PREFERRED -> {
sendUserVerificationEvent(isRequired = false, selectedCipher = selectedCipher)
}
UserVerificationRequirement.REQUIRED -> {
sendUserVerificationEvent(isRequired = true, selectedCipher = selectedCipher)
}
}
}
private fun handleFido2AssertionResultReceive(
action: VaultItemListingsAction.Internal.Fido2AssertionResultReceive,
) {
fido2CredentialManager.isUserVerified = false
clearDialogState()
sendEvent(
VaultItemListingEvent.CompleteFido2Assertion(action.result),
)
}
private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) {
mutableStateFlow.update { currentState ->
currentState.copy(
@ -1505,27 +1384,6 @@ class VaultItemListingViewModel @Inject constructor(
}
}
/**
* Takes the given vault data and filters it for FIDO 2 credential selection.
*/
@Suppress("MaxLineLength")
private suspend fun DataState<VaultData>.filterForFidoGetCredentialsIfNecessary(): DataState<VaultData> {
val request = state.fido2GetCredentialsRequest ?: return this
return this.map { vaultData ->
val matchUri = request.origin
?: request.packageName
.toAndroidAppUriString()
vaultData.copy(
cipherViewList = cipherMatchingManager.filterCiphersForMatches(
ciphers = vaultData.cipherViewList,
matchUri = matchUri,
),
fido2CredentialAutofillViewList = vaultData.toFido2CredentialAutofillViews(),
)
}
}
/**
* Takes the given vault data and filters it for totp data.
*/
@ -2404,20 +2262,6 @@ sealed class VaultItemListingsAction {
data class Fido2RegisterCredentialResultReceive(
val result: Fido2RegisterCredentialResult,
) : Internal()
/**
* Indicates that FIDO 2 assertion request data has been received.
*/
data class Fido2AssertionDataReceive(
val data: Fido2CredentialAssertionRequest,
) : Internal()
/**
* Indicates that a result of a FIDO 2 credential assertion has been received.
*/
data class Fido2AssertionResultReceive(
val result: Fido2CredentialAssertionResult,
) : Internal()
}
}

View file

@ -31,12 +31,14 @@ import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2Credential
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@ -75,6 +77,8 @@ class Fido2ProviderProcessorTest {
private val vaultRepository: VaultRepository = mockk {
every { ciphersStateFlow } returns mutableCiphersStateFlow
}
private val environmentRepository: EnvironmentRepository = mockk {
}
private val passkeyAssertionOptions = createMockPasskeyAssertionOptions(number = 1)
private val fido2CredentialManager: Fido2CredentialManager = mockk()
private val fido2CredentialStore: Fido2CredentialStore = mockk()
@ -294,11 +298,11 @@ class Fido2ProviderProcessorTest {
)
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
every { context.getString(any()) } returns "mockTitle"
every { context.getString(any(), any()) } returns "mockTitle"
every {
intentManager.createFido2UnlockPendingIntent(
action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT",
userId = "mockUserId-1",
userId = DEFAULT_USER_STATE.activeUserId,
requestCode = any(),
)
} returns mockIntent
@ -315,7 +319,7 @@ class Fido2ProviderProcessorTest {
callback.onResult(any())
intentManager.createFido2UnlockPendingIntent(
action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT",
userId = "mockUserId-1",
userId = DEFAULT_USER_STATE.activeUserId,
requestCode = any(),
)
}
@ -339,8 +343,16 @@ class Fido2ProviderProcessorTest {
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
val captureSlot = slot<GetCredentialException>()
mutableUserStateFlow.value = DEFAULT_USER_STATE
every { context.getString(any(), any()) } returns "mockEmail-0"
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onError(capture(captureSlot)) } just runs
every {
intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT,
userId = "mockUserId-0",
requestCode = 0,
)
} returns mockk()
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
@ -365,8 +377,16 @@ class Fido2ProviderProcessorTest {
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
val captureSlot = slot<GetCredentialException>()
mutableUserStateFlow.value = DEFAULT_USER_STATE
every { context.getString(any(), any()) } returns "mockEmail-0"
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onError(capture(captureSlot)) } just runs
every {
intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT,
userId = "mockUserId-0",
requestCode = 0,
)
} returns mockk()
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
@ -393,8 +413,16 @@ class Fido2ProviderProcessorTest {
val mockCipherViews = listOf(createMockCipherView(number = 1))
mutableUserStateFlow.value = DEFAULT_USER_STATE
mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews)
every { context.getString(any(), any()) } returns "mockEmail-0"
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onError(capture(captureSlot)) } just runs
every {
intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT,
userId = "mockUserId-0",
requestCode = 0,
)
} returns mockk()
coEvery {
vaultRepository.getDecryptedFido2CredentialAutofillViews(any())
} returns DecryptFido2CredentialAutofillViewResult.Error
@ -426,6 +454,10 @@ class Fido2ProviderProcessorTest {
@Suppress("MaxLineLength")
@Test
fun `processGetCredentialRequest should invoke callback with filtered and discovered passkeys`() {
val userState = UserState(
activeUserId = "mockUserId-0",
accounts = createMockAccounts(1),
)
val mockOption = BeginGetPublicKeyCredentialOption(
candidateQueryData = Bundle(),
id = "",
@ -436,19 +468,28 @@ class Fido2ProviderProcessorTest {
}
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
val captureSlot = slot<BeginGetCredentialResponse>()
val mockCipherViews = listOf(createMockCipherView(number = 1))
val mockCipherView = createMockCipherView(
number = 1,
fido2Credentials = listOf(createMockSdkFido2Credential(number = 1)),
)
val mockCipherViews = listOf(mockCipherView)
val mockFido2CredentialAutofillViews = listOf(
createMockFido2CredentialAutofillView(number = 1),
createMockFido2CredentialAutofillView(
number = 1,
cipherId = mockCipherView.id,
rpId = "mockRelyingPartyId-1",
),
)
val mockIntent: PendingIntent = mockk()
val mockPublicKeyCredentialEntry: PublicKeyCredentialEntry = mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE
mutableUserStateFlow.value = userState
mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews)
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { context.getString(any(), any()) } returns "mockEmail-0"
every { callback.onResult(capture(captureSlot)) } just runs
coEvery {
vaultRepository.silentlyDiscoverCredentials(
userId = DEFAULT_USER_STATE.activeUserId,
userId = userState.activeUserId,
fido2CredentialStore = fido2CredentialStore,
relyingPartyId = "mockRelyingPartyId-1",
)
@ -458,8 +499,8 @@ class Fido2ProviderProcessorTest {
} returns DecryptFido2CredentialAutofillViewResult.Success(mockFido2CredentialAutofillViews)
every {
intentManager.createFido2GetCredentialPendingIntent(
action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY",
userId = DEFAULT_USER_STATE.activeUserId,
action = GET_PASSKEY_INTENT,
userId = userState.activeUserId,
credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(),
cipherId = mockFido2CredentialAutofillViews.first().cipherId,
requestCode = any(),

View file

@ -6,12 +6,10 @@ import android.content.Intent
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.PublicKeyCredentialEntry
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.processor.GET_PASSKEY_INTENT
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.Called
import io.mockk.MockKVerificationScope
import io.mockk.Ordering
@ -84,8 +82,6 @@ class Fido2CompletionManagerTest {
@Nested
inner class DefaultImplementation {
private val mockIntentManager = mockk<IntentManager>()
@BeforeEach
fun setUp() {
fido2CompletionManager = Fido2CompletionManagerImpl(mockActivity)
@ -189,20 +185,10 @@ class Fido2CompletionManagerTest {
mockkStatic(PendingIntent::class)
val mockCredentialEntry = mockk<PublicKeyCredentialEntry>()
val mockFido2AutofillView = createMockFido2CredentialAutofillView(number = 1)
every {
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
} returns mockCredentialEntry
every {
mockIntentManager.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT,
userId = "mockUserId",
credentialId = mockFido2AutofillView.credentialId.toString(),
cipherId = mockFido2AutofillView.cipherId,
requestCode = any(),
)
} returns mockk()
every { mockActivity.getString(any()) } returns "No username"
fido2CompletionManager
@ -230,6 +216,42 @@ class Fido2CompletionManagerTest {
assertTrue(responseSlot.captured.authenticationActions.isEmpty())
}
@Suppress("MaxLineLength")
@Test
fun `completeFido2GetCredentials should set username to default value when userNameForUi is null`() {
mockkConstructor(PublicKeyCredentialEntry.Builder::class)
mockkStatic(PendingIntent::class)
val mockCredentialEntry = mockk<PublicKeyCredentialEntry>()
every {
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
} returns mockCredentialEntry
every { mockActivity.getString(any()) } returns "No Username"
fido2CompletionManager
.completeFido2GetCredentialRequest(
Fido2GetCredentialsResult.Success(
userId = "mockUserId",
options = mockk(),
credentialEntries = listOf(mockCredentialEntry),
),
)
val responseSlot = slot<BeginGetCredentialResponse>()
verify {
mockActivity.getString(R.string.no_username)
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
PendingIntentHandler.setBeginGetCredentialResponse(
intent = any(),
response = capture(responseSlot),
)
}
assertEquals(
listOf(mockCredentialEntry),
responseSlot.captured.credentialEntries,
)
}
@Suppress("MaxLineLength")
@Test
fun `completeFido2GetCredentials should set GetCredentialException, set activity result, then finish activity when result is Error`() {