Commit 93573367 authored by Balazs Engedy's avatar Balazs Engedy Committed by Commit Bot

Implement WebAuthn transport auto selection.

This CL also delays creating the AuthenticatorRequestDialogModel and
showing the dialog until after ChromeAuthenticatorRequestDelegate's
OnTransportAvailabilityEnumerated is called, plus renames Step::kInitial
to ::kWelcome.

Bug: 847985
Change-Id: I3b46781ccea0309e1efc7c3fec45cbe4c43e9017
Reviewed-on: https://chromium-review.googlesource.com/1175120
Commit-Queue: Balazs Engedy <engedy@chromium.org>
Reviewed-by: default avatarJun Choi <hongjunchoi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#584041}
parent 1af355b7
......@@ -98,7 +98,11 @@ class AuthenticatorDialogViewTest : public DialogBrowserTest {
void ShowUi(const std::string& name) override {
content::WebContents* const web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
auto dialog_model = std::make_unique<AuthenticatorRequestDialogModel>();
dialog_model->StartFlow(
::device::FidoRequestHandlerBase::TransportAvailabilityInfo(),
base::nullopt);
auto dialog = std::make_unique<AuthenticatorRequestDialogView>(
web_contents, std::move(dialog_model));
......
......@@ -36,7 +36,7 @@ std::unique_ptr<AuthenticatorRequestSheetView> CreateSheetViewForCurrentStepOf(
switch (dialog_model->current_step()) {
case Step::kWelcomeScreen:
sheet_view = std::make_unique<AuthenticatorRequestSheetView>(
std::make_unique<AuthenticatorInitialSheetModel>(dialog_model));
std::make_unique<AuthenticatorWelcomeSheetModel>(dialog_model));
break;
case Step::kTransportSelection:
sheet_view = std::make_unique<AuthenticatorTransportSelectorSheetView>(
......@@ -92,6 +92,7 @@ std::unique_ptr<AuthenticatorRequestSheetView> CreateSheetViewForCurrentStepOf(
sheet_view = std::make_unique<AuthenticatorRequestSheetView>(
std::make_unique<AuthenticatorPaaskSheetModel>(dialog_model));
break;
case Step::kNotStarted:
case Step::kCompleted:
case Step::kBlePowerOnAutomatic:
sheet_view = std::make_unique<AuthenticatorRequestSheetView>(
......
......@@ -19,6 +19,9 @@ class AuthenticatorDialogTest : public DialogBrowserTest {
// DialogBrowserTest:
void ShowUi(const std::string& name) override {
auto model = std::make_unique<AuthenticatorRequestDialogModel>();
model->StartFlow(
::device::FidoRequestHandlerBase::TransportAvailabilityInfo(),
base::nullopt);
// The dialog should immediately close as soon as it is displayed.
if (name == "completed") {
......
......@@ -80,13 +80,13 @@ void AuthenticatorSheetModelBase::OnModelDestroyed() {
dialog_model_ = nullptr;
}
// AuthenticatorInitialSheetModel ---------------------------------------------
// AuthenticatorWelcomeSheetModel ---------------------------------------------
gfx::ImageSkia* AuthenticatorInitialSheetModel::GetStepIllustration() const {
gfx::ImageSkia* AuthenticatorWelcomeSheetModel::GetStepIllustration() const {
return GetImage(IDR_WEBAUTHN_ILLUSTRATION_WELCOME_1X);
}
base::string16 AuthenticatorInitialSheetModel::GetStepTitle() const {
base::string16 AuthenticatorWelcomeSheetModel::GetStepTitle() const {
// TODO(hongjunchoi): Insert actual domain name from model to
// |application_name|.
base::string16 application_name = base::UTF8ToUTF16("example.com");
......@@ -94,27 +94,25 @@ base::string16 AuthenticatorInitialSheetModel::GetStepTitle() const {
application_name);
}
base::string16 AuthenticatorInitialSheetModel::GetStepDescription() const {
base::string16 AuthenticatorWelcomeSheetModel::GetStepDescription() const {
return l10n_util::GetStringUTF16(IDS_WEBAUTHN_WELCOME_SCREEN_DESCRIPTION);
}
bool AuthenticatorInitialSheetModel::IsAcceptButtonVisible() const {
bool AuthenticatorWelcomeSheetModel::IsAcceptButtonVisible() const {
return true;
}
bool AuthenticatorInitialSheetModel::IsAcceptButtonEnabled() const {
bool AuthenticatorWelcomeSheetModel::IsAcceptButtonEnabled() const {
return true;
}
base::string16 AuthenticatorInitialSheetModel::GetAcceptButtonLabel() const {
base::string16 AuthenticatorWelcomeSheetModel::GetAcceptButtonLabel() const {
return l10n_util::GetStringUTF16(IDS_WEBAUTHN_WELCOME_SCREEN_NEXT);
}
void AuthenticatorInitialSheetModel::OnAccept() {
// TODO(hongjunchoi): Check whether Bluetooth adapter is enabled and if it is,
// set current step to |kTransportSelection|.
dialog_model()->SetCurrentStep(
AuthenticatorRequestDialogModel::Step::kUsbInsertAndActivate);
void AuthenticatorWelcomeSheetModel::OnAccept() {
dialog_model()
->StartGuidedFlowForMostLikelyTransportOrShowTransportSelection();
}
// AuthenticatorTransportSelectorSheetModel -----------------------------------
......
......@@ -50,7 +50,7 @@ class AuthenticatorSheetModelBase
};
// The initial sheet shown when the UX flow starts.
class AuthenticatorInitialSheetModel : public AuthenticatorSheetModelBase {
class AuthenticatorWelcomeSheetModel : public AuthenticatorSheetModelBase {
public:
using AuthenticatorSheetModelBase::AuthenticatorSheetModelBase;
......
......@@ -4,6 +4,61 @@
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "base/stl_util.h"
namespace {
// Attempts to auto-select the most likely transport that will be used to
// service this request, or returns base::nullopt if unsure.
base::Optional<device::FidoTransportProtocol> SelectMostLikelyTransport(
device::FidoRequestHandlerBase::TransportAvailabilityInfo
transport_availability,
base::Optional<device::FidoTransportProtocol> last_used_transport) {
// If the KeyChain contains one of the |allowedCredentials|, then we are
// certain we can service the request using Touch ID, as long as allowed by
// the RP, so go for the certain choice here.
if (transport_availability.has_recognized_mac_touch_id_credential &&
base::ContainsKey(transport_availability.available_transports,
device::FidoTransportProtocol::kInternal)) {
return device::FidoTransportProtocol::kInternal;
}
// If the |last_used_transport| is available, use that.
if (last_used_transport &&
base::ContainsKey(transport_availability.available_transports,
*last_used_transport)) {
return *last_used_transport;
}
// If there is only one transport available we can use, select that, instead
// of showing a transport selection screen with only a single transport.
if (transport_availability.available_transports.size() == 1) {
return *transport_availability.available_transports.begin();
}
return base::nullopt;
}
AuthenticatorTransport ToAuthenticatorTransport(
device::FidoTransportProtocol transport) {
switch (transport) {
case device::FidoTransportProtocol::kUsbHumanInterfaceDevice:
return AuthenticatorTransport::kUsb;
case device::FidoTransportProtocol::kNearFieldCommunication:
return AuthenticatorTransport::kNearFieldCommunication;
case device::FidoTransportProtocol::kBluetoothLowEnergy:
return AuthenticatorTransport::kBluetoothLowEnergy;
case device::FidoTransportProtocol::kInternal:
return AuthenticatorTransport::kInternal;
case device::FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy:
return AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy;
}
NOTREACHED();
return AuthenticatorTransport::kUsb;
}
} // namespace
// AuthenticatorRequestDialogModel::AuthenticatorReference --------------------
AuthenticatorRequestDialogModel::AuthenticatorReference::AuthenticatorReference(
......@@ -20,7 +75,7 @@ AuthenticatorRequestDialogModel::AuthenticatorReference::
// AuthenticatorRequestDialogModel --------------------------------------------
AuthenticatorRequestDialogModel::AuthenticatorRequestDialogModel() = default;
AuthenticatorRequestDialogModel::AuthenticatorRequestDialogModel() {}
AuthenticatorRequestDialogModel::~AuthenticatorRequestDialogModel() {
for (auto& observer : observers_)
observer.OnModelDestroyed();
......@@ -32,19 +87,57 @@ void AuthenticatorRequestDialogModel::SetCurrentStep(Step step) {
observer.OnStepTransition();
}
void AuthenticatorRequestDialogModel::StartFlow(
TransportAvailabilityInfo transport_availability,
base::Optional<device::FidoTransportProtocol> last_used_transport) {
DCHECK_EQ(current_step(), Step::kNotStarted);
transport_availability_ = std::move(transport_availability);
last_used_transport_ = last_used_transport;
for (const auto transport : transport_availability.available_transports) {
transport_list_model_.AppendTransport(ToAuthenticatorTransport(transport));
}
if (last_used_transport) {
StartGuidedFlowForMostLikelyTransportOrShowTransportSelection();
} else {
SetCurrentStep(Step::kWelcomeScreen);
}
}
void AuthenticatorRequestDialogModel::
StartGuidedFlowForMostLikelyTransportOrShowTransportSelection() {
DCHECK(current_step() == Step::kWelcomeScreen ||
current_step() == Step::kNotStarted);
auto most_likely_transport =
SelectMostLikelyTransport(transport_availability_, last_used_transport_);
if (most_likely_transport) {
StartGuidedFlowForTransport(
ToAuthenticatorTransport(*most_likely_transport));
} else {
// TODO(engedy): Show error screen if no transport available at all.
SetCurrentStep(Step::kTransportSelection);
}
}
void AuthenticatorRequestDialogModel::StartGuidedFlowForTransport(
AuthenticatorTransport transport) {
DCHECK_EQ(current_step(), Step::kTransportSelection);
DCHECK(current_step() == Step::kTransportSelection ||
current_step() == Step::kWelcomeScreen ||
current_step() == Step::kNotStarted);
switch (transport) {
case AuthenticatorTransport::kUsb:
SetCurrentStep(Step::kUsbInsertAndActivate);
break;
case AuthenticatorTransport::kBluetoothLowEnergy:
SetCurrentStep(Step::kBlePowerOnManual);
case AuthenticatorTransport::kNearFieldCommunication:
SetCurrentStep(Step::kTransportSelection);
break;
case AuthenticatorTransport::kInternal:
SetCurrentStep(Step::kTouchId);
break;
case AuthenticatorTransport::kBluetoothLowEnergy:
SetCurrentStep(Step::kBleActivate);
break;
case AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy:
SetCurrentStep(Step::kCableActivate);
break;
......@@ -111,3 +204,8 @@ void AuthenticatorRequestDialogModel::OnRequestComplete() {
void AuthenticatorRequestDialogModel::OnRequestTimeout() {
SetCurrentStep(Step::kErrorTimedOut);
}
void AuthenticatorRequestDialogModel::OnBluetoothPoweredStateChanged(
bool powered) {
transport_availability_.is_ble_powered = powered;
}
......@@ -9,8 +9,10 @@
#include <vector>
#include "base/observer_list.h"
#include "base/optional.h"
#include "base/strings/string_piece.h"
#include "chrome/browser/webauthn/transport_list_model.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_transport_protocol.h"
// Encapsulates the model behind the Web Authentication request dialog's UX
......@@ -22,8 +24,14 @@
// order, to complete the authentication flow.
class AuthenticatorRequestDialogModel {
public:
using TransportAvailabilityInfo =
device::FidoRequestHandlerBase::TransportAvailabilityInfo;
// Defines the potential steps of the Web Authentication API request UX flow.
enum class Step {
// The UX flow has not started yet, the dialog should still be hidden.
kNotStarted,
kWelcomeScreen,
kTransportSelection,
kErrorTimedOut,
......@@ -90,10 +98,27 @@ class AuthenticatorRequestDialogModel {
TransportListModel* transport_list_model() { return &transport_list_model_; }
// Starts the UX flow, by either showing the welcome screen, the transport
// selection screen, or the guided flow for them most likely transport.
//
// Valid action when at step: kNotStarted.
void StartFlow(
TransportAvailabilityInfo transport_availability,
base::Optional<device::FidoTransportProtocol> last_used_transport);
// Starts the UX flow. Tries to figure out the most likely transport to be
// used, and starts the guided flow for that transport; or shows the manual
// transport selection screen if the transport could not be uniquely
// identified.
//
// Valid action when at step: kNotStarted, kWelcomeScreen.
void StartGuidedFlowForMostLikelyTransportOrShowTransportSelection();
// Requests that the step-by-step wizard flow commence, guiding the user
// through using the Secutity Key with the given |transport|.
//
// Valid action when at step: kTransportSelection.
// Valid action when at step: kNotStarted, kWelcomeScreen,
// kTransportSelection.
void StartGuidedFlowForTransport(AuthenticatorTransport transport);
// Tries if the BLE adapter is now powered -- the user claims they turned it
......@@ -155,17 +180,24 @@ class AuthenticatorRequestDialogModel {
// To be called when Web Authentication request times-out.
void OnRequestTimeout();
// To be called when the Bluetooth adapter powered state changes.
void OnBluetoothPoweredStateChanged(bool powered);
std::vector<AuthenticatorReference>& saved_authenticators() {
return saved_authenticators_;
}
private:
// The current step of the request UX flow that is currently shown.
Step current_step_ = Step::kWelcomeScreen;
Step current_step_ = Step::kNotStarted;
TransportListModel transport_list_model_;
base::ObserverList<Observer> observers_;
// These fields are only filled out when the UX flow is started.
TransportAvailabilityInfo transport_availability_;
base::Optional<device::FidoTransportProtocol> last_used_transport_;
// Transport type and id of Mac TouchId and BLE authenticators are cached so
// that the WebAuthN request for the corresponding authenticators can be
// dispatched lazily after the user interacts with the UI element.
......
// 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 "base/containers/flat_set.h"
#include "base/macros.h"
#include "base/optional.h"
#include "chrome/browser/webauthn/transport_list_model.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
class AuthenticatorRequestDialogModelTest : public ::testing::Test {
public:
AuthenticatorRequestDialogModelTest() {}
~AuthenticatorRequestDialogModelTest() override {}
private:
DISALLOW_COPY_AND_ASSIGN(AuthenticatorRequestDialogModelTest);
};
TEST_F(AuthenticatorRequestDialogModelTest, TransportAutoSelection) {
using FidoTransportProtocol = ::device::FidoTransportProtocol;
using Step = AuthenticatorRequestDialogModel::Step;
const base::flat_set<FidoTransportProtocol> kAllTransports = {
FidoTransportProtocol::kUsbHumanInterfaceDevice,
FidoTransportProtocol::kNearFieldCommunication,
FidoTransportProtocol::kBluetoothLowEnergy,
FidoTransportProtocol::kInternal,
FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy,
};
const struct {
base::flat_set<FidoTransportProtocol> available_transports;
base::Optional<FidoTransportProtocol> last_used_transport;
bool has_touch_id_credential;
Step expected_first_step;
} kTestCases[] = {
// Only a single transport is available.
{{FidoTransportProtocol::kUsbHumanInterfaceDevice},
base::nullopt,
false,
Step::kUsbInsertAndActivate},
{{FidoTransportProtocol::kNearFieldCommunication},
base::nullopt,
false,
Step::kTransportSelection},
{{FidoTransportProtocol::kBluetoothLowEnergy},
base::nullopt,
false,
Step::kBleActivate},
{{FidoTransportProtocol::kInternal},
base::nullopt,
false,
Step::kTouchId},
{{FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy},
base::nullopt,
false,
Step::kCableActivate},
// The last used transport is available.
{kAllTransports, FidoTransportProtocol::kUsbHumanInterfaceDevice, false,
Step::kUsbInsertAndActivate},
// The last used transport is not available.
{{FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy,
FidoTransportProtocol::kUsbHumanInterfaceDevice},
FidoTransportProtocol::kNearFieldCommunication,
false,
Step::kTransportSelection},
// The KeyChain contains an allowed Touch ID credential.
{kAllTransports, FidoTransportProtocol::kUsbHumanInterfaceDevice, true,
Step::kTouchId},
// The KeyChain contains an allowed Touch ID credential, but Touch ID is
// not enabled by the relying party.
{{FidoTransportProtocol::kUsbHumanInterfaceDevice},
base::nullopt,
true,
Step::kUsbInsertAndActivate},
{{FidoTransportProtocol::kUsbHumanInterfaceDevice,
FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy},
base::nullopt,
true,
Step::kTransportSelection},
// No transports available.
{{},
FidoTransportProtocol::kNearFieldCommunication,
false,
// TODO: Update this when the error screen is implemented.
Step::kTransportSelection},
};
for (const auto& test_case : kTestCases) {
::device::FidoRequestHandlerBase::TransportAvailabilityInfo transports_info;
transports_info.available_transports = test_case.available_transports;
transports_info.has_recognized_mac_touch_id_credential =
test_case.has_touch_id_credential;
AuthenticatorRequestDialogModel model;
model.StartFlow(std::move(transports_info), test_case.last_used_transport);
// Expect and advance through the welcome screen if this is the first time
// the user goes through the flow (there is no |last_used_transport|.
if (!test_case.last_used_transport) {
ASSERT_EQ(Step::kWelcomeScreen, model.current_step());
model.StartGuidedFlowForMostLikelyTransportOrShowTransportSelection();
}
EXPECT_EQ(test_case.expected_first_step, model.current_step());
}
}
TEST_F(AuthenticatorRequestDialogModelTest, TransportList) {
using FidoTransportProtocol = ::device::FidoTransportProtocol;
::device::FidoRequestHandlerBase::TransportAvailabilityInfo transports_info_1;
transports_info_1.available_transports = {
FidoTransportProtocol::kUsbHumanInterfaceDevice,
FidoTransportProtocol::kNearFieldCommunication,
FidoTransportProtocol::kBluetoothLowEnergy,
FidoTransportProtocol::kInternal,
FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy,
};
AuthenticatorRequestDialogModel model;
model.StartFlow(std::move(transports_info_1), base::nullopt);
EXPECT_THAT(model.transport_list_model()->transports(),
::testing::UnorderedElementsAre(
AuthenticatorTransport::kUsb,
AuthenticatorTransport::kNearFieldCommunication,
AuthenticatorTransport::kBluetoothLowEnergy,
AuthenticatorTransport::kInternal,
AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy));
}
......@@ -70,26 +70,6 @@ bool ShouldDispatchRequestImmediately(
return true;
}
#if !defined(OS_ANDROID)
void SetInitialUiModelBasedOnPreviouslyUsedTransport(
AuthenticatorRequestDialogModel* model,
base::Optional<device::FidoTransportProtocol> previous_transport) {
if (!previous_transport)
return;
// TODO(hongjunchoi): Add UI component for defaulting to BLE, Cable and Mac
// TouchID transports.
switch (*previous_transport) {
case device::FidoTransportProtocol::kUsbHumanInterfaceDevice:
model->SetCurrentStep(
AuthenticatorRequestDialogModel::Step::kUsbInsertAndActivate);
break;
default:
return;
}
}
#endif
} // namespace
#if defined(OS_MACOSX)
......@@ -154,6 +134,11 @@ void ChromeAuthenticatorRequestDelegate::RegisterActionCallbacks(
device::FidoRequestHandlerBase::RequestCallback request_callback) {
request_callback_ = request_callback;
cancel_callback_ = std::move(cancel_callback);
transient_dialog_model_holder_ =
std::make_unique<AuthenticatorRequestDialogModel>();
weak_dialog_model_ = transient_dialog_model_holder_.get();
weak_dialog_model_->AddObserver(this);
}
bool ChromeAuthenticatorRequestDelegate::ShouldPermitIndividualAttestation(
......@@ -267,10 +252,10 @@ void ChromeAuthenticatorRequestDelegate::OnTransportAvailabilityEnumerated(
if (!IsWebAuthnUiEnabled())
return;
weak_dialog_model_ = transient_dialog_model_holder_.get();
SetInitialUiModelBasedOnPreviouslyUsedTransport(weak_dialog_model_,
GetLastTransportUsed());
weak_dialog_model_->AddObserver(this);
DCHECK(weak_dialog_model_);
weak_dialog_model_->StartFlow(std::move(data), GetLastTransportUsed());
DCHECK(transient_dialog_model_holder_);
ShowAuthenticatorRequestDialog(
content::WebContents::FromRenderFrameHost(render_frame_host()),
std::move(transient_dialog_model_holder_));
......@@ -286,7 +271,9 @@ void ChromeAuthenticatorRequestDelegate::FidoAuthenticatorAdded(
if (!IsWebAuthnUiEnabled())
return;
DCHECK(weak_dialog_model_);
if (!weak_dialog_model_)
return;
weak_dialog_model_->saved_authenticators().emplace_back(
authenticator.GetId(), authenticator.AuthenticatorTransport());
}
......@@ -296,7 +283,9 @@ void ChromeAuthenticatorRequestDelegate::FidoAuthenticatorRemoved(
if (!IsWebAuthnUiEnabled())
return;
DCHECK(weak_dialog_model_);
if (!weak_dialog_model_)
return;
auto& saved_authenticators = weak_dialog_model_->saved_authenticators();
saved_authenticators.erase(
std::remove_if(saved_authenticators.begin(), saved_authenticators.end(),
......
......@@ -3197,6 +3197,7 @@ test("unit_tests") {
"../browser/usb/usb_chooser_controller_unittest.cc",
"../browser/usb/web_usb_detector_unittest.cc",
"../browser/usb/web_usb_service_impl_unittest.cc",
"../browser/webauthn/authenticator_request_dialog_model_unittest.cc",
# The importer code is not used on Android.
"../common/importer/firefox_importer_utils_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment