blob: 62d6f8a49191c232c5e6d22c3a67c5f4368a3347 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "chrome/browser/webauthn/authenticator_reference.h"
#include "chrome/browser/webauthn/authenticator_transport.h"
#include "device/fido/authenticator_data.h"
#include "device/fido/authenticator_get_assertion_response.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 "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace {
using testing::ElementsAre;
using RequestType = device::FidoRequestType;
const base::flat_set<AuthenticatorTransport> kAllTransports = {
AuthenticatorTransport::kUsbHumanInterfaceDevice,
AuthenticatorTransport::kNearFieldCommunication,
AuthenticatorTransport::kInternal,
AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy,
};
const base::flat_set<AuthenticatorTransport> kAllTransportsWithoutCable = {
AuthenticatorTransport::kUsbHumanInterfaceDevice,
AuthenticatorTransport::kNearFieldCommunication,
AuthenticatorTransport::kInternal,
};
using TransportAvailabilityInfo =
::device::FidoRequestHandlerBase::TransportAvailabilityInfo;
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 {
kHasPlatformCredential,
kHasWinNativeAuthenticator,
kHasCableV1Extension,
kHasCableV2Extension,
kPreferNativeAPI,
};
base::StringPiece TransportAvailabilityParamToString(
TransportAvailabilityParam param) {
switch (param) {
case TransportAvailabilityParam::kHasPlatformCredential:
return "kHasPlatformCredential";
case TransportAvailabilityParam::kHasWinNativeAuthenticator:
return "kHasWinNativeAuthenticator";
case TransportAvailabilityParam::kHasCableV1Extension:
return "kHasCableV1Extension";
case TransportAvailabilityParam::kHasCableV2Extension:
return "kHasCableV2Extension";
case TransportAvailabilityParam::kPreferNativeAPI:
return "kPreferNativeAPI";
}
}
template <typename T, base::StringPiece (*F)(T)>
std::string SetToString(base::flat_set<T> s) {
std::vector<base::StringPiece> names;
std::transform(s.begin(), s.end(), std::back_inserter(names), F);
return base::JoinString(names, ", ");
}
} // namespace
class AuthenticatorRequestDialogModelTest : public ::testing::Test {
public:
using Step = AuthenticatorRequestDialogModel::Step;
AuthenticatorRequestDialogModelTest() = default;
AuthenticatorRequestDialogModelTest(
const AuthenticatorRequestDialogModelTest&) = delete;
AuthenticatorRequestDialogModelTest& operator=(
const AuthenticatorRequestDialogModelTest&) = delete;
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};
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::kCloudAssistedBluetoothLowEnergy;
const auto aoa = AuthenticatorTransport::kAndroidAccessory;
const auto v1 = TransportAvailabilityParam::kHasCableV1Extension;
const auto v2 = TransportAvailabilityParam::kHasCableV2Extension;
const auto has_winapi =
TransportAvailabilityParam::kHasWinNativeAuthenticator;
const auto has_plat = TransportAvailabilityParam::kHasPlatformCredential;
const auto native = TransportAvailabilityParam::kPreferNativeAPI;
using t = AuthenticatorRequestDialogModel::Mechanism::Transport;
using p = AuthenticatorRequestDialogModel::Mechanism::Phone;
const auto winapi =
AuthenticatorRequestDialogModel::Mechanism::WindowsAPI(true);
const auto add = AuthenticatorRequestDialogModel::Mechanism::AddPhone(false);
const auto usb_ui = Step::kUsbInsertAndActivate;
const auto mss = Step::kMechanismSelection;
const auto plat_ui = Step::kNotStarted;
const auto cable_ui = Step::kCableActivate;
const struct {
RequestType request_type;
base::flat_set<AuthenticatorTransport> transports;
base::flat_set<TransportAvailabilityParam> params;
std::vector<std::string> phone_names;
std::vector<AuthenticatorRequestDialogModel::Mechanism::Type>
expected_mechanisms;
Step expected_first_step;
} kTests[] = {
// If there's only a single mechanism, it should activate.
{mc, {usb}, {}, {}, {t(usb)}, usb_ui},
{ga, {usb}, {}, {}, {t(usb)}, usb_ui},
// ... otherwise should the selection sheet.
{mc, {usb, internal}, {}, {}, {t(usb), t(internal)}, mss},
{ga, {usb, internal}, {}, {}, {t(usb), t(internal)}, mss},
// If the platform authenticator has a credential it should activate.
{ga, {usb, internal}, {has_plat}, {}, {t(usb), t(internal)}, plat_ui},
// If the Windows API is available without caBLE, it should activate.
{mc, {}, {has_winapi}, {}, {winapi}, plat_ui},
{ga, {}, {has_winapi}, {}, {winapi}, plat_ui},
// ... even if, somehow, there's another transport.
{mc, {usb}, {has_winapi}, {}, {winapi, t(usb)}, plat_ui},
{ga, {usb}, {has_winapi}, {}, {winapi, t(usb)}, plat_ui},
// A caBLEv1 extension should cause us to go directly to caBLE.
{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.
{ga, {usb, aoa, cable}, {v2}, {}, {t(usb), t(aoa), t(cable)}, cable_ui},
// If there are linked phones then AOA doesn't show up, but the phones do,
// and sorted. The selection sheet should show.
{mc,
{usb, aoa, cable},
{},
{"a", "b"},
{add, t(usb), p("a"), p("b")},
mss},
{ga,
{usb, aoa, cable},
{},
{"a", "b"},
{add, t(usb), p("a"), p("b")},
mss},
// On Windows, if there are linked phones we'll show a selection sheet.
{mc, {cable}, {has_winapi}, {"a"}, {winapi, add, p("a")}, mss},
{ga, {cable}, {has_winapi}, {"a"}, {winapi, add, p("a")}, mss},
// ... unless the `prefer_native_api` flag is set because Chrome
// remembered that the last successful security key operation was via the
// Windows API. In that case we'll still jump directly to the native UI.
{mc,
{cable},
{has_winapi, native},
{"a"},
{winapi, add, p("a")},
plat_ui},
{ga,
{cable},
{has_winapi, native},
{"a"},
{winapi, add, p("a")},
plat_ui},
// Even without `prefer_native_api`, if there aren't any linked phones
// we'll still jump directly to the native UI, at least until we enable
// the "Add phone" option.
{mc, {cable}, {has_winapi}, {}, {winapi}, plat_ui},
{ga, {cable}, {has_winapi}, {}, {winapi}, plat_ui},
};
unsigned test_num = 0;
for (const auto& test : kTests) {
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(test_num++);
TransportAvailabilityInfo transports_info;
transports_info.is_ble_powered = true;
transports_info.request_type = test.request_type;
transports_info.available_transports = test.transports;
transports_info.has_platform_authenticator_credential =
base::Contains(test.params,
TransportAvailabilityParam::kHasPlatformCredential)
? device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential
: device::FidoRequestHandlerBase::RecognizedCredential::
kNoRecognizedCredential;
if (base::Contains(
test.params,
TransportAvailabilityParam::kHasWinNativeAuthenticator)) {
transports_info.has_win_native_api_authenticator = true;
transports_info.win_native_api_authenticator_id = "some_authenticator_id";
}
AuthenticatorRequestDialogModel model(/*web_contents=*/nullptr);
absl::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;
}
if (has_v2_cable_extension.has_value() || !test.phone_names.empty()) {
std::vector<AuthenticatorRequestDialogModel::PairedPhone> phones;
for (const auto& name : test.phone_names) {
std::array<uint8_t, device::kP256X962Length> public_key = {0};
public_key[0] = base::checked_cast<uint8_t>(phones.size());
phones.emplace_back(name, /*contact_id=*/0, public_key);
}
model.set_cable_transport_info(has_v2_cable_extension, std::move(phones),
base::DoNothing(), absl::nullopt);
}
model.StartFlow(
std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/
base::Contains(test.params,
TransportAvailabilityParam::kPreferNativeAPI));
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()) {
continue;
}
model.StartOver();
EXPECT_EQ(Step::kMechanismSelection, model.current_step());
}
}
#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.
for (const bool prefer_native_api : {false, true}) {
SCOPED_TRACE(prefer_native_api);
AuthenticatorRequestDialogModel::TransportAvailabilityInfo tai;
tai.has_win_native_api_authenticator = true;
tai.win_native_api_authenticator_id = "ID";
tai.available_transports.insert(
device::FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy);
AuthenticatorRequestDialogModel model(/*web_contents=*/nullptr);
model.set_cable_transport_info(absl::nullopt, {}, base::DoNothing(),
"fido:/1234");
model.StartFlow(std::move(tai),
/*is_conditional_mediation=*/false, prefer_native_api);
if (prefer_native_api) {
// 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());
}
}
#endif
TEST_F(AuthenticatorRequestDialogModelTest, NoAvailableTransports) {
testing::StrictMock<MockDialogModelObserver> mock_observer;
AuthenticatorRequestDialogModel model(/*web_contents=*/nullptr);
model.AddObserver(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(TransportAvailabilityInfo(),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/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) {
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;
transports_info.available_transports = {
AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy};
transports_info.is_off_the_record_context =
test.profile == Profile::INCOGNITO;
AuthenticatorRequestDialogModel model(/*web_contents=*/nullptr);
std::array<uint8_t, device::kP256X962Length> public_key = {0};
std::vector<AuthenticatorRequestDialogModel::PairedPhone> phones(
{{"phone", /*contact_id=*/0, public_key}});
model.set_cable_transport_info(/*extension_is_v2=*/absl::nullopt,
std::move(phones), base::DoNothing(),
absl::nullopt);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/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(/*web_contents=*/nullptr);
model.AddObserver(&mock_observer);
TransportAvailabilityInfo transports_info;
transports_info.available_transports = kAllTransportsWithoutCable;
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/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::kCloudAssistedBluetoothLowEnergy,
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(/*web_contents=*/nullptr);
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), absl::nullopt);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/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::kCloudAssistedBluetoothLowEnergy,
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(/*web_contents=*/nullptr);
model.AddObserver(&mock_observer);
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), absl::nullopt);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/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::kCloudAssistedBluetoothLowEnergy,
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(/*web_contents=*/nullptr);
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), absl::nullopt);
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/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());
}
}
TEST_F(AuthenticatorRequestDialogModelTest,
RequestCallbackOnlyCalledOncePerAuthenticator) {
::device::FidoRequestHandlerBase::TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kMakeCredential;
transports_info.available_transports = {
AuthenticatorTransport::kInternal,
AuthenticatorTransport::kUsbHumanInterfaceDevice};
int num_called = 0;
AuthenticatorRequestDialogModel model(/*web_contents=*/nullptr);
model.SetRequestCallback(base::BindRepeating(
[](int* i, const std::string& authenticator_id) { ++(*i); },
&num_called));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"authenticator", AuthenticatorTransport::kInternal));
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/false);
EXPECT_EQ(AuthenticatorRequestDialogModel::Step::kMechanismSelection,
model.current_step());
EXPECT_EQ(0, num_called);
// Simulate switching back and forth between transports. The request callback
// should only be invoked once (USB is not dispatched through the UI).
model.StartTransportFlowForTesting(AuthenticatorTransport::kInternal);
EXPECT_TRUE(model.should_dialog_be_closed());
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(1, num_called);
model.StartTransportFlowForTesting(
AuthenticatorTransport::kUsbHumanInterfaceDevice);
EXPECT_EQ(AuthenticatorRequestDialogModel::Step::kUsbInsertAndActivate,
model.current_step());
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_FALSE(model.should_dialog_be_closed());
EXPECT_EQ(1, num_called);
model.StartTransportFlowForTesting(AuthenticatorTransport::kInternal);
EXPECT_TRUE(model.should_dialog_be_closed());
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(1, num_called);
}
TEST_F(AuthenticatorRequestDialogModelTest,
RequestCallbackForWindowsAuthenticatorIsInvokedAutomatically) {
constexpr char kWinAuthenticatorId[] = "some_authenticator_id";
::device::FidoRequestHandlerBase::TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kMakeCredential;
transports_info.available_transports = {};
transports_info.has_win_native_api_authenticator = true;
transports_info.win_native_api_authenticator_id = kWinAuthenticatorId;
std::vector<std::string> dispatched_authenticator_ids;
AuthenticatorRequestDialogModel model(/*web_contents=*/nullptr);
model.SetRequestCallback(base::BindRepeating(
[](std::vector<std::string>* ids, const std::string& authenticator_id) {
ids->push_back(authenticator_id);
},
&dispatched_authenticator_ids));
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/false);
EXPECT_TRUE(model.should_dialog_be_closed());
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_THAT(dispatched_authenticator_ids, ElementsAre(kWinAuthenticatorId));
}
TEST_F(AuthenticatorRequestDialogModelTest,
ConditionalUINoRecognizedCredential) {
AuthenticatorRequestDialogModel model(/*web_contents=*/nullptr);
int preselect_num_called = 0;
model.SetAccountPreselectedCallback(base::BindRepeating(
[](int* i, std::vector<uint8_t> credential_id) {
EXPECT_EQ(credential_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));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"authenticator", AuthenticatorTransport::kInternal));
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,
/*prefer_native_api=*/false);
task_environment_.FastForwardUntilNoTasksRemain();
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(/*web_contents=*/nullptr);
int preselect_num_called = 0;
model.SetAccountPreselectedCallback(base::BindRepeating(
[](int* i, std::vector<uint8_t> credential_id) {
EXPECT_EQ(credential_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));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal", AuthenticatorTransport::kInternal));
TransportAvailabilityInfo transports_info;
transports_info.available_transports = kAllTransports;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
device::DiscoverableCredentialMetadata cred_1(
"rp.com", {0}, device::PublicKeyCredentialUserEntity({1, 2, 3, 4}));
device::DiscoverableCredentialMetadata cred_2(
"rp.com", {1}, device::PublicKeyCredentialUserEntity({5, 6, 7, 8}));
transports_info.recognized_platform_authenticator_credentials = {cred_1,
cred_2};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/true,
/*prefer_native_api=*/false);
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(cred_1.cred_id);
task_environment_.FastForwardUntilNoTasksRemain();
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(/*web_contents=*/nullptr);
model.AddObserver(&mock_observer);
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal", AuthenticatorTransport::kInternal));
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(std::move(TransportAvailabilityInfo()),
/*is_conditional_mediation=*/true,
/*prefer_native_api=*/false);
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);
}
#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(/*web_contents=*/nullptr);
model.AddObserver(&mock_observer);
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal", AuthenticatorTransport::kInternal));
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(std::move(TransportAvailabilityInfo()),
/*is_conditional_mediation=*/true,
/*prefer_native_api=*/false);
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)
class AuthenticatorRequestDialogModelPreselectCredentialTest
: public AuthenticatorRequestDialogModelTest {
protected:
base::test::ScopedFeatureList scoped_feature_list_{
device::kWebAuthnNewDiscoverableCredentialsUi};
};
TEST_F(AuthenticatorRequestDialogModelPreselectCredentialTest,
PreSelectWithEmptyAllowList) {
AuthenticatorRequestDialogModel model(/*web_contents=*/nullptr);
int preselect_num_called = 0;
model.SetAccountPreselectedCallback(base::BindLambdaForTesting(
[&preselect_num_called](std::vector<uint8_t> credential_id) {
EXPECT_EQ(credential_id, std::vector<uint8_t>({0}));
++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));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal-authenticator",
AuthenticatorTransport::kInternal));
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.available_transports = kAllTransports;
transports_info.has_empty_allow_list = true;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
constexpr char kRpId[] = "example.com";
device::DiscoverableCredentialMetadata cred_1(
kRpId, {0}, device::PublicKeyCredentialUserEntity({1, 2, 3, 4}));
device::DiscoverableCredentialMetadata cred_2(
kRpId, {1}, device::PublicKeyCredentialUserEntity({5, 6, 7, 8}));
transports_info.recognized_platform_authenticator_credentials = {cred_1,
cred_2};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false,
/*prefer_native_api=*/false);
EXPECT_EQ(model.current_step(), Step::kPreSelectAccount);
EXPECT_EQ(request_num_called, 0);
// After preselecting an account, the request should be dispatched to the
// platform authenticator.
model.OnAccountPreselected(cred_1.cred_id);
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(preselect_num_called, 1);
EXPECT_EQ(request_num_called, 1);
}