Skip to content

Commit 72a2018

Browse files
authored
[PM-10450] Fix FIdo2 never lock user verification not appearing (#787)
1 parent 42acf6f commit 72a2018

File tree

6 files changed

+50
-5
lines changed

6 files changed

+50
-5
lines changed

BitwardenAutoFillExtension/CredentialProviderViewController.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AuthenticationServices
22
import BitwardenSdk
33
import BitwardenShared
4+
import Combine
45
import OSLog
56

67
/// An `ASCredentialProviderViewController` that implements credential autofill.
@@ -11,12 +12,21 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
1112
/// The app's theme.
1213
var appTheme: AppTheme = .default
1314

15+
/// A subject containing whether the controller did appear.
16+
private var didAppearSubject = CurrentValueSubject<Bool, Never>(false)
17+
1418
/// The processor that manages application level logic.
1519
private var appProcessor: AppProcessor?
1620

1721
/// The context of the credential provider to see how the extension is being used.
1822
private var context: CredentialProviderContext?
1923

24+
override func viewDidAppear(_ animated: Bool) {
25+
super.viewDidAppear(animated)
26+
27+
didAppearSubject.send(true)
28+
}
29+
2030
// MARK: ASCredentialProviderViewController
2131

2232
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
@@ -307,6 +317,12 @@ extension CredentialProviderViewController: Fido2AppExtensionDelegate {
307317
extensionContext.completeRegistrationRequest(using: asPasskeyRegistrationCredential)
308318
}
309319

320+
func getDidAppearPublisher() -> AsyncPublisher<AnyPublisher<Bool, Never>> {
321+
didAppearSubject
322+
.eraseToAnyPublisher()
323+
.values
324+
}
325+
310326
func setUserInteractionRequired() {
311327
context?.flowFailedBecauseUserInteractionRequired = true
312328
cancel(error: ASExtensionError(.userInteractionRequired))

BitwardenShared/Core/Auth/Repositories/AuthRepositoryTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,8 +1579,6 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
15791579

15801580
/// `validatePin(_:)` returns `false` if the there is no active account.
15811581
func test_validatePin_noActiveAccount() async throws {
1582-
let account = Account.fixture()
1583-
15841582
let isPinValid = try await subject.validatePin(pin: "123")
15851583

15861584
XCTAssertFalse(isPinValid)

BitwardenShared/UI/Autofill/Application/Fido2AppExtensionDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import AuthenticationServices
2+
import Combine
23

34
/// A delegate that is used to handle actions and retrieve information from within an Autofill extension
45
/// on Fido2 flows.
@@ -19,6 +20,9 @@ public protocol Fido2AppExtensionDelegate: AppExtensionDelegate {
1920
@available(iOSApplicationExtension 17.0, *)
2021
func completeRegistrationRequest(asPasskeyRegistrationCredential: ASPasskeyRegistrationCredential)
2122

23+
/// Gets a publisher for when `didAppear` happens.
24+
func getDidAppearPublisher() -> AsyncPublisher<AnyPublisher<Bool, Never>>
25+
2226
/// Marks that user interaction is required.
2327
func setUserInteractionRequired()
2428
}

BitwardenShared/UI/Autofill/Application/TestHelpers/MockFido2AppExtensionDelegate.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import AuthenticationServices
2+
import Combine
23
import Foundation
34

45
@testable import BitwardenShared
@@ -8,6 +9,7 @@ class MockFido2AppExtensionDelegate: MockAppExtensionDelegate, Fido2AppExtension
89
var completeAssertionRequestMocker = InvocationMocker<ASPasskeyAssertionCredential>()
910
var completeRegistrationRequestMocker = InvocationMocker<ASPasskeyRegistrationCredential>()
1011
var extensionMode: AutofillExtensionMode = .configureAutofill
12+
var didAppearPublisher = CurrentValueSubject<Bool, Never>(false)
1113
var setUserInteractionRequiredCalled = false
1214

1315
var flowWithUserInteraction: Bool = true
@@ -20,6 +22,12 @@ class MockFido2AppExtensionDelegate: MockAppExtensionDelegate, Fido2AppExtension
2022
completeRegistrationRequestMocker.invoke(param: asPasskeyRegistrationCredential)
2123
}
2224

25+
func getDidAppearPublisher() -> AsyncPublisher<AnyPublisher<Bool, Never>> {
26+
didAppearPublisher
27+
.eraseToAnyPublisher()
28+
.values
29+
}
30+
2331
func setUserInteractionRequired() {
2432
setUserInteractionRequiredCalled = true
2533
}

BitwardenShared/UI/Platform/Application/AppProcessor+Fido2Tests.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,15 @@ class AppProcessorFido2Tests: BitwardenTestCase {
129129
func test_onNeedsUserInteraction_flowWithUserInteraction() async {
130130
appExtensionDelegate.flowWithUserInteraction = true
131131

132-
await assertAsyncDoesNotThrow {
132+
let taskResult = Task {
133133
try await subject.onNeedsUserInteraction()
134134
}
135+
136+
appExtensionDelegate.didAppearPublisher.send(true)
137+
138+
await assertAsyncDoesNotThrow {
139+
try await taskResult.value
140+
}
135141
XCTAssertFalse(appExtensionDelegate.setUserInteractionRequiredCalled)
136142
}
137143

BitwardenShared/UI/Platform/Application/AppProcessor.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import AuthenticationServices
22
import BitwardenSdk
33
import Combine
44
import Foundation
5+
import OSLog
56
import UIKit
67

78
/// The `AppProcessor` processes actions received at the application level and contains the logic
@@ -423,11 +424,23 @@ extension AppProcessor: Fido2UserInterfaceHelperDelegate {
423424
}
424425

425426
func onNeedsUserInteraction() async throws {
426-
if let fido2AppExtensionDelegate = appExtensionDelegate as? Fido2AppExtensionDelegate,
427-
!fido2AppExtensionDelegate.flowWithUserInteraction {
427+
guard let fido2AppExtensionDelegate = appExtensionDelegate as? Fido2AppExtensionDelegate else {
428+
return
429+
}
430+
431+
if !fido2AppExtensionDelegate.flowWithUserInteraction {
428432
fido2AppExtensionDelegate.setUserInteractionRequired()
429433
throw Fido2Error.userInteractionRequired
430434
}
435+
436+
// WORKAROUND: We need to wait until the view controller appears in order to perform any
437+
// action that needs user interaction or it might not show the prompt to the user.
438+
// E.g. without this there are certain devices that don't show the FaceID prompt
439+
// and the user only sees the screen dimming a bit and failing the flow.
440+
for await didAppear in fido2AppExtensionDelegate.getDidAppearPublisher() {
441+
guard didAppear else { continue }
442+
return
443+
}
431444
}
432445

433446
func showAlert(_ alert: Alert) {

0 commit comments

Comments
 (0)