blob: a32b3d1377ebeaca03a1b6aac2f4948374a677f8 [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/chrome_authenticator_request_delegate.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <variant>
#include <vector>
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "chrome/browser/webauthn/chrome_web_authentication_delegate.h"
#include "chrome/browser/webauthn/immediate_request_rate_limiter_factory.h"
#include "chrome/browser/webauthn/passkey_model_factory.h"
#include "chrome/browser/webauthn/password_credential_controller.h"
#include "chrome/browser/webauthn/webauthn_pref_names.h"
#include "chrome/browser/webauthn/webauthn_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/sync/base/user_selectable_type.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "components/sync/test/test_sync_service.h"
#include "components/webauthn/core/browser/immediate_request_rate_limiter.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "components/webauthn/core/browser/test_passkey_model.h"
#include "content/public/browser/authenticator_request_client_delegate.h"
#include "content/public/browser/browser_context.h"
#include "content/public/test/web_contents_tester.h"
#include "crypto/scoped_fake_unexportable_key_provider.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/cable/v2_constants.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/features.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_discovery_factory.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_types.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/permissions/permissions_data.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/credentialmanagement/credential_type_flags.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#if BUILDFLAG(IS_MAC)
#include "chrome/test/base/testing_profile.h"
#endif // BUILDFLAG(IS_MAC)
namespace {
static constexpr char kRpId[] = "example.com";
static constexpr char kOrigin[] = "https://example.com";
constexpr int kRequestPassword =
static_cast<int>(blink::mojom::CredentialTypeFlags::kPassword);
constexpr int kRequestPublicKey =
static_cast<int>(blink::mojom::CredentialTypeFlags::kPublicKey);
using TransportAvailabilityInfo =
device::FidoRequestHandlerBase::TransportAvailabilityInfo;
using UIPresentation =
content::AuthenticatorRequestClientDelegate::UIPresentation;
class Observer : public testing::NiceMock<
ChromeAuthenticatorRequestDelegate::TestObserver> {
public:
MOCK_METHOD(void,
Created,
(ChromeAuthenticatorRequestDelegate * delegate),
(override));
MOCK_METHOD(void,
OnTransportAvailabilityEnumerated,
(ChromeAuthenticatorRequestDelegate * delegate,
TransportAvailabilityInfo* tai),
(override));
MOCK_METHOD(void,
UIShown,
(ChromeAuthenticatorRequestDelegate * delegate),
(override));
MOCK_METHOD(void,
CableV2ExtensionSeen,
(base::span<const uint8_t> server_link_data),
(override));
};
class MockPasswordCredentialController : public PasswordCredentialController {
public:
MockPasswordCredentialController(
content::GlobalRenderFrameHostId render_frame_host_id,
AuthenticatorRequestDialogModel* model)
: PasswordCredentialController(render_frame_host_id, model) {}
MOCK_METHOD(
void,
FetchPasswords,
(const GURL&,
PasswordCredentialController::PasswordCredentialsReceivedCallback),
(override));
MOCK_METHOD(
void,
SetPasswordSelectedCallback,
(content::AuthenticatorRequestClientDelegate::PasswordSelectedCallback),
(override));
};
class MockCableDiscoveryFactory : public device::FidoDiscoveryFactory {
public:
void set_cable_data(
device::FidoRequestType request_type,
std::vector<device::CableDiscoveryData> data,
const std::optional<std::array<uint8_t, device::cablev2::kQRKeySize>>&
qr_generator_key) override {
cable_data = std::move(data);
qr_key = qr_generator_key;
}
std::vector<device::CableDiscoveryData> cable_data;
std::optional<std::array<uint8_t, device::cablev2::kQRKeySize>> qr_key;
};
class ChromeAuthenticatorRequestDelegateTest
: public ChromeRenderViewHostTestHarness {
public:
ChromeAuthenticatorRequestDelegateTest()
: ChromeRenderViewHostTestHarness(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
PasskeyModelFactory::GetInstance()->SetTestingFactoryAndUse(
profile(),
base::BindRepeating(
[](content::BrowserContext*) -> std::unique_ptr<KeyedService> {
return std::make_unique<webauthn::TestPasskeyModel>();
}));
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(&observer_);
ImmediateRequestRateLimiterFactory::GetInstance()->SetTestingFactoryAndUse(
profile(), base::BindRepeating([](content::BrowserContext* context)
-> std::unique_ptr<KeyedService> {
return std::make_unique<webauthn::ImmediateRequestRateLimiter>();
}));
}
void TearDown() override {
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
ChromeRenderViewHostTestHarness::TearDown();
}
protected:
Observer observer_;
};
class TestAuthenticatorModelObserver final
: public AuthenticatorRequestDialogModel::Observer {
public:
explicit TestAuthenticatorModelObserver(
AuthenticatorRequestDialogModel* model)
: model_(model) {
last_step_ = model_->step();
}
~TestAuthenticatorModelObserver() override {
if (model_) {
model_->observers.RemoveObserver(this);
}
}
AuthenticatorRequestDialogModel::Step last_step() { return last_step_; }
// AuthenticatorRequestDialogModel::Observer:
void OnStepTransition() override { last_step_ = model_->step(); }
void OnModelDestroyed(AuthenticatorRequestDialogModel* model) override {
model_ = nullptr;
}
private:
raw_ptr<AuthenticatorRequestDialogModel> model_;
AuthenticatorRequestDialogModel::Step last_step_;
};
TEST_F(ChromeAuthenticatorRequestDelegateTest, CableConfiguration) {
const std::array<uint8_t, 16> eid = {1, 2, 3, 4};
const std::array<uint8_t, 32> prekey = {5, 6, 7, 8};
const device::CableDiscoveryData v1_extension(
device::CableDiscoveryData::Version::V1, eid, eid, prekey);
device::CableDiscoveryData v2_extension;
v2_extension.version = device::CableDiscoveryData::Version::V2;
v2_extension.v2.emplace(std::vector<uint8_t>(prekey.begin(), prekey.end()),
std::vector<uint8_t>());
enum class Result {
kNone,
kV1,
kServerLink,
k3rdParty,
};
#if BUILDFLAG(IS_LINUX)
// On Linux, some configurations aren't supported because of bluez
// limitations. This macro maps the expected result in that case.
#define NONE_ON_LINUX(r) (Result::kNone)
#else
#define NONE_ON_LINUX(r) (r)
#endif
const struct {
const char* origin;
std::vector<device::CableDiscoveryData> extensions;
device::FidoRequestType request_type;
std::optional<device::ResidentKeyRequirement> resident_key_requirement;
Result expected_result;
} kTests[] = {
{
"https://example.com",
{},
device::FidoRequestType::kGetAssertion,
std::nullopt,
Result::k3rdParty,
},
{
// Extensions should be ignored on a 3rd-party site.
"https://example.com",
{v1_extension},
device::FidoRequestType::kGetAssertion,
std::nullopt,
Result::k3rdParty,
},
{
// Extensions should be ignored on a 3rd-party site.
"https://example.com",
{v2_extension},
device::FidoRequestType::kGetAssertion,
std::nullopt,
Result::k3rdParty,
},
{
// a.g.c should still be able to get 3rd-party caBLE
// if it doesn't send an extension in an assertion request.
"https://accounts.google.com",
{},
device::FidoRequestType::kGetAssertion,
std::nullopt,
Result::k3rdParty,
},
{
// ... but not for non-discoverable registration.
"https://accounts.google.com",
{},
device::FidoRequestType::kMakeCredential,
device::ResidentKeyRequirement::kDiscouraged,
Result::kNone,
},
{
// ... but yes for rk=preferred
"https://accounts.google.com",
{},
device::FidoRequestType::kMakeCredential,
device::ResidentKeyRequirement::kPreferred,
Result::k3rdParty,
},
{
// ... or rk=required.
"https://accounts.google.com",
{},
device::FidoRequestType::kMakeCredential,
device::ResidentKeyRequirement::kRequired,
Result::k3rdParty,
},
{
"https://accounts.google.com",
{v1_extension},
device::FidoRequestType::kGetAssertion,
std::nullopt,
NONE_ON_LINUX(Result::kV1),
},
{
"https://accounts.google.com",
{v2_extension},
device::FidoRequestType::kGetAssertion,
std::nullopt,
Result::kServerLink,
},
};
unsigned test_case = 0;
for (const auto& test : kTests) {
SCOPED_TRACE(test_case);
test_case++;
MockCableDiscoveryFactory discovery_factory;
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetRelyingPartyId(kRpId);
delegate.ConfigureDiscoveries(
url::Origin::Create(GURL(test.origin)), test.origin,
content::AuthenticatorRequestClientDelegate::RequestSource::
kWebAuthentication,
test.request_type, test.resident_key_requirement,
device::UserVerificationRequirement::kRequired,
/*user_name=*/std::nullopt, test.extensions,
/*is_enclave_authenticator_available=*/false, &discovery_factory);
switch (test.expected_result) {
case Result::kNone:
EXPECT_FALSE(discovery_factory.qr_key.has_value());
EXPECT_TRUE(discovery_factory.cable_data.empty());
break;
case Result::kV1:
EXPECT_FALSE(discovery_factory.qr_key.has_value());
EXPECT_FALSE(discovery_factory.cable_data.empty());
EXPECT_EQ(delegate.dialog_model()->cable_ui_type,
AuthenticatorRequestDialogModel::CableUIType::CABLE_V1);
break;
case Result::kServerLink:
EXPECT_TRUE(discovery_factory.qr_key.has_value());
EXPECT_FALSE(discovery_factory.cable_data.empty());
EXPECT_EQ(
delegate.dialog_model()->cable_ui_type,
AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_SERVER_LINK);
break;
case Result::k3rdParty:
EXPECT_TRUE(discovery_factory.qr_key.has_value());
EXPECT_TRUE(discovery_factory.cable_data.empty());
EXPECT_EQ(
delegate.dialog_model()->cable_ui_type,
AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_2ND_FACTOR);
break;
}
}
}
TEST_F(ChromeAuthenticatorRequestDelegateTest, NoExtraDiscoveriesWithoutUI) {
for (const bool disable_ui : {false, true}) {
SCOPED_TRACE(disable_ui);
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetRelyingPartyId(kRpId);
if (disable_ui) {
delegate.SetUIPresentation(UIPresentation::kDisabled);
}
MockCableDiscoveryFactory discovery_factory;
delegate.ConfigureDiscoveries(
url::Origin::Create(GURL(kOrigin)), kOrigin,
content::AuthenticatorRequestClientDelegate::RequestSource::
kWebAuthentication,
device::FidoRequestType::kMakeCredential,
device::ResidentKeyRequirement::kPreferred,
device::UserVerificationRequirement::kRequired,
/*user_name=*/std::nullopt, {},
/*is_enclave_authenticator_available=*/false, &discovery_factory);
EXPECT_EQ(discovery_factory.qr_key.has_value(), !disable_ui);
// `discovery_factory.nswindow_` won't be set in any case because it depends
// on the `RenderFrameHost` having a `BrowserWindow`, which it doesn't in
// this context.
}
}
TEST_F(ChromeAuthenticatorRequestDelegateTest, ConditionalUI) {
// The RenderFrame has to be live for the ChromeWebAuthnCredentialsDelegate to
// be created.
content::WebContentsTester::For(web_contents())
->NavigateAndCommit(GURL("https://example.com"));
ChromeWebAuthnCredentialsDelegateFactory::CreateForWebContents(
web_contents());
// Enabling conditional mode should cause the modal dialog to stay hidden at
// the beginning of a request. An omnibar icon might be shown instead.
for (bool conditional_ui : {true, false}) {
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetUIPresentation(conditional_ui ? UIPresentation::kAutofill
: UIPresentation::kModal);
delegate.SetRelyingPartyId(kRpId);
AuthenticatorRequestDialogModel* model = delegate.dialog_model();
TestAuthenticatorModelObserver observer(model);
model->observers.AddObserver(&observer);
EXPECT_EQ(observer.last_step(),
AuthenticatorRequestDialogModel::Step::kNotStarted);
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
delegate.OnTransportAvailabilityEnumerated(std::move(transports_info));
EXPECT_EQ(observer.last_step() ==
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill,
conditional_ui);
}
}
TEST_F(ChromeAuthenticatorRequestDelegateTest, FilterGoogleComPasskeys) {
auto HasCreds = device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential;
auto NoCreds = device::FidoRequestHandlerBase::RecognizedCredential::
kNoRecognizedCredential;
auto UnknownCreds =
device::FidoRequestHandlerBase::RecognizedCredential::kUnknown;
constexpr char kGoogle[] = "google.com";
constexpr char kOtherRpId[] = "example.com";
struct {
std::string rp_id;
device::FidoRequestHandlerBase::RecognizedCredential recognized_credential;
std::vector<std::string> user_ids;
device::FidoRequestHandlerBase::RecognizedCredential
expected_recognized_credential;
std::vector<std::string> expected_user_ids;
} kTests[] = {
{kOtherRpId,
HasCreds,
{"GOOGLE_ACCOUNT:c1", "c2"},
HasCreds,
{"GOOGLE_ACCOUNT:c1", "c2"}},
{kGoogle,
HasCreds,
{"GOOGLE_ACCOUNT:c1", "c2", "AUTOFILL_AUTH:c3"},
HasCreds,
{"GOOGLE_ACCOUNT:c1"}},
{kGoogle, HasCreds, {"c2", "AUTOFILL_AUTH:c3"}, NoCreds, {}},
{kGoogle, UnknownCreds, {}, UnknownCreds, {}},
{kGoogle, HasCreds, {}, HasCreds, {}},
};
for (const auto& test : kTests) {
SCOPED_TRACE(::testing::Message() << "rp_id=" << test.rp_id);
SCOPED_TRACE(::testing::Message()
<< "creds=" << base::JoinString(test.user_ids, ","));
TransportAvailabilityInfo data;
data.has_empty_allow_list = true;
data.request_type = device::FidoRequestType::kGetAssertion;
TransportAvailabilityInfo result;
EXPECT_CALL(observer_, OnTransportAvailabilityEnumerated)
.WillOnce([&result](const auto* _, const auto* new_tai) {
result = std::move(*new_tai);
});
for (const std::string& user_id : test.user_ids) {
data.recognized_credentials.emplace_back(
device::AuthenticatorType::kOther, test.rp_id,
std::vector<uint8_t>{0},
device::PublicKeyCredentialUserEntity(
std::vector<uint8_t>(user_id.begin(), user_id.end())),
/*provider_name=*/std::nullopt);
}
data.has_platform_authenticator_credential = test.recognized_credential;
// Mix in an icloud keychain credential. These should not be filtered or
// affect setting the recognized credentials flag.
data.recognized_credentials.emplace_back(
device::AuthenticatorType::kICloudKeychain, test.rp_id,
std::vector<uint8_t>{0}, device::PublicKeyCredentialUserEntity({1}),
/*provider_name=*/std::nullopt);
data.has_icloud_keychain_credential = device::FidoRequestHandlerBase::
RecognizedCredential::kHasRecognizedCredential;
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetRelyingPartyId(test.rp_id);
delegate.RegisterActionCallbacks(
base::DoNothing(), base::DoNothing(), base::DoNothing(),
base::DoNothing(), base::DoNothing(), base::DoNothing(),
base::DoNothing(), base::DoNothing(), base::DoNothing());
delegate.OnTransportAvailabilityEnumerated(std::move(data));
EXPECT_EQ(result.has_platform_authenticator_credential,
test.expected_recognized_credential);
EXPECT_EQ(result.has_icloud_keychain_credential,
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential);
ASSERT_EQ(result.recognized_credentials.size(),
test.expected_user_ids.size() + 1);
for (size_t i = 0; i < test.expected_user_ids.size(); ++i) {
std::string expected_id = test.expected_user_ids[i];
EXPECT_EQ(result.recognized_credentials[i].user.id,
std::vector<uint8_t>(expected_id.begin(), expected_id.end()));
}
EXPECT_EQ(result.recognized_credentials.back().source,
device::AuthenticatorType::kICloudKeychain);
testing::Mock::VerifyAndClearExpectations(&observer_);
}
}
TEST_F(ChromeAuthenticatorRequestDelegateTest,
FilterGoogleComPasskeysWithNonEmptyAllowList) {
// Regression test for crbug.com/40071851, b/366128135.
constexpr char kGoogleRpId[] = "google.com";
TransportAvailabilityInfo data;
data.has_empty_allow_list = false;
data.request_type = device::FidoRequestType::kGetAssertion;
TransportAvailabilityInfo result;
EXPECT_CALL(observer_, OnTransportAvailabilityEnumerated)
.WillOnce([&result](const auto* _, const auto* new_tai) {
result = std::move(*new_tai);
});
// User ID doesn't start with the `GOOGLE_ACCOUNT:` prefix that distinguishes
// them as suitable for login auth.
std::string user_id = "test user id";
data.recognized_credentials.emplace_back(
device::AuthenticatorType::kOther, kGoogleRpId, std::vector<uint8_t>{0},
device::PublicKeyCredentialUserEntity(
std::vector<uint8_t>(user_id.begin(), user_id.end())),
/*provider_name=*/std::nullopt);
data.has_platform_authenticator_credential = device::FidoRequestHandlerBase::
RecognizedCredential::kHasRecognizedCredential;
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetRelyingPartyId(kGoogleRpId);
delegate.RegisterActionCallbacks(
base::DoNothing(), base::DoNothing(), base::DoNothing(),
base::DoNothing(), base::DoNothing(), base::DoNothing(),
base::DoNothing(), base::DoNothing(), base::DoNothing());
delegate.OnTransportAvailabilityEnumerated(std::move(data));
// Despite lacking the user ID prefix, credentials are not filtered from
// `recognized_credentials` because the request has a non-empty allow list.
// The RecognizedCredential status isn't adjusted either.
ASSERT_EQ(result.recognized_credentials.size(), 1u);
EXPECT_EQ(result.has_platform_authenticator_credential,
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential);
testing::Mock::VerifyAndClearExpectations(&observer_);
}
class EnclaveAuthenticatorRequestDelegateTest
: public ChromeAuthenticatorRequestDelegateTest {
public:
void SetUp() override {
ChromeAuthenticatorRequestDelegateTest::SetUp();
SyncServiceFactory::GetInstance()->SetTestingFactory(
browser_context(),
base::BindRepeating([](content::BrowserContext* context)
-> std::unique_ptr<KeyedService> {
return std::make_unique<syncer::TestSyncService>();
}));
}
};
TEST_F(EnclaveAuthenticatorRequestDelegateTest,
BrowserProvidedPasskeysAvailable) {
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile());
signin::MakePrimaryAccountAvailable(identity_manager, "hikari@example.com",
signin::ConsentLevel::kSignin);
struct {
bool is_syncing_passwords;
bool has_unexportable_keys;
bool expected_passkeys_available;
} kTestCases[] = {
// sync unexp result
{true, true, true},
{false, true, false},
{true, false, false},
};
for (const auto& test : kTestCases) {
SCOPED_TRACE(testing::Message()
<< "is_syncing_passwords=" << test.is_syncing_passwords);
SCOPED_TRACE(testing::Message()
<< "has_unexportable_keys=" << test.has_unexportable_keys);
ChromeWebAuthenticationDelegate delegate;
auto* test_sync_service = static_cast<syncer::TestSyncService*>(
SyncServiceFactory::GetInstance()->GetForProfile(profile()));
test_sync_service->GetUserSettings()->SetSelectedType(
syncer::UserSelectableType::kPasswords, test.is_syncing_passwords);
std::variant<crypto::ScopedNullUnexportableKeyProvider,
crypto::ScopedFakeUnexportableKeyProvider>
unexportable_key_provider;
if (test.has_unexportable_keys) {
unexportable_key_provider
.emplace<crypto::ScopedFakeUnexportableKeyProvider>();
}
base::test::TestFuture<bool> future;
delegate.BrowserProvidedPasskeysAvailable(browser_context(),
future.GetCallback());
EXPECT_TRUE(future.Wait());
EXPECT_EQ(future.Get(), test.expected_passkeys_available);
}
}
// This test is separated from BrowserProvidedPasskeysAvailable because ChromeOS
// does not support clearing the primary account once Chrome is running.
TEST_F(EnclaveAuthenticatorRequestDelegateTest,
BrowserProvidedPasskeysAvailable_NoAccount) {
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile());
auto* test_sync_service = static_cast<syncer::TestSyncService*>(
SyncServiceFactory::GetInstance()->GetForProfile(profile()));
test_sync_service->GetUserSettings()->SetSelectedType(
syncer::UserSelectableType::kPasswords, true);
crypto::ScopedFakeUnexportableKeyProvider unexportable_key_provider;
{
base::test::TestFuture<bool> future;
ChromeWebAuthenticationDelegate delegate;
delegate.BrowserProvidedPasskeysAvailable(browser_context(),
future.GetCallback());
ASSERT_TRUE(future.Wait());
EXPECT_FALSE(future.Get());
}
signin::MakePrimaryAccountAvailable(identity_manager, "hikari@example.com",
signin::ConsentLevel::kSignin);
{
base::test::TestFuture<bool> future;
ChromeWebAuthenticationDelegate delegate;
delegate.BrowserProvidedPasskeysAvailable(browser_context(),
future.GetCallback());
ASSERT_TRUE(future.Wait());
EXPECT_TRUE(future.Get());
}
}
// Regression test for crbug.com/377724726.
// Tests that being signed in is enough to have
// BrowserProvidedPasskeysAvailable() return true. Sync-the-feature should not
// be necessary as long as the user consented to using passwords and passkeys
// from their Google account.
TEST_F(EnclaveAuthenticatorRequestDelegateTest,
BrowserProvidedPasskeysAvailableForSignedInUsers) {
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile());
signin::MakePrimaryAccountAvailable(identity_manager, "hikari@example.com",
signin::ConsentLevel::kSignin);
auto* test_sync_service = static_cast<syncer::TestSyncService*>(
SyncServiceFactory::GetInstance()->GetForProfile(profile()));
// ConsentLevel::kSignin + passwords syncing should be enough.
test_sync_service->SetSignedIn(signin::ConsentLevel::kSignin);
test_sync_service->GetUserSettings()->SetSelectedType(
syncer::UserSelectableType::kPasswords, true);
crypto::ScopedFakeUnexportableKeyProvider unexportable_key_provider;
base::test::TestFuture<bool> future;
ChromeWebAuthenticationDelegate delegate;
delegate.BrowserProvidedPasskeysAvailable(browser_context(),
future.GetCallback());
ASSERT_TRUE(future.Wait());
EXPECT_TRUE(future.Get());
}
#if BUILDFLAG(IS_MAC)
std::string TouchIdMetadataSecret(ChromeWebAuthenticationDelegate& delegate,
content::BrowserContext* browser_context) {
return delegate.GetTouchIdAuthenticatorConfig(browser_context)
->metadata_secret;
}
TEST_F(ChromeAuthenticatorRequestDelegateTest, TouchIdMetadataSecret) {
ChromeWebAuthenticationDelegate delegate;
std::string secret = TouchIdMetadataSecret(delegate, GetBrowserContext());
EXPECT_EQ(secret.size(), 32u);
// The secret should be stable.
EXPECT_EQ(secret, TouchIdMetadataSecret(delegate, GetBrowserContext()));
}
TEST_F(ChromeAuthenticatorRequestDelegateTest,
TouchIdMetadataSecret_EqualForSameProfile) {
// Different delegates on the same BrowserContext (Profile) should return
// the same secret.
ChromeWebAuthenticationDelegate delegate1;
ChromeWebAuthenticationDelegate delegate2;
EXPECT_EQ(TouchIdMetadataSecret(delegate1, GetBrowserContext()),
TouchIdMetadataSecret(delegate2, GetBrowserContext()));
}
TEST_F(ChromeAuthenticatorRequestDelegateTest,
TouchIdMetadataSecret_NotEqualForDifferentProfiles) {
// Different profiles have different secrets.
auto other_browser_context = CreateBrowserContext();
ChromeWebAuthenticationDelegate delegate;
EXPECT_NE(TouchIdMetadataSecret(delegate, GetBrowserContext()),
TouchIdMetadataSecret(delegate, other_browser_context.get()));
// Ensure this second secret is actually valid.
EXPECT_EQ(
32u, TouchIdMetadataSecret(delegate, other_browser_context.get()).size());
}
#endif // BUILDFLAG(IS_MAC)
TEST_F(ChromeAuthenticatorRequestDelegateTest, DiscoverPasswords) {
for (const auto enable_password : {false, true}) {
content::WebContentsTester::For(web_contents())
->NavigateAndCommit(GURL(kOrigin));
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
auto password_controller =
std::make_unique<testing::NiceMock<MockPasswordCredentialController>>(
main_rfh()->GetGlobalId(), delegate.dialog_model());
auto raw_password_controller = password_controller.get();
delegate.SetPasswordControllerForTesting(std::move(password_controller));
delegate.SetUIPresentation(enable_password ? UIPresentation::kModalImmediate
: UIPresentation::kModal);
delegate.SetCredentialTypes((enable_password
? (kRequestPassword | kRequestPublicKey)
: (kRequestPublicKey)));
delegate.SetRelyingPartyId(kRpId);
MockCableDiscoveryFactory discovery_factory;
EXPECT_CALL(*raw_password_controller, FetchPasswords)
.Times(enable_password);
delegate.ConfigureDiscoveries(
url::Origin::Create(GURL(kOrigin)), kOrigin,
content::AuthenticatorRequestClientDelegate::RequestSource::
kWebAuthentication,
device::FidoRequestType::kGetAssertion,
device::ResidentKeyRequirement::kPreferred,
device::UserVerificationRequirement::kRequired,
/*user_name=*/std::nullopt, {},
/*is_enclave_authenticator_available=*/false, &discovery_factory);
}
}
TEST_F(ChromeAuthenticatorRequestDelegateTest,
TryToShowUiNoImmediateCredentials) {
content::WebContentsTester::For(web_contents())
->NavigateAndCommit(GURL(kOrigin));
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
auto password_controller =
std::make_unique<testing::NiceMock<MockPasswordCredentialController>>(
main_rfh()->GetGlobalId(), delegate.dialog_model());
auto raw_password_controller = password_controller.get();
delegate.SetPasswordControllerForTesting(std::move(password_controller));
base::MockCallback<base::OnceClosure> mock_closure;
delegate.RegisterActionCallbacks(
base::DoNothing(), mock_closure.Get(), base::DoNothing(),
base::DoNothing(), base::DoNothing(), base::DoNothing(),
base::DoNothing(), base::DoNothing(), base::DoNothing());
delegate.SetUIPresentation(UIPresentation::kModalImmediate);
delegate.SetCredentialTypes(kRequestPassword | kRequestPublicKey);
delegate.SetRelyingPartyId(kRpId);
MockCableDiscoveryFactory discovery_factory;
PasswordCredentialController::PasswordCredentialsReceivedCallback callback;
EXPECT_CALL(*raw_password_controller, FetchPasswords)
.WillOnce([&callback](auto _, auto receive_callback) {
callback = std::move(receive_callback);
});
delegate.ConfigureDiscoveries(url::Origin::Create(GURL(kOrigin)), kOrigin,
content::AuthenticatorRequestClientDelegate::
RequestSource::kWebAuthentication,
device::FidoRequestType::kGetAssertion,
device::ResidentKeyRequirement::kPreferred,
device::UserVerificationRequirement::kRequired,
/*user_name=*/std::nullopt, {},
/*is_enclave_authenticator_available=*/false,
&discovery_factory);
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
// still waiting for passwords.
EXPECT_CALL(mock_closure, Run).Times(0);
delegate.OnTransportAvailabilityEnumerated(std::move(transports_info));
EXPECT_CALL(mock_closure, Run).Times(1);
std::move(callback).Run({});
}
TEST_F(ChromeAuthenticatorRequestDelegateTest,
TryToShowUiHasImmediateCredentials) {
content::WebContentsTester::For(web_contents())
->NavigateAndCommit(GURL(kOrigin));
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
auto password_controller =
std::make_unique<testing::NiceMock<MockPasswordCredentialController>>(
main_rfh()->GetGlobalId(), delegate.dialog_model());
auto raw_password_controller = password_controller.get();
delegate.SetPasswordControllerForTesting(std::move(password_controller));
base::MockCallback<base::OnceClosure> mock_closure;
delegate.RegisterActionCallbacks(
base::DoNothing(), mock_closure.Get(), base::DoNothing(),
base::DoNothing(), base::DoNothing(), base::DoNothing(),
base::DoNothing(), base::DoNothing(), base::DoNothing());
delegate.SetUIPresentation(UIPresentation::kModalImmediate);
delegate.SetCredentialTypes(kRequestPassword | kRequestPublicKey);
delegate.SetRelyingPartyId(kRpId);
MockCableDiscoveryFactory discovery_factory;
PasswordCredentialController::PasswordCredentialsReceivedCallback callback;
EXPECT_CALL(*raw_password_controller, FetchPasswords)
.WillOnce([&callback](auto _, auto receive_callback) {
callback = std::move(receive_callback);
});
delegate.ConfigureDiscoveries(url::Origin::Create(GURL(kOrigin)), kOrigin,
content::AuthenticatorRequestClientDelegate::
RequestSource::kWebAuthentication,
device::FidoRequestType::kGetAssertion,
device::ResidentKeyRequirement::kPreferred,
device::UserVerificationRequirement::kRequired,
/*user_name=*/std::nullopt, {},
/*is_enclave_authenticator_available=*/false,
&discovery_factory);
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.recognized_credentials = {
device::DiscoverableCredentialMetadata(
device::AuthenticatorType::kEnclave, kRpId, {},
device::PublicKeyCredentialUserEntity(),
/*provider_name=*/std::nullopt)};
// still waiting for passwords.
EXPECT_CALL(mock_closure, Run).Times(0);
EXPECT_CALL(observer_, OnTransportAvailabilityEnumerated).Times(0);
delegate.OnTransportAvailabilityEnumerated(std::move(transports_info));
EXPECT_CALL(mock_closure, Run).Times(0);
EXPECT_CALL(observer_, OnTransportAvailabilityEnumerated).Times(1);
std::move(callback).Run({});
}
TEST_F(ChromeAuthenticatorRequestDelegateTest, ImmediateMediationRateLimit) {
constexpr base::TimeDelta kWindowSize = base::Minutes(1);
constexpr int kMaxRequestsPerWindow = 2;
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
device::kWebAuthnImmediateRequestRateLimit,
{{"max_requests", base::NumberToString(kMaxRequestsPerWindow)},
{"window_seconds", "60"}});
// Navigate to commit the origin.
content::WebContentsTester::For(web_contents())
->NavigateAndCommit(GURL(kOrigin));
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetRelyingPartyId(kRpId);
delegate.SetUIPresentation(UIPresentation::kModalImmediate);
// Register a mock callback for the immediate_not_found case.
// This is called when MaybeHandleImmediateMediation returns true (e.g., rate
// limited).
base::MockCallback<base::OnceClosure> mock_immediate_not_found_callback;
delegate.RegisterActionCallbacks(
/*cancel_callback=*/base::DoNothing(),
mock_immediate_not_found_callback.Get(),
/*start_over_callback=*/base::DoNothing(),
/*account_preselected_callback=*/base::DoNothing(),
/*password_selected_callback=*/base::DoNothing(),
/*request_callback=*/base::DoNothing(),
/*cancel_ui_timeout_callback=*/base::DoNothing(),
/*bluetooth_adapter_power_on_callback=*/base::DoNothing(),
/*bluetooth_query_status_callback=*/base::DoNothing());
TransportAvailabilityInfo transports_info;
transports_info.request_type = device::FidoRequestType::kGetAssertion;
transports_info.recognized_credentials = {
device::DiscoverableCredentialMetadata(
device::AuthenticatorType::kEnclave, kRpId, {},
device::PublicKeyCredentialUserEntity(),
/*provider_name=*/std::nullopt)};
for (int i = 0; i < kMaxRequestsPerWindow; ++i) {
SCOPED_TRACE(testing::Message() << "Request " << i + 1);
EXPECT_CALL(mock_immediate_not_found_callback, Run).Times(0);
// Need to pass a copy as OnTransportAvailabilityEnumerated takes by value.
TransportAvailabilityInfo info_copy = transports_info;
delegate.OnTransportAvailabilityEnumerated(std::move(info_copy));
testing::Mock::VerifyAndClearExpectations(
&mock_immediate_not_found_callback);
}
// The next request should be rate-limited (callback is called).
{
SCOPED_TRACE(testing::Message() << "Request " << kMaxRequestsPerWindow + 1);
EXPECT_CALL(mock_immediate_not_found_callback, Run).Times(1);
TransportAvailabilityInfo info_copy = transports_info;
delegate.OnTransportAvailabilityEnumerated(std::move(info_copy));
testing::Mock::VerifyAndClearExpectations(
&mock_immediate_not_found_callback);
}
// Advance time beyond the window.
task_environment()->FastForwardBy(kWindowSize + base::Seconds(1));
// The next request should be allowed again (callback not called).
{
SCOPED_TRACE(testing::Message() << "Request after time window");
EXPECT_CALL(mock_immediate_not_found_callback, Run).Times(0);
TransportAvailabilityInfo info_copy = transports_info;
delegate.OnTransportAvailabilityEnumerated(std::move(info_copy));
testing::Mock::VerifyAndClearExpectations(
&mock_immediate_not_found_callback);
}
}
} // namespace
#if BUILDFLAG(IS_MAC)
// These test functions are outside of the anonymous namespace so that
// `FRIEND_TEST_ALL_PREFIXES` works to let them test private functions.
class ChromeAuthenticatorRequestDelegatePrivateTest : public testing::Test {
// A `BrowserTaskEnvironment` needs to be in-scope in order to create a
// `TestingProfile`.
content::BrowserTaskEnvironment task_environment_;
};
TEST_F(ChromeAuthenticatorRequestDelegatePrivateTest, DaysSinceDate) {
const base::Time now = base::Time::FromTimeT(1691188997); // 2023-08-04
const struct {
char input[16];
std::optional<int> expected_result;
} kTestCases[] = {
{"", std::nullopt}, //
{"2023-08-", std::nullopt}, //
{"2023-08-04", 0}, //
{"2023-08-03", 1}, //
{"2023-8-3", 1}, //
{"2023-07-04", 31}, //
{"2001-11-23", 7924}, //
};
for (const auto& test : kTestCases) {
SCOPED_TRACE(test.input);
const std::optional<int> result =
ChromeAuthenticatorRequestDelegate::DaysSinceDate(test.input, now);
EXPECT_EQ(result, test.expected_result);
}
}
TEST_F(ChromeAuthenticatorRequestDelegatePrivateTest, GetICloudKeychainPref) {
TestingProfile profile;
// We use a boolean preference as a tristate, so it's worth checking that
// an unset preference is recognised as such.
EXPECT_FALSE(ChromeAuthenticatorRequestDelegate::GetICloudKeychainPref(
profile.GetPrefs())
.has_value());
profile.GetPrefs()->SetBoolean(prefs::kCreatePasskeysInICloudKeychain, true);
EXPECT_EQ(*ChromeAuthenticatorRequestDelegate::GetICloudKeychainPref(
profile.GetPrefs()),
true);
}
TEST_F(ChromeAuthenticatorRequestDelegatePrivateTest,
ShouldCreateInICloudKeychain) {
// Safety check that SPC requests never default to iCloud Keychain.
EXPECT_FALSE(ChromeAuthenticatorRequestDelegate::ShouldCreateInICloudKeychain(
ChromeAuthenticatorRequestDelegate::RequestSource::
kSecurePaymentConfirmation,
/*is_active_profile_authenticator_user=*/false,
/*has_icloud_drive_enabled=*/true, /*request_is_for_google_com=*/true,
/*preference=*/true));
// For the valid request type, the preference should be controlling if set.
for (const bool preference : {false, true}) {
EXPECT_EQ(preference,
ChromeAuthenticatorRequestDelegate::ShouldCreateInICloudKeychain(
ChromeAuthenticatorRequestDelegate::RequestSource::
kWebAuthentication,
/*is_active_profile_authenticator_user=*/false,
/*has_icloud_drive_enabled=*/true,
/*request_is_for_google_com=*/true,
/*preference=*/preference));
// Otherwise the default is controlled by several feature flags. Testing
// them would just be a change detector.
}
}
#endif