mirror of
https://github.com/bitwarden/android.git
synced 2026-08-23 17:47:57 -05:00
[PM-20127] Only prompt passkey user verification once (#5026)
This commit is contained in:
@@ -374,10 +374,8 @@ class MainViewModel @Inject constructor(
|
||||
// Set the user's verification status when a new FIDO 2 request is received to force
|
||||
// explicit verification if the user's vault is unlocked when the request is
|
||||
// received.
|
||||
fido2CreateCredentialRequest.providerRequest
|
||||
.biometricPromptResult
|
||||
?.isSuccessful
|
||||
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
|
||||
fido2CredentialManager.isUserVerified =
|
||||
fido2CreateCredentialRequest.isUserPreVerified
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
@@ -393,12 +391,11 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fido2AssertCredentialRequest != null -> {
|
||||
// If device biometric verification was performed as part of single-tap
|
||||
// authentication, set the user's verification state to the device result.
|
||||
// Otherwise, retain the verification state as-is.
|
||||
fido2AssertCredentialRequest.providerRequest.biometricPromptResult
|
||||
?.isSuccessful
|
||||
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
|
||||
// Set the user's verification status when a new FIDO 2 request is received to force
|
||||
// explicit verification if the user's vault is unlocked when the request is
|
||||
// received.
|
||||
fido2CredentialManager.isUserVerified =
|
||||
fido2AssertCredentialRequest.isUserPreVerified
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Assertion(
|
||||
|
||||
+89
-86
@@ -65,92 +65,6 @@ class Fido2CredentialManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialForUnprivilegedApp(
|
||||
userId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = ClientData.DefaultWithExtraData(callingAppInfo.packageName)
|
||||
|
||||
val host = getOriginUrlFromAttestationOptionsOrNull(
|
||||
requestJson = createPublicKeyCredentialRequest.requestJson,
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
|
||||
|
||||
val signatureFingerprint = callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
|
||||
|
||||
val sdkOrigin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = signatureFingerprint,
|
||||
host = host,
|
||||
assetLinkUrl = host,
|
||||
),
|
||||
)
|
||||
|
||||
return registerFido2CredentialInternal(
|
||||
userId = userId,
|
||||
sdkOrigin = sdkOrigin,
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
clientData = clientData,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialForPrivilegedApp(
|
||||
userId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
|
||||
|
||||
val sdkOrigin = createPublicKeyCredentialRequest.origin
|
||||
?.let { Origin.Web(it) }
|
||||
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
|
||||
|
||||
return registerFido2CredentialInternal(
|
||||
userId = userId,
|
||||
sdkOrigin = sdkOrigin,
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
clientData = clientData,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialInternal(
|
||||
userId: String,
|
||||
sdkOrigin: Origin,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
clientData: ClientData,
|
||||
): Fido2RegisterCredentialResult = vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${createPublicKeyCredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
// `true` so that the SDK does not fail if the relying party requests UV.
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidAttestationResponse() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = { Fido2RegisterCredentialResult.Error.InternalError },
|
||||
)
|
||||
|
||||
override fun getPasskeyAttestationOptionsOrNull(
|
||||
requestJson: String,
|
||||
): PasskeyAttestationOptions? =
|
||||
@@ -254,6 +168,95 @@ class Fido2CredentialManagerImpl(
|
||||
?.userVerification
|
||||
?: fallbackRequirement
|
||||
|
||||
private suspend fun registerFido2CredentialForUnprivilegedApp(
|
||||
userId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = ClientData.DefaultWithExtraData(callingAppInfo.packageName)
|
||||
|
||||
val host = getOriginUrlFromAttestationOptionsOrNull(
|
||||
requestJson = createPublicKeyCredentialRequest.requestJson,
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
|
||||
|
||||
val signatureFingerprint = callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
|
||||
|
||||
val sdkOrigin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = signatureFingerprint,
|
||||
host = host,
|
||||
assetLinkUrl = host,
|
||||
),
|
||||
)
|
||||
|
||||
return registerFido2CredentialInternal(
|
||||
userId = userId,
|
||||
sdkOrigin = sdkOrigin,
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
clientData = clientData,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialForPrivilegedApp(
|
||||
userId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
|
||||
|
||||
val sdkOrigin = createPublicKeyCredentialRequest.origin
|
||||
?.let { Origin.Web(it) }
|
||||
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
|
||||
|
||||
return registerFido2CredentialInternal(
|
||||
userId = userId,
|
||||
sdkOrigin = sdkOrigin,
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
clientData = clientData,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialInternal(
|
||||
userId: String,
|
||||
sdkOrigin: Origin,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
clientData: ClientData,
|
||||
): Fido2RegisterCredentialResult = vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${createPublicKeyCredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
// `true` so that the SDK does not fail if the relying party requests UV.
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidAttestationResponse() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to register FIDO2 credential.")
|
||||
Fido2RegisterCredentialResult.Error.InternalError
|
||||
},
|
||||
)
|
||||
|
||||
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAssertionOptionsOrNull(requestJson)
|
||||
?.relyingPartyId
|
||||
|
||||
+3
@@ -14,12 +14,15 @@ import kotlinx.parcelize.Parcelize
|
||||
* credential manager framework.
|
||||
*
|
||||
* @property userId The ID of the user creating the passkey.
|
||||
* @property isUserPreVerified Whether the user has already been verified by the OS biometric
|
||||
* prompt.
|
||||
* @property requestData Provider request data in the form of a [Bundle].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CreateCredentialRequest(
|
||||
val userId: String,
|
||||
val requestData: Bundle,
|
||||
val isUserPreVerified: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
|
||||
+7
-4
@@ -11,16 +11,19 @@ import kotlinx.parcelize.Parcelize
|
||||
/**
|
||||
* Models a FIDO 2 credential authentication request parsed from the launching intent.
|
||||
*
|
||||
* @param userId ID of the user requesting credential authentication.
|
||||
* @param cipherId ID of the cipher to be authenticated against.
|
||||
* @param credentialId ID of the credential to authenticate.
|
||||
* @param requestData Provider request data in the form of a [Bundle].
|
||||
* @property userId ID of the user requesting credential authentication.
|
||||
* @property cipherId ID of the cipher to be authenticated against.
|
||||
* @property credentialId ID of the credential to authenticate.
|
||||
* @property isUserPreVerified Whether the user has already been verified by the OS biometric
|
||||
* prompt.
|
||||
* @property requestData Provider request data in the form of a [Bundle].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CredentialAssertionRequest(
|
||||
val userId: String,
|
||||
val cipherId: String,
|
||||
val credentialId: String,
|
||||
val isUserPreVerified: Boolean,
|
||||
private val requestData: Bundle,
|
||||
) : Parcelable {
|
||||
|
||||
|
||||
@@ -27,8 +27,16 @@ fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest
|
||||
val userId = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
// Extract the OS biometric prompt result from the request data because it is not included in
|
||||
// the bundle returned by `ProviderGetCredentialRequest.asBundle()`.
|
||||
val isUserPreVerified = systemRequest
|
||||
.biometricPromptResult
|
||||
?.isSuccessful
|
||||
?: false
|
||||
|
||||
return Fido2CreateCredentialRequest(
|
||||
userId = userId,
|
||||
isUserPreVerified = isUserPreVerified,
|
||||
requestData = ProviderCreateCredentialRequest.asBundle(systemRequest),
|
||||
)
|
||||
}
|
||||
@@ -53,10 +61,18 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
// Extract the OS biometric prompt result from the request data because it is not included in
|
||||
// the bundle returned by `ProviderGetCredentialRequest.asBundle()`.
|
||||
val isUserPreVerified = systemRequest
|
||||
.biometricPromptResult
|
||||
?.isSuccessful
|
||||
?: false
|
||||
|
||||
return Fido2CredentialAssertionRequest(
|
||||
userId = userId,
|
||||
cipherId = cipherId,
|
||||
credentialId = credentialId,
|
||||
isUserPreVerified = isUserPreVerified,
|
||||
requestData = ProviderGetCredentialRequest.asBundle(systemRequest),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -691,6 +691,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
val viewModel = createViewModel()
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
val fido2Intent = createMockIntent(
|
||||
@@ -715,7 +716,10 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with fido2 create request data should set the user verification based on request`() {
|
||||
val viewModel = createViewModel()
|
||||
val createCredentialRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
val createCredentialRequest = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
isUserPreVerified = true,
|
||||
)
|
||||
val fido2Intent = createMockIntent(
|
||||
mockFido2CreateCredentialRequest = createCredentialRequest,
|
||||
)
|
||||
@@ -739,6 +743,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
val viewModel = createViewModel()
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "selectedUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
val mockIntent = createMockIntent(
|
||||
@@ -769,6 +774,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
val viewModel = createViewModel()
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
val mockIntent = createMockIntent(
|
||||
|
||||
+1
@@ -10,5 +10,6 @@ fun createMockFido2CredentialAssertionRequest(
|
||||
userId = userId,
|
||||
cipherId = "mockCipherId-$number",
|
||||
credentialId = "mockCredentialId-$number",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
+2
@@ -8,8 +8,10 @@ import androidx.core.os.bundleOf
|
||||
*/
|
||||
fun createMockFido2CreateCredentialRequest(
|
||||
number: Int,
|
||||
isUserPreVerified: Boolean = false,
|
||||
requestData: Bundle = bundleOf(),
|
||||
): Fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-$number",
|
||||
isUserPreVerified = isUserPreVerified,
|
||||
requestData = requestData,
|
||||
)
|
||||
|
||||
+81
-2
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.autofill.fido2.util
|
||||
import android.content.Intent
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BiometricPromptResult
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
@@ -18,6 +19,7 @@ import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -58,7 +60,11 @@ class Fido2IntentUtilsTest {
|
||||
every { ProviderCreateCredentialRequest.asBundle(any()) } returns bundleOf()
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||
} returns mockk()
|
||||
} returns mockk(relaxed = true) {
|
||||
every { biometricPromptResult } returns mockk(relaxed = true) {
|
||||
every { isSuccessful } returns false
|
||||
}
|
||||
}
|
||||
|
||||
val createRequest = intent.getFido2CreateCredentialRequestOrNull()
|
||||
assertEquals(
|
||||
@@ -67,6 +73,39 @@ class Fido2IntentUtilsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2CreateCredentialRequestOrNull should set user verification based on biometric prompt result`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
|
||||
}
|
||||
val mockBiometricPromptResult = mockk<BiometricPromptResult>(relaxed = true) {
|
||||
every { isSuccessful } returns false
|
||||
}
|
||||
val mockProviderCreateCredentialRequest =
|
||||
mockk<ProviderCreateCredentialRequest>(relaxed = true) {
|
||||
every { biometricPromptResult } returns mockBiometricPromptResult
|
||||
}
|
||||
every { ProviderCreateCredentialRequest.asBundle(any()) } returns bundleOf()
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||
} returns mockProviderCreateCredentialRequest
|
||||
|
||||
// Verify false is returned when biometric prompt is unsuccessful
|
||||
var createRequest = intent.getFido2CreateCredentialRequestOrNull()
|
||||
assertFalse(createRequest!!.isUserPreVerified)
|
||||
|
||||
// Verify true is returned when biometric prompt is successful
|
||||
every { mockBiometricPromptResult.isSuccessful } returns true
|
||||
createRequest = intent.getFido2CreateCredentialRequestOrNull()
|
||||
assert(createRequest!!.isUserPreVerified)
|
||||
|
||||
// Verify false is returned when biometric prompt result is null
|
||||
every { mockProviderCreateCredentialRequest.biometricPromptResult } returns null
|
||||
createRequest = intent.getFido2CreateCredentialRequestOrNull()
|
||||
assertFalse(createRequest!!.isUserPreVerified)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2CreateCredentialRequestOrNull should return null when build version is below 34`() {
|
||||
@@ -108,7 +147,11 @@ class Fido2IntentUtilsTest {
|
||||
every { ProviderGetCredentialRequest.asBundle(any()) } returns bundleOf()
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||
} returns mockk()
|
||||
} returns mockk(relaxed = true) {
|
||||
every { biometricPromptResult } returns mockk(relaxed = true) {
|
||||
every { isSuccessful } returns false
|
||||
}
|
||||
}
|
||||
|
||||
val assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
|
||||
@@ -116,6 +159,42 @@ class Fido2IntentUtilsTest {
|
||||
assertEquals("mockUserId", assertionRequest?.userId)
|
||||
assertEquals("mockCipherId", assertionRequest?.cipherId)
|
||||
assertEquals("mockCredentialId", assertionRequest?.credentialId)
|
||||
assertEquals(false, assertionRequest?.isUserPreVerified)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2AssertionRequestOrNull should set user verification based on biometric prompt result`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
|
||||
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId"
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
|
||||
}
|
||||
val mockBiometricPromptResult = mockk<BiometricPromptResult>(relaxed = true) {
|
||||
every { isSuccessful } returns false
|
||||
}
|
||||
val mockGetCredentialRequest =
|
||||
mockk<ProviderGetCredentialRequest>(relaxed = true) {
|
||||
every { biometricPromptResult } returns mockBiometricPromptResult
|
||||
}
|
||||
every { ProviderCreateCredentialRequest.asBundle(any()) } returns bundleOf()
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||
} returns mockGetCredentialRequest
|
||||
|
||||
// Verify false is returned when biometric prompt is unsuccessful
|
||||
var assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
assertFalse(assertionRequest!!.isUserPreVerified)
|
||||
|
||||
// Verify true is returned when biometric prompt is successful
|
||||
every { mockBiometricPromptResult.isSuccessful } returns true
|
||||
assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
assert(assertionRequest!!.isUserPreVerified)
|
||||
|
||||
// Verify false is returned when biometric prompt result is null
|
||||
every { mockGetCredentialRequest.biometricPromptResult } returns null
|
||||
assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
assertFalse(assertionRequest!!.isUserPreVerified)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
@@ -148,6 +148,7 @@ class SpecialCircumstanceExtensionsTest {
|
||||
fun `toFido2RequestOrNull should return a non-null value for Fido2Save`() {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
assertEquals(
|
||||
|
||||
+1
@@ -664,6 +664,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
fun `when the active user has an unlocked vault but there is a Fido2Save special circumstance the nav state should be VaultUnlockedForFido2Save`() {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "activeUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
|
||||
+3
@@ -396,6 +396,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
} returns mockk(relaxed = true)
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-1",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
@@ -880,6 +881,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
@@ -959,6 +961,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
val mockUserId = "mockUserId"
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = mockUserId,
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
|
||||
+2
@@ -59,6 +59,7 @@ class Fido2CredentialRequestExtensionsTest {
|
||||
),
|
||||
Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-1",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
.toDefaultAddTypeContent(
|
||||
@@ -96,6 +97,7 @@ class Fido2CredentialRequestExtensionsTest {
|
||||
),
|
||||
Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-1",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
.toDefaultAddTypeContent(
|
||||
|
||||
+309
-1
@@ -4,6 +4,8 @@ import android.net.Uri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
@@ -37,6 +39,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
@@ -68,6 +71,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
@@ -217,17 +221,30 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
mockk<GetPublicKeyCredentialOption>(relaxed = true),
|
||||
)
|
||||
}
|
||||
private val mockBeginGetPublicKeyCredentialOption =
|
||||
mockk<BeginGetPublicKeyCredentialOption>(relaxed = true)
|
||||
private val mockBeginGetCredentialRequest = mockk<BeginGetCredentialRequest>(relaxed = true) {
|
||||
every {
|
||||
beginGetCredentialOptions
|
||||
} returns listOf(
|
||||
mockBeginGetPublicKeyCredentialOption,
|
||||
)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkObject(
|
||||
ProviderCreateCredentialRequest.Companion,
|
||||
ProviderGetCredentialRequest.Companion,
|
||||
BeginGetCredentialRequest.Companion,
|
||||
)
|
||||
every { ProviderCreateCredentialRequest.fromBundle(any()) } returns mockk(relaxed = true)
|
||||
every {
|
||||
ProviderGetCredentialRequest.fromBundle(any())
|
||||
} returns mockProviderGetCredentialRequest
|
||||
every {
|
||||
BeginGetCredentialRequest.fromBundle(any())
|
||||
} returns mockBeginGetCredentialRequest
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -235,6 +252,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
unmockkObject(
|
||||
ProviderCreateCredentialRequest.Companion,
|
||||
ProviderGetCredentialRequest.Companion,
|
||||
BeginGetCredentialRequest.Companion,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -252,7 +270,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
fun `initial dialog state should be correct when Fido2CreateCredentialRequest is present`() =
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
"mockUserId",
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
@@ -1859,6 +1878,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "activeUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
@@ -2553,10 +2573,12 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
//region FIDO2 process handling
|
||||
@Test
|
||||
fun `Fido2CreateCredentialRequest should be evaluated before observing vault data`() {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
@@ -2590,6 +2612,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
fun `Fido2ValidateOriginResult should update dialog state on Unknown error`() = runTest {
|
||||
val mockFido2CreateRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
@@ -2628,6 +2651,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
@@ -2666,6 +2690,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
@@ -2704,6 +2729,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
@@ -2742,6 +2768,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
@@ -2780,6 +2807,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
@@ -2818,6 +2846,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
@@ -2990,6 +3019,261 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2GetCredentialsRequest should validate request and emit CompleteFido2GetCredentialsRequest event`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val mockGetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
val mockFido2CredentialsList = createMockSdkFido2CredentialList(number = 1)
|
||||
val mockCipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = mockFido2CredentialsList,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
mockGetCredentialsRequest,
|
||||
)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any())
|
||||
} returns mockk(relaxed = true)
|
||||
coEvery {
|
||||
fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = any(),
|
||||
relyingPartyId = any(),
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success("mockOrigin")
|
||||
every {
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
} returns listOf(mockCipherView)
|
||||
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(mockCipherView),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
mutableVaultDataStateFlow.value = dataState
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingEvent.CompleteFido2GetCredentialsRequest(
|
||||
result = GetFido2CredentialsResult.Success(
|
||||
userId = "mockUserId-1",
|
||||
option = mockBeginGetPublicKeyCredentialOption,
|
||||
credentials = emptyList(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2GetCredentialsRequest should display error dialog when request is invalid`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val mockGetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
val mockFido2CredentialsList = createMockSdkFido2CredentialList(number = 1)
|
||||
val mockCipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = mockFido2CredentialsList,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
mockGetCredentialsRequest,
|
||||
)
|
||||
|
||||
every {
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
} returns listOf(mockCipherView)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any())
|
||||
} returns mockk(relaxed = true)
|
||||
every {
|
||||
mockBeginGetCredentialRequest.beginGetCredentialOptions
|
||||
} returns emptyList()
|
||||
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(mockCipherView),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
mutableVaultDataStateFlow.value = dataState
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2OperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message =
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2GetCredentialsRequest should display error dialog when relyingParty cannot be identified`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val mockGetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
val mockFido2CredentialsList = createMockSdkFido2CredentialList(number = 1)
|
||||
val mockCipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = mockFido2CredentialsList,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
mockGetCredentialsRequest,
|
||||
)
|
||||
|
||||
every {
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
} returns listOf(mockCipherView)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any())
|
||||
} returns mockk(relaxed = true) {
|
||||
every { relyingPartyId } returns null
|
||||
}
|
||||
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(mockCipherView),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
mutableVaultDataStateFlow.value = dataState
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2OperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message =
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2GetCredentialsRequest should display error dialog when callingApp cannot be verified`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val mockGetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
val mockFido2CredentialsList = createMockSdkFido2CredentialList(number = 1)
|
||||
val mockCipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = mockFido2CredentialsList,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
mockGetCredentialsRequest,
|
||||
)
|
||||
|
||||
every {
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
} returns listOf(mockCipherView)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any())
|
||||
} returns mockk(relaxed = true)
|
||||
every {
|
||||
mockBeginGetCredentialRequest.callingAppInfo
|
||||
} returns null
|
||||
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(mockCipherView),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
mutableVaultDataStateFlow.value = dataState
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2OperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message =
|
||||
R.string.passkey_operation_failed_because_app_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2GetCredentialsRequest should display error dialog when origin validation fails`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val mockGetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
val mockFido2CredentialsList = createMockSdkFido2CredentialList(number = 1)
|
||||
val mockCipherView = createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = mockFido2CredentialsList,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
mockGetCredentialsRequest,
|
||||
)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any())
|
||||
} returns mockk(relaxed = true)
|
||||
coEvery {
|
||||
fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = any(),
|
||||
relyingPartyId = any(),
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Error.Unknown
|
||||
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(mockCipherView),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
mutableVaultDataStateFlow.value = dataState
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2OperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2AssertionRequest should display loading dialog then request user verification when user is not verified and verification is REQUIRED`() =
|
||||
@@ -3651,6 +3935,28 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteFido2Assertion with cancelled result`() =
|
||||
runTest {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
|
||||
createMockFido2CredentialAssertionRequest(number = 1),
|
||||
)
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled)
|
||||
|
||||
verify { fido2CredentialManager.isUserVerified = false }
|
||||
assertNull(viewModel.stateFlow.value.dialogState)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingEvent.CompleteFido2Assertion(
|
||||
result = AssertFido2CredentialResult.Cancelled,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserVerificationFail should display Fido2ErrorDialog and set isUserVerified to false`() {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -4511,6 +4817,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
//endregion FIDO2 process handling
|
||||
|
||||
@Test
|
||||
fun `InternetConnectionErrorReceived should show network error if no internet connection`() =
|
||||
runTest {
|
||||
|
||||
+1
@@ -887,6 +887,7 @@ class VaultItemListingDataExtensionsTest {
|
||||
autofillSelectionData = null,
|
||||
fido2CreationData = Fido2CreateCredentialRequest(
|
||||
userId = "userId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
),
|
||||
fido2CredentialAutofillViews = null,
|
||||
|
||||
Reference in New Issue
Block a user