diff --git a/src/Core/Utilities/Fido2/Fido2DelegatedUserInterface.cs b/src/Core/Utilities/Fido2/Fido2DelegatedUserInterface.cs
new file mode 100644
index 000000000..25be4ae20
--- /dev/null
+++ b/src/Core/Utilities/Fido2/Fido2DelegatedUserInterface.cs
@@ -0,0 +1,65 @@
+using Bit.Core.Abstractions;
+
+namespace Bit.Core.Utilities.Fido2
+{
+ ///
+ /// This implementation is used when all interactions are delegated to the operating system.
+ /// Most often these decisions have already been made by the time the Authenticator is called.
+ ///
+ /// This is only supported for assertion operations. Attestation requires the user to interact
+ /// with the app directly.
+ ///
+ public class Fido2DelegatedUserInterface : IFido2UserInterface
+ {
+ private string _cipherId = null;
+ private bool _userVerified = false;
+ private Func _ensureUnlockedVaultAsyncCallback;
+
+ ///
+ /// Indicates that the user has already picked a credential from a list of existing credentials.
+ /// Picking a credential also assumes user presence.
+ ///
+ public Fido2DelegatedUserInterface UserPickedCredential(string cipherId)
+ {
+ _cipherId = cipherId;
+ return this;
+ }
+
+ ///
+ /// Indicates that the user was verified by the OS, e.g. by a fingerprint or face scan.
+ ///
+ public Fido2DelegatedUserInterface UserIsVerified()
+ {
+ _userVerified = true;
+ return this;
+ }
+
+ public Fido2DelegatedUserInterface WithEnsureUnlockedVaultAsyncCallback(Func callback)
+ {
+ _ensureUnlockedVaultAsyncCallback = callback;
+ return this;
+ }
+
+ public Task PickCredentialAsync(Fido2PickCredentialParams parameters)
+ {
+ return Task.FromResult(new Fido2PickCredentialResult
+ {
+ CipherId = _cipherId,
+ UserVerified = _userVerified
+ });
+ }
+
+ public Task EnsureUnlockedVaultAsync()
+ {
+ if (_ensureUnlockedVaultAsyncCallback != null)
+ {
+ return _ensureUnlockedVaultAsyncCallback();
+ }
+
+ throw new Exception("No callback provided to ensure the vault is unlocked");
+ }
+
+ public Task InformExcludedCredential(string[] existingCipherIds) => throw new NotImplementedException();
+ public Task ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams parameters) => throw new NotImplementedException();
+ }
+}
diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs
index b51827c07..d0bb228c0 100644
--- a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs
+++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs
@@ -21,6 +21,8 @@ namespace Bit.iOS.Autofill
{
private readonly LazyResolve _cipherService = new LazyResolve();
+ private readonly Fido2DelegatedUserInterface _userInterface = new Fido2DelegatedUserInterface();
+
private IFido2AuthenticatorService _fido2AuthService;
private IFido2AuthenticatorService Fido2AuthService
{
@@ -29,7 +31,7 @@ namespace Bit.iOS.Autofill
if (_fido2AuthService is null)
{
_fido2AuthService = ServiceContainer.Resolve();
- _fido2AuthService.Init(this);
+ _fido2AuthService.Init(_userInterface);
}
return _fido2AuthService;
}
@@ -159,18 +161,20 @@ namespace Bit.iOS.Autofill
}).ToArray();
}
+ public class UserInteractionRequiredException : Exception {}
+
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest)
{
InitAppIfNeeded();
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
- if (!await IsAuthed() || await IsLocked())
- {
- CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
- return;
- }
_context.PasskeyCredentialRequest = passkeyCredentialRequest;
- await ProvideCredentialAsync(false);
+
+ try {
+ await ProvideCredentialAsync(false);
+ } catch (UserInteractionRequiredException) {
+ CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
+ }
}
public async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId)
@@ -209,6 +213,10 @@ namespace Bit.iOS.Autofill
}
});
+ _userInterface.UserPickedCredential(cipherId);
+ // if (os.PerformedFaceID) {
+ _userInterface.UserIsVerified();
+ //}
ClipLogger.Log("fido2AssertionResult:" + fido2AssertionResult);
diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs
index c5e98f167..4a20e405d 100644
--- a/src/iOS.Autofill/CredentialProviderViewController.cs
+++ b/src/iOS.Autofill/CredentialProviderViewController.cs
@@ -506,6 +506,13 @@ namespace Bit.iOS.Autofill
private async Task ProvideCredentialAsync(bool userInteraction = true)
{
+ _userInterface.WithEnsureUnlockedVaultAsyncCallback(async () => {
+ if (!userInteraction && (!await IsAuthed() || await IsLocked())) {
+ throw new UserInteractionRequiredException();
+ }
+
+ await EnsureUnlockedVaultAsync();
+ });
try
{
ClipLogger.Log("ProvideCredentialAsync");