blob: 31222bd05dedf757f65f451c0aca4e191fcdad85 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include <optional>
#include <utility>
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/to_vector.h"
#include "base/time/time.h"
#include "base/types/strong_alias.h"
#include "build/build_config.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate_factory.h"
#include "chrome/browser/webauthn/authenticator_reference.h"
#include "chrome/browser/webauthn/authenticator_transport.h"
#include "chrome/browser/webauthn/webauthn_pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync/base/features.h"
#include "components/vector_icons/vector_icons.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/features.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/fido_types.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/vector_icon_types.h"
#if BUILDFLAG(IS_WIN)
#include "device/fido/win/fake_webauthn_api.h"
#include "device/fido/win/webauthn_api.h"
#endif
namespace {
using testing::ElementsAre;
using RequestType = device::FidoRequestType;
const base::flat_set<AuthenticatorTransport> kAllTransports = {
AuthenticatorTransport::kUsbHumanInterfaceDevice,
AuthenticatorTransport::kNearFieldCommunication,
AuthenticatorTransport::kInternal,
AuthenticatorTransport::kHybrid,
};
const base::flat_set<AuthenticatorTransport> kAllTransportsWithoutCable = {
AuthenticatorTransport::kUsbHumanInterfaceDevice,
AuthenticatorTransport::kNearFieldCommunication,
AuthenticatorTransport::kInternal,
};
using TransportAvailabilityInfo =
::device::FidoRequestHandlerBase::TransportAvailabilityInfo;
class RequestCallbackReceiver {
public:
base::RepeatingCallback<void(const std::string&)> Callback() {
return base::BindRepeating(&RequestCallbackReceiver::OnRequest,
weak_factory_.GetWeakPtr());
}
std::string WaitForResult() {
if (!authenticator_id_) {
run_loop_->Run();
}
std::string ret = std::move(*authenticator_id_);
authenticator_id_.reset();
run_loop_ = std::make_unique<base::RunLoop>();
return ret;
}
private:
void OnRequest(const std::string& authenticator_id) {
authenticator_id_ = authenticator_id;
run_loop_->Quit();
}
std::optional<std::string> authenticator_id_;
std::unique_ptr<base::RunLoop> run_loop_ = std::make_unique<base::RunLoop>();
base::WeakPtrFactory<RequestCallbackReceiver> weak_factory_{this};
};
class MockDialogModelObserver
: public AuthenticatorRequestDialogModel::Observer {
public:
MockDialogModelObserver() = default;
MockDialogModelObserver(const MockDialogModelObserver&) = delete;
MockDialogModelObserver& operator=(const MockDialogModelObserver&) = delete;
MOCK_METHOD0(OnStartOver, void());
MOCK_METHOD1(OnModelDestroyed, void(AuthenticatorRequestDialogModel*));
MOCK_METHOD0(OnStepTransition, void());
MOCK_METHOD0(OnCancelRequest, void());
MOCK_METHOD0(OnBluetoothPoweredStateChanged, void());
};
class BluetoothAdapterPowerOnCallbackReceiver {
public:
BluetoothAdapterPowerOnCallbackReceiver() = default;
BluetoothAdapterPowerOnCallbackReceiver(
const BluetoothAdapterPowerOnCallbackReceiver&) = delete;
BluetoothAdapterPowerOnCallbackReceiver& operator=(
const BluetoothAdapterPowerOnCallbackReceiver&) = delete;
base::RepeatingClosure GetCallback() {
return base::BindRepeating(
&BluetoothAdapterPowerOnCallbackReceiver::CallbackTarget,
base::Unretained(this));
}
bool was_called() const { return was_called_; }
private:
void CallbackTarget() {
ASSERT_FALSE(was_called_);
was_called_ = true;
}
bool was_called_ = false;
};
base::StringPiece RequestTypeToString(RequestType req_type) {
switch (req_type) {
case RequestType::kGetAssertion:
return "GetAssertion";
case RequestType::kMakeCredential:
return "MakeCredential";
}
}
enum class TransportAvailabilityParam {
kMaybeHasPlatformCredential,
kHasPlatformCredential,
kOneRecognizedCred,
kTwoRecognizedCreds,
kOnePhoneRecognizedCred,
kTwoPhoneRecognizedCred,
kOneTouchIDRecognizedCred,
kEmptyAllowList,
kOnlyInternal,
kOnlyHybridOrInternal,
kHasWinNativeAuthenticator,
kWindowsHandlesHybrid,
kHasCableV1Extension,
kHasCableV2Extension,
kRequireResidentKey,
kIsConditionalUI,
kAttachmentAny,
kAttachmentCrossPlatform,
kBleDisabled,
kBleAccessDenied,
kHasICloudKeychain,
kHasICloudKeychainCreds,
kCreateInICloudKeychain,
kNoTouchId,
kUVRequired,
kHintSecurityKeys,
kHintHybrid,
kHintClientDevice,
};
base::StringPiece TransportAvailabilityParamToString(
TransportAvailabilityParam param) {
switch (param) {
case TransportAvailabilityParam::kMaybeHasPlatformCredential:
return "kMaybeHasPlatformCredential";
case TransportAvailabilityParam::kHasPlatformCredential:
return "kHasPlatformCredential";
case TransportAvailabilityParam::kOneRecognizedCred:
return "kOneRecognizedCred";
case TransportAvailabilityParam::kTwoRecognizedCreds:
return "kTwoRecognizedCreds";
case TransportAvailabilityParam::kOnePhoneRecognizedCred:
return "kOnePhoneRecognizedCred";
case TransportAvailabilityParam::kTwoPhoneRecognizedCred:
return "kTwoPhoneRecognizedCred";
case TransportAvailabilityParam::kOneTouchIDRecognizedCred:
return "kOneTouchIDRecognizedCred";
case TransportAvailabilityParam::kEmptyAllowList:
return "kEmptyAllowList";
case TransportAvailabilityParam::kOnlyInternal:
return "kOnlyInternal";
case TransportAvailabilityParam::kOnlyHybridOrInternal:
return "kOnlyHybridOrInternal";
case TransportAvailabilityParam::kHasWinNativeAuthenticator:
return "kHasWinNativeAuthenticator";
case TransportAvailabilityParam::kWindowsHandlesHybrid:
return "kWindowsHandlesHybrid";
case TransportAvailabilityParam::kHasCableV1Extension:
return "kHasCableV1Extension";
case TransportAvailabilityParam::kHasCableV2Extension:
return "kHasCableV2Extension";
case TransportAvailabilityParam::kRequireResidentKey:
return "kRequireResidentKey";
case TransportAvailabilityParam::kIsConditionalUI:
return "kIsConditionalUI";
case TransportAvailabilityParam::kAttachmentAny:
return "kAttachmentAny";
case TransportAvailabilityParam::kAttachmentCrossPlatform:
return "kAttachmentCrossPlatform";
case TransportAvailabilityParam::kBleDisabled:
return "kBleDisabled";
case TransportAvailabilityParam::kBleAccessDenied:
return "kBleAccessDenied";
case TransportAvailabilityParam::kHasICloudKeychain:
return "kHasICloudKeychain";
case TransportAvailabilityParam::kHasICloudKeychainCreds:
return "kHasICloudKeychainCreds";
case TransportAvailabilityParam::kCreateInICloudKeychain:
return "kCreateInICloudKeychain";
case TransportAvailabilityParam::kNoTouchId:
return "kNoTouchId";
case TransportAvailabilityParam::kUVRequired:
return "kUVRequired";
case TransportAvailabilityParam::kHintSecurityKeys:
return "kHintSecurityKeys";
case TransportAvailabilityParam::kHintHybrid:
return "kHintHybrid";
case TransportAvailabilityParam::kHintClientDevice:
return "kHintClientDevice";
}
}
template <typename T, base::StringPiece (*F)(T)>
std::string SetToString(base::flat_set<T> s) {
return base::JoinString(base::test::ToVector(s, F), ", ");
}
std::unique_ptr<device::cablev2::Pairing> GetPairingFromSync() {
auto pairing = std::make_unique<device::cablev2::Pairing>();
pairing->name = "Phone from sync";
pairing->from_sync_deviceinfo = true;
return pairing;
}
std::unique_ptr<device::cablev2::Pairing> GetPairingFromQR() {
auto pairing = std::make_unique<device::cablev2::Pairing>();
pairing->name = "Phone from QR";
pairing->from_sync_deviceinfo = false;
return pairing;
}
const device::PublicKeyCredentialUserEntity kUser1({1, 2, 3, 4},
"A",
std::nullopt);
const device::PublicKeyCredentialUserEntity kUser2({5, 6, 7, 8},
"B",
std::nullopt);
const device::PublicKeyCredentialUserEntity kPhoneUser1({9, 0, 1, 2},
"C",
std::nullopt);
const device::PublicKeyCredentialUserEntity kPhoneUser2({3, 4, 5, 6},
"D",
std::nullopt);
const device::DiscoverableCredentialMetadata
kCred1(device::AuthenticatorType::kOther, "rp.com", {0}, kUser1);
const device::DiscoverableCredentialMetadata kCred1FromICloudKeychain(
device::AuthenticatorType::kICloudKeychain,
"rp.com",
{4},
kUser1);
const device::DiscoverableCredentialMetadata kCred1FromChromeOS(
device::AuthenticatorType::kChromeOS,
"rp.com",
{4},
kUser1);
const device::DiscoverableCredentialMetadata
kCred2(device::AuthenticatorType::kOther, "rp.com", {1}, kUser2);
const device::DiscoverableCredentialMetadata
kPhoneCred1(device::AuthenticatorType::kPhone, "rp.com", {2}, kPhoneUser1);
const device::DiscoverableCredentialMetadata
kPhoneCred2(device::AuthenticatorType::kPhone, "rp.com", {3}, kPhoneUser2);
const device::DiscoverableCredentialMetadata
kWinCred1(device::AuthenticatorType::kWinNative, "rp.com", {0}, kUser1);
const device::DiscoverableCredentialMetadata
kWinCred2(device::AuthenticatorType::kWinNative, "rp.com", {1}, kUser2);
const device::DiscoverableCredentialMetadata
kTouchIDCred1(device::AuthenticatorType::kTouchID, "rp.com", {4}, kUser1);
AuthenticatorRequestDialogModel::Mechanism::CredentialInfo CredentialInfoFrom(
const device::DiscoverableCredentialMetadata& metadata) {
return AuthenticatorRequestDialogModel::Mechanism::CredentialInfo(
metadata.source, metadata.user.id);
}
} // namespace
// TODO(crbug.com/1489482): Remove non NEW_UI paths after passkey metadata
// syncing is enabled by default.
#define NEW_UI
class AuthenticatorRequestDialogModelTest
: public ChromeRenderViewHostTestHarness {
public:
using Step = AuthenticatorRequestDialogModel::Step;
AuthenticatorRequestDialogModelTest()
: ChromeRenderViewHostTestHarness(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
AuthenticatorRequestDialogModelTest(
const AuthenticatorRequestDialogModelTest&) = delete;
AuthenticatorRequestDialogModelTest& operator=(
const AuthenticatorRequestDialogModelTest&) = delete;
};
TEST_F(AuthenticatorRequestDialogModelTest, Mechanisms) {
const auto mc = RequestType::kMakeCredential;
const auto ga = RequestType::kGetAssertion;
const auto usb = AuthenticatorTransport::kUsbHumanInterfaceDevice;
const auto internal = AuthenticatorTransport::kInternal;
const auto cable = AuthenticatorTransport::kHybrid;
const auto aoa = AuthenticatorTransport::kAndroidAccessory;
const auto cred1 = CredentialInfoFrom(kCred1);
const auto cred2 = CredentialInfoFrom(kCred2);
const auto phonecred1 = CredentialInfoFrom(kPhoneCred1);
const auto phonecred2 = CredentialInfoFrom(kPhoneCred2);
const auto ickc_cred1 = CredentialInfoFrom(kCred1FromICloudKeychain);
const auto wincred1 = CredentialInfoFrom(kWinCred1);
const auto wincred2 = CredentialInfoFrom(kWinCred2);
[[maybe_unused]] const auto touchid_cred1 = CredentialInfoFrom(kTouchIDCred1);
const auto v1 = TransportAvailabilityParam::kHasCableV1Extension;
const auto v2 = TransportAvailabilityParam::kHasCableV2Extension;
const auto has_winapi =
TransportAvailabilityParam::kHasWinNativeAuthenticator;
const auto win_hybrid =
TransportAvailabilityParam::kWindowsHandlesHybrid;
const auto has_plat = TransportAvailabilityParam::kHasPlatformCredential;
const auto maybe_plat =
TransportAvailabilityParam::kMaybeHasPlatformCredential;
const auto one_cred = TransportAvailabilityParam::kOneRecognizedCred;
[[maybe_unused]] const auto one_touchid_cred =
TransportAvailabilityParam::kOneTouchIDRecognizedCred;
const auto two_cred = TransportAvailabilityParam::kTwoRecognizedCreds;
const auto one_phone_cred =
TransportAvailabilityParam::kOnePhoneRecognizedCred;
const auto two_phone_cred =
TransportAvailabilityParam::kTwoPhoneRecognizedCred;
const auto empty_al = TransportAvailabilityParam::kEmptyAllowList;
const auto only_internal = TransportAvailabilityParam::kOnlyInternal;
const auto only_hybrid_or_internal =
TransportAvailabilityParam::kOnlyHybridOrInternal;
const auto rk = TransportAvailabilityParam::kRequireResidentKey;
const auto c_ui = TransportAvailabilityParam::kIsConditionalUI;
const auto att_any = TransportAvailabilityParam::kAttachmentAny;
const auto att_xplat = TransportAvailabilityParam::kAttachmentCrossPlatform;
const auto ble_off = TransportAvailabilityParam::kBleDisabled;
const auto ble_denied = TransportAvailabilityParam::kBleAccessDenied;
const auto hint_sk = TransportAvailabilityParam::kHintSecurityKeys;
const auto hint_hybrid = TransportAvailabilityParam::kHintHybrid;
const auto hint_plat = TransportAvailabilityParam::kHintClientDevice;
[[maybe_unused]] const auto has_ickc =
TransportAvailabilityParam::kHasICloudKeychain;
[[maybe_unused]] const auto create_ickc =
TransportAvailabilityParam::kCreateInICloudKeychain;
[[maybe_unused]] const auto no_touchid =
TransportAvailabilityParam::kNoTouchId;
[[maybe_unused]] const auto ickc_creds =
TransportAvailabilityParam::kHasICloudKeychainCreds;
[[maybe_unused]] const auto uv_req = TransportAvailabilityParam::kUVRequired;
using c = AuthenticatorRequestDialogModel::Mechanism::Credential;
using t = AuthenticatorRequestDialogModel::Mechanism::Transport;
using p = AuthenticatorRequestDialogModel::Mechanism::Phone;
const auto winapi = AuthenticatorRequestDialogModel::Mechanism::WindowsAPI();
const auto add = AuthenticatorRequestDialogModel::Mechanism::AddPhone();
[[maybe_unused]] const auto ickc =
AuthenticatorRequestDialogModel::Mechanism::ICloudKeychain();
const auto usb_ui = Step::kUsbInsertAndActivate;
const auto mss = Step::kMechanismSelection;
const auto plat_ui = Step::kNotStarted;
const auto cable_ui = Step::kCableActivate;
[[maybe_unused]] const auto create_pk = Step::kCreatePasskey;
[[maybe_unused]] const auto use_pk = Step::kPreSelectSingleAccount;
[[maybe_unused]] const auto use_pk_multi = Step::kPreSelectAccount;
const auto qr = Step::kCableV2QRCode;
const auto pconf = Step::kPhoneConfirmationSheet;
const auto hero = Step::kSelectPriorityMechanism;
using psync = base::StrongAlias<class PhoneFromSyncTag, std::string>;
using pqr = base::StrongAlias<class PhoneFromQrTag, std::string>;
using PhoneVariant = absl::variant<psync, pqr>;
struct Test {
int line_num;
RequestType request_type;
base::flat_set<AuthenticatorTransport> transports;
base::flat_set<TransportAvailabilityParam> params;
std::vector<PhoneVariant> phones;
std::vector<AuthenticatorRequestDialogModel::Mechanism::Type>
expected_mechanisms;
Step expected_first_step;
};
#define L __LINE__
// clang-format off
Test kTests[]{
// If there's only a single mechanism, it should activate.
{L, mc, {usb}, {}, {}, {t(usb)}, usb_ui},
{L, ga, {usb}, {}, {}, {t(usb)}, usb_ui},
#if defined(NEW_UI)
{L, ga, {usb, cable}, {}, {}, {add}, qr},
{L, ga, {usb, cable}, {}, {}, {add}, qr},
#else
// ... otherwise show the selection sheet.
{L, ga, {usb, cable}, {}, {}, {add, t(usb)}, mss},
{L, ga, {usb, cable}, {}, {}, {add, t(usb)}, mss},
#endif
#if defined(NEW_UI)
// If the platform authenticator has a credential it should activate.
{L,
ga,
{},
{has_plat, one_cred},
{},
{c(cred1)},
plat_ui,
},
// If the platform authenticator has a credential it should activate.
{L,
ga,
{usb, internal},
{has_plat, one_cred},
{},
{c(cred1), t(usb)},
#if BUILDFLAG(IS_MAC)
plat_ui
#else
use_pk
#endif
},
#if BUILDFLAG(IS_MAC)
// Without Touch ID, the profile authenticator will show a confirmation
// prompt.
{L, ga, {usb, internal}, {has_plat, one_cred, no_touchid}, {},
{c(cred1), t(usb)}, use_pk},
// When a single profile credential is available with uv!=required and no
// Touch ID, the UI must show the confirmation because, otherwise,
// there'll be no UI at all.
{L, ga, {internal}, {has_plat, one_touchid_cred, no_touchid}, {},
{c(touchid_cred1)}, hero},
// When TouchID is present, we can jump directly to the platform UI, which
// will be a Touch ID prompt.
{L, ga, {internal}, {has_plat, one_touchid_cred}, {}, {c(touchid_cred1)},
plat_ui},
// Or if uv=required, plat_ui is also ok because it'll be a password
// prompt.
{L, ga, {internal}, {has_plat, one_touchid_cred, uv_req, no_touchid}, {},
{c(touchid_cred1)}, plat_ui},
#endif
// Even with an empty allow list.
{L,
ga,
{usb, internal},
{has_plat, one_cred, empty_al},
{},
{c(cred1), t(usb)},
hero},
// Two credentials shows mechanism selection.
{L,
ga,
{usb, internal},
{has_plat, two_cred, empty_al},
{},
{c(cred1), c(cred2), t(usb)},
mss},
#else
{L, ga, {usb, internal}, {has_plat}, {}, {t(internal), t(usb)}, plat_ui},
// ... but with an empty allow list the user should be prompted first.
{L,
ga,
{usb, internal},
{has_plat, one_cred, empty_al},
{},
{t(internal), t(usb)},
use_pk},
{L,
ga,
{usb, internal},
{has_plat, two_cred, empty_al},
{},
{t(internal), t(usb)},
use_pk_multi},
#endif
// MakeCredential with attachment=platform shows the 'Create a passkey'
// step, but only on macOS. On other OSes, we defer to the platform.
{L,
mc,
{internal},
{},
{},
{t(internal)},
#if BUILDFLAG(IS_MAC)
create_pk
#else
plat_ui
#endif
},
// MakeCredential with attachment=undefined also shows the 'Create a
// passkey' step on macOS. On other OSes, we show mechanism selection.
{L,
mc,
{usb, internal},
{},
{},
{t(internal), t(usb)},
#if BUILDFLAG(IS_MAC)
create_pk
#else
mss
#endif
},
// If the Windows API is available without caBLE, it should activate.
{L, mc, {}, {has_winapi}, {}, {winapi}, plat_ui},
{L, ga, {}, {has_winapi}, {}, {winapi}, plat_ui},
#if defined(NEW_UI)
// ...even if there are discovered Windows credentials.
{L, ga, {}, {has_winapi, one_cred}, {}, {c(wincred1), winapi}, plat_ui},
#endif
// A caBLEv1 extension should cause us to go directly to caBLE.
#if defined(NEW_UI)
{L, ga, {usb, cable}, {v1}, {}, {t(cable), t(usb)}, cable_ui},
// A caBLEv2 extension should cause us to go directly to caBLE, but also
// show the AOA option.
{L,
ga,
{usb, aoa, cable},
{v2},
{},
{t(aoa), t(cable), t(usb)},
cable_ui},
#else
{L, ga, {usb, cable}, {v1}, {}, {t(usb), t(cable)}, cable_ui},
// A caBLEv2 extension should cause us to go directly to caBLE, but also
// show the AOA option.
{L,
ga,
{usb, aoa, cable},
{v2},
{},
{t(usb), t(aoa), t(cable)},
cable_ui},
#endif
#if defined(NEW_UI)
// If there are linked phones then AOA doesn't show up, but the phones do,
// and sorted. The selection sheet should show.
{L,
mc,
{usb, aoa, cable},
{},
{pqr("a"), pqr("b")},
{p("a"), p("b"), add},
mss},
{L,
ga,
{usb, aoa, cable},
{},
{pqr("a"), pqr("b")},
{p("a"), p("b"), add},
mss},
#else
// If there are linked phones then AOA doesn't show up, but the phones do,
// and sorted. The selection sheet should show.
{L,
mc,
{usb, aoa, cable},
{},
{pqr("a"), pqr("b")},
{p("a"), p("b"), add, t(usb)},
mss},
{L,
ga,
{usb, aoa, cable},
{},
{pqr("a"), pqr("b")},
{p("a"), p("b"), add, t(usb)},
mss},
#endif
// If this is a Conditional UI request, don't offer the platform
// authenticator.
{L, ga, {usb, internal}, {c_ui}, {}, {t(usb)}, usb_ui},
#if defined(NEW_UI)
{L,
ga,
{usb, internal, cable},
{c_ui},
{pqr("a")},
{p("a"), add},
mss},
#else
{L,
ga,
{usb, internal, cable},
{c_ui},
{pqr("a")},
{p("a"), add, t(usb)},
mss},
#endif
// On Windows, mc with rk=required jumps to the platform UI when caBLE
// isn't an option. The case where caBLE is possible is tested below.
{L, mc, {}, {has_winapi, rk}, {}, {winapi}, plat_ui},
// For rk=discouraged, always jump to Windows UI.
{L, mc, {cable}, {has_winapi}, {}, {winapi, add}, plat_ui},
{L, mc, {}, {has_winapi}, {}, {winapi}, plat_ui},
// On Windows, ga with an empty allow list goes to the platform UI unless
// caBLE is an option and resident-key is required, which is tested below.
{L, ga, {}, {has_winapi, empty_al}, {}, {winapi}, plat_ui},
#if defined(NEW_UI)
// With a non-empty allow list containing non phone credentials, always
// jump to Windows UI.
// TODO(NEWUI): we should maintain this behaviour on Windows.
{L, ga, {cable}, {has_winapi}, {}, {add, winapi}, mss},
#else
// With a non-empty allow list containing non phone credentials, always
// jump to Windows UI.
{L, ga, {cable}, {has_winapi}, {}, {winapi, add}, plat_ui},
#endif
{L, ga, {}, {has_winapi}, {}, {winapi}, plat_ui},
// Except when the request is legacy cable.
#if defined(NEW_UI)
{L, ga, {cable, aoa}, {has_winapi, v1}, {}, {t(cable), winapi}, cable_ui},
{L,
ga,
{cable, aoa},
{has_winapi, v2},
{},
{t(aoa), t(cable), winapi},
cable_ui},
#else
{L, ga, {cable, aoa}, {has_winapi, v1}, {}, {winapi, t(cable)}, cable_ui},
{L,
ga,
{cable, aoa},
{has_winapi, v2},
{},
{winapi, t(aoa), t(cable)},
cable_ui},
#endif
// With attachment=undefined, the UI should jump to mechanism selection.
{L, mc, {usb, internal, cable}, {att_any}, {}, {add, t(internal)}, mss},
{L, mc, {usb, internal}, {att_any, rk}, {}, {t(internal), t(usb)}, mss},
#if defined(NEW_UI)
// QR code first: Make credential should jump to the QR code with
// RK=true.
{L,
mc,
{usb, internal, cable},
{rk, att_xplat},
{},
{add, t(internal)},
qr},
// Unless there is a phone paired already.
{L,
mc,
{usb, internal, cable},
{rk, att_xplat},
{pqr("a")},
{p("a"), add, t(internal)},
mss},
// Or if attachment=any
{L,
mc,
{usb, internal, cable},
{rk, att_any},
{},
{add, t(internal)},
mss},
// If RK=false, go to the default for the platform instead.
{
L,
mc,
{usb, internal, cable},
{},
{},
{add, t(internal)},
#if BUILDFLAG(IS_MAC)
create_pk,
#else
mss,
#endif
},
#else
// QR code first: Make credential should jump to the QR code with
// RK=true.
{L,
mc,
{usb, internal, cable},
{rk, att_xplat},
{},
{add, t(internal), t(usb)},
qr},
// Unless there is a phone paired already.
{L,
mc,
{usb, internal, cable},
{rk, att_xplat},
{pqr("a")},
{p("a"), add, t(internal), t(usb)},
mss},
// Or if attachment=any
{L,
mc,
{usb, internal, cable},
{rk, att_any},
{},
{add, t(internal), t(usb)},
mss},
// If RK=false, go to the default for the platform instead.
{
L,
mc,
{usb, internal, cable},
{},
{},
{add, t(internal), t(usb)},
#if BUILDFLAG(IS_MAC)
create_pk,
#else
mss,
#endif
},
#endif
// Windows should also jump to the QR code first.
{L, mc, {cable}, {rk, has_winapi}, {}, {winapi, add}, qr},
#if defined(NEW_UI)
// QR code first: Get assertion should jump to the QR code with empty
// allow-list.
{L,
ga,
{usb, internal, cable},
{empty_al},
{},
{add},
qr},
// And if the allow list only contains phones.
{L,
ga,
{internal, cable},
{only_hybrid_or_internal},
{},
{add},
qr},
// Unless there is a QR-paired phone already.
{L,
ga,
{usb, internal, cable},
{empty_al},
{pqr("a")},
{p("a"), add},
mss},
// Or a recognized platform credential.
{L,
ga,
{usb, internal, cable},
{empty_al, has_plat, one_cred},
{},
{c(cred1), add},
hero},
// Ignore the platform credential for conditional ui requests
{L,
ga,
{usb, internal, cable},
{c_ui, empty_al, has_plat, one_cred},
{},
{add},
qr},
// If there is an allow-list containing USB, go to QR code as well.
{L, ga, {usb, internal, cable}, {}, {}, {add}, qr},
// Windows should also jump to the QR code first.
// TODO: the expectation here (mss) doesn't match the comment.
{L, ga, {cable}, {empty_al, has_winapi}, {}, {add, winapi}, mss},
// Unless there is a recognized platform credential.
{L,
ga,
{cable},
{empty_al, has_winapi, has_plat, one_cred},
{},
{c(wincred1), add, winapi},
hero},
#else
// QR code first: Get assertion should jump to the QR code with empty
// allow-list.
{L,
ga,
{usb, internal, cable},
{empty_al},
{},
{add, t(internal), t(usb)},
qr},
// And if the allow list only contains phones.
{L,
ga,
{internal, cable},
{only_hybrid_or_internal},
{},
{add, t(internal)},
qr},
// Unless there is a phone paired already.
{L,
ga,
{usb, internal, cable},
{empty_al},
{pqr("a")},
{p("a"), add, t(internal), t(usb)},
mss},
// Or a recognized platform credential.
{L,
ga,
{usb, internal, cable},
{empty_al, has_plat},
{},
{add, t(internal), t(usb)},
plat_ui},
// Ignore the platform credential for conditional ui requests
{L,
ga,
{usb, internal, cable},
{c_ui, empty_al, has_plat},
{},
{add, t(usb)},
qr},
// If there is an allow-list containing USB, go to transport selection
// instead.
{L, ga, {usb, internal, cable}, {}, {}, {add, t(internal), t(usb)}, mss},
// Windows should also jump to the QR code first.
{L, ga, {cable}, {empty_al, has_winapi}, {}, {winapi, add}, qr},
// Unless there is a recognized platform credential.
{L,
ga,
{cable},
{empty_al, has_winapi, has_plat},
{},
{winapi, add},
plat_ui},
#endif
// For <=Win 10, we can't tell if there is a credential or not. Show the
// mechanism selection screen instead.
{L,
ga,
{cable},
{empty_al, has_winapi, maybe_plat},
{},
{winapi, add},
mss},
#if defined(NEW_UI)
// Phone confirmation sheet: Get assertion should jump to it if there is
// a single phone paired.
{L,
ga,
{cable, internal},
{only_hybrid_or_internal},
{pqr("a")},
{p("a"), add},
pconf},
// Even on Windows.
{L,
ga,
{cable},
{only_hybrid_or_internal, has_winapi},
{pqr("a")},
{p("a"), add},
pconf},
// Unless there is a recognized platform credential.
{L,
ga,
{cable, internal},
{only_hybrid_or_internal, has_plat},
{pqr("a")},
{p("a"), add, t(internal)},
plat_ui},
// Or a USB credential.
{L,
ga,
{usb, cable, internal},
{},
{pqr("a")},
{p("a"), add},
mss},
// iCloud Keychain counts as a recognised platform credential too.
{L,
ga,
{cable, internal},
{only_hybrid_or_internal, has_ickc, ickc_creds},
{pqr("a")},
{c(ickc_cred1), p("a"), add},
plat_ui},
#else
// Phone confirmation sheet: Get assertion should jump to it if there is
// a single phone paired.
{L,
ga,
{cable, internal},
{only_hybrid_or_internal},
{pqr("a")},
{p("a"), add, t(internal)},
pconf},
// Even on Windows.
{L,
ga,
{cable},
{only_hybrid_or_internal, has_winapi},
{pqr("a")},
{winapi, p("a"), add},
pconf},
// Unless there is a recognized platform credential.
{L,
ga,
{cable, internal},
{only_hybrid_or_internal, has_plat},
{pqr("a")},
{p("a"), add, t(internal)},
plat_ui},
// Or a USB credential.
{L,
ga,
{usb, cable, internal},
{},
{pqr("a")},
{p("a"), add, t(internal), t(usb)},
mss},
#endif
// Or this is a conditional UI request.
{L,
ga,
{cable, internal},
{only_hybrid_or_internal, c_ui},
{pqr("a")},
{p("a"), add},
mss},
#if defined(NEW_UI)
// Go to the mechanism selection screen if there are more phones paired.
{L,
ga,
{cable, internal},
{only_hybrid_or_internal},
{pqr("a"), pqr("b")},
{p("a"), p("b"), add},
mss},
#else
// Go to the mechanism selection screen if there are more phones paired.
{L,
ga,
{cable, internal},
{only_hybrid_or_internal},
{pqr("a"), pqr("b")},
{p("a"), p("b"), add, t(internal)},
mss},
#endif
};
// Tests for the new UI that lists synced passkeys mixed with local
// credentials.
Test kListSyncedPasskeysTests[]{
// Mac & Linux:
// Mix of phone and internal credentials.
{L,
ga,
{usb, cable, internal},
{one_phone_cred, two_cred, has_plat, empty_al},
{psync("a")},
{c(cred1), c(cred2), c(phonecred1), add},
mss},
// Internal credentials + qr code.
{L,
ga,
{usb, cable, internal},
{two_cred, has_plat, empty_al},
{psync("a")},
{c(cred1), c(cred2), add},
mss},
// qr code with ble disabled shows usb option.
{L, ga, {usb, cable}, {ble_off}, {}, {add, t(usb)}, mss},
// qr code with ble access denied shows usb option.
{L, ga, {usb, cable}, {ble_denied}, {}, {add, t(usb)}, mss},
// Internal credentials, no qr code.
{L,
ga,
{usb, internal},
{two_cred, has_plat, empty_al},
{psync("a")},
{c(cred1), c(cred2), t(usb)},
mss},
// Phone credentials only.
{L,
ga,
{usb, cable, internal},
{two_phone_cred, empty_al},
{psync("a")},
{c(phonecred1), c(phonecred2), add},
mss},
// Single internal credential with empty allow list.
{L,
ga,
{usb, cable, internal},
{one_cred, has_plat, empty_al},
{psync("a")},
{c(cred1), add},
hero,
},
// Single internal credential with non-empty allow list.
{L,
ga,
{usb, cable, internal},
{one_cred, has_plat},
{psync("a")},
{c(cred1), p("a"), add},
#if BUILDFLAG(IS_MAC)
plat_ui,
#else
use_pk,
#endif
},
// Single phone credential with empty allow list.
{L,
ga,
{usb, cable, internal},
{one_phone_cred, empty_al},
{psync("a")},
{c(phonecred1), add},
hero},
// Single phone credential with non-empty allow list.
{L,
ga,
{usb, cable, internal},
{one_phone_cred},
{psync("a")},
{c(phonecred1), add},
pconf},
// Phone from sync that has no credentials for empty allow-list request.
{L,
ga,
{usb, cable, internal},
{},
{psync("a")},
{p("a"), add},
mss},
// Regression test for crbug.com/1484660.
// A platform authenticator that reports the availability of credentials
// but does not enumerate them should be listed.
{L,
ga,
{usb, cable, internal},
{has_plat},
{psync("a")},
{p("a"), add, t(internal)},
plat_ui},
#if BUILDFLAG(IS_MAC)
// Even with iCloud Keychain present, we shouldn't jump to it without
// additional flags set.
{L, mc, {internal}, {rk, has_ickc}, {}, {ickc, t(internal)}, create_pk},
// iCloud Keychain should be the default if the request delegate
// configured that.
{L,
mc,
{internal},
{rk, has_ickc, create_ickc},
{},
{ickc, t(internal)},
plat_ui},
// ... and only for attachment=platform
{L,
mc,
{internal},
{rk, att_any, has_ickc, create_ickc},
{},
{ickc, t(internal)},
mss},
#endif
// Tests for RP hints.
//
// create(): Security key hint should show security key UI.
{L, mc, {usb, internal, cable}, {rk, hint_sk}, {},
{add, t(internal), t(usb)}, usb_ui},
// But not if USB isn't a valid transport.
{L, mc, {internal, cable}, {rk, hint_sk}, {}, {add, t(internal)},
#if BUILDFLAG(IS_MAC)
create_pk,
#else
qr,
#endif
},
// If webauthn.dll is present, jump to it.
{L, mc, {cable}, {has_winapi, rk, hint_sk}, {}, {winapi, add}, plat_ui},
// create(): Hybrid hint should show QR.
{L, mc, {usb, internal, cable}, {rk, hint_hybrid}, {},
{add, t(internal)}, qr},
// Unless there are paired phones, in which case show the MSS.
{L, mc, {usb, internal, cable}, {rk, hint_hybrid}, {psync("a")}, {p("a"),
add, t(internal)}, mss},
// But not if Hybrid isn't a valid transport.
{L, mc, {usb, internal}, {rk, hint_hybrid}, {}, {t(internal), t(usb)},
#if BUILDFLAG(IS_MAC)
create_pk,
#else
mss,
#endif
},
// If older webauthn.dll is present, don't jump to it since it doesn't do
// hybrid.
{L, mc, {cable}, {has_winapi, rk, hint_hybrid}, {}, {winapi, add}, qr},
#if BUILDFLAG(IS_WIN)
// ... but do if it supports hybrid.
{L, mc, {cable}, {has_winapi, win_hybrid, rk, hint_hybrid}, {}, {winapi},
plat_ui},
#endif
// create(): Client device hint should jump to the platform
// authenticator.
{L, mc, {usb, internal, cable}, {rk, hint_plat}, {}, {add, t(internal)},
#if BUILDFLAG(IS_MAC)
create_pk,
#else
plat_ui,
#endif
},
// But not if there isn't a platform authenticator.
{L, mc, {usb, cable}, {rk, hint_plat}, {}, {add}, qr},
// If webauthn.dll is present, jump to it.
{L, mc, {cable}, {has_winapi, rk, hint_plat}, {}, {winapi, add},
plat_ui},
// Or if there's iCloud Keychain.
{L, mc, {cable}, {has_ickc, create_ickc, rk, hint_plat}, {}, {ickc, add},
plat_ui},
// get(): Security key hint should show security key UI.
{L, ga, {usb, internal, cable}, {rk, hint_sk}, {}, {add, t(usb)},
usb_ui},
// But not if USB isn't a valid transport.
{L, ga, {internal, cable}, {rk, hint_sk}, {}, {add}, qr},
// If credentials are found on a platform authenticator, they are still
// shown.
{L, ga, {usb, internal, cable}, {one_cred, rk, hint_sk}, {},
{c(cred1), add, t(usb)}, mss},
// If webauthn.dll is present, jump to it.
{L, ga, {cable}, {has_winapi, rk, hint_sk}, {}, {add, winapi}, plat_ui},
// get(): Hybrid hint should show QR.
{L, ga, {usb, internal, cable}, {rk, hint_hybrid}, {}, {add}, qr},
// Unless there are paired phones listed, in which case show the MSS
{L, ga, {usb, internal, cable}, {rk, hint_hybrid}, {psync("a")},
{p("a"), add}, mss},
// But not if hybrid isn't available.
{L, ga, {usb, internal}, {rk, hint_hybrid}, {}, {t(usb)}, usb_ui},
// If older webauthn.dll is present, don't jump to it since it doesn't do
// hybrid.
{L, ga, {cable}, {has_winapi, rk, hint_hybrid}, {}, {add, winapi}, qr},
#if BUILDFLAG(IS_WIN)
// ... but do if it supports hybrid.
{L, ga, {cable}, {has_winapi, win_hybrid, rk, hint_hybrid}, {}, {winapi},
plat_ui},
#endif
// If credentials are found on a platform authenticator, they are still
// shown.
{L, ga, {usb, internal, cable}, {one_cred, rk, hint_hybrid}, {},
{c(cred1), add}, mss},
// get(): Client device hint should trigger webauthn.dll, if it exists.
{L, ga, {cable}, {rk, has_winapi, hint_plat}, {}, {add, winapi},
plat_ui},
// But not if there's a credential match.
{L, ga, {usb, cable, internal}, {one_cred, has_winapi, rk, hint_plat},
{}, {c(wincred1), add, winapi}, mss},
// And otherwise it doesn't do anything because we generally assume that
// we can enumerate platform authenticators and do a good job.
{L, ga, {usb, cable, internal}, {rk, hint_plat}, {}, {add}, qr},
};
Test kListSyncedPasskeysTests_Windows[] {
// Mix of phone and internal credentials, but no USB/NFC.
// This should jump to Windows, as there is a match with the local
// authenticator.
{L,
ga,
{cable},
{one_phone_cred, two_cred, has_winapi, only_hybrid_or_internal,
has_plat},
{psync("a")},
{c(wincred1), c(wincred2), c(phonecred1), add},
plat_ui},
// Mix of phone, internal credentials, and USB/NFC (empty allow list).
// This should offer dispatching to the Windows API for USB/NFC.
{L,
ga,
{cable},
{one_phone_cred, two_cred, has_winapi, empty_al, has_plat},
{psync("a")},
{c(wincred1), c(wincred2), c(phonecred1), add, winapi},
mss},
// Phone credentials and unknown Windows Hello credential status. This
// should offer dispatching to the Windows API for Windows Hello.
{L,
ga,
{cable},
{two_phone_cred, has_winapi, maybe_plat, empty_al},
{psync("a")},
{c(phonecred1), c(phonecred2), winapi, add},
mss},
// Tests where Windows handles hybrid:
// Mix of phone and internal credentials (empty allow list).
{L,
ga,
{cable},
{one_phone_cred, two_cred, has_winapi, win_hybrid, empty_al, has_plat},
{psync("a")},
{c(wincred1), c(wincred2), c(phonecred1), winapi},
mss},
// Internal credentials only.
// This should dispatch directly to the Windows API.
{L,
ga,
{},
{two_cred, has_winapi, win_hybrid, only_internal, has_plat},
{},
{c(wincred1), c(wincred2)},
plat_ui},
};
// clang-format on
#undef L
#if BUILDFLAG(IS_WIN)
device::FakeWinWebAuthnApi fake_win_webauthn_api;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
&fake_win_webauthn_api);
#endif
auto RunTest = [&](const Test& test, bool windows_hybrid_smoke_test) {
SCOPED_TRACE(static_cast<int>(test.expected_first_step));
SCOPED_TRACE(
(SetToString<TransportAvailabilityParam,
TransportAvailabilityParamToString>(test.params)));
SCOPED_TRACE((SetToString<device::FidoTransportProtocol, device::ToString>(
test.transports)));
SCOPED_TRACE(RequestTypeToString(test.request_type));
SCOPED_TRACE(testing::Message() << "At line number: " << test.line_num);
#if BUILDFLAG(IS_WIN)
bool has_win_hybrid =
windows_hybrid_smoke_test ||
base::Contains(test.params,
TransportAvailabilityParam::kWindowsHandlesHybrid);
fake_win_webauthn_api.set_version(has_win_hybrid ? 6 : 4);
SCOPED_TRACE(windows_hybrid_smoke_test);
#endif
TransportAvailabilityInfo transports_info;
transports_info.is_ble_powered =
!base::Contains(test.params, TransportAvailabilityParam::kBleDisabled);
transports_info.ble_access_denied = base::Contains(
test.params, TransportAvailabilityParam::kBleAccessDenied);
transports_info.request_type = test.request_type;
transports_info.available_transports = test.transports;
transports_info.user_verification_requirement =
base::Contains(test.params, TransportAvailabilityParam::kUVRequired)
? device::UserVerificationRequirement::kRequired
: device::UserVerificationRequirement::kDiscouraged;
if (base::Contains(test.params,
TransportAvailabilityParam::kHasPlatformCredential)) {
transports_info.has_platform_authenticator_credential =
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential;
} else if (base::Contains(
test.params,
TransportAvailabilityParam::kMaybeHasPlatformCredential)) {
transports_info.has_platform_authenticator_credential =
device::FidoRequestHandlerBase::RecognizedCredential::kUnknown;
} else {
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential;
}
device::DiscoverableCredentialMetadata cred1;
device::DiscoverableCredentialMetadata cred2;
if (base::Contains(
test.params,
TransportAvailabilityParam::kHasWinNativeAuthenticator)) {
cred1 = kWinCred1;
cred2 = kWinCred2;
} else {
cred1 = kCred1;
cred2 = kCred2;
}
device::DiscoverableCredentialMetadata touchid_cred1 = kTouchIDCred1;
if (base::Contains(test.params,
TransportAvailabilityParam::kHasICloudKeychainCreds)) {
transports_info.has_icloud_keychain_credential =
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential;
transports_info.recognized_credentials.emplace_back(
kCred1FromICloudKeychain);
} else {
transports_info.has_icloud_keychain_credential =
device::FidoRequestHandlerBase::RecognizedCredential::
kNoRecognizedCredential;
}
if (base::Contains(test.params,
TransportAvailabilityParam::kOneRecognizedCred)) {
transports_info.recognized_credentials = {std::move(cred1)};
} else if (base::Contains(
test.params,
TransportAvailabilityParam::kTwoRecognizedCreds)) {
transports_info.recognized_credentials = {std::move(cred1),
std::move(cred2)};
} else if (base::Contains(
test.params,
TransportAvailabilityParam::kOneTouchIDRecognizedCred)) {
transports_info.recognized_credentials = {std::move(touchid_cred1)};
}
if (base::Contains(test.params,
TransportAvailabilityParam::kOnePhoneRecognizedCred)) {
transports_info.recognized_credentials.emplace_back(kPhoneCred1);
}
if (base::Contains(test.params,
TransportAvailabilityParam::kTwoPhoneRecognizedCred)) {
transports_info.recognized_credentials.emplace_back(kPhoneCred1);
transports_info.recognized_credentials.emplace_back(kPhoneCred2);
}
transports_info.has_icloud_keychain = base::Contains(
test.params, TransportAvailabilityParam::kHasICloudKeychain);
transports_info.has_empty_allow_list = base::Contains(
test.params, TransportAvailabilityParam::kEmptyAllowList);
if (base::Contains(test.params,
TransportAvailabilityParam::kOnlyInternal)) {
transports_info.request_is_internal_only = true;
transports_info.transport_list_did_include_hybrid = false;
transports_info.transport_list_did_include_security_key = false;
} else if (base::Contains(
test.params,
TransportAvailabilityParam::kOnlyHybridOrInternal)) {
transports_info.is_only_hybrid_or_internal = true;
transports_info.transport_list_did_include_hybrid = true;
transports_info.transport_list_did_include_security_key = false;
} else {
transports_info.transport_list_did_include_hybrid = true;
transports_info.transport_list_did_include_security_key = true;
}
transports_info.transport_list_did_include_internal = true;
if (base::Contains(
test.params,
TransportAvailabilityParam::kHasWinNativeAuthenticator) ||
windows_hybrid_smoke_test) {
transports_info.has_win_native_api_authenticator = true;
transports_info.win_native_ui_shows_resident_credential_notice = true;
transports_info.win_is_uvpaa = true;
}
transports_info.resident_key_requirement =
base::Contains(test.params,
TransportAvailabilityParam::kRequireResidentKey)
? device::ResidentKeyRequirement::kRequired
: device::ResidentKeyRequirement::kDiscouraged;
if (base::Contains(test.params,
TransportAvailabilityParam::kAttachmentAny)) {
CHECK(transports_info.request_type == RequestType::kMakeCredential);
transports_info.make_credential_attachment =
device::AuthenticatorAttachment::kAny;
}
if (base::Contains(test.params,
TransportAvailabilityParam::kAttachmentCrossPlatform)) {
CHECK(transports_info.request_type == RequestType::kMakeCredential);
CHECK(!transports_info.make_credential_attachment.has_value());
transports_info.make_credential_attachment =
device::AuthenticatorAttachment::kCrossPlatform;
}
if (!transports_info.make_credential_attachment.has_value() &&
transports_info.request_type == RequestType::kMakeCredential) {
transports_info.make_credential_attachment =
device::AuthenticatorAttachment::kPlatform;
}
AuthenticatorRequestDialogModel model(main_rfh());
std::optional<bool> has_v2_cable_extension;
if (base::Contains(test.params,
TransportAvailabilityParam::kHasCableV1Extension)) {
has_v2_cable_extension = false;
}
if (base::Contains(test.params,
TransportAvailabilityParam::kHasCableV2Extension)) {
CHECK(!has_v2_cable_extension.has_value());
has_v2_cable_extension = true;
}
model.set_allow_icloud_keychain(transports_info.has_icloud_keychain);
if (base::Contains(test.params,
TransportAvailabilityParam::kCreateInICloudKeychain)) {
model.set_should_create_in_icloud_keychain(true);
}
#if BUILDFLAG(IS_MAC)
if (base::Contains(test.params, TransportAvailabilityParam::kNoTouchId)) {
model.set_local_biometrics_override_for_testing(false);
} else {
model.set_local_biometrics_override_for_testing(true);
}
#endif
std::optional<device::FidoTransportProtocol> hint_transport;
if (base::Contains(test.params,
TransportAvailabilityParam::kHintSecurityKeys)) {
CHECK(!hint_transport.has_value());
hint_transport = device::FidoTransportProtocol::kUsbHumanInterfaceDevice;
}
if (base::Contains(test.params, TransportAvailabilityParam::kHintHybrid)) {
CHECK(!hint_transport.has_value());
hint_transport = device::FidoTransportProtocol::kHybrid;
}
if (base::Contains(test.params,
TransportAvailabilityParam::kHintClientDevice)) {
CHECK(!hint_transport.has_value());
hint_transport = device::FidoTransportProtocol::kInternal;
}
if (hint_transport.has_value()) {
content::AuthenticatorRequestClientDelegate::Hints hints;
hints.transport = hint_transport;
model.SetHints(hints);
}
model.SetAccountPreselectedCallback(
base::BindRepeating([](device::PublicKeyCredentialDescriptor cred) {}));
if (has_v2_cable_extension.has_value() || !test.phones.empty() ||
base::Contains(test.transports,
device::FidoTransportProtocol::kHybrid)) {
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
for (const auto& phone : test.phones) {
auto pairing = std::make_unique<device::cablev2::Pairing>();
if (absl::holds_alternative<pqr>(phone)) {
pairing->name = absl::get<pqr>(phone).value();
pairing->from_sync_deviceinfo = false;
} else {
pairing->name = absl::get<psync>(phone).value();
pairing->from_sync_deviceinfo = true;
}
pairing->peer_public_key_x962 = {0};
pairing->peer_public_key_x962[0] =
base::checked_cast<uint8_t>(phones.size());
phones.emplace_back(std::move(pairing));
}
model.set_cable_transport_info(has_v2_cable_extension, std::move(phones),
base::DoNothing(), std::nullopt);
}
bool is_conditional_ui = base::Contains(
test.params, TransportAvailabilityParam::kIsConditionalUI);
model.StartFlow(std::move(transports_info), is_conditional_ui);
if (is_conditional_ui) {
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
model.TransitionToModalWebAuthnRequest();
}
if (windows_hybrid_smoke_test) {
// Before the new synced passkeys UI, caBLEv1 and server-link are the only
// cases that Windows _doesn't_ handle when it has hybrid support because
// those are legacy protocol variants.
if (!base::FeatureList::IsEnabled(device::kWebAuthnNewPasskeyUI) &&
test.expected_first_step != cable_ui) {
EXPECT_EQ(plat_ui, model.current_step());
}
return;
}
EXPECT_EQ(test.expected_first_step, model.current_step());
std::vector<AuthenticatorRequestDialogModel::Mechanism::Type>
mechanism_types;
for (const auto& mech : model.mechanisms()) {
mechanism_types.push_back(mech.type);
}
EXPECT_EQ(test.expected_mechanisms, mechanism_types);
if (!model.offer_try_again_in_ui()) {
return;
}
model.StartOver();
EXPECT_EQ(Step::kMechanismSelection, model.current_step());
};
for (const auto& test : kTests) {
// On Windows, all the tests are run twice. Once to check that, when Windows
// has hybrid support, we always jump the Windows, and then to test the
// prior behaviour.
for (const bool windows_hybrid_smoke_test : {
false
#if BUILDFLAG(IS_WIN)
,
true
#endif
}) {
RunTest(test, windows_hybrid_smoke_test);
}
}
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{device::kWebAuthnNewPasskeyUI, syncer::kSyncWebauthnCredentials},
/*disabled_features=*/{});
for (const auto& test : kListSyncedPasskeysTests) {
RunTest(test, /*windows_hybrid_smoke_test=*/false);
}
#if BUILDFLAG(IS_WIN)
for (const auto& test : kListSyncedPasskeysTests_Windows) {
RunTest(test, /*windows_hybrid_smoke_test=*/false);
}
#endif
}
#if BUILDFLAG(IS_WIN)
TEST_F(AuthenticatorRequestDialogModelTest, WinCancel) {
// Simulate the user canceling the Windows native UI, both with and without
// that UI being immediately triggered. If it was immediately triggered then
// canceling it should show the mechanism selection UI.
device::FakeWinWebAuthnApi fake_win_webauthn_api;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
&fake_win_webauthn_api);
for (const int win_webauthn_api_version : {4, 6}) {
fake_win_webauthn_api.set_version(win_webauthn_api_version);
for (const bool is_passkey_request : {false, true}) {
SCOPED_TRACE(testing::Message() << "passkey req? " << is_passkey_request);
SCOPED_TRACE(testing::Message() << "win v" << win_webauthn_api_version);
AuthenticatorRequestDialogModel::TransportAvailabilityInfo tai;
tai.make_credential_attachment =
device::AuthenticatorAttachment::kCrossPlatform;
tai.request_type = device::FidoRequestType::kMakeCredential;
tai.has_win_native_api_authenticator = true;
tai.win_native_ui_shows_resident_credential_notice = true;
tai.available_transports.insert(device::FidoTransportProtocol::kHybrid);
tai.resident_key_requirement =
is_passkey_request ? device::ResidentKeyRequirement::kRequired
: device::ResidentKeyRequirement::kDiscouraged;
tai.is_ble_powered = true;
AuthenticatorRequestDialogModel model(main_rfh());
model.saved_authenticators().AddAuthenticator(
AuthenticatorReference("ID", AuthenticatorTransport::kInternal,
device::AuthenticatorType::kWinNative));
model.set_cable_transport_info(std::nullopt, {}, base::DoNothing(),
"fido:/1234");
model.StartFlow(std::move(tai),
/*is_conditional_mediation=*/false);
const bool win_ui_was_immediately_triggered =
!is_passkey_request || win_webauthn_api_version == 6;
if (!win_ui_was_immediately_triggered) {
EXPECT_NE(model.current_step(), Step::kNotStarted);
// Canceling the Windows UI ends the request because the user must have
// selected the Windows option first.
EXPECT_FALSE(model.OnWinUserCancelled());
continue;
}
EXPECT_EQ(model.current_step(), Step::kNotStarted);
if (win_webauthn_api_version >= 6) {
// Windows handles hybrid itself starting with this version, so
// canceling shouldn't try to show Chrome UI.
EXPECT_FALSE(model.OnWinUserCancelled());
continue;
}
// Canceling the Windows native UI should be handled.
EXPECT_TRUE(model.OnWinUserCancelled());
// The mechanism selection sheet should now be showing.
EXPECT_EQ(model.current_step(), Step::kMechanismSelection);
// Canceling the Windows UI ends the request because the user must have
// selected the Windows option first.
EXPECT_FALSE(model.OnWinUserCancelled());
}
}
}
// Simulate the user cancelling the Windows native UI after it was automatically
// dispatched to because a matching credential for Windows Hello was found for
// an allow-list request.
// Regression test for crbug.com/1479142.
TEST_F(AuthenticatorRequestDialogModelTest, WinCancel_AfterMatchingLocalCred) {
base::test::ScopedFeatureList scoped_feature_list{
device::kWebAuthnNewPasskeyUI};
device::FakeWinWebAuthnApi fake_win_webauthn_api;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
&fake_win_webauthn_api);
fake_win_webauthn_api.set_version(4);
AuthenticatorRequestDialogModel::TransportAvailabilityInfo tai;
tai.request_type = device::FidoRequestType::kGetAssertion;
tai.has_win_native_api_authenticator = true;
tai.has_empty_allow_list = false;
tai.available_transports.insert(device::FidoTransportProtocol::kHybrid);
tai.is_ble_powered = true;
tai.recognized_credentials = {kWinCred1};
tai.has_platform_authenticator_credential = device::FidoRequestHandlerBase::
RecognizedCredential::kHasRecognizedCredential;
AuthenticatorRequestDialogModel model(main_rfh());
model.saved_authenticators().AddAuthenticator(
AuthenticatorReference("ID", AuthenticatorTransport::kInternal,
device::AuthenticatorType::kWinNative));
model.set_cable_transport_info(std::nullopt, {}, base::DoNothing(),
"fido:/1234");
model.StartFlow(std::move(tai),
/*is_conditional_mediation=*/false);
// The Windows native UI should have been triggered.
EXPECT_EQ(model.current_step(), Step::kNotStarted);
// Canceling the Windows native UI should be handled.
EXPECT_TRUE(model.OnWinUserCancelled());
// The mechanism selection sheet should now be showing.
EXPECT_EQ(model.current_step(), Step::kMechanismSelection);
// Canceling the Windows UI ends the request because the user must have
// selected the Windows option first.
EXPECT_FALSE(model.OnWinUserCancelled());
}
TEST_F(AuthenticatorRequestDialogModelTest, WinNoPlatformAuthenticator) {
AuthenticatorRequestDialogModel::TransportAvailabilityInfo tai;
tai.request_type = device::FidoRequestType::kMakeCredential;
tai.make_credential_attachment = device::AuthenticatorAttachment::kAny;
tai.request_is_internal_only = true;
tai.win_is_uvpaa = false;
tai.has_win_native_api_authenticator = true;
AuthenticatorRequestDialogModel model(main_rfh());
model.StartFlow(std::move(tai), /*is_conditional_mediation=*/false);
EXPECT_EQ(
model.current_step(),
AuthenticatorRequestDialogModel::Step::kErrorWindowsHelloNotEnabled);
EXPECT_FALSE(model.offer_try_again_in_ui());
}
#endif
TEST_F(AuthenticatorRequestDialogModelTest, NoAvailableTransports) {
testing::StrictMock<MockDialogModelObserver> mock_observer;
AuthenticatorRequestDialogModel model(main_rfh());
model.AddObserver(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(TransportAvailabilityInfo(),
/*is_conditional_mediation=*/false);
EXPECT_EQ(Step::kErrorNoAvailableTransports, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnCancelRequest());
model.Cancel();
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
model.OnRequestComplete();
EXPECT_EQ(Step::kClosed, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnModelDestroyed(&model));
}
TEST_F(AuthenticatorRequestDialogModelTest, Cable2ndFactorFlows) {
#if BUILDFLAG(IS_WIN)
// TODO(https://crbug.com/1517923): Get test to pass in the webauthn supports
// hybrid case.
device::FakeWinWebAuthnApi fake_win_webauthn_api;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
&fake_win_webauthn_api);
fake_win_webauthn_api.set_version(4);
#endif // BUILDFLAG(IS_WIN)
enum class BLEPower {
ON,
OFF,
};
enum class Profile {
NORMAL,
INCOGNITO,
};
const auto mc = RequestType::kMakeCredential;
const auto ga = RequestType::kGetAssertion;
const auto on_ = BLEPower::ON;
const auto off = BLEPower::OFF;
const auto normal = Profile::NORMAL;
const auto otr___ = Profile::INCOGNITO;
const auto mss = Step::kMechanismSelection;
const auto activate = Step::kCableActivate;
const auto interstitial = Step::kOffTheRecordInterstitial;
const auto power = Step::kBlePowerOnAutomatic;
const struct {
RequestType request_type;
BLEPower ble_power;
Profile profile;
std::vector<Step> steps;
} kTests[] = {
// | Expected UI steps in order.
{mc, on_, normal, {mss, activate}},
{mc, on_, otr___, {mss, interstitial, activate}},
{mc, off, normal, {mss, power, activate}},
{mc, off, otr___, {mss, interstitial, power, activate}},
{ga, on_, normal, {mss, activate}},
{ga, on_, otr___, {mss, activate}},
{ga, off, normal, {mss, power, activate}},
{ga, off, otr___, {mss, power, activate}},
};
unsigned test_num = 0;
for (const auto& test : kTests) {
SCOPED_TRACE(test_num++);
TransportAvailabilityInfo transports_info;
transports_info.is_ble_powered = test.ble_power == BLEPower::ON;
transports_info.can_power_on_ble_adapter = true;
transports_info.request_type = test.request_type;
if (transports_info.request_type == RequestType::kMakeCredential) {
transports_info.make_credential_attachment =
device::AuthenticatorAttachment::kAny;
}
transports_info.available_transports = {AuthenticatorTransport::kHybrid};
transports_info.is_off_the_record_context =
test.profile == Profile::INCOGNITO;
AuthenticatorRequestDialogModel model(main_rfh());
std::vector<std::unique_ptr<device::cablev2::Pairing>> pairings;
pairings.emplace_back(GetPairingFromQR());
model.set_cable_transport_info(
/*extension_is_v2=*/std::nullopt, std::move(pairings),
base::DoNothing(), std::nullopt);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
ASSERT_EQ(model.mechanisms().size(), 2u);
for (const auto step : test.steps) {
ASSERT_EQ(step, model.current_step())
<< static_cast<int>(step)
<< " != " << static_cast<int>(model.current_step());
switch (step) {
case Step::kMechanismSelection:
// Click the first (and only) phone.
for (const auto& mechanism : model.mechanisms()) {
if (absl::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Phone>(
mechanism.type)) {
mechanism.callback.Run();
break;
}
}
break;
case Step::kBlePowerOnAutomatic:
model.OnBluetoothPoweredStateChanged(/*powered=*/true);
break;
case Step::kOffTheRecordInterstitial:
model.OnOffTheRecordInterstitialAccepted();
break;
case Step::kCableActivate:
break;
default:
NOTREACHED();
}
}
}
}
TEST_F(AuthenticatorRequestDialogModelTest, AwaitingAcknowledgement) {
const struct {
void (AuthenticatorRequestDialogModel::*event)();
Step expected_sheet;
} kTestCases[] = {
{&AuthenticatorRequestDialogModel::OnRequestTimeout, Step::kTimedOut},
{&AuthenticatorRequestDialogModel::OnActivatedKeyNotRegistered,
Step::kKeyNotRegistered},
{&AuthenticatorRequestDialogModel::OnActivatedKeyAlreadyRegistered,
Step::kKeyAlreadyRegistered},
};
for (const auto& test_case : kTestCases) {
testing::StrictMock<MockDialogModelObserver> mock_observer;
AuthenticatorRequestDialogModel model(main_rfh());
model.AddObserver(&mock_observer);
TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kMakeCredential;
transports_info.make_credential_attachment =
device::AuthenticatorAttachment::kAny;
transports_info.available_transports = kAllTransportsWithoutCable;
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(Step::kMechanismSelection, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
(model.*test_case.event)();
EXPECT_EQ(test_case.expected_sheet, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
EXPECT_CALL(mock_observer, OnCancelRequest());
model.Cancel();
EXPECT_EQ(Step::kClosed, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnModelDestroyed(&model));
}
}
TEST_F(AuthenticatorRequestDialogModelTest, BleAdapterAlreadyPowered) {
const struct {
AuthenticatorTransport transport;
Step expected_final_step;
} kTestCases[] = {
{AuthenticatorTransport::kHybrid, Step::kCableActivate},
};
for (const auto test_case : kTestCases) {
TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kGetAssertion;
transports_info.available_transports = {test_case.transport};
transports_info.can_power_on_ble_adapter = true;
transports_info.is_ble_powered = true;
BluetoothAdapterPowerOnCallbackReceiver power_receiver;
AuthenticatorRequestDialogModel model(main_rfh());
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), std::nullopt);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(test_case.expected_final_step, model.current_step());
EXPECT_TRUE(model.ble_adapter_is_powered());
EXPECT_FALSE(power_receiver.was_called());
}
}
TEST_F(AuthenticatorRequestDialogModelTest, BleAdapterNeedToBeManuallyPowered) {
const struct {
AuthenticatorTransport transport;
Step expected_final_step;
} kTestCases[] = {
{AuthenticatorTransport::kHybrid, Step::kCableActivate},
};
for (const auto test_case : kTestCases) {
TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kGetAssertion;
transports_info.available_transports = {test_case.transport};
transports_info.can_power_on_ble_adapter = false;
transports_info.is_ble_powered = false;
testing::NiceMock<MockDialogModelObserver> mock_observer;
BluetoothAdapterPowerOnCallbackReceiver power_receiver;
AuthenticatorRequestDialogModel model(main_rfh());
model.AddObserver(&mock_observer);
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), std::nullopt);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(Step::kBlePowerOnManual, model.current_step());
EXPECT_FALSE(model.ble_adapter_is_powered());
EXPECT_CALL(mock_observer, OnBluetoothPoweredStateChanged());
model.OnBluetoothPoweredStateChanged(true /* powered */);
EXPECT_EQ(Step::kBlePowerOnManual, model.current_step());
EXPECT_TRUE(model.ble_adapter_is_powered());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
model.ContinueWithFlowAfterBleAdapterPowered();
EXPECT_EQ(test_case.expected_final_step, model.current_step());
EXPECT_FALSE(power_receiver.was_called());
}
}
TEST_F(AuthenticatorRequestDialogModelTest,
BleAdapterCanBeAutomaticallyPowered) {
const struct {
AuthenticatorTransport transport;
Step expected_final_step;
} kTestCases[] = {
{AuthenticatorTransport::kHybrid, Step::kCableActivate},
};
for (const auto test_case : kTestCases) {
TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kGetAssertion;
transports_info.available_transports = {test_case.transport};
transports_info.can_power_on_ble_adapter = true;
transports_info.is_ble_powered = false;
BluetoothAdapterPowerOnCallbackReceiver power_receiver;
AuthenticatorRequestDialogModel model(main_rfh());
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), std::nullopt);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(Step::kBlePowerOnAutomatic, model.current_step());
model.PowerOnBleAdapter();
EXPECT_EQ(Step::kBlePowerOnAutomatic, model.current_step());
EXPECT_TRUE(power_receiver.was_called());
EXPECT_FALSE(model.ble_adapter_is_powered());
model.OnBluetoothPoweredStateChanged(true /* powered */);
EXPECT_EQ(test_case.expected_final_step, model.current_step());
EXPECT_TRUE(model.ble_adapter_is_powered());
}
}
#if !defined(NEW_UI)
// TODO: reenable this test. I'm not sure that the intended behaviour on
// Windows will be.
TEST_F(AuthenticatorRequestDialogModelTest,
RequestCallbackForWindowsAuthenticatorIsInvokedAutomatically) {
constexpr char kWinAuthenticatorId[] = "some_authenticator_id";
::device::FidoRequestHandlerBase::TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kMakeCredential;
transports_info.make_credential_attachment =
device::AuthenticatorAttachment::kPlatform;
transports_info.available_transports = {};
transports_info.has_win_native_api_authenticator = true;
std::vector<std::string> dispatched_authenticator_ids;
AuthenticatorRequestDialogModel model(main_rfh());
model.SetRequestCallback(base::BindRepeating(
[](std::vector<std::string>* ids, const std::string& authenticator_id) {
ids->push_back(authenticator_id);
},
&dispatched_authenticator_ids));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
kWinAuthenticatorId, AuthenticatorTransport::kInternal,
device::AuthenticatorType::kWinNative));
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_TRUE(model.should_dialog_be_closed());
task_environment()->RunUntilIdle();
EXPECT_THAT(dispatched_authenticator_ids, ElementsAre(kWinAuthenticatorId));
}
#endif
TEST_F(AuthenticatorRequestDialogModelTest,
ConditionalUINoRecognizedCredential) {
AuthenticatorRequestDialogModel model(main_rfh());
int preselect_num_called = 0;
model.SetAccountPreselectedCallback(base::BindRepeating(
[](int* i, device::PublicKeyCredentialDescriptor cred) {
EXPECT_EQ(cred.id, std::vector<uint8_t>({1, 2, 3, 4}));
++(*i);
},
&preselect_num_called));
int request_num_called = 0;
model.SetRequestCallback(base::BindRepeating(
[](int* i, const std::string& authenticator_id) { ++(*i); },
&request_num_called));
model.saved_authenticators().AddAuthenticator(
AuthenticatorReference(/*device_id=*/"authenticator",
AuthenticatorTransport::kUsbHumanInterfaceDevice,
device::AuthenticatorType::kOther));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"authenticator", AuthenticatorTransport::kInternal,
device::AuthenticatorType::kOther));
TransportAvailabilityInfo transports_info;
transports_info.available_transports = kAllTransports;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/true);
task_environment()->RunUntilIdle();
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
EXPECT_TRUE(model.should_dialog_be_closed());
EXPECT_EQ(preselect_num_called, 0);
EXPECT_EQ(request_num_called, 0);
}
TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUIRecognizedCredential) {
AuthenticatorRequestDialogModel model(main_rfh());
int preselect_num_called = 0;
model.SetAccountPreselectedCallback(base::BindRepeating(
[](int* i, device::PublicKeyCredentialDescriptor cred) {
EXPECT_EQ(cred.id, std::vector<uint8_t>({0}));
++(*i);
},
&preselect_num_called));
int request_num_called = 0;
model.SetRequestCallback(base::BindRepeating(
[](int* i, const std::string& authenticator_id) {
EXPECT_EQ(authenticator_id, "internal");
++(*i);
},
&request_num_called));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"usb", AuthenticatorTransport::kUsbHumanInterfaceDevice,
device::AuthenticatorType::kOther));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal", AuthenticatorTransport::kInternal,
device::AuthenticatorType::kOther));
TransportAvailabilityInfo transports_info;
transports_info.available_transports = kAllTransports;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
transports_info.recognized_credentials = {kCred1, kCred2};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/true);
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
EXPECT_TRUE(model.should_dialog_be_closed());
EXPECT_EQ(request_num_called, 0);
// After preselecting an account, the request should be dispatched to the
// platform authenticator.
model.OnAccountPreselected(kCred1.cred_id);
task_environment()->RunUntilIdle();
EXPECT_EQ(preselect_num_called, 1);
EXPECT_EQ(request_num_called, 1);
}
// Tests that cancelling a Conditional UI request that has completed restarts
// it.
TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUICancelRequest) {
testing::StrictMock<MockDialogModelObserver> mock_observer;
AuthenticatorRequestDialogModel model(main_rfh());
model.AddObserver(&mock_observer);
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal", AuthenticatorTransport::kInternal,
device::AuthenticatorType::kOther));
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(std::move(TransportAvailabilityInfo()),
/*is_conditional_mediation=*/true);
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
testing::Mock::VerifyAndClearExpectations(&mock_observer);
// Cancel an ongoing request (as if e.g. the user clicked the accept button).
// The request should be restarted.
EXPECT_CALL(mock_observer, OnStartOver());
EXPECT_CALL(mock_observer, OnStepTransition()).Times(2);
model.SetCurrentStepForTesting(Step::kKeyAlreadyRegistered);
model.Cancel();
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
testing::Mock::VerifyAndClearExpectations(&mock_observer);
model.RemoveObserver(&mock_observer);
}
// Tests that selecting a phone passkey on Conditional UI contacts the priority
// phone from sync.
TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUIPhonePasskey) {
constexpr char kLinkedPhoneName[] = "Phone from QR";
constexpr char kOldSyncedPhoneName[] = "Old synced phone";
constexpr char kNewSyncedPhoneName[] = "New synced phone";
std::optional<std::string> phone_name;
// Creates a new dialog model for the given list of |phones|.
auto MakeModel = [&](bool include_old_phone)
-> std::unique_ptr<AuthenticatorRequestDialogModel> {
auto model = std::make_unique<AuthenticatorRequestDialogModel>(main_rfh());
model->SetAccountPreselectedCallback(base::DoNothing());
// Store the contacted phone.
base::RepeatingCallback<void(std::unique_ptr<device::cablev2::Pairing>)>
callback = base::BindLambdaForTesting(
[&](std::unique_ptr<device::cablev2::Pairing> value) {
ASSERT_FALSE(phone_name);
phone_name = value->name;
});
phone_name.reset();
// Set up a linked phone and two phones from sync: an "old" one that last
// contacted sync yesterday, and a "new" one that last contacted sync today.
base::Time today = base::Time::Now();
base::Time yesterday = today - base::Days(1);
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
std::unique_ptr<device::cablev2::Pairing> qr_phone = GetPairingFromQR();
qr_phone->name = kLinkedPhoneName;
phones.emplace_back(std::move(qr_phone));
if (include_old_phone) {
std::unique_ptr<device::cablev2::Pairing> old_synced_phone =
GetPairingFromSync();
old_synced_phone->last_updated = yesterday;
old_synced_phone->name = kOldSyncedPhoneName;
phones.emplace_back(std::move(old_synced_phone));
}
std::unique_ptr<device::cablev2::Pairing> recently_synced_phone =
GetPairingFromSync();
recently_synced_phone->last_updated = today;
recently_synced_phone->name = kNewSyncedPhoneName;
phones.emplace_back(std::move(recently_synced_phone));
model->set_cable_transport_info(/*extension_is_v2=*/std::nullopt,
std::move(phones), std::move(callback),
std::nullopt);
// Set up a single credential from a phone.
device::DiscoverableCredentialMetadata credential = kCred1;
credential.source = device::AuthenticatorType::kPhone;
TransportAvailabilityInfo tai;
tai.recognized_credentials = {credential};
tai.is_ble_powered = true;
tai.request_type = device::FidoRequestType::kGetAssertion;
tai.available_transports = {AuthenticatorTransport::kHybrid};
model->StartFlow(tai, /*is_conditional_mediation=*/true);
CHECK_EQ(model->current_step(), Step::kConditionalMediation);
return model;
};
// Preselect the credential. This should select the phone that last contacted
// sync.
std::unique_ptr<AuthenticatorRequestDialogModel> model =
MakeModel(/*include_old_phone=*/true);
model->OnAccountPreselected(kCred1.cred_id);
EXPECT_EQ(model->current_step(), Step::kCableActivate);
EXPECT_EQ(phone_name, kNewSyncedPhoneName);
// Manually contact the "old" phone from sync. This should give it priority as
// the most recently used.
model = MakeModel(/*include_old_phone=*/true);
model->ContactPhoneForTesting(kOldSyncedPhoneName);
ASSERT_EQ(phone_name, kOldSyncedPhoneName);
// Preselect the credential. This should contact the priority phone, which is
// the "old" phone now.
model = MakeModel(/*include_old_phone=*/true);
model->OnAccountPreselected(kCred1.cred_id);
EXPECT_EQ(model->current_step(), Step::kCableActivate);
EXPECT_EQ(phone_name, kOldSyncedPhoneName);
// Remove the "old" phone so that preselecting the credential again picks the
// "new" one.
model = MakeModel(/*include_old_phone=*/false);
model->OnAccountPreselected(kCred1.cred_id);
EXPECT_EQ(model->current_step(), Step::kCableActivate);
EXPECT_EQ(phone_name, kNewSyncedPhoneName);
}
// Tests that if GPM passkeys change during a conditional UI request, the
// request is restarted.
TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUIPhonePasskeyUpdated) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(syncer::kSyncWebauthnCredentials);
auto model = std::make_unique<AuthenticatorRequestDialogModel>(main_rfh());
model->StartFlow(TransportAvailabilityInfo(),
/*is_conditional_mediation=*/true);
ASSERT_EQ(model->current_step(),
AuthenticatorRequestDialogModel::Step::kConditionalMediation);
testing::NiceMock<MockDialogModelObserver> mock_observer;
model->AddObserver(&mock_observer);
// Notifying that passkeys changed during a conditional request should restart
// it.
EXPECT_CALL(mock_observer, OnStartOver());
static_cast<webauthn::PasskeyModel::Observer*>(model.get())
->OnPasskeysChanged({});
testing::Mock::VerifyAndClearExpectations(&mock_observer);
// Notifying that passkeys changed during any other step should be ignored.
model->SetCurrentStepForTesting(Step::kUsbInsertAndActivate);
static_cast<webauthn::PasskeyModel::Observer*>(model.get())
->OnPasskeysChanged({});
EXPECT_CALL(mock_observer, OnStartOver()).Times(0);
model->RemoveObserver(&mock_observer);
}
// Tests that if the transport availability is updated during a conditional UI
// request, the list of passkeys is updated.
TEST_F(AuthenticatorRequestDialogModelTest,
ConditionalUITransportAvailabilityUpdated) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(syncer::kSyncWebauthnCredentials);
NavigateAndCommit(GURL("rp.com"));
ChromeWebAuthnCredentialsDelegate* delegate =
ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents())
->GetDelegateForFrame(web_contents()->GetPrimaryMainFrame());
ASSERT_TRUE(delegate);
auto model = std::make_unique<AuthenticatorRequestDialogModel>(main_rfh());
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.recognized_credentials = {};
model->StartFlow(transports_info, /*is_conditional_mediation=*/true);
EXPECT_TRUE(delegate->GetPasskeys()->empty());
transports_info.recognized_credentials = {kCred1};
model->OnTransportAvailabilityChanged(transports_info);
EXPECT_FALSE(delegate->GetPasskeys()->empty());
}
// Tests that if the stored preference for the most recently used phone is not
// valid base64, the value is ignored.
TEST_F(AuthenticatorRequestDialogModelTest, InvalidPriorityPhonePref) {
auto model = std::make_unique<AuthenticatorRequestDialogModel>(main_rfh());
model->SetAccountPreselectedCallback(base::DoNothing());
// Store the contacted phone.
std::unique_ptr<device::cablev2::Pairing> contacted_phone;
base::RepeatingCallback<void(std::unique_ptr<device::cablev2::Pairing>)>
callback = base::BindLambdaForTesting(
[&](std::unique_ptr<device::cablev2::Pairing> value) {
ASSERT_FALSE(contacted_phone);
contacted_phone = std::move(value);
});
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
phones.emplace_back(GetPairingFromSync());
model->set_cable_transport_info(/*extension_is_v2=*/std::nullopt,
std::move(phones), std::move(callback),
std::nullopt);
// Set up a single credential from a phone.
device::DiscoverableCredentialMetadata credential = kCred1;
credential.source = device::AuthenticatorType::kPhone;
TransportAvailabilityInfo tai;
tai.recognized_credentials = {credential};
tai.is_ble_powered = true;
tai.request_type = device::FidoRequestType::kGetAssertion;
tai.available_transports = {AuthenticatorTransport::kHybrid};
model->StartFlow(tai, /*is_conditional_mediation=*/true);
ASSERT_EQ(model->current_step(), Step::kConditionalMediation);
// Set an invalid base64 string as the last used pairing preference.
profile()->GetPrefs()->SetString(
webauthn::pref_names::kLastUsedPairingFromSyncPublicKey, "oops!");
model->OnAccountPreselected(credential.cred_id);
EXPECT_EQ(model->current_step(), Step::kCableActivate);
EXPECT_TRUE(contacted_phone);
}
#if BUILDFLAG(IS_WIN)
// Tests that cancelling the Windows Platform authenticator during a Conditional
// UI request restarts it.
TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUIWindowsCancel) {
testing::StrictMock<MockDialogModelObserver> mock_observer;
AuthenticatorRequestDialogModel model(main_rfh());
model.AddObserver(&mock_observer);
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal", AuthenticatorTransport::kInternal,
device::AuthenticatorType::kOther));
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(std::move(TransportAvailabilityInfo()),
/*is_conditional_mediation=*/true);
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
testing::Mock::VerifyAndClearExpectations(&mock_observer);
// Simulate the Windows authenticator cancelling.
EXPECT_CALL(mock_observer, OnStepTransition());
EXPECT_CALL(mock_observer, OnStartOver());
model.OnWinUserCancelled();
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
testing::Mock::VerifyAndClearExpectations(&mock_observer);
model.RemoveObserver(&mock_observer);
}
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_MAC)
// Tests that a transport = internal virtual authenticator can be dispatched to
// on Mac.
// Regression test for crbug.com/1520898.
TEST_F(AuthenticatorRequestDialogModelTest, PlatformVirtualAuthenticator) {
AuthenticatorRequestDialogModel model(main_rfh());
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"virtual-authenticator", AuthenticatorTransport::kInternal,
device::AuthenticatorType::kOther));
model.SetAccountPreselectedCallback(base::DoNothing());
base::RunLoop run_loop;
model.SetRequestCallback(
base::BindLambdaForTesting([&](const std::string& authenticator_id) {
EXPECT_EQ(authenticator_id, "virtual-authenticator");
run_loop.Quit();
}));
TransportAvailabilityInfo transports_info;
transports_info.user_verification_requirement =
device::UserVerificationRequirement::kRequired;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.has_empty_allow_list = false;
transports_info.recognized_credentials = {kCred2};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
run_loop.Run();
}
#endif // BUILDFLAG(IS_MAC)
TEST_F(AuthenticatorRequestDialogModelTest, PreSelect) {
for (const bool has_empty_allow_list : {false, true}) {
SCOPED_TRACE(::testing::Message()
<< "has_empty_allow_list=" << has_empty_allow_list);
AuthenticatorRequestDialogModel model(main_rfh());
int preselect_num_called = 0;
model.SetAccountPreselectedCallback(base::BindLambdaForTesting(
[&preselect_num_called](device::PublicKeyCredentialDescriptor cred) {
EXPECT_EQ(cred.id, std::vector<uint8_t>({1}));
++preselect_num_called;
}));
int request_num_called = 0;
model.SetRequestCallback(base::BindLambdaForTesting(
[&request_num_called](const std::string& authenticator_id) {
EXPECT_EQ(authenticator_id, "internal-authenticator");
++request_num_called;
}));
model.saved_authenticators().AddAuthenticator(
AuthenticatorReference(/*device_id=*/"usb-authenticator",
AuthenticatorTransport::kUsbHumanInterfaceDevice,
device::AuthenticatorType::kOther));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal-authenticator",
AuthenticatorTransport::kInternal, device::AuthenticatorType::kOther));
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = kAllTransports;
transports_info.has_empty_allow_list = has_empty_allow_list;
#if BUILDFLAG(IS_MAC)
// The TouchID authenticator will be immediately dispatched to if the device
// has biometrics configured. Simulate a lack of biometrics to align with
// other platforms.
model.set_local_biometrics_override_for_testing(false);
#endif // BUILDFLAG(IS_MAC)
transports_info.user_verification_requirement =
device::UserVerificationRequirement::kPreferred;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
transports_info.recognized_credentials = {kCred1FromICloudKeychain, kCred2};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
#if defined(NEW_UI)
if (has_empty_allow_list) {
EXPECT_EQ(model.current_step(), Step::kSelectPriorityMechanism);
} else {
EXPECT_EQ(model.current_step(), Step::kPreSelectSingleAccount);
}
#else
if (has_empty_allow_list) {
EXPECT_EQ(model.current_step(), Step::kPreSelectAccount);
} else {
EXPECT_EQ(model.current_step(), Step::kPreSelectSingleAccount);
}
#endif
task_environment()->RunUntilIdle();
if (has_empty_allow_list) {
EXPECT_EQ(preselect_num_called, 0);
EXPECT_EQ(request_num_called, 0);
// After preselecting an account, the request should be dispatched to the
// platform authenticator.
model.OnAccountPreselected(kCred2.cred_id);
task_environment()->RunUntilIdle();
EXPECT_EQ(preselect_num_called, 1);
EXPECT_EQ(request_num_called, 1);
} else {
EXPECT_EQ(request_num_called, 0);
ASSERT_EQ(model.creds().size(), 1u);
if (base::FeatureList::IsEnabled(device::kWebAuthnNewPasskeyUI)) {
// `kCred1FromICloudKeychain` is an iCloud Keychain credential so,
// even though it's in `recognized_credentials`, it shouldn't have been
// used by the standard platform authenticator code.
EXPECT_EQ(model.creds()[0].cred_id, std::vector<uint8_t>({1}));
} else {
// Without the new UI flag set, the iCloud Keychain credential won't
// be filtered out when triggering the platform authenticator.
EXPECT_EQ(model.creds()[0].cred_id, std::vector<uint8_t>({4}));
}
}
}
}
#if BUILDFLAG(IS_WIN)
// Regression test for crbug.com/1476884.
TEST_F(AuthenticatorRequestDialogModelTest, JumpToWindowsWithNewUI) {
base::test::ScopedFeatureList scoped_feature_list{
device::kWebAuthnNewPasskeyUI};
AuthenticatorRequestDialogModel model(main_rfh());
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = kAllTransports;
transports_info.has_win_native_api_authenticator = true;
transports_info.has_empty_allow_list = false;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
transports_info.recognized_credentials = {kWinCred1, kWinCred2};
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"win", AuthenticatorTransport::kInternal,
device::AuthenticatorType::kWinNative));
RequestCallbackReceiver request_callback;
model.SetRequestCallback(request_callback.Callback());
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(request_callback.WaitForResult(), "win");
}
#endif // BUILDFLAG(IS_WIN)
// Tests that if the user does not have a phone from sync, Chrome offers a phone
// confirmation screen for an allow-list request when there is a single
// previously paired phone, no local matches, and only hybrid or internal
// credentials in the allow-list.
TEST_F(AuthenticatorRequestDialogModelTest, ContactPriorityPhone_NoSync) {
#if BUILDFLAG(IS_WIN)
// TODO(https://crbug.com/1517923): Get test to pass in the webauthn supports
// hybrid case.
device::FakeWinWebAuthnApi fake_win_webauthn_api;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
&fake_win_webauthn_api);
fake_win_webauthn_api.set_version(4);
#endif // BUILDFLAG(IS_WIN)
AuthenticatorRequestDialogModel model(main_rfh());
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
phones.emplace_back(GetPairingFromQR());
model.set_cable_transport_info(/*extension_is_v2=*/std::nullopt,
std::move(phones), base::DoNothing(),
std::nullopt);
TransportAvailabilityInfo transports_info;
transports_info.is_ble_powered = true;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = {AuthenticatorTransport::kHybrid};
transports_info.is_only_hybrid_or_internal = true;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential;
transports_info.has_icloud_keychain_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential;
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(model.current_step(), Step::kPhoneConfirmationSheet);
EXPECT_EQ(model.GetPriorityPhoneName(), u"Phone from QR");
model.ContactPriorityPhone();
EXPECT_EQ(model.current_step(), Step::kCableActivate);
EXPECT_EQ(model.selected_phone_name(), "Phone from QR");
}
// Tests that if the user has a phone from sync, Chrome offers a phone
// confirmation screen for an allow-list request when there is a phone passkey
// match and no local matches.
TEST_F(AuthenticatorRequestDialogModelTest, ContactPriorityPhone_WithSync) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{device::kWebAuthnNewPasskeyUI, syncer::kSyncWebauthnCredentials},
/*disabled_features=*/{});
AuthenticatorRequestDialogModel model(main_rfh());
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
phones.emplace_back(GetPairingFromQR());
phones.emplace_back(GetPairingFromSync());
model.set_cable_transport_info(/*extension_is_v2=*/std::nullopt,
std::move(phones), base::DoNothing(),
std::nullopt);
TransportAvailabilityInfo transports_info;
transports_info.recognized_credentials = {kPhoneCred1, kPhoneCred2};
transports_info.is_ble_powered = true;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = {AuthenticatorTransport::kHybrid};
transports_info.is_only_hybrid_or_internal = true;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential;
transports_info.has_icloud_keychain_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential;
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(model.current_step(), Step::kPhoneConfirmationSheet);
EXPECT_EQ(model.GetPriorityPhoneName(), u"Phone from sync");
model.ContactPriorityPhone();
EXPECT_EQ(model.current_step(), Step::kCableActivate);
EXPECT_EQ(model.selected_phone_name(), "Phone from sync");
}
#if BUILDFLAG(IS_MAC)
TEST_F(AuthenticatorRequestDialogModelTest, BluetoothPermissionPrompt) {
// When BLE permission is denied on macOS, we should jump to the sheet that
// explains that if the user tries to use a linked phone or tries to show the
// QR code.
for (const bool ble_access_denied : {false, true}) {
for (const bool click_specific_phone : {false, true}) {
SCOPED_TRACE(::testing::Message()
<< "ble_access_denied=" << ble_access_denied);
SCOPED_TRACE(::testing::Message()
<< "click_specific_phone=" << click_specific_phone);
AuthenticatorRequestDialogModel model(main_rfh());
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
phones.emplace_back(GetPairingFromQR());
model.set_cable_transport_info(/*extension_is_v2=*/std::nullopt,
std::move(phones), base::DoNothing(),
std::nullopt);
TransportAvailabilityInfo transports_info;
transports_info.is_ble_powered = true;
transports_info.ble_access_denied = ble_access_denied;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = {
AuthenticatorTransport::kHybrid,
AuthenticatorTransport::kUsbHumanInterfaceDevice};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
base::ranges::find_if(
model.mechanisms(),
[click_specific_phone](const auto& m) -> bool {
if (click_specific_phone) {
return absl::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Phone>(m.type);
} else {
return absl::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::AddPhone>(m.type);
}
})
->callback.Run();
if (ble_access_denied) {
EXPECT_EQ(model.current_step(), Step::kBlePermissionMac);
} else if (click_specific_phone) {
EXPECT_EQ(model.current_step(), Step::kCableActivate);
} else {
EXPECT_EQ(model.current_step(), Step::kCableV2QRCode);
}
}
}
}
#endif
TEST_F(AuthenticatorRequestDialogModelTest, AdvanceThroughCableV2States) {
AuthenticatorRequestDialogModel model(main_rfh());
model.set_cable_transport_info(/*extension_is_v2=*/std::nullopt, {},
base::DoNothing(), std::nullopt);
TransportAvailabilityInfo transports_info;
transports_info.is_ble_powered = true;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = {AuthenticatorTransport::kHybrid};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
model.OnCableEvent(device::cablev2::Event::kPhoneConnected);
EXPECT_EQ(model.current_step(), Step::kCableV2Connecting);
model.OnCableEvent(device::cablev2::Event::kBLEAdvertReceived);
EXPECT_EQ(model.current_step(), Step::kCableV2Connecting);
model.OnCableEvent(device::cablev2::Event::kReady);
// kCableV2Connecting won't flash by too quickly, so it'll still be showing.
EXPECT_EQ(model.current_step(), Step::kCableV2Connecting);
task_environment()->FastForwardBy(base::Seconds(2));
EXPECT_EQ(model.current_step(), Step::kCableV2Connected);
}
TEST_F(AuthenticatorRequestDialogModelTest,
AdvanceThroughCableV2StatesStopTimer) {
AuthenticatorRequestDialogModel model(main_rfh());
model.set_cable_transport_info(/*extension_is_v2=*/std::nullopt, {},
base::DoNothing(), std::nullopt);
TransportAvailabilityInfo transports_info;
transports_info.is_ble_powered = true;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = {AuthenticatorTransport::kHybrid};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
model.OnCableEvent(device::cablev2::Event::kPhoneConnected);
EXPECT_EQ(model.current_step(), Step::kCableV2Connecting);
model.OnCableEvent(device::cablev2::Event::kBLEAdvertReceived);
EXPECT_EQ(model.current_step(), Step::kCableV2Connecting);
model.OnCableEvent(device::cablev2::Event::kReady);
// kCableV2Connecting won't flash by too quickly, so it'll still be showing.
EXPECT_EQ(model.current_step(), Step::kCableV2Connecting);
// Moving to a different step should stop the timer so that kCableV2Connected
// never shows.
model.SetCurrentStepForTesting(Step::kCableActivate);
task_environment()->FastForwardBy(base::Seconds(10));
EXPECT_EQ(model.current_step(), Step::kCableActivate);
}
template <class Value>
class RepeatingValueCallbackReceiver {
public:
base::RepeatingCallback<void(Value)> Callback() {
return base::BindRepeating(&RepeatingValueCallbackReceiver::OnCallback,
base::Unretained(this));
}
Value WaitForResult() {
if (!value_) {
run_loop_->Run();
}
Value ret = std::move(*value_);
run_loop_ = std::make_unique<base::RunLoop>();
return ret;
}
private:
void OnCallback(Value value) {
value_ = std::move(value);
run_loop_->Quit();
}
std::optional<Value> value_;
std::unique_ptr<base::RunLoop> run_loop_ = std::make_unique<base::RunLoop>();
};
TEST_F(AuthenticatorRequestDialogModelTest, Crbug1503187) {
// This test reproduces the crash from crbug.com/1503187.
base::test::ScopedFeatureList scoped_feature_list{
device::kWebAuthnNewPasskeyUI};
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = {
AuthenticatorTransport::kInternal,
AuthenticatorTransport::kUsbHumanInterfaceDevice};
transports_info.recognized_credentials = {kCred1FromChromeOS};
transports_info.has_empty_allow_list = false;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
AuthenticatorRequestDialogModel model(main_rfh());
RepeatingValueCallbackReceiver<device::PublicKeyCredentialDescriptor>
account_preselected_callback;
model.SetAccountPreselectedCallback(account_preselected_callback.Callback());
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
}
class MultiplePlatformAuthenticatorsTest
: public AuthenticatorRequestDialogModelTest {
private:
base::test::ScopedFeatureList scoped_feature_list_{
device::kWebAuthnNewPasskeyUI};
};
TEST_F(MultiplePlatformAuthenticatorsTest, DeduplicateAccounts) {
using Mechanism = AuthenticatorRequestDialogModel::Mechanism;
const struct {
std::vector<device::DiscoverableCredentialMetadata> recognized_credentials;
std::optional<Mechanism::Type> type_of_priority_mechanism;
} kTests[] = {
{{kCred1, kCred2, kPhoneCred1}, std::nullopt},
{{kCred1, kCred2}, std::nullopt},
{{kCred1, kCred1FromICloudKeychain},
Mechanism::Credential(CredentialInfoFrom(kCred1FromICloudKeychain))},
{{kCred1FromICloudKeychain, kCred1},
Mechanism::Credential(CredentialInfoFrom(kCred1FromICloudKeychain))},
};
for (const auto& test : kTests) {
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = {AuthenticatorTransport::kInternal};
transports_info.recognized_credentials = test.recognized_credentials;
transports_info.has_empty_allow_list = true;
AuthenticatorRequestDialogModel model(main_rfh());
model.set_allow_icloud_keychain(true);
RepeatingValueCallbackReceiver<device::PublicKeyCredentialDescriptor>
account_preselected_callback;
model.SetAccountPreselectedCallback(
account_preselected_callback.Callback());
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
ASSERT_EQ(model.ephemeral_state_.priority_mechanism_index_.has_value(),
test.type_of_priority_mechanism.has_value());
if (!test.type_of_priority_mechanism.has_value()) {
continue;
}
EXPECT_EQ(
*test.type_of_priority_mechanism,
model.mechanisms_[*model.ephemeral_state_.priority_mechanism_index_]
.type);
}
}
#if BUILDFLAG(IS_MAC)
TEST_F(MultiplePlatformAuthenticatorsTest, Dispatch) {
base::test::ScopedFeatureList scoped_feature_list_{
device::kWebAuthnICloudKeychain};
for (const bool should_create_in_icloud_keychain : {false, true}) {
for (const bool platform_attachment : {false, true}) {
if (!platform_attachment && should_create_in_icloud_keychain) {
// Without `platform_attachment`, `should_create_in_icloud_keychain` is
// moot.
continue;
}
SCOPED_TRACE(testing::Message() << "should_create_in_icloud_keychain: "
<< should_create_in_icloud_keychain);
SCOPED_TRACE(testing::Message()
<< "platform_attachment: " << platform_attachment);
TransportAvailabilityInfo transports_info;
transports_info.has_icloud_keychain = true;
transports_info.available_transports = {
AuthenticatorTransport::kInternal,
AuthenticatorTransport::kUsbHumanInterfaceDevice};
transports_info.request_type = device::FidoRequestType::kMakeCredential;
transports_info.resident_key_requirement =
device::ResidentKeyRequirement::kRequired;
transports_info.make_credential_attachment =
platform_attachment ? device::AuthenticatorAttachment::kPlatform
: device::AuthenticatorAttachment::kAny;
AuthenticatorRequestDialogModel model(main_rfh());
model.set_allow_icloud_keychain(true);
model.set_should_create_in_icloud_keychain(
should_create_in_icloud_keychain);
RequestCallbackReceiver request_callback;
model.SetRequestCallback(request_callback.Callback());
const std::string kProfileAuthenticatorId = "platauth";
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
kProfileAuthenticatorId, AuthenticatorTransport::kInternal,
device::AuthenticatorType::kTouchID));
const std::string kICloudKeychainId = "ickc";
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
kICloudKeychainId, AuthenticatorTransport::kInternal,
device::AuthenticatorType::kICloudKeychain));
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
if (platform_attachment) {
if (should_create_in_icloud_keychain) {
EXPECT_EQ(request_callback.WaitForResult(), kICloudKeychainId);
} else {
EXPECT_EQ(model.current_step(),
AuthenticatorRequestDialogModel::Step::kCreatePasskey);
model.HideDialogAndDispatchToPlatformAuthenticator();
EXPECT_EQ(request_callback.WaitForResult(), kProfileAuthenticatorId);
}
} else {
EXPECT_EQ(model.current_step(),
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
}
if (!platform_attachment) {
// Dispatch to iCloud Keychain to check that canceling doesn't show
// a Chrome error dialog.
model.HideDialogAndDispatchToPlatformAuthenticator(
device::AuthenticatorType::kICloudKeychain);
}
model.OnUserConsentDenied();
if (platform_attachment) {
EXPECT_EQ(
model.current_step(),
should_create_in_icloud_keychain
? AuthenticatorRequestDialogModel::Step::kMechanismSelection
: AuthenticatorRequestDialogModel::Step::
kErrorInternalUnrecognized);
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
kProfileAuthenticatorId, AuthenticatorTransport::kInternal,
device::AuthenticatorType::kTouchID));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
kICloudKeychainId, AuthenticatorTransport::kInternal,
device::AuthenticatorType::kICloudKeychain));
// Dispatch and cancel again to confirm that canceling the non-automatic
// dispatch cancels the whole request.
model.HideDialogAndDispatchToPlatformAuthenticator(
device::AuthenticatorType::kICloudKeychain);
model.OnUserConsentDenied();
}
// Canceling after a non-automatic dispatch to iCloud Keychain should
// end the request.
EXPECT_EQ(model.current_step(),
AuthenticatorRequestDialogModel::Step::kNotStarted);
}
}
}
TEST_F(MultiplePlatformAuthenticatorsTest,
OnlyShowConfirmationSheetForProfileAuthenticator) {
base::test::ScopedFeatureList scoped_feature_list_{
device::kWebAuthnICloudKeychain};
for (const auto credential_source :
{device::AuthenticatorType::kTouchID,
device::AuthenticatorType::kICloudKeychain}) {
SCOPED_TRACE(static_cast<int>(credential_source));
TransportAvailabilityInfo transports_info;
transports_info.has_icloud_keychain = true;
transports_info.available_transports = {AuthenticatorTransport::kInternal};
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.has_empty_allow_list = false;
if (credential_source == device::AuthenticatorType::kTouchID) {
transports_info.recognized_credentials = {kCred2};
transports_info.has_platform_authenticator_credential =
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential;
} else {
transports_info.recognized_credentials = {kCred1FromICloudKeychain};
transports_info.has_icloud_keychain_credential =
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential;
}
AuthenticatorRequestDialogModel model(main_rfh());
model.set_allow_icloud_keychain(true);
RepeatingValueCallbackReceiver<device::PublicKeyCredentialDescriptor>
account_preselected_callback;
model.SetAccountPreselectedCallback(
account_preselected_callback.Callback());
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(model.current_step(), Step::kNotStarted);
device::PublicKeyCredentialDescriptor descriptor =
account_preselected_callback.WaitForResult();
if (credential_source == device::AuthenticatorType::kTouchID) {
EXPECT_EQ(descriptor.id, kCred2.cred_id);
} else {
EXPECT_EQ(descriptor.id, kCred1FromICloudKeychain.cred_id);
}
}
}
#endif
class ListPasskeysFromSyncTest : public AuthenticatorRequestDialogModelTest {
public:
ListPasskeysFromSyncTest() {
scoped_feature_list_.InitWithFeatures(
{device::kWebAuthnNewPasskeyUI, syncer::kSyncWebauthnCredentials},
/*disabled_features=*/{});
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(ListPasskeysFromSyncTest, ListGPMPasskeysInConditionalUI) {
NavigateAndCommit(GURL("rp.com"));
// Tests that passkeys are listed in conditional UI, but only if there is a
// phone from sync available.
ChromeWebAuthnCredentialsDelegate* delegate =
ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents())
->GetDelegateForFrame(web_contents()->GetPrimaryMainFrame());
ASSERT_TRUE(delegate);
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.recognized_credentials = {kPhoneCred1};
{
AuthenticatorRequestDialogModel model(main_rfh());
model.StartFlow(transports_info,
/*is_conditional_mediation=*/true);
// There is no phone available, so no passkeys should be sent to autofill.
EXPECT_TRUE(delegate->GetPasskeys()->empty());
}
{
AuthenticatorRequestDialogModel model(main_rfh());
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
phones.emplace_back(GetPairingFromQR());
model.set_cable_transport_info(
/*extension_is_v2=*/std::nullopt, std::move(phones), base::DoNothing(),
std::nullopt);
model.StartFlow(transports_info,
/*is_conditional_mediation=*/true);
// There is no phone from sync, so no passkeys should be sent to autofill.
EXPECT_TRUE(delegate->GetPasskeys()->empty());
}
{
AuthenticatorRequestDialogModel model(main_rfh());
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
phones.emplace_back(GetPairingFromSync());
model.set_cable_transport_info(
/*extension_is_v2=*/std::nullopt, std::move(phones), base::DoNothing(),
std::nullopt);
model.StartFlow(transports_info,
/*is_conditional_mediation=*/true);
ASSERT_EQ(delegate->GetPasskeys()->size(), 1u);
const password_manager::PasskeyCredential& passkey =
delegate->GetPasskeys()->at(0);
EXPECT_EQ(passkey.credential_id(), kPhoneCred1.cred_id);
EXPECT_EQ(passkey.display_name(), "");
EXPECT_EQ(passkey.username(), kPhoneCred1.user.name);
EXPECT_EQ(passkey.GetAuthenticatorLabel(),
l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_PASSKEY_FROM_PHONE, u"Phone from sync"));
EXPECT_EQ(passkey.user_id(), kPhoneCred1.user.id);
EXPECT_EQ(passkey.rp_id(), kPhoneCred1.rp_id);
EXPECT_EQ(passkey.source(),
password_manager::PasskeyCredential::Source::kAndroidPhone);
}
}
TEST_F(ListPasskeysFromSyncTest, MechanismsFromUserAccounts) {
// Set up a model with two local passkeys and a GPM passkey.
AuthenticatorRequestDialogModel model(main_rfh());
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = {AuthenticatorTransport::kInternal};
transports_info.recognized_credentials = {kCred1, kCred2, kPhoneCred1};
transports_info.ble_access_denied = false;
transports_info.is_ble_powered = true;
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
phones.emplace_back(GetPairingFromSync());
RepeatingValueCallbackReceiver<std::unique_ptr<device::cablev2::Pairing>>
contact_phone_callback;
model.set_cable_transport_info(
/*extension_is_v2=*/std::nullopt, std::move(phones),
contact_phone_callback.Callback(), std::nullopt);
RepeatingValueCallbackReceiver<device::PublicKeyCredentialDescriptor>
account_preselected_callback;
model.SetAccountPreselectedCallback(account_preselected_callback.Callback());
RequestCallbackReceiver request_callback;
model.SetRequestCallback(request_callback.Callback());
const std::string kLocalAuthenticatorId = "local-authenticator";
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
kLocalAuthenticatorId, AuthenticatorTransport::kInternal,
device::AuthenticatorType::kOther));
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
// Entries will be sorted by username. So the first entry should correspond to
// the first local passkey.
const AuthenticatorRequestDialogModel::Mechanism& mech1 =
model.mechanisms()[0];
EXPECT_EQ(mech1.name, base::UTF8ToUTF16(*kUser1.name));
EXPECT_EQ(mech1.short_name, base::UTF8ToUTF16(*kUser1.name));
EXPECT_EQ(mech1.description,
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_USE_GENERIC_DEVICE));
EXPECT_EQ(mech1.icon, vector_icons::kPasskeyIcon);
mech1.callback.Run();
device::PublicKeyCredentialDescriptor result =
account_preselected_callback.WaitForResult();
EXPECT_EQ(result.id, kCred1.cred_id);
EXPECT_THAT(result.transports,
testing::ElementsAre(device::FidoTransportProtocol::kInternal));
EXPECT_EQ(request_callback.WaitForResult(), kLocalAuthenticatorId);
// Reset the model as if the user had cancelled out of the operation.
model.StartOver();
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
kLocalAuthenticatorId, AuthenticatorTransport::kInternal,
device::AuthenticatorType::kOther));
// The second entry will be `kCred2`.
const AuthenticatorRequestDialogModel::Mechanism& mech2 =
model.mechanisms()[1];
EXPECT_EQ(mech2.name, base::UTF8ToUTF16(*kUser2.name));
EXPECT_EQ(mech2.short_name, base::UTF8ToUTF16(*kUser2.name));
EXPECT_EQ(mech2.description, u"Use device sign-in");
EXPECT_EQ(mech2.icon, vector_icons::kPasskeyIcon);
mech2.callback.Run();
result = account_preselected_callback.WaitForResult();
EXPECT_EQ(result.id, kCred2.cred_id);
EXPECT_THAT(result.transports,
testing::ElementsAre(device::FidoTransportProtocol::kInternal));
EXPECT_EQ(request_callback.WaitForResult(), kLocalAuthenticatorId);
// Reset the model as if the user had cancelled out of the operation.
model.StartOver();
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
kLocalAuthenticatorId, AuthenticatorTransport::kInternal,
device::AuthenticatorType::kOther));
// The third entry should correspond to `kPhoneCred1`.
const AuthenticatorRequestDialogModel::Mechanism& mech3 =
model.mechanisms()[2];
EXPECT_EQ(mech3.name, base::UTF8ToUTF16(*kPhoneUser1.name));
EXPECT_EQ(mech3.short_name, base::UTF8ToUTF16(*kPhoneUser1.name));
EXPECT_EQ(mech3.description,
l10n_util::GetStringFUTF16(IDS_WEBAUTHN_SOURCE_PHONE,
u"Phone from sync"));
EXPECT_EQ(mech3.icon, kSmartphoneIcon);
mech3.callback.Run();
result = account_preselected_callback.WaitForResult();
EXPECT_EQ(result.id, kPhoneCred1.cred_id);
EXPECT_THAT(result.transports,
testing::ElementsAre(device::FidoTransportProtocol::kHybrid));
EXPECT_TRUE(contact_phone_callback.WaitForResult());
}
#if BUILDFLAG(IS_WIN)
using HasCreds = device::FidoRequestHandlerBase::RecognizedCredential;
constexpr int kNoWinButton = -1;
constexpr int kNoChromeUI = -2;
constexpr int kHelloOrSk = IDS_WEBAUTHN_TRANSPORT_WINDOWS_HELLO_OR_SECURITY_KEY;
constexpr int kHello = IDS_WEBAUTHN_TRANSPORT_WINDOWS_HELLO;
constexpr int kSk = IDS_WEBAUTHN_TRANSPORT_EXTERNAL_SECURITY_KEY;
constexpr int kPhoneOrSk =
IDS_WEBAUTHN_PASSKEY_PHONE_TABLET_OR_SECURITY_KEY_LABEL;
constexpr int kPhone = IDS_WEBAUTHN_PASSKEY_PHONE_OR_TABLET_LABEL;
#define L __LINE__
struct {
int line_num;
bool has_sk;
bool has_hybrid;
bool has_internal;
bool supports_hybrid;
HasCreds has_creds;
int expected_button;
} kWinHelloButtonGetAssertionTestCases[] = {
// Windows v7+ with all transports.
{L, true, true, true, true, HasCreds::kHasRecognizedCredential, kPhoneOrSk},
// Windows v7+ with only security keys.
{L, true, false, false, true, HasCreds::kNoRecognizedCredential, kSk},
// Windows v7+ with only phones.
{L, false, true, false, true, HasCreds::kNoRecognizedCredential, kPhone},
// Windows v7+ with only internal creds.
{L, false, false, true, true, HasCreds::kHasRecognizedCredential,
kNoChromeUI},
// Windows v7+ with empty allow-list.
{L, false, false, false, true, HasCreds::kHasRecognizedCredential,
kPhoneOrSk},
// Windows v5+ with all transports.
{L, true, true, true, false, HasCreds::kHasRecognizedCredential, kSk},
// Windows v5+ with only security keys
{L, true, false, false, false, HasCreds::kNoRecognizedCredential, kSk},
// Windows v5+ with only phones.
{L, false, true, false, false, HasCreds::kNoRecognizedCredential,
kNoWinButton},
// Windows v5+ with only internal creds.
{L, false, false, true, false, HasCreds::kHasRecognizedCredential,
kNoChromeUI},
// Windows v5+ with empty allow-list.
{L, false, false, false, false, HasCreds::kHasRecognizedCredential, kSk},
// Windows <v4 with all transports.
{L, true, true, true, false, HasCreds::kUnknown, kHelloOrSk},
// Windows <v4 with only security keys.
{L, true, false, false, false, HasCreds::kUnknown, kSk},
// Windows <v4 with only phones.
{L, false, true, false, false, HasCreds::kUnknown, kNoWinButton},
// Windows <v4 with only internal creds.
{L, false, false, true, false, HasCreds::kUnknown, kHello},
// Windows <v4 with empty allow-list.
{L, false, false, false, false, HasCreds::kUnknown, kHelloOrSk},
};
#undef L
TEST_F(ListPasskeysFromSyncTest, WindowsHelloButtonLabel_GetAssertion) {
device::FakeWinWebAuthnApi fake_win_webauthn_api;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
&fake_win_webauthn_api);
for (const auto& test_case : kWinHelloButtonGetAssertionTestCases) {
AuthenticatorRequestDialogModel model(main_rfh());
model.SetAccountPreselectedCallback(
base::BindRepeating([](device::PublicKeyCredentialDescriptor cred) {}));
TransportAvailabilityInfo transports_info;
transports_info.has_win_native_api_authenticator = true;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.transport_list_did_include_security_key = test_case.has_sk;
transports_info.transport_list_did_include_hybrid = test_case.has_hybrid;
transports_info.transport_list_did_include_internal =
test_case.has_internal;
transports_info.has_platform_authenticator_credential = test_case.has_creds;
if (test_case.has_creds == HasCreds::kHasRecognizedCredential) {
transports_info.recognized_credentials = {kCred1};
}
if (!test_case.has_sk && !test_case.has_hybrid && !test_case.has_internal) {
transports_info.has_empty_allow_list = true;
}
fake_win_webauthn_api.set_version(test_case.supports_hybrid ? 7 : 4);
SCOPED_TRACE(testing::Message() << "Line number: " << test_case.line_num);
SCOPED_TRACE(testing::Message() << "SK: " << test_case.has_sk);
SCOPED_TRACE(testing::Message() << "Hybrid: " << test_case.has_hybrid);
SCOPED_TRACE(testing::Message() << "Internal: " << test_case.has_internal);
SCOPED_TRACE(testing::Message()
<< "Has creds: " << static_cast<int>(test_case.has_creds));
SCOPED_TRACE(testing::Message()
<< "Handles hybrid: " << test_case.supports_hybrid);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
auto win_button_it =
base::ranges::find_if(model.mechanisms(), [](const auto& m) {
return absl::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::WindowsAPI>(m.type);
});
if (test_case.expected_button == kNoWinButton) {
EXPECT_EQ(win_button_it, model.mechanisms().end());
} else if (test_case.expected_button == kNoChromeUI) {
// In these cases, Chrome should have invoked the Windows UI immediately.
EXPECT_EQ(model.current_step(), Step::kNotStarted);
} else {
ASSERT_NE(win_button_it, model.mechanisms().end());
EXPECT_EQ(win_button_it->name,
l10n_util::GetStringUTF16(test_case.expected_button));
EXPECT_EQ(win_button_it->short_name,
l10n_util::GetStringUTF16(test_case.expected_button));
switch (test_case.expected_button) {
case kHelloOrSk:
case kHello:
EXPECT_EQ(win_button_it->icon, kLaptopIcon);
break;
case kSk:
EXPECT_EQ(win_button_it->icon, kUsbSecurityKeyIcon);
break;
case kPhoneOrSk:
case kPhone:
EXPECT_EQ(win_button_it->icon, kSmartphoneIcon);
break;
default:
NOTREACHED();
}
}
}
}
struct {
device::AuthenticatorAttachment attachment;
int expected_button;
} kWinHelloButtonMakeCredentialTestCases[] = {
// For make credential, we will only show the authenticator picker when
// Windows does not do hybrid. Therefore, there is no option for "Hello,
// Security Key, or Phone".
{device::AuthenticatorAttachment::kAny, kHelloOrSk},
{device::AuthenticatorAttachment::kCrossPlatform, kSk},
{device::AuthenticatorAttachment::kPlatform, kHello},
};
TEST_F(ListPasskeysFromSyncTest, WindowsHelloButtonLabel_MakeCredential) {
device::FakeWinWebAuthnApi fake_win_webauthn_api;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
&fake_win_webauthn_api);
for (const auto& test_case : kWinHelloButtonMakeCredentialTestCases) {
AuthenticatorRequestDialogModel model(main_rfh());
TransportAvailabilityInfo transports_info;
transports_info.has_win_native_api_authenticator = true;
transports_info.request_type = device::FidoRequestType::kMakeCredential;
transports_info.make_credential_attachment = test_case.attachment;
fake_win_webauthn_api.set_version(4);
SCOPED_TRACE(testing::Message()
<< "Attachment: " << static_cast<int>(test_case.attachment));
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
auto win_button_it =
base::ranges::find_if(model.mechanisms(), [](const auto& m) {
return absl::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::WindowsAPI>(m.type);
});
ASSERT_NE(win_button_it, model.mechanisms().end());
EXPECT_EQ(win_button_it->name,
l10n_util::GetStringUTF16(test_case.expected_button));
EXPECT_EQ(win_button_it->short_name,
l10n_util::GetStringUTF16(test_case.expected_button));
switch (test_case.expected_button) {
case kHelloOrSk:
case kHello:
EXPECT_EQ(win_button_it->icon, kLaptopIcon);
break;
case kSk:
EXPECT_EQ(win_button_it->icon, kUsbSecurityKeyIcon);
break;
default:
NOTREACHED();
}
}
}
#endif // BUILDFLAG(IS_WIN)