blob: 39acfcbb43a30c3c3647da8303002a1ebed4a51c [file] [log] [blame]
// Copyright 2015 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 <initializer_list>
#include <iterator>
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/login_screen_test_api.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/files/file_util.h"
#include "base/guid.h"
#include "base/json/json_writer.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/ash/login/helper.h"
#include "chrome/browser/ash/login/lock/screen_locker_tester.h"
#include "chrome/browser/ash/login/saml/lockscreen_reauth_dialog_test_helper.h"
#include "chrome/browser/ash/login/signin_partition_manager.h"
#include "chrome/browser/ash/login/test/device_state_mixin.h"
#include "chrome/browser/ash/login/test/embedded_policy_test_server_mixin.h"
#include "chrome/browser/ash/login/test/fake_gaia_mixin.h"
#include "chrome/browser/ash/login/test/js_checker.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/test/oobe_base_test.h"
#include "chrome/browser/ash/login/test/oobe_screen_exit_waiter.h"
#include "chrome/browser/ash/login/test/oobe_screen_waiter.h"
#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
#include "chrome/browser/ash/login/test/user_policy_mixin.h"
#include "chrome/browser/ash/login/test/webview_content_extractor.h"
#include "chrome/browser/ash/login/ui/login_display_host.h"
#include "chrome/browser/ash/login/wizard_controller.h"
#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
#include "chrome/browser/ash/policy/core/device_policy_builder.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ash/scoped_test_system_nss_key_slot_mixin.h"
#include "chrome/browser/ash/settings/scoped_testing_cros_settings.h"
#include "chrome/browser/ash/settings/stub_cros_settings_provider.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ssl/ssl_client_certificate_selector.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/login/login_handler.h"
#include "chrome/browser/ui/webui/chromeos/login/error_screen_handler.h"
#include "chrome/browser/ui/webui/chromeos/login/eula_screen_handler.h"
#include "chrome/browser/ui/webui/chromeos/login/gaia_screen_handler.h"
#include "chrome/browser/ui/webui/chromeos/login/user_creation_screen_handler.h"
#include "chrome/browser/ui/webui/signin/signin_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/dbus/session_manager/fake_session_manager_client.h"
#include "chromeos/dbus/tpm_manager/fake_tpm_manager_client.h"
#include "chromeos/dbus/tpm_manager/tpm_manager_client.h"
#include "chromeos/tpm/tpm_token_loader.h"
#include "components/account_id/account_id.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/guest_view/browser/guest_view_manager.h"
#include "components/onc/onc_constants.h"
#include "components/onc/onc_pref_names.h"
#include "components/policy/core/common/cloud/device_management_service.h"
#include "components/policy/core/common/policy_service.h"
#include "components/policy/core/common/policy_switches.h"
#include "components/policy/policy_constants.h"
#include "components/policy/proto/chrome_device_policy.pb.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/sync/driver/sync_driver_switches.h"
#include "components/sync/driver/sync_service_impl.h"
#include "components/sync/driver/trusted_vault_client.h"
#include "components/sync/trusted_vault/securebox.h"
#include "components/sync/trusted_vault/standalone_trusted_vault_client.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/client_certificate_delegate.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "crypto/nss_util.h"
#include "crypto/nss_util_internal.h"
#include "crypto/scoped_test_nss_db.h"
#include "crypto/scoped_test_system_nss_key_slot.h"
#include "google_apis/gaia/gaia_urls.h"
#include "media/base/media_switches.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/net_errors.h"
#include "net/cert/x509_certificate.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_access_result.h"
#include "net/cookies/cookie_util.h"
#include "net/http/http_status_code.h"
#include "net/ssl/client_cert_identity.h"
#include "net/ssl/ssl_cert_request_info.h"
#include "net/ssl/ssl_info.h"
#include "net/ssl/ssl_server_config.h"
#include "net/test/cert_test_util.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/spawned_test_server/spawned_test_server.h"
#include "net/test/test_data_directory.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace ash {
namespace {
namespace em = ::enterprise_management;
constexpr char kTestGuid[] = "cccccccc-cccc-4ccc-0ccc-ccccccccccc1";
constexpr char kTestCookieName[] = "TestCookie";
constexpr char kTestCookieValue[] = "present";
constexpr char kTestCookieHost[] = "host1.com";
constexpr char kClientCert1Name[] = "client_1";
constexpr char kClientCert2Name[] = "client_2";
constexpr test::UIPath kPrimaryButton = {"gaia-signin", "signin-frame-dialog",
"primary-action-button"};
constexpr test::UIPath kSecondaryButton = {"gaia-signin", "signin-frame-dialog",
"secondary-action-button"};
constexpr test::UIPath kBackButton = {"gaia-signin", "signin-frame-dialog",
"signin-back-button"};
constexpr char kSigninWebview[] = "$('gaia-signin').getSigninFrame_()";
constexpr char kSigninWebviewOnLockScreen[] =
"$('main-element').getSigninFrame_()";
// UMA names for better test reading.
const char kLoginRequests[] = "OOBE.GaiaScreen.LoginRequests";
const char kSuccessLoginRequests[] = "OOBE.GaiaScreen.SuccessLoginRequests";
void InjectCookieDoneCallback(base::OnceClosure done_closure,
net::CookieAccessResult result) {
ASSERT_TRUE(result.status.IsInclude());
std::move(done_closure).Run();
}
// Injects a cookie into `storage_partition`, so we can test for cookie presence
// later to infer if the StoragePartition has been cleared.
void InjectCookie(content::StoragePartition* storage_partition) {
mojo::Remote<network::mojom::CookieManager> cookie_manager;
storage_partition->GetNetworkContext()->GetCookieManager(
cookie_manager.BindNewPipeAndPassReceiver());
std::unique_ptr<net::CanonicalCookie> cookie =
net::CanonicalCookie::CreateUnsafeCookieForTesting(
kTestCookieName, kTestCookieValue, kTestCookieHost, "/", base::Time(),
base::Time(), base::Time(), true /* secure */, false /* httponly*/,
net::CookieSameSite::NO_RESTRICTION, net::COOKIE_PRIORITY_MEDIUM,
false /* same_party */);
base::RunLoop run_loop;
cookie_manager->SetCanonicalCookie(
*cookie, net::cookie_util::SimulatedCookieSource(*cookie, "https"),
net::CookieOptions(),
base::BindOnce(&InjectCookieDoneCallback, run_loop.QuitClosure()));
run_loop.Run();
}
void GetAllCookiesCallback(std::string* cookies_out,
base::OnceClosure done_closure,
const std::vector<net::CanonicalCookie>& cookies) {
*cookies_out = net::CanonicalCookie::BuildCookieLine(cookies);
std::move(done_closure).Run();
}
// Returns all cookies present in `storage_partition` as a HTTP header cookie
// line. Will be an empty string if there are no cookies.
std::string GetAllCookies(content::StoragePartition* storage_partition) {
mojo::Remote<network::mojom::CookieManager> cookie_manager;
storage_partition->GetNetworkContext()->GetCookieManager(
cookie_manager.BindNewPipeAndPassReceiver());
std::string cookies;
base::RunLoop run_loop;
cookie_manager->GetAllCookies(
base::BindOnce(&GetAllCookiesCallback, &cookies, run_loop.QuitClosure()));
run_loop.Run();
return cookies;
}
void PolicyChangedCallback(base::RepeatingClosure callback,
const base::Value* old_value,
const base::Value* new_value) {
callback.Run();
}
// Spins the loop until a notification is received from `prefs` that the value
// of `pref_name` has changed. If the notification is received before Wait()
// has been called, Wait() returns immediately and no loop is spun.
class PrefChangeWatcher {
public:
PrefChangeWatcher(const std::string& pref_name, PrefService* prefs);
PrefChangeWatcher(const PrefChangeWatcher&) = delete;
PrefChangeWatcher& operator=(const PrefChangeWatcher&) = delete;
void Wait();
private:
void OnPrefChange();
bool pref_changed_ = false;
base::RunLoop run_loop_;
PrefChangeRegistrar registrar_;
};
PrefChangeWatcher::PrefChangeWatcher(const std::string& pref_name,
PrefService* prefs) {
registrar_.Init(prefs);
registrar_.Add(pref_name,
base::BindRepeating(&PrefChangeWatcher::OnPrefChange,
base::Unretained(this)));
}
void PrefChangeWatcher::Wait() {
if (!pref_changed_)
run_loop_.Run();
}
void PrefChangeWatcher::OnPrefChange() {
pref_changed_ = true;
run_loop_.Quit();
}
// Observes OOBE screens and can be queried to see if the error screen has been
// displayed since ErrorScreenWatcher has been constructed.
class ErrorScreenWatcher : public OobeUI::Observer {
public:
ErrorScreenWatcher() {
OobeUI* oobe_ui = LoginDisplayHost::default_host()->GetOobeUI();
oobe_ui_observation_.Observe(oobe_ui);
if (oobe_ui->current_screen() == ErrorScreenView::kScreenId)
has_error_screen_been_shown_ = true;
}
ErrorScreenWatcher(const ErrorScreenWatcher& other) = delete;
ErrorScreenWatcher& operator=(const ErrorScreenWatcher& other) = delete;
~ErrorScreenWatcher() override = default;
bool has_error_screen_been_shown() const {
return has_error_screen_been_shown_;
}
// OobeUI::Observer:
void OnCurrentScreenChanged(OobeScreenId current_screen,
OobeScreenId new_screen) override {
if (new_screen == ErrorScreenView::kScreenId)
has_error_screen_been_shown_ = true;
}
// OobeUI::Observer:
void OnDestroyingOobeUI() override {}
private:
base::ScopedObservation<OobeUI, OobeUI::Observer> oobe_ui_observation_{this};
bool has_error_screen_been_shown_ = false;
};
bool EqualsTestCert(const net::X509Certificate& cert,
const std::string& expected_test_cert_name) {
const base::FilePath cert_file_name =
base::FilePath::FromASCII(expected_test_cert_name)
.AddExtensionASCII("pem");
scoped_refptr<net::X509Certificate> expected = net::ImportCertFromFile(
net::GetTestCertsDirectory(), cert_file_name.MaybeAsASCII());
if (!expected) {
ADD_FAILURE() << "Failed to read test certificate "
<< expected_test_cert_name;
return false;
}
return expected->EqualsExcludingChain(&cert);
}
MATCHER_P(EqualsCert,
cert_name,
base::StringPrintf("Is test certificate %s", cert_name.c_str())) {
return EqualsTestCert(arg, cert_name);
}
} // namespace
class WebviewLoginTest : public OobeBaseTest {
public:
WebviewLoginTest() = default;
WebviewLoginTest(const WebviewLoginTest&) = delete;
WebviewLoginTest& operator=(const WebviewLoginTest&) = delete;
~WebviewLoginTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kOobeSkipPostLogin);
OobeBaseTest::SetUpCommandLine(command_line);
}
base::HistogramTester histogram_tester_;
protected:
void ExpectIdentifierPage() {
// First page: back button, #identifier input field.
test::OobeJS().ExpectVisiblePath(kBackButton);
test::OobeJS().ExpectTrue(
base::StrCat({kSigninWebview, ".src.indexOf('#identifier') != -1"}));
}
void ExpectPasswordPage() {
// Second page: back button, #challengepassword input field.
test::OobeJS().ExpectVisiblePath(kBackButton);
test::OobeJS().ExpectTrue(base::StrCat(
{kSigninWebview, ".src.indexOf('#challengepassword') != -1"}));
}
bool WebViewVisited(content::BrowserContext* browser_context,
content::StoragePartition* expected_storage_partition,
bool* out_web_view_found,
content::WebContents* guest_contents) {
content::StoragePartition* guest_storage_partition =
browser_context->GetStoragePartition(guest_contents->GetSiteInstance());
if (guest_storage_partition == expected_storage_partition)
*out_web_view_found = true;
// Returns true if found - this will exit the iteration early.
return *out_web_view_found;
}
// Returns true if a webview which has a WebContents associated with
// `storage_partition` currently exists in the login UI's main WebContents.
bool IsLoginScreenHasWebviewWithStoragePartition(
content::StoragePartition* storage_partition) {
bool web_view_found = false;
content::WebContents* web_contents = GetLoginUI()->GetWebContents();
content::BrowserContext* browser_context =
web_contents->GetBrowserContext();
guest_view::GuestViewManager* guest_view_manager =
guest_view::GuestViewManager::FromBrowserContext(browser_context);
guest_view_manager->ForEachGuest(
web_contents,
base::BindRepeating(&WebviewLoginTest::WebViewVisited,
base::Unretained(this), browser_context,
storage_partition, &web_view_found));
return web_view_found;
}
void DisableImplicitServices() {
SigninFrameJS().ExecuteAsync(
"gaia.chromeOSLogin.shouldSendImplicitServices = false");
}
void DisableCloseViewMessage() {
SigninFrameJS().ExecuteAsync(
"gaia.chromeOSLogin.shouldSendCloseView = false");
}
void WaitForServicesSet() {
test::OobeJS()
.CreateWaiter("$('gaia-signin').authenticator_.services_")
->Wait();
}
protected:
ScopedTestingCrosSettings scoped_testing_cros_settings_;
FakeGaiaMixin fake_gaia_{&mixin_host_};
base::test::ScopedFeatureList scoped_feature_list_;
};
/* is kGaiaCloseViewMessage enabled */
/* Does Gaia send the 'closeView' message */
using CloseViewParam = std::tuple<bool, bool>;
class WebviewCloseViewLoginTest
: public WebviewLoginTest,
public ::testing::WithParamInterface<CloseViewParam> {
public:
WebviewCloseViewLoginTest() {
scoped_feature_list_.Reset();
if (IsFeatureEnabled(GetParam())) {
scoped_feature_list_.InitAndEnableFeature(
features::kGaiaCloseViewMessage);
} else {
scoped_feature_list_.InitAndDisableFeature(
features::kGaiaCloseViewMessage);
}
}
static std::string GetName(
const testing::TestParamInfo<CloseViewParam>& param) {
std::string result;
result +=
IsFeatureEnabled(param.param) ? "ClientEnabled" : "ClientDisabled";
result += "_";
result +=
GaiaSendsCloseView(param.param) ? "ServerEnabled" : "ServerDisabled";
return result;
}
protected:
static bool IsFeatureEnabled(const CloseViewParam& param) {
return std::get<0>(param);
}
static bool GaiaSendsCloseView(const CloseViewParam& param) {
return std::get<1>(param);
}
void SendCloseViewOrEmulateTimeout() {
if (GaiaSendsCloseView(GetParam())) {
SigninFrameJS().ExecuteAsync("gaia.chromeOSLogin.sendCloseView()");
return;
}
if (!IsFeatureEnabled(GetParam()))
return;
EmulateGaiaDoneTimeout();
}
void EmulateGaiaDoneTimeout() {
// Wait for user info timer to be set.
test::OobeJS()
.CreateWaiter("$('gaia-signin').authenticator_.gaiaDoneTimer_")
->Wait();
// Emulate timeout fire.
test::OobeJS().ExecuteAsync(
"$('gaia-signin').authenticator_.onGaiaDoneTimeout_()");
}
};
// Basic signin with username and password.
IN_PROC_BROWSER_TEST_P(WebviewCloseViewLoginTest, NativeTest) {
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
// Test will send `closerView` manually (if the feature is enabled).
DisableCloseViewMessage();
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
WaitForGaiaPageBackButtonUpdate();
ExpectPasswordPage();
test::OobeJS().ExpectVisiblePath(kSecondaryButton);
test::OobeJS().ExpectEnabledPath(kSecondaryButton);
// Check events propagation.
SigninFrameJS().ExecuteAsync("sendSetAllActionsEnabled(false)");
test::OobeJS().CreateEnabledWaiter(false, kPrimaryButton)->Wait();
test::OobeJS().CreateEnabledWaiter(false, kSecondaryButton)->Wait();
test::OobeJS().ExpectVisiblePath(kPrimaryButton);
test::OobeJS().ExpectVisiblePath(kSecondaryButton);
SigninFrameJS().ExecuteAsync("sendSetSecondaryActionEnabled(true)");
test::OobeJS().CreateEnabledWaiter(true, kSecondaryButton)->Wait();
test::OobeJS().ExpectVisiblePath(kSecondaryButton);
// Click on the secondary button disables it.
test::OobeJS().ClickOnPath(kSecondaryButton);
test::OobeJS().CreateEnabledWaiter(false, kSecondaryButton)->Wait();
SigninFrameJS().ExecuteAsync("sendSetPrimaryActionEnabled(true)");
test::OobeJS().CreateEnabledWaiter(true, kPrimaryButton)->Wait();
test::OobeJS().ExpectVisiblePath(kPrimaryButton);
SigninFrameJS().ExecuteAsync("sendSetPrimaryActionLabel(null)");
test::OobeJS().CreateVisibilityWaiter(false, kPrimaryButton)->Wait();
SigninFrameJS().ExecuteAsync("sendSetSecondaryActionLabel(null)");
test::OobeJS().CreateVisibilityWaiter(false, kSecondaryButton)->Wait();
SigninFrameJS().ExecuteAsync("sendSetPrimaryActionLabel('Submit')");
test::OobeJS().CreateVisibilityWaiter(true, kPrimaryButton)->Wait();
test::OobeJS().ExpectElementText("Submit", kPrimaryButton);
SigninFrameJS().TypeIntoPath("[]", {"services"});
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
if (IsFeatureEnabled(GetParam()))
WaitForServicesSet();
SendCloseViewOrEmulateTimeout();
test::WaitForPrimaryUserSessionStart();
histogram_tester_.ExpectUniqueSample("ChromeOS.Gaia.Message.Gaia.UserInfo",
true, 1);
if (!IsFeatureEnabled(GetParam())) {
histogram_tester_.ExpectTotalCount("ChromeOS.Gaia.Message.Gaia.CloseView",
0);
return;
}
histogram_tester_.ExpectUniqueSample("ChromeOS.Gaia.Message.Gaia.CloseView",
GaiaSendsCloseView(GetParam()), 1);
}
// Basic signin with username and password.
IN_PROC_BROWSER_TEST_P(WebviewCloseViewLoginTest, Basic) {
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
// Test will send `closerView` manually (if the feature is enabled).
DisableCloseViewMessage();
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
WaitForGaiaPageBackButtonUpdate();
ExpectPasswordPage();
ASSERT_TRUE(LoginDisplayHost::default_host());
EXPECT_TRUE(LoginDisplayHost::default_host()->GetWebUILoginView());
SigninFrameJS().TypeIntoPath("[]", {"services"});
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
if (IsFeatureEnabled(GetParam()))
WaitForServicesSet();
SendCloseViewOrEmulateTimeout();
// The login view should be destroyed after the browser window opens.
ui_test_utils::WaitForBrowserToOpen();
EXPECT_FALSE(LoginDisplayHost::default_host()->GetWebUILoginView());
test::WaitForPrimaryUserSessionStart();
// Wait for the LoginDisplayHost to delete itself, which is a posted task.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(LoginDisplayHost::default_host());
histogram_tester_.ExpectUniqueSample("ChromeOS.SAML.APILogin", 0, 1);
histogram_tester_.ExpectTotalCount("OOBE.GaiaLoginTime", 1);
histogram_tester_.ExpectUniqueSample(kLoginRequests,
GaiaView::GaiaLoginVariant::kOobe, 1);
histogram_tester_.ExpectUniqueSample(kSuccessLoginRequests,
GaiaView::GaiaLoginVariant::kOobe, 1);
}
IN_PROC_BROWSER_TEST_P(WebviewCloseViewLoginTest, BackButton) {
WaitForGaiaPageLoadAndPropertyUpdate();
// Start with identifer page.
ExpectIdentifierPage();
// Test will send `closerView` manually (if the feature is enabled).
DisableCloseViewMessage();
// Move to password page.
auto back_button_waiter = CreateGaiaPageEventWaiter("backButton");
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
back_button_waiter->Wait();
ExpectPasswordPage();
// Click back to identifier page.
back_button_waiter = CreateGaiaPageEventWaiter("backButton");
test::OobeJS().ClickOnPath(kBackButton);
back_button_waiter->Wait();
ExpectIdentifierPage();
back_button_waiter = CreateGaiaPageEventWaiter("backButton");
// Click next to password page, user id is remembered.
test::OobeJS().ClickOnPath(kPrimaryButton);
back_button_waiter->Wait();
ExpectPasswordPage();
// Finish sign-up.
SigninFrameJS().TypeIntoPath("[]", {"services"});
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
if (IsFeatureEnabled(GetParam()))
WaitForServicesSet();
SendCloseViewOrEmulateTimeout();
test::WaitForPrimaryUserSessionStart();
}
class WebviewLoginTestWithSyncTrustedVaultEnabled : public WebviewLoginTest {
public:
WebviewLoginTestWithSyncTrustedVaultEnabled() {
scoped_feature_list_.Reset();
scoped_feature_list_.InitAndEnableFeature(
::switches::kSyncTrustedVaultPassphraseRecovery);
}
};
IN_PROC_BROWSER_TEST_F(WebviewLoginTestWithSyncTrustedVaultEnabled,
BasicWithKeys) {
// Set up some fake keys in the server.
FakeGaia::SyncTrustedVaultKeys fake_gaia_keys;
// Create an arbitrary encryption key, the precisely value is not relevant,
// but used as test expectation later down.
fake_gaia_keys.encryption_key.resize(16, 123);
fake_gaia_keys.encryption_key_version = 91;
// Create a random-but-valid public key, the precisely value is not relevant.
fake_gaia_keys.trusted_public_keys.push_back(
syncer::SecureBoxKeyPair::GenerateRandom()->public_key().ExportToBytes());
fake_gaia_.fake_gaia()->SetSyncTrustedVaultKeys(FakeGaiaMixin::kFakeUserEmail,
fake_gaia_keys);
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
WaitForGaiaPageBackButtonUpdate();
ExpectPasswordPage();
ASSERT_TRUE(LoginDisplayHost::default_host());
SigninFrameJS().TypeIntoPath("[]", {"services"});
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
Browser* browser = ui_test_utils::WaitForBrowserToOpen();
test::WaitForPrimaryUserSessionStart();
// AddRecoveryMethod() logic is deferred until refresh tokens are loaded.
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(browser->profile());
signin::WaitForRefreshTokensLoaded(identity_manager);
syncer::SyncServiceImpl* sync_service =
SyncServiceFactory::GetAsSyncServiceImplForProfile(browser->profile());
syncer::TrustedVaultClient* trusted_vault_client =
sync_service->GetSyncClientForTest()->GetTrustedVaultClient();
// Verify that the sync trusted vault keys have been received and stored.
{
base::RunLoop loop;
std::vector<std::vector<uint8_t>> actual_keys;
trusted_vault_client->FetchKeys(
sync_service->GetAccountInfo(),
base::BindLambdaForTesting(
[&](const std::vector<std::vector<uint8_t>>& keys) {
actual_keys = keys;
loop.Quit();
}));
loop.Run();
EXPECT_THAT(actual_keys,
testing::ElementsAre(fake_gaia_keys.encryption_key));
}
// Verify that the recovery method was passed too.
{
base::RunLoop loop;
std::vector<uint8_t> actual_public_key;
static_cast<syncer::StandaloneTrustedVaultClient*>(
sync_service->GetSyncClientForTest()->GetTrustedVaultClient())
->GetLastAddedRecoveryMethodPublicKeyForTesting(
base::BindLambdaForTesting([&](const std::vector<uint8_t>& key) {
actual_public_key = key;
loop.Quit();
}));
loop.Run();
EXPECT_EQ(actual_public_key, fake_gaia_keys.trusted_public_keys.back());
}
}
IN_PROC_BROWSER_TEST_F(WebviewLoginTest, ErrorScreenOnGaiaError) {
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
// Make gaia landing page unreachable
fake_gaia_.fake_gaia()->SetErrorResponse(
GaiaUrls::GetInstance()->embedded_setup_chromeos_url(2),
net::HTTP_NOT_FOUND);
// Click back to reload (unreachable) identifier page.
test::OobeJS().ClickOnPath(kBackButton);
OobeScreenWaiter(ErrorScreenView::kScreenId).Wait();
}
// Create new account option should be available only if the settings allow it.
IN_PROC_BROWSER_TEST_F(WebviewLoginTest, AllowNewUser) {
WaitForGaiaPageLoad();
std::string frame_url = "$('gaia-signin').authenticator_.reloadUrl_";
// New users are allowed.
test::OobeJS().ExpectTrue(frame_url + ".search('flow=nosignup') == -1");
// Disallow new users - we also need to set an allowlist due to weird logic.
scoped_testing_cros_settings_.device_settings()->Set(kAccountsPrefUsers,
base::ListValue());
scoped_testing_cros_settings_.device_settings()->Set(
kAccountsPrefAllowNewUser, base::Value(false));
WaitForGaiaPageReload();
// flow=nosignup indicates that user creation is not allowed.
test::OobeJS().ExpectTrue(frame_url + ".search('flow=nosignup') != -1");
}
class ReauthWebviewLoginTest : public WebviewLoginTest {
protected:
LoginManagerMixin::TestUserInfo reauth_user_{
AccountId::FromUserEmailGaiaId(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kFakeUserGaiaId),
user_manager::USER_TYPE_REGULAR,
/* invalid token status to force online signin */
user_manager::User::OAUTH2_TOKEN_STATUS_INVALID};
LoginManagerMixin login_manager_mixin_{&mixin_host_, {reauth_user_}};
};
IN_PROC_BROWSER_TEST_F(ReauthWebviewLoginTest, EmailPrefill) {
EXPECT_TRUE(
LoginScreenTestApi::IsForcedOnlineSignin(reauth_user_.account_id));
// Focus triggers online signin.
EXPECT_TRUE(LoginScreenTestApi::FocusUser(reauth_user_.account_id));
WaitForGaiaPageLoad();
EXPECT_TRUE(LoginScreenTestApi::IsOobeDialogVisible());
EXPECT_EQ(fake_gaia_.fake_gaia()->prefilled_email(),
reauth_user_.account_id.GetUserEmail());
}
class ReauthEndpointWebviewLoginTest : public WebviewLoginTest {
protected:
ReauthEndpointWebviewLoginTest() {
// TODO(https://crbug.com/1153912) Makes tests work with
// kParentAccessCodeForOnlineLogin enabled.
scoped_feature_list_.Reset();
scoped_feature_list_.InitWithFeatures(
{features::kGaiaReauthEndpoint},
{::features::kParentAccessCodeForOnlineLogin});
}
~ReauthEndpointWebviewLoginTest() override = default;
LoginManagerMixin::TestUserInfo reauth_user_{
AccountId::FromUserEmailGaiaId(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kFakeUserGaiaId),
user_manager::USER_TYPE_CHILD,
/* invalid token status to force online signin */
user_manager::User::OAUTH2_TOKEN_STATUS_INVALID};
LoginManagerMixin login_manager_mixin_{&mixin_host_, {reauth_user_}};
};
IN_PROC_BROWSER_TEST_F(ReauthEndpointWebviewLoginTest, SupervisedUser) {
EXPECT_TRUE(
LoginScreenTestApi::IsForcedOnlineSignin(reauth_user_.account_id));
// Focus triggers online signin.
EXPECT_TRUE(LoginScreenTestApi::FocusUser(reauth_user_.account_id));
WaitForGaiaPageLoad();
EXPECT_TRUE(LoginScreenTestApi::IsOobeDialogVisible());
EXPECT_EQ(fake_gaia_.fake_gaia()->prefilled_email(),
reauth_user_.account_id.GetUserEmail());
EXPECT_EQ(fake_gaia_.fake_gaia()->is_supervised(), "1");
EXPECT_TRUE(fake_gaia_.fake_gaia()->is_device_owner().empty());
}
class ReauthEndpointWebviewLoginOwnerTest
: public ReauthEndpointWebviewLoginTest {
protected:
ReauthEndpointWebviewLoginOwnerTest() {
scoped_testing_cros_settings_.device_settings()->Set(
kDeviceOwner, base::Value(FakeGaiaMixin::kFakeUserEmail));
}
~ReauthEndpointWebviewLoginOwnerTest() override = default;
};
IN_PROC_BROWSER_TEST_F(ReauthEndpointWebviewLoginOwnerTest, SupervisedUser) {
EXPECT_TRUE(
LoginScreenTestApi::IsForcedOnlineSignin(reauth_user_.account_id));
// Focus triggers online signin.
EXPECT_TRUE(LoginScreenTestApi::FocusUser(reauth_user_.account_id));
WaitForGaiaPageLoad();
EXPECT_TRUE(LoginScreenTestApi::IsOobeDialogVisible());
EXPECT_EQ(fake_gaia_.fake_gaia()->prefilled_email(),
reauth_user_.account_id.GetUserEmail());
EXPECT_EQ(fake_gaia_.fake_gaia()->is_supervised(), "1");
EXPECT_EQ(fake_gaia_.fake_gaia()->is_device_owner(), "1");
histogram_tester_.ExpectTotalCount("OOBE.GaiaLoginTime", 0);
}
IN_PROC_BROWSER_TEST_F(WebviewLoginTest, StoragePartitionHandling) {
WaitForGaiaPageLoadAndPropertyUpdate();
// Start with identifier page.
ExpectIdentifierPage();
// WebContents of the embedding frame
content::WebContents* web_contents = GetLoginUI()->GetWebContents();
content::BrowserContext* browser_context = web_contents->GetBrowserContext();
std::string signin_frame_partition_name_1 =
test::OobeJS().GetString(base::StrCat({kSigninWebview, ".partition"}));
content::StoragePartition* signin_frame_partition_1 =
login::GetSigninPartition();
EXPECT_FALSE(signin_frame_partition_name_1.empty());
EXPECT_EQ(login::SigninPartitionManager::Factory::GetForBrowserContext(
browser_context)
->GetCurrentStoragePartitionName(),
signin_frame_partition_name_1);
EXPECT_TRUE(
IsLoginScreenHasWebviewWithStoragePartition(signin_frame_partition_1));
// Inject a cookie into the currently used StoragePartition, so we can test
// later if it has been cleared.
InjectCookie(signin_frame_partition_1);
// Press the back button at a sign-in screen without pre-existing users to
// start a new sign-in attempt.
test::OobeJS().ClickOnPath(kBackButton);
WaitForGaiaPageBackButtonUpdate();
// Expect that we got back to the identifier page, as there are no known users
// so the sign-in screen will not display user pods.
ExpectIdentifierPage();
std::string signin_frame_partition_name_2 =
test::OobeJS().GetString(base::StrCat({kSigninWebview, ".partition"}));
content::StoragePartition* signin_frame_partition_2 =
login::GetSigninPartition();
EXPECT_FALSE(signin_frame_partition_name_2.empty());
EXPECT_EQ(login::SigninPartitionManager::Factory::GetForBrowserContext(
browser_context)
->GetCurrentStoragePartitionName(),
signin_frame_partition_name_2);
EXPECT_TRUE(
IsLoginScreenHasWebviewWithStoragePartition(signin_frame_partition_2));
InjectCookie(signin_frame_partition_2);
// Make sure that the partitions differ and that the old one is not in use
// anymore.
EXPECT_NE(signin_frame_partition_name_1, signin_frame_partition_name_2);
EXPECT_NE(signin_frame_partition_1, signin_frame_partition_2);
EXPECT_FALSE(
IsLoginScreenHasWebviewWithStoragePartition(signin_frame_partition_1));
// The StoragePartition which is not in use is supposed to have been cleared.
EXPECT_EQ("", GetAllCookies(signin_frame_partition_1));
EXPECT_NE("", GetAllCookies(signin_frame_partition_2));
}
// Tests that requesting webcam access from the login screen works correctly.
// This is needed for taking profile pictures.
IN_PROC_BROWSER_TEST_F(WebviewLoginTest, RequestCamera) {
WaitForGaiaPageLoad();
// Video devices should be allowed from the login screen.
content::WebContents* web_contents = GetLoginUI()->GetWebContents();
bool getUserMediaSuccess = false;
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
web_contents->GetMainFrame(),
"navigator.getUserMedia("
" {video: true},"
" function() { window.domAutomationController.send(true); },"
" function() { window.domAutomationController.send(false); });",
&getUserMediaSuccess));
EXPECT_TRUE(getUserMediaSuccess);
// Audio devices should be denied from the login screen.
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
web_contents->GetMainFrame(),
"navigator.getUserMedia("
" {audio: true},"
" function() { window.domAutomationController.send(true); },"
" function() { window.domAutomationController.send(false); });",
&getUserMediaSuccess));
EXPECT_FALSE(getUserMediaSuccess);
}
enum class FrameUrlOrigin { kSameOrigin, kDifferentOrigin };
// Parametrized test fixture that configures FakeGaia to server an iframe in the
// embedded ChromeOS setup response. If the parameter is
// FrameUrlOrigin::kSameOrigin, the frame URL will be on the same origin as fake
// gaia. If it's FrameUrlOrigin::kDifferentOrigin, it will be on a different
// origin.
// The frame URL serves an empty HTTP document with the X-Frame-Options header
// set to SAMEORIGIN, so the frame load will fail when
// FrameUrlOrigin::kDifferentOrigin is set as the parameter.
class WebviewLoginWithIframeTest
: public WebviewLoginTest,
public ::testing::WithParamInterface<FrameUrlOrigin> {
public:
WebviewLoginWithIframeTest() = default;
~WebviewLoginWithIframeTest() override = default;
WebviewLoginWithIframeTest(const WebviewLoginWithIframeTest& other) = delete;
WebviewLoginWithIframeTest& operator=(
const WebviewLoginWithIframeTest& other) = delete;
// WebviewLoginTest:
void RegisterAdditionalRequestHandlers() override {
WebviewLoginTest::RegisterAdditionalRequestHandlers();
// For simplicity the request handler is registered on both servers. The
// test will only request the path from one of them, depending on the
// FrameUrlOrigin test parameter.
fake_gaia_.gaia_server()->RegisterRequestHandler(base::BindRepeating(
&WebviewLoginWithIframeTest::HandleFrameRelativePath));
other_origin_server_.RegisterRequestHandler(base::BindRepeating(
&WebviewLoginWithIframeTest::HandleFrameRelativePath));
}
void SetUpInProcessBrowserTestFixture() override {
WebviewLoginTest::SetUpInProcessBrowserTestFixture();
net::EmbeddedTestServer::ServerCertificateConfig other_origin_cert_config;
other_origin_cert_config.dns_names = {kOtherOriginHost};
other_origin_server_.SetSSLConfig(other_origin_cert_config);
// Initialize the server so the port is known, but don't start the IO thread
// until SetupThreadMain().
ASSERT_TRUE(other_origin_server_.InitializeAndListen());
switch (GetParam()) {
case FrameUrlOrigin::kSameOrigin:
frame_url_ = fake_gaia_.GetFakeGaiaURL(kFrameRelativePath);
break;
case FrameUrlOrigin::kDifferentOrigin:
frame_url_ =
other_origin_server_.GetURL(kOtherOriginHost, kFrameRelativePath);
break;
}
fake_gaia_.fake_gaia()->SetIframeOnEmbeddedSetupChromeosUrl(frame_url_);
}
void SetUpOnMainThread() override {
other_origin_server_.StartAcceptingConnections();
WebviewLoginTest::SetUpOnMainThread();
}
protected:
static constexpr const char* kOtherOriginHost = "other.example.com";
static constexpr const char* kFrameRelativePath =
"/frame_with_same_origin_requirement";
static std::unique_ptr<net::test_server::HttpResponse>
HandleFrameRelativePath(const net::test_server::HttpRequest& request) {
if (request.relative_url != kFrameRelativePath) {
return nullptr;
}
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content("<!DOCTYPE html>");
response->AddCustomHeader("X-Frame-Options", "SAMEORIGIN");
return response;
}
net::EmbeddedTestServer other_origin_server_{
net::EmbeddedTestServer::TYPE_HTTPS};
GURL frame_url_;
};
IN_PROC_BROWSER_TEST_P(WebviewLoginWithIframeTest, GaiaWithIframe) {
ErrorScreenWatcher error_screen_watcher;
content::TestNavigationObserver navigation_observer(frame_url_);
navigation_observer.StartWatchingNewWebContents();
WaitForGaiaPageLoadAndPropertyUpdate();
navigation_observer.WaitForNavigationFinished();
EXPECT_EQ(navigation_observer.last_navigation_url(), frame_url_);
const net::Error expected_error = (GetParam() == FrameUrlOrigin::kSameOrigin)
? net::OK
: net::ERR_BLOCKED_BY_RESPONSE;
EXPECT_EQ(navigation_observer.last_net_error_code(), expected_error);
ExpectIdentifierPage();
OobeScreenWaiter(GaiaView::kScreenId).Wait();
// Make sure that the error screen has not been shown in the meantime.
// It is not sufficient to just wait for the Gaia screen / check that the gaia
// screen is currently being replaced, because the error screen could have
// been shown in the meantime (and then exited again because the "device" has
// internet connectivity).
EXPECT_FALSE(error_screen_watcher.has_error_screen_been_shown());
}
INSTANTIATE_TEST_SUITE_P(All,
WebviewLoginWithIframeTest,
testing::Values(FrameUrlOrigin::kSameOrigin,
FrameUrlOrigin::kDifferentOrigin));
// Base class for tests of the client certificates in the sign-in frame.
class WebviewClientCertsLoginTestBase : public WebviewLoginTest {
public:
WebviewClientCertsLoginTestBase() = default;
WebviewClientCertsLoginTestBase(const WebviewClientCertsLoginTestBase&) =
delete;
WebviewClientCertsLoginTestBase& operator=(
const WebviewClientCertsLoginTestBase&) = delete;
// Sets up the DeviceLoginScreenAutoSelectCertificateForUrls policy.
void SetAutoSelectCertificatePatterns(
const std::vector<std::string>& autoselect_patterns) {
em::ChromeDeviceSettingsProto& proto(device_policy_builder_.payload());
auto* field =
proto.mutable_device_login_screen_auto_select_certificate_for_urls();
for (const std::string& autoselect_pattern : autoselect_patterns)
field->add_login_screen_auto_select_certificate_rules(autoselect_pattern);
device_policy_builder_.Build();
FakeSessionManagerClient::Get()->set_device_policy(
device_policy_builder_.GetBlob());
PrefChangeWatcher watcher(prefs::kManagedAutoSelectCertificateForUrls,
ProfileHelper::GetSigninProfile()->GetPrefs());
FakeSessionManagerClient::Get()->OnPropertyChangeComplete(true);
watcher.Wait();
}
// Adds the certificate from `authority_file_path` (PEM) as untrusted
// authority in device OpenNetworkConfiguration policy.
void SetIntermediateAuthorityInDeviceOncPolicy(
const base::FilePath& authority_file_path) {
std::string x509_contents;
{
base::ScopedAllowBlockingForTesting allow_io;
ASSERT_TRUE(base::ReadFileToString(authority_file_path, &x509_contents));
}
base::DictionaryValue onc_dict =
BuildDeviceOncDictForUntrustedAuthority(x509_contents);
em::ChromeDeviceSettingsProto& proto(device_policy_builder_.payload());
base::JSONWriter::Write(onc_dict,
proto.mutable_open_network_configuration()
->mutable_open_network_configuration());
device_policy_builder_.Build();
FakeSessionManagerClient::Get()->set_device_policy(
device_policy_builder_.GetBlob());
PrefChangeWatcher watcher(onc::prefs::kDeviceOpenNetworkConfiguration,
g_browser_process->local_state());
FakeSessionManagerClient::Get()->OnPropertyChangeComplete(true);
watcher.Wait();
}
// Sets the DeviceLoginScreenPromptOnMultipleMatchingCertificates device
// policy.
void SetPromptOnMultipleMatchingCertificatesPolicy(
bool prompt_on_multiple_matches) {
em::ChromeDeviceSettingsProto& proto(device_policy_builder_.payload());
proto.mutable_login_screen_prompt_on_multiple_matching_certificates()
->set_value(prompt_on_multiple_matches);
device_policy_builder_.Build();
FakeSessionManagerClient::Get()->set_device_policy(
device_policy_builder_.GetBlob());
PrefChangeWatcher watcher(prefs::kPromptOnMultipleMatchingCertificates,
ProfileHelper::GetSigninProfile()->GetPrefs());
FakeSessionManagerClient::Get()->OnPropertyChangeComplete(true);
watcher.Wait();
}
// Starts the Test HTTPS server with `ssl_options`.
void StartHttpsServer(const net::SSLServerConfig& server_config) {
https_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_OK,
server_config);
https_server_->RegisterRequestHandler(base::BindLambdaForTesting(
[this](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.relative_url != "/client-cert") {
return nullptr;
}
{
// Save the `SSLInfo` for `RequestClientCertTestPageInFrame`.
base::AutoLock lock(server_ssl_info_lock_);
DCHECK(request.ssl_info);
server_ssl_info_ = request.ssl_info;
}
return std::make_unique<net::test_server::BasicHttpResponse>();
}));
ASSERT_TRUE(https_server_->Start());
}
// Requests `http_server_`'s client-cert test page in the webview specified by
// the given `webview_path`. Returns the `net::SSLInfo` as observed by the
// server, or `absl::nullopt` if the server did not report any such value.
absl::optional<net::SSLInfo> RequestClientCertTestPageInFrame(
test::JSChecker js_checker,
const std::string& webview_path) {
const GURL url = https_server_->GetURL("/client-cert");
content::TestNavigationObserver navigation_observer(url);
navigation_observer.WatchExistingWebContents();
navigation_observer.StartWatchingNewWebContents();
js_checker.Evaluate(base::StringPrintf("%s.src='%s'", webview_path.c_str(),
url.spec().c_str()));
navigation_observer.Wait();
base::AutoLock lock(server_ssl_info_lock_);
absl::optional<net::SSLInfo> server_ssl_info = std::move(server_ssl_info_);
server_ssl_info_ = absl::nullopt;
return server_ssl_info;
}
void ShowEulaScreen() {
LoginDisplayHost::default_host()->StartWizard(EulaView::kScreenId);
OobeScreenWaiter(EulaView::kScreenId).Wait();
}
protected:
void SetUpInProcessBrowserTestFixture() override {
// Override FakeSessionManagerClient. This will be shut down by the browser.
chromeos::SessionManagerClient::InitializeFakeInMemory();
device_policy_builder_.Build();
FakeSessionManagerClient::Get()->set_device_policy(
device_policy_builder_.GetBlob());
WebviewLoginTest::SetUpInProcessBrowserTestFixture();
}
void ImportSystemSlotClientCerts(
const std::vector<std::string>& client_cert_names,
PK11SlotInfo* system_slot) {
base::ScopedAllowBlockingForTesting allow_io;
for (const auto& client_cert_name : client_cert_names) {
const base::FilePath base_file_name =
base::FilePath::FromASCII(client_cert_name);
const base::FilePath pem_file_name =
base_file_name.AddExtensionASCII("pem");
const base::FilePath pk8_file_name =
base_file_name.AddExtensionASCII("pk8");
scoped_refptr<net::X509Certificate> client_cert =
net::ImportClientCertAndKeyFromFile(
net::GetTestCertsDirectory(), pem_file_name.MaybeAsASCII(),
pk8_file_name.MaybeAsASCII(), system_slot);
if (!client_cert)
ADD_FAILURE() << "Failed to import cert from " << client_cert_name;
}
}
private:
// Builds a device ONC dictionary defining a single untrusted authority
// certificate.
static base::DictionaryValue BuildDeviceOncDictForUntrustedAuthority(
const std::string& x509_authority_cert) {
base::DictionaryValue onc_certificate;
onc_certificate.SetKey(onc::certificate::kGUID, base::Value(kTestGuid));
onc_certificate.SetKey(onc::certificate::kType,
base::Value(onc::certificate::kAuthority));
onc_certificate.SetKey(onc::certificate::kX509,
base::Value(x509_authority_cert));
base::ListValue onc_certificates;
onc_certificates.Append(std::move(onc_certificate));
base::DictionaryValue onc_dict;
onc_dict.SetKey(onc::toplevel_config::kCertificates,
std::move(onc_certificates));
onc_dict.SetKey(
onc::toplevel_config::kType,
base::Value(onc::toplevel_config::kUnencryptedConfiguration));
return onc_dict;
}
policy::DevicePolicyBuilder device_policy_builder_;
std::unique_ptr<net::EmbeddedTestServer> https_server_;
// `net::EmbeddedTestServer`'s callbacks run on a background thread, so this
// field must be protected with a lock.
base::Lock server_ssl_info_lock_;
absl::optional<net::SSLInfo> server_ssl_info_
GUARDED_BY(server_ssl_info_lock_);
DeviceStateMixin device_state_{
&mixin_host_, DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED};
};
// Tests of the client certificates in the sign-in frame. The testing system
// slot is pre-initialized with a client cert.
class WebviewClientCertsLoginTest : public WebviewClientCertsLoginTestBase {
public:
WebviewClientCertsLoginTest() = default;
WebviewClientCertsLoginTest(const WebviewClientCertsLoginTest&) = delete;
WebviewClientCertsLoginTest& operator=(const WebviewClientCertsLoginTest&) =
delete;
// Imports specified client certificates into the system slot.
void SetUpClientCertsInSystemSlot(
const std::vector<std::string>& client_cert_names) {
ImportSystemSlotClientCerts(client_cert_names,
system_nss_key_slot_mixin_.slot());
}
protected:
LoginManagerMixin::TestUserInfo test_user_{
AccountId::FromUserEmailGaiaId(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kFakeUserGaiaId),
user_manager::USER_TYPE_REGULAR};
LoginManagerMixin login_manager_mixin_{&mixin_host_, {test_user_}};
private:
ScopedTestSystemNSSKeySlotMixin system_nss_key_slot_mixin_{&mixin_host_};
};
// Tests that client certificate authentication is not enabled in a webview on
// the sign-in screen which is not the sign-in frame. In this case, the EULA
// webview is used.
// TODO(pmarko): This is DISABLED because the eula UI it depends on has been
// deprecated and removed. https://crbug.com/849710.
IN_PROC_BROWSER_TEST_F(WebviewClientCertsLoginTest,
DISABLED_ClientCertRequestedInOtherWebView) {
ASSERT_NO_FATAL_FAILURE(SetUpClientCertsInSystemSlot({kClientCert1Name}));
net::SSLServerConfig server_config;
server_config.client_cert_type = net::SSLServerConfig::OPTIONAL_CLIENT_CERT;
ASSERT_NO_FATAL_FAILURE(StartHttpsServer(server_config));
const std::vector<std::string> autoselect_patterns = {
R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"};
SetAutoSelectCertificatePatterns(autoselect_patterns);
ShowEulaScreen();
// Use `watch_new_webcontents` because the EULA webview has not navigated yet.
absl::optional<net::SSLInfo> ssl_info =
RequestClientCertTestPageInFrame(test::OobeJS(), "$('cros-eula-frame')");
ASSERT_TRUE(ssl_info);
EXPECT_FALSE(ssl_info->cert);
}
namespace {
// Parameter type for the `SigninFrameWebviewClientCertsLoginTest` parameterized
// test fixture.
struct SigninCertParam {
// Arrange the test to install these client certificates (specified by name,
// e.g., "client1") into the system slot - see
// `SetUpClientCertsInSystemSlot()`.
std::vector<std::string> client_certs;
// If non-null, arrange the test to configure this intermediate CA (specified
// by name, e.g., "client_1_ca") as known to the client via device policy -
// see `SetIntermediateAuthorityInDeviceOncPolicy()`.
absl::optional<std::string> intermediate_cert;
// Arrange the test to configure these certificate auto-selection patterns in
// device policy - see `SetAutoSelectCertificatePatterns()`.
std::vector<std::string> autoselect_patterns;
// If non-null, arrange the test to configure the device policy for prompting
// when multiple certificates are auto-selected - see
// `SetPromptOnMultipleMatchingCertificatesPolicy()`.
absl::optional<bool> prompt_on_multiple_matches;
// Make the web server include the specified CA certificates in its client
// certificate request. Entries should be DER-encoded X.509 names.
std::vector<std::string> ca_certs;
// If non-null, simulate a user gesture to select the given client certificate
// (specified by name, e.g., "client1") in the cert selector dialog.
absl::optional<std::string> manually_select_cert;
// Assert that the selected certificate is the one specified here. When null,
// asserts that no certificate is selected.
absl::optional<std::string> assert_cert;
};
} // namespace
// Parameterized test fixture for simple testing of the client certificate
// selection behavior in the sign-in frame.
class SigninFrameWebviewClientCertsLoginTest
: public WebviewClientCertsLoginTest,
public ::testing::WithParamInterface<SigninCertParam> {
protected:
// Configures the specified certificate to be chosen in the certificate
// selector dialog once it's opened.
void SimulateUserWillSelectClientCert(
const std::string& cert_name_to_select) {
chrome::SetShowSSLClientCertificateSelectorHookForTest(base::BindRepeating(
&SigninFrameWebviewClientCertsLoginTest::OnClientCertSelectorRequested,
cert_name_to_select));
}
private:
static base::OnceClosure OnClientCertSelectorRequested(
const std::string& cert_name_to_select,
content::WebContents* contents,
net::SSLCertRequestInfo* cert_request_info,
net::ClientCertIdentityList client_certs,
std::unique_ptr<content::ClientCertificateDelegate> delegate) {
for (auto& cert_identity : client_certs) {
if (EqualsTestCert(*cert_identity->certificate(), cert_name_to_select)) {
scoped_refptr<net::X509Certificate> cert = cert_identity->certificate();
net::ClientCertIdentity::SelfOwningAcquirePrivateKey(
std::move(cert_identity),
base::BindOnce(
&content::ClientCertificateDelegate::ContinueWithCertificate,
std::move(delegate), cert));
// Return a null cancellation callback - cancelling is not supported.
return base::OnceClosure();
}
}
ADD_FAILURE() << "Cannot select cert " << cert_name_to_select
<< ": not present in the cert selector";
// Return a null cancellation callback - cancelling is not supported.
return base::OnceClosure();
}
};
IN_PROC_BROWSER_TEST_P(SigninFrameWebviewClientCertsLoginTest,
LoginScreenTest) {
// Arrange the system slot.
ASSERT_NO_FATAL_FAILURE(
SetUpClientCertsInSystemSlot(GetParam().client_certs));
// Arrange the device policy.
if (GetParam().intermediate_cert) {
const base::FilePath intermediate_cert_path =
net::GetTestCertsDirectory()
.AppendASCII(*GetParam().intermediate_cert)
.AddExtensionASCII("pem");
ASSERT_NO_FATAL_FAILURE(
SetIntermediateAuthorityInDeviceOncPolicy(intermediate_cert_path));
}
SetAutoSelectCertificatePatterns(GetParam().autoselect_patterns);
if (GetParam().prompt_on_multiple_matches) {
SetPromptOnMultipleMatchingCertificatesPolicy(
*GetParam().prompt_on_multiple_matches);
}
// Prepare the test server for the "act" part of the test.
net::SSLServerConfig server_config;
server_config.client_cert_type = net::SSLServerConfig::OPTIONAL_CLIENT_CERT;
server_config.cert_authorities = GetParam().ca_certs;
ASSERT_NO_FATAL_FAILURE(StartHttpsServer(server_config));
// Prepare the certificate selector hook for simulating the user gesture in
// the "act" part of the test.
if (GetParam().manually_select_cert)
SimulateUserWillSelectClientCert(*GetParam().manually_select_cert);
EXPECT_TRUE(LoginScreenTestApi::ClickAddUserButton());
WaitForGaiaPageLoadAndPropertyUpdate();
// Act: navigate to the page hosted by the test server.
absl::optional<net::SSLInfo> ssl_info =
RequestClientCertTestPageInFrame(test::OobeJS(), kSigninWebview);
ASSERT_TRUE(ssl_info);
// Assert the expectation on the client certificate that got selected.
if (GetParam().assert_cert) {
ASSERT_TRUE(ssl_info->cert);
EXPECT_THAT(*ssl_info->cert, EqualsCert(*GetParam().assert_cert));
} else {
EXPECT_FALSE(ssl_info->cert);
}
}
IN_PROC_BROWSER_TEST_P(SigninFrameWebviewClientCertsLoginTest, LockscreenTest) {
// Arrange the system slot.
ASSERT_NO_FATAL_FAILURE(
SetUpClientCertsInSystemSlot(GetParam().client_certs));
// Arrange the device policy.
if (GetParam().intermediate_cert) {
const base::FilePath intermediate_cert_path =
net::GetTestCertsDirectory()
.AppendASCII(*GetParam().intermediate_cert)
.AddExtensionASCII("pem");
ASSERT_NO_FATAL_FAILURE(
SetIntermediateAuthorityInDeviceOncPolicy(intermediate_cert_path));
}
SetAutoSelectCertificatePatterns(GetParam().autoselect_patterns);
if (GetParam().prompt_on_multiple_matches) {
SetPromptOnMultipleMatchingCertificatesPolicy(
*GetParam().prompt_on_multiple_matches);
}
// Prepare the test server for the "act" part of the test.
net::SSLServerConfig server_config;
server_config.client_cert_type = net::SSLServerConfig::OPTIONAL_CLIENT_CERT;
server_config.cert_authorities = GetParam().ca_certs;
ASSERT_NO_FATAL_FAILURE(StartHttpsServer(server_config));
// Prepare the certificate selector hook for simulating the user gesture in
// the "act" part of the test.
if (GetParam().manually_select_cert)
SimulateUserWillSelectClientCert(*GetParam().manually_select_cert);
// Log in a user and lock the screen, then trigger the lock screen SAML reauth
// dialog.
login_manager_mixin_.LoginWithDefaultContext(test_user_);
ScreenLockerTester().Lock();
absl::optional<LockScreenReauthDialogTestHelper> lock_screen_reauth_dialog =
LockScreenReauthDialogTestHelper::ShowDialogAndWait();
ASSERT_TRUE(lock_screen_reauth_dialog);
lock_screen_reauth_dialog->ForceSamlRedirect();
lock_screen_reauth_dialog->ExpectVerifyAccountScreenVisible();
lock_screen_reauth_dialog->ClickVerifyButton();
lock_screen_reauth_dialog->WaitForSamlScreen();
// Act: navigate to the page hosted by the test server in the sign-in frame of
// the lock screen SAML reauth dialog.
absl::optional<net::SSLInfo> ssl_info = RequestClientCertTestPageInFrame(
lock_screen_reauth_dialog->DialogJS(), kSigninWebviewOnLockScreen);
ASSERT_TRUE(ssl_info);
// Assert the expectation on the client certificate that got selected.
if (GetParam().assert_cert) {
ASSERT_TRUE(ssl_info->cert);
EXPECT_THAT(*ssl_info->cert, EqualsCert(*GetParam().assert_cert));
} else {
EXPECT_FALSE(ssl_info->cert);
}
}
// Test that client certificate authentication using certificates from the
// system slot is enabled in the sign-in frame. The server does not request
// certificates signed by a specific authority.
INSTANTIATE_TEST_SUITE_P(
SuccessSimple,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(SigninCertParam{
/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"},
/*prompt_on_multiple_matches=*/absl::nullopt,
/*ca_certs=*/{},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/kClientCert1Name}));
// Test that client certificate autoselect selects the right certificate even
// with multiple filters for the same pattern.
INSTANTIATE_TEST_SUITE_P(
SuccessMultipleFilters,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(SigninCertParam{
/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})",
R"({"pattern": "*", "filter": {"ISSUER": {"CN": "foo bar"}}})"},
/*prompt_on_multiple_matches=*/absl::nullopt,
/*ca_certs=*/{},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/kClientCert1Name}));
// Test that client certificate authentication using certificates from the
// system slot is enabled in the sign-in frame. The server requests a
// certificate signed by a specific authority.
INSTANTIATE_TEST_SUITE_P(
SuccessViaCa,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(SigninCertParam{
/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"},
/*prompt_on_multiple_matches=*/absl::nullopt,
/*ca_certs=*/
{// client_1_ca ("B CA")
{0x30, 0x0f, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x03, 0x55, 0x04, 0x03,
0x0c, 0x04, 0x42, 0x20, 0x43, 0x41}},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/kClientCert1Name}));
// Test that client certificate will be discovered if the server requests
// certificates signed by a root authority, the installed certificate has been
// issued by an intermediate authority, and the intermediate authority is
// known on the device (it has been made available through device ONC policy).
INSTANTIATE_TEST_SUITE_P(
SuccessViaCaAndIntermediate,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(SigninCertParam{
/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/"client_1_ca",
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"},
/*prompt_on_multiple_matches=*/absl::nullopt,
/*ca_certs=*/
{// client_root_ca ("C Root CA")
{0x30, 0x14, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x03,
0x0c, 0x09, 0x43, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41}},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/kClientCert1Name}));
// Test that if no client certificate is auto-selected using policy on the
// sign-in frame, the client does not send up any client certificate.
INSTANTIATE_TEST_SUITE_P(ErrorNoAutoSelect,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(SigninCertParam{
/*client_certs=*/{kClientCert1Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/{},
/*prompt_on_multiple_matches=*/absl::nullopt,
/*ca_certs=*/
{// client_1_ca ("B CA")
{0x30, 0x0f, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x03,
0x55, 0x04, 0x03, 0x0c, 0x04, 0x42, 0x20, 0x43,
0x41}},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/absl::nullopt}));
// Test that client certificate authentication using certificates from the
// system slot is enabled in the sign-in frame. The server requests
// a certificate signed by a specific authority. The client doesn't have a
// matching certificate.
INSTANTIATE_TEST_SUITE_P(
ErrorWrongCa,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(SigninCertParam{
/*client_certs=*/{kClientCert1Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"},
/*prompt_on_multiple_matches=*/absl::nullopt,
/*ca_certs=*/
{// client_2_ca ("E CA")
{0x30, 0x0f, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x03, 0x55, 0x04, 0x03,
0x0c, 0x04, 0x45, 0x20, 0x43, 0x41}},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/absl::nullopt}));
// Test that client certificate will not be discovered if the server requests
// certificates signed by a root authority, the installed certificate has been
// issued by an intermediate authority, and the intermediate authority is not
// known on the device (it has not been made available through device ONC
// policy).
INSTANTIATE_TEST_SUITE_P(
ErrorNoIntermediateCa,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(SigninCertParam{
/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"},
/*prompt_on_multiple_matches=*/absl::nullopt,
/*ca_certs=*/
{// client_root_ca ("C Root CA")
{0x30, 0x14, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x03,
0x0c, 0x09, 0x43, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41}},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/absl::nullopt}));
// Test that the DeviceLoginScreenPromptOnMultipleMatchingCertificates policy
// doesn't prevent the client cert from being auto-selected via policy.
INSTANTIATE_TEST_SUITE_P(
SuccessRegardlessOfPromptPolicy,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(
SigninCertParam{
/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"},
/*prompt_on_multiple_matches=*/false,
/*ca_certs=*/{},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/kClientCert1Name},
SigninCertParam{
/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"},
/*prompt_on_multiple_matches=*/true,
/*ca_certs=*/{},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/kClientCert1Name}));
// Test that the DeviceLoginScreenPromptOnMultipleMatchingCertificates policy
// doesn't affect the failure to select a client cert when no auto-selection is
// configured.
INSTANTIATE_TEST_SUITE_P(
ErrorNoPatternRegardlessOfPromptPolicy,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(SigninCertParam{/*client_certs=*/{kClientCert1Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/{},
/*prompt_on_multiple_matches=*/false,
/*ca_certs=*/{},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/absl::nullopt},
SigninCertParam{/*client_certs=*/{kClientCert1Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/{},
/*prompt_on_multiple_matches=*/true,
/*ca_certs=*/{},
/*manually_select_cert=*/absl::nullopt,
/*assert_cert=*/absl::nullopt}));
// Test that the certificate can be manually selected in case the auto-selection
// matches multiple certificates and the
// DeviceLoginScreenPromptOnMultipleMatchingCertificates policy is set to true.
INSTANTIATE_TEST_SUITE_P(
SuccessManualSelection,
SigninFrameWebviewClientCertsLoginTest,
testing::Values(
SigninCertParam{/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {}})"},
/*prompt_on_multiple_matches=*/true,
/*ca_certs=*/{},
/*manually_select_cert=*/kClientCert1Name,
/*assert_cert=*/kClientCert1Name},
SigninCertParam{/*client_certs=*/{kClientCert1Name, kClientCert2Name},
/*intermediate_cert=*/absl::nullopt,
/*autoselect_patterns=*/
{R"({"pattern": "*", "filter": {}})"},
/*prompt_on_multiple_matches=*/true,
/*ca_certs=*/{},
/*manually_select_cert=*/kClientCert2Name,
/*assert_cert=*/kClientCert2Name}));
// Tests the scenario where the system token is not initialized initially (due
// to the TPM not being ready).
class WebviewClientCertsTokenLoadingLoginTest
: public WebviewClientCertsLoginTestBase {
public:
WebviewClientCertsTokenLoadingLoginTest() {
// At very early stage, the system slot is being initialized becuase fake
// tpm manager tells the TPM is owned by default. So, it has to be overriden
// here instead of in the test body or `SetUpOnMainThread()`.
TpmManagerClient::InitializeFake();
TpmManagerClient::Get()
->GetTestInterface()
->mutable_nonsensitive_status_reply()
->set_is_owned(false);
}
WebviewClientCertsTokenLoadingLoginTest(
const WebviewClientCertsTokenLoadingLoginTest&) = delete;
WebviewClientCertsTokenLoadingLoginTest& operator=(
const WebviewClientCertsTokenLoadingLoginTest&) = delete;
// Prepares a testing system slot (without injecting it as an already
// initialized yet) and imports a client certificate into it.
void PrepareSystemSlot() {
bool out_system_slot_prepared_successfully = false;
base::RunLoop loop;
content::GetIOThreadTaskRunner({})->PostTaskAndReply(
FROM_HERE,
base::BindOnce(
&WebviewClientCertsTokenLoadingLoginTest::PrepareSystemSlotOnIO,
base::Unretained(this), &out_system_slot_prepared_successfully),
loop.QuitClosure());
loop.Run();
ASSERT_TRUE(out_system_slot_prepared_successfully);
ASSERT_NO_FATAL_FAILURE(ImportSystemSlotClientCerts(
{kClientCert1Name}, test_system_slot_nss_db_->slot()));
}
protected:
void SetUpOnMainThread() override {
TPMTokenLoader::Get()->enable_tpm_loading_for_testing(true);
WebviewClientCertsLoginTestBase::SetUpOnMainThread();
}
void TearDownOnMainThread() override {
TearDownTestSystemSlot();
WebviewClientCertsLoginTestBase::TearDownOnMainThread();
}
private:
void PrepareSystemSlotOnIO(bool* out_system_slot_prepared_successfully) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
test_system_slot_nss_db_ =
std::make_unique<crypto::ScopedTestSystemNSSKeySlot>(
/*simulate_token_loader=*/false);
*out_system_slot_prepared_successfully =
test_system_slot_nss_db_->ConstructedSuccessfully();
}
void TearDownTestSystemSlot() {
base::RunLoop loop;
content::GetIOThreadTaskRunner({})->PostTaskAndReply(
FROM_HERE,
base::BindOnce(&WebviewClientCertsTokenLoadingLoginTest::
TearDownTestSystemSlotOnIO,
base::Unretained(this)),
loop.QuitClosure());
loop.Run();
}
void TearDownTestSystemSlotOnIO() { test_system_slot_nss_db_.reset(); }
std::unique_ptr<crypto::ScopedTestSystemNSSKeySlot> test_system_slot_nss_db_;
};
namespace {
void GotIsTpmTokenEnabledOnUIThread(base::OnceClosure run_loop_quit_closure,
bool* is_ready,
bool is_tpm_token_enabled) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
*is_ready = is_tpm_token_enabled;
std::move(run_loop_quit_closure).Run();
}
void GotIsTpmTokenEnabledOnIOThread(base::OnceCallback<void(bool)> ui_callback,
bool is_tpm_token_enabled) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(ui_callback), is_tpm_token_enabled));
}
bool IsTpmTokenEnabled() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::RunLoop run_loop;
bool is_ready = false;
auto ui_callback =
base::BindOnce(&GotIsTpmTokenEnabledOnUIThread, run_loop.QuitClosure(),
base::Unretained(&is_ready));
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&crypto::IsTPMTokenEnabled,
base::BindOnce(&GotIsTpmTokenEnabledOnIOThread,
std::move(ui_callback))));
run_loop.Run();
return is_ready;
}
void GotIsSystemSlotAvailableOnUIThread(base::OnceClosure run_loop_quit_closure,
bool* result,
bool is_system_slot_available) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
*result = is_system_slot_available;
std::move(run_loop_quit_closure).Run();
}
void GotSystemSlotOnIOThread(base::OnceCallback<void(bool)> ui_callback,
crypto::ScopedPK11Slot system_slot) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(ui_callback), !!system_slot));
}
bool IsSystemSlotAvailable() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::RunLoop run_loop;
bool result = false;
auto ui_callback =
base::BindOnce(&GotIsSystemSlotAvailableOnUIThread,
run_loop.QuitClosure(), base::Unretained(&result));
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&crypto::GetSystemNSSKeySlot,
base::BindOnce(&GotSystemSlotOnIOThread,
std::move(ui_callback))));
run_loop.Run();
return result;
}
} // namespace
// Test that the system slot becomes initialized and the client certificate
// authentication works in the sign-in frame after the TPM gets reported as
// ready.
IN_PROC_BROWSER_TEST_F(WebviewClientCertsTokenLoadingLoginTest,
SystemSlotEnabled) {
ASSERT_NO_FATAL_FAILURE(PrepareSystemSlot());
net::SSLServerConfig server_config;
server_config.client_cert_type = net::SSLServerConfig::OPTIONAL_CLIENT_CERT;
ASSERT_NO_FATAL_FAILURE(StartHttpsServer(server_config));
const std::vector<std::string> autoselect_patterns = {
R"({"pattern": "*", "filter": {"ISSUER": {"CN": "B CA"}}})"};
SetAutoSelectCertificatePatterns(autoselect_patterns);
WaitForGaiaPageLoadAndPropertyUpdate();
// Report the TPM as ready, triggering the system token initialization by
// SystemTokenCertDBInitializer.
TpmManagerClient::Get()
->GetTestInterface()
->mutable_nonsensitive_status_reply()
->set_is_owned(true);
TpmManagerClient::Get()->GetTestInterface()->EmitOwnershipTakenSignal();
absl::optional<net::SSLInfo> ssl_info =
RequestClientCertTestPageInFrame(test::OobeJS(), kSigninWebview);
ASSERT_TRUE(ssl_info);
ASSERT_TRUE(ssl_info->cert);
EXPECT_THAT(*ssl_info->cert, EqualsCert(std::string(kClientCert1Name)));
EXPECT_TRUE(IsTpmTokenEnabled());
EXPECT_TRUE(IsSystemSlotAvailable());
}
IN_PROC_BROWSER_TEST_F(WebviewClientCertsTokenLoadingLoginTest,
SystemSlotDisabled) {
WaitForGaiaPageLoadAndPropertyUpdate();
// Report the TPM as ready, triggering the system token initialization by
// SystemTokenCertDBInitializer.
TpmManagerClient::Get()
->GetTestInterface()
->mutable_nonsensitive_status_reply()
->set_is_owned(false);
TpmManagerClient::Get()->GetTestInterface()->EmitOwnershipTakenSignal();
EXPECT_FALSE(IsTpmTokenEnabled());
EXPECT_FALSE(IsSystemSlotAvailable());
}
class WebviewProxyAuthLoginTest : public WebviewLoginTest {
public:
WebviewProxyAuthLoginTest()
: auth_proxy_server_(std::make_unique<net::SpawnedTestServer>(
net::SpawnedTestServer::TYPE_BASIC_AUTH_PROXY,
base::FilePath())) {}
WebviewProxyAuthLoginTest(const WebviewProxyAuthLoginTest&) = delete;
WebviewProxyAuthLoginTest& operator=(const WebviewProxyAuthLoginTest&) =
delete;
protected:
void SetUp() override {
// Start proxy server
auth_proxy_server_->set_redirect_connect_to_localhost(true);
ASSERT_TRUE(auth_proxy_server_->Start());
WebviewLoginTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(
::switches::kProxyServer,
auth_proxy_server_->host_port_pair().ToString());
WebviewLoginTest::SetUpCommandLine(command_line);
}
void SetUpInProcessBrowserTestFixture() override {
WebviewLoginTest::SetUpInProcessBrowserTestFixture();
// Prepare device policy which will be used for two purposes:
// - given to FakeSessionManagerClient, so the device appears to have
// registered for policy.
// - the payload is given to `policy_test_server_`, so we can download fresh
// policy.
device_policy_builder()->policy_data().set_public_key_version(1);
device_policy_builder()->Build();
UpdateServedPolicyFromDevicePolicyTestHelper();
FakeSessionManagerClient::Get()->set_device_policy(
device_policy_builder()->GetBlob());
// Set some fake state keys to make sure they are not empty.
std::vector<std::string> state_keys;
state_keys.push_back("1");
FakeSessionManagerClient::Get()->set_server_backed_state_keys(state_keys);
}
void SetUpOnMainThread() override {
// Setup the observer reacting on NOTIFICATION_AUTH_NEEDED before the test
// runs because there is no action we actively trigger to request proxy
// authentication. Instead, the sign-in screen automatically shows the gaia
// webview, which will request the gaia URL, which leads to a login prompt.
auth_needed_wait_loop_ = std::make_unique<base::RunLoop>();
auth_needed_observer_ =
std::make_unique<content::WindowedNotificationObserver>(
chrome::NOTIFICATION_AUTH_NEEDED,
base::BindRepeating(&WebviewProxyAuthLoginTest::OnAuthRequested,
base::Unretained(this)));
WebviewLoginTest::SetUpOnMainThread();
}
void TearDownOnMainThread() override {
WebviewLoginTest::TearDownOnMainThread();
auth_needed_observer_.reset();
auth_needed_wait_loop_.reset();
}
bool OnAuthRequested(const content::NotificationSource& source,
const content::NotificationDetails& details) {
// Only care for notifications originating from the frame which is
// displaying gaia.
content::WebContents* main_web_contents = GetLoginUI()->GetWebContents();
content::WebContents* gaia_frame_web_contents =
signin::GetAuthFrameWebContents(main_web_contents, gaia_frame_parent_);
LoginHandler* login_handler =
content::Details<LoginNotificationDetails>(details)->handler();
if (login_handler->web_contents() != gaia_frame_web_contents)
return false;
gaia_frame_login_handler_ = login_handler;
auth_needed_wait_loop_->Quit();
return true;
}
// Waits until proxy authentication has been requested by the frame displaying
// gaia. Returns the LoginHandler handling this authentication request.
LoginHandler* WaitForAuthRequested() {
auth_needed_wait_loop_->Run();
return gaia_frame_login_handler_;
}
void UpdateServedPolicyFromDevicePolicyTestHelper() {
policy_test_server_mixin_.UpdateDevicePolicy(
device_policy_builder()->payload());
}
policy::DevicePolicyBuilder* device_policy_builder() {
return &device_policy_builder_;
}
content::WindowedNotificationObserver* auth_needed_observer() {
return auth_needed_observer_.get();
}
private:
std::unique_ptr<content::WindowedNotificationObserver> auth_needed_observer_;
std::unique_ptr<base::RunLoop> auth_needed_wait_loop_;
// Unowned pointer - set to the LoginHandler of the frame displaying gaia.
LoginHandler* gaia_frame_login_handler_ = nullptr;
// A proxy server which requires authentication using the 'Basic'
// authentication method.
std::unique_ptr<net::SpawnedTestServer> auth_proxy_server_;
EmbeddedPolicyTestServerMixin policy_test_server_mixin_{&mixin_host_};
policy::DevicePolicyBuilder device_policy_builder_;
DeviceStateMixin device_state_{
&mixin_host_, DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED};
};
// Disabled fails on msan and also non-msan bots: https://crbug.com/849128.
IN_PROC_BROWSER_TEST_F(WebviewProxyAuthLoginTest, DISABLED_ProxyAuthTransfer) {
WaitForSigninScreen();
LoginHandler* login_handler = WaitForAuthRequested();
// Before entering auth data, make `policy_test_server_` serve a policy that
// we can use to detect if policies have been fetched.
em::ChromeDeviceSettingsProto& device_policy =
device_policy_builder()->payload();
device_policy.mutable_device_login_screen_auto_select_certificate_for_urls()
->add_login_screen_auto_select_certificate_rules("test_pattern");
UpdateServedPolicyFromDevicePolicyTestHelper();
policy::PolicyChangeRegistrar policy_change_registrar(
g_browser_process->platform_part()
->browser_policy_connector_ash()
->GetPolicyService(),
policy::PolicyNamespace(policy::POLICY_DOMAIN_CHROME,
std::string() /* component_id */));
// Now enter auth data
login_handler->SetAuth(u"foo", u"bar");
WaitForGaiaPageLoad();
base::RunLoop run_loop;
policy_change_registrar.Observe(
policy::key::kDeviceLoginScreenAutoSelectCertificateForUrls,
base::BindRepeating(&PolicyChangedCallback, run_loop.QuitClosure()));
run_loop.Run();
// Press the back button at a sign-in screen without pre-existing users to
// start a new sign-in attempt.
// This will re-load gaia, rotating the StoragePartition. The new
// StoragePartition must also have the proxy auth details.
test::OobeJS().ClickOnPath(kBackButton);
WaitForGaiaPageBackButtonUpdate();
// Expect that we got back to the identifier page, as there are no known users
// so the sign-in screen will not display user pods.
ExpectIdentifierPage();
}
class WebviewChildLoginTest : public WebviewLoginTest {
public:
WebviewChildLoginTest() = default;
// WebviewLoginTest:
void SetUpInProcessBrowserTestFixture() override {
user_policy_mixin_.RequestPolicyUpdate();
fake_gaia_.SetupFakeGaiaForChildUser(
child_account_id_.GetUserEmail(), child_account_id_.GetGaiaId(),
FakeGaiaMixin::kFakeRefreshToken, false /*issue_any_scope_token*/);
WebviewLoginTest::SetUpInProcessBrowserTestFixture();
}
protected:
AccountId child_account_id_{
AccountId::FromUserEmailGaiaId(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kFakeUserGaiaId)};
EmbeddedPolicyTestServerMixin policy_test_server_mixin_{&mixin_host_};
UserPolicyMixin user_policy_mixin_{&mixin_host_, child_account_id_,
&policy_test_server_mixin_};
};
// Test verfies case when user info message sent before authentication is
// finished.
IN_PROC_BROWSER_TEST_F(WebviewChildLoginTest, UserInfoSentBeforeAuthFinished) {
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
DisableImplicitServices();
SigninFrameJS().TypeIntoPath(child_account_id_.GetUserEmail(),
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
SigninFrameJS().ExecuteAsync("gaia.chromeOSLogin.sendUserInfo(['uca'])");
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
WaitForServicesSet();
// Timer should not be set.
test::OobeJS().ExpectFalse("$('gaia-signin').authenticator_.gaiaDoneTimer_");
test::WaitForPrimaryUserSessionStart();
const user_manager::UserManager* const user_manager =
user_manager::UserManager::Get();
EXPECT_TRUE(user_manager->GetActiveUser()->IsChild());
}
// Test verfies that user info message sent after authentication is finished
// still passes through.
IN_PROC_BROWSER_TEST_F(WebviewChildLoginTest, UserInfoSentAfterTimerSet) {
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
DisableImplicitServices();
SigninFrameJS().TypeIntoPath(child_account_id_.GetUserEmail(),
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
// Wait for user info timer to be set.
test::OobeJS()
.CreateWaiter("$('gaia-signin').authenticator_.gaiaDoneTimer_")
->Wait();
// Send user info after that.
SigninFrameJS().ExecuteAsync("gaia.chromeOSLogin.sendUserInfo(['uca'])");
test::WaitForPrimaryUserSessionStart();
const user_manager::UserManager* const user_manager =
user_manager::UserManager::Get();
EXPECT_TRUE(user_manager->GetActiveUser()->IsChild());
}
// Verifies flow when user info message is never sent.
IN_PROC_BROWSER_TEST_P(WebviewCloseViewLoginTest, UserInfoNeverSent) {
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
DisableImplicitServices();
// Test will send `closerView` manually (if the feature is enabled).
DisableCloseViewMessage();
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
if (GaiaSendsCloseView(GetParam()))
SigninFrameJS().ExecuteAsync("gaia.chromeOSLogin.sendCloseView()");
EmulateGaiaDoneTimeout();
test::WaitForPrimaryUserSessionStart();
histogram_tester_.ExpectUniqueSample("ChromeOS.Gaia.Message.Gaia.UserInfo",
false, 1);
const user_manager::UserManager* const user_manager =
user_manager::UserManager::Get();
EXPECT_FALSE(user_manager->GetActiveUser()->IsChild());
}
// Verifies `ChromeOS.Gaia.PasswordFlow` events are recorded.
IN_PROC_BROWSER_TEST_F(WebviewLoginTest, PasswordMetrics) {
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
// This should generate first "Started" event.
SigninFrameJS().ExecuteAsync(
"gaia.chromeOSLogin.attemptLogin('email@email.com', 'password')");
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
// This should generate second "Started" event. And also eventually
// "Completed" event.
test::OobeJS().ClickOnPath(kPrimaryButton);
test::WaitForPrimaryUserSessionStart();
histogram_tester_.ExpectBucketCount("ChromeOS.Gaia.PasswordFlow", 0, 2);
histogram_tester_.ExpectBucketCount("ChromeOS.Gaia.PasswordFlow", 1, 1);
}
class WebviewLoginEnrolledTest : public WebviewLoginTest {
public:
WebviewLoginEnrolledTest() = default;
~WebviewLoginEnrolledTest() override = default;
private:
DeviceStateMixin device_state_{
&mixin_host_, DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED};
};
// Verifies `OOBE.GaiaScreen.LoginRequests` and
// `OOBE.GaiaScreen.SuccessLoginRequests` are correctly recorded.
IN_PROC_BROWSER_TEST_F(WebviewLoginEnrolledTest, GaiaLoginVariantMetrics) {
WaitForGaiaPageLoadAndPropertyUpdate();
ExpectIdentifierPage();
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserEmail,
FakeGaiaMixin::kEmailPath);
test::OobeJS().ClickOnPath(kPrimaryButton);
// This should generate first "Started" event.
SigninFrameJS().TypeIntoPath(FakeGaiaMixin::kFakeUserPassword,
FakeGaiaMixin::kPasswordPath);
// This should generate second "Started" event. And also eventually
// "Completed" event.
test::OobeJS().ClickOnPath(kPrimaryButton);
test::WaitForPrimaryUserSessionStart();
histogram_tester_.ExpectUniqueSample(kLoginRequests,
GaiaView::GaiaLoginVariant::kAddUser, 1);
histogram_tester_.ExpectUniqueSample(kSuccessLoginRequests,
GaiaView::GaiaLoginVariant::kAddUser, 1);
}
INSTANTIATE_TEST_SUITE_P(All,
WebviewCloseViewLoginTest,
testing::Combine(testing::Bool(), testing::Bool()),
&WebviewCloseViewLoginTest::GetName);
} // namespace ash