| // 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/macros.h" |
| #include "base/optional.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/bind.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/fido_constants.h" |
| #include "device/fido/fido_transport_protocol.h" |
| #include "device/fido/public_key_credential_user_entity.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.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; |
| |
| MOCK_METHOD1(OnModelDestroyed, void(AuthenticatorRequestDialogModel*)); |
| MOCK_METHOD0(OnStepTransition, void()); |
| MOCK_METHOD0(OnCancelRequest, void()); |
| MOCK_METHOD0(OnBluetoothPoweredStateChanged, void()); |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockDialogModelObserver); |
| }; |
| |
| class BluetoothAdapterPowerOnCallbackReceiver { |
| public: |
| BluetoothAdapterPowerOnCallbackReceiver() = default; |
| |
| 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; |
| |
| DISALLOW_COPY_AND_ASSIGN(BluetoothAdapterPowerOnCallbackReceiver); |
| }; |
| |
| 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, |
| }; |
| |
| 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"; |
| } |
| } |
| |
| 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; |
| |
| protected: |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(AuthenticatorRequestDialogModelTest); |
| }; |
| |
| 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; |
| using t = AuthenticatorRequestDialogModel::Mechanism::Transport; |
| using p = AuthenticatorRequestDialogModel::Mechanism::Phone; |
| const auto winapi = |
| AuthenticatorRequestDialogModel::Mechanism::WindowsAPI(true); |
| const auto usb_ui = Step::kUsbInsertAndActivate; |
| const auto tss = Step::kTransportSelection; |
| 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)}, tss}, |
| {ga, {usb, internal}, {}, {}, {t(usb), t(internal)}, tss}, |
| |
| // 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}, {}, {t(usb), winapi}, plat_ui}, |
| {ga, {usb}, {has_winapi}, {}, {t(usb), winapi}, 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}, {}, {"b", "a"}, {t(usb), p("a"), p("b")}, tss}, |
| {ga, {usb, aoa, cable}, {}, {"b", "a"}, {t(usb), p("a"), p("b")}, tss}, |
| |
| // On Windows, if there are linked phones we'll show a selection sheet for |
| // makeCredential. |
| {mc, {cable}, {has_winapi}, {"a"}, {winapi, p("a")}, tss}, |
| // ... but not for getAssertion (currently). |
| {ga, {cable}, {has_winapi}, {"a"}, {winapi, p("a")}, 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_recognized_platform_authenticator_credential = |
| base::Contains(test.params, |
| TransportAvailabilityParam::kHasPlatformCredential); |
| |
| 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(/*relying_party_id=*/"example.com"); |
| |
| base::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(), base::nullopt); |
| } |
| |
| model.StartFlow(std::move(transports_info), |
| /*use_location_bar_bubble=*/false); |
| 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::kTransportSelection, model.current_step()); |
| } |
| } |
| |
| TEST_F(AuthenticatorRequestDialogModelTest, NoAvailableTransports) { |
| testing::StrictMock<MockDialogModelObserver> mock_observer; |
| AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com"); |
| model.AddObserver(&mock_observer); |
| |
| EXPECT_CALL(mock_observer, OnStepTransition()); |
| model.StartFlow(TransportAvailabilityInfo(), |
| /*use_location_bar_bubble=*/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, 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(/*relying_party_id=*/"example.com"); |
| model.AddObserver(&mock_observer); |
| |
| TransportAvailabilityInfo transports_info; |
| transports_info.available_transports = kAllTransportsWithoutCable; |
| |
| EXPECT_CALL(mock_observer, OnStepTransition()); |
| model.StartFlow(std::move(transports_info), |
| /*use_location_bar_bubble=*/false); |
| EXPECT_EQ(Step::kTransportSelection, 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(/*relying_party_id=*/"example.com"); |
| model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback()); |
| model.set_cable_transport_info(true, {}, base::DoNothing(), base::nullopt); |
| model.StartFlow(std::move(transports_info), |
| /*use_location_bar_bubble=*/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(/*relying_party_id=*/"example.com"); |
| model.AddObserver(&mock_observer); |
| model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback()); |
| model.set_cable_transport_info(true, {}, base::DoNothing(), base::nullopt); |
| model.StartFlow(std::move(transports_info), |
| /*use_location_bar_bubble=*/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(/*relying_party_id=*/"example.com"); |
| model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback()); |
| model.set_cable_transport_info(true, {}, base::DoNothing(), base::nullopt); |
| model.StartFlow(std::move(transports_info), |
| /*use_location_bar_bubble=*/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(/*relying_party_id=*/"example.com"); |
| 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), |
| /*use_location_bar_bubble=*/false); |
| EXPECT_EQ(AuthenticatorRequestDialogModel::Step::kTransportSelection, |
| 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_hidden()); |
| task_environment_.FastForwardUntilNoTasksRemain(); |
| EXPECT_EQ(1, num_called); |
| model.StartTransportFlowForTesting( |
| AuthenticatorTransport::kUsbHumanInterfaceDevice); |
| EXPECT_EQ(AuthenticatorRequestDialogModel::Step::kUsbInsertAndActivate, |
| model.current_step()); |
| task_environment_.FastForwardUntilNoTasksRemain(); |
| EXPECT_EQ(1, num_called); |
| model.StartTransportFlowForTesting(AuthenticatorTransport::kInternal); |
| EXPECT_TRUE(model.should_dialog_be_hidden()); |
| 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(/*relying_party_id=*/"example.com"); |
| 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), |
| /*use_location_bar_bubble=*/false); |
| |
| EXPECT_TRUE(model.should_dialog_be_hidden()); |
| task_environment_.FastForwardUntilNoTasksRemain(); |
| EXPECT_THAT(dispatched_authenticator_ids, ElementsAre(kWinAuthenticatorId)); |
| } |
| |
| TEST_F(AuthenticatorRequestDialogModelTest, |
| ConditionalUINoRecognizedCredential) { |
| AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com"); |
| |
| int num_called = 0; |
| model.SetRequestCallback(base::BindRepeating( |
| [](int* i, const std::string& authenticator_id) { ++(*i); }, |
| &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_recognized_platform_authenticator_credential = true; |
| model.StartFlow(std::move(transports_info), |
| /*use_location_bar_bubble=*/true); |
| EXPECT_EQ(model.current_step(), Step::kLocationBarBubble); |
| EXPECT_TRUE(model.should_dialog_be_hidden()); |
| EXPECT_EQ(num_called, 0); |
| } |
| |
| TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUIRecognizedCredential) { |
| AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com"); |
| |
| int num_called = 0; |
| model.SetRequestCallback(base::BindRepeating( |
| [](int* i, const std::string& authenticator_id) { |
| EXPECT_EQ(authenticator_id, "internal"); |
| ++(*i); |
| }, |
| &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_recognized_platform_authenticator_credential = true; |
| device::PublicKeyCredentialUserEntity user_1({1, 2, 3, 4}); |
| device::PublicKeyCredentialUserEntity user_2({5, 6, 7, 8}); |
| transports_info.recognized_platform_authenticator_credentials = {user_1, |
| user_2}; |
| model.StartFlow(std::move(transports_info), |
| /*is_location_bar_bubble_ui==*/true); |
| EXPECT_EQ(model.current_step(), Step::kLocationBarBubble); |
| EXPECT_TRUE(model.should_dialog_be_hidden()); |
| EXPECT_EQ(num_called, 0); |
| |
| // After selecting an account, the request should be dispatched to the |
| // platform authenticator. |
| model.OnAccountSelected(0); |
| task_environment_.FastForwardUntilNoTasksRemain(); |
| EXPECT_EQ(num_called, 1); |
| |
| static const uint8_t kAppParam[32] = {0}; |
| static const uint8_t kSignatureCounter[4] = {0}; |
| device::AuthenticatorData auth_data(kAppParam, /*flags=*/0, kSignatureCounter, |
| base::nullopt); |
| device::AuthenticatorGetAssertionResponse response_1( |
| device::AuthenticatorData(kAppParam, /*flags=*/0, kSignatureCounter, |
| base::nullopt), |
| /*signature=*/{1}); |
| response_1.user_entity = user_1; |
| device::AuthenticatorGetAssertionResponse response_2( |
| device::AuthenticatorData(kAppParam, /*flags=*/0, kSignatureCounter, |
| base::nullopt), |
| /*signature=*/{2}); |
| response_2.user_entity = user_2; |
| |
| uint8_t selected_id = -1; |
| std::vector<device::AuthenticatorGetAssertionResponse> responses; |
| responses.push_back(std::move(response_1)); |
| responses.push_back(std::move(response_2)); |
| model.SelectAccount( |
| std::move(responses), |
| base::BindLambdaForTesting( |
| [&](device::AuthenticatorGetAssertionResponse selected) { |
| selected_id = selected.signature[0]; |
| })); |
| EXPECT_EQ(selected_id, 1); |
| } |