summaryrefslogtreecommitdiff
path: root/chromium/chrome
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2022-05-04 11:03:41 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2022-05-11 13:53:16 +0000
commit9729c4479fe23554eae6e6dd1f30ff488f470c84 (patch)
treee72e659f304518ffb630835747cb9797d43f77b0 /chromium/chrome
parentc1eb7fe66e0e7a6e787f9127d99d293f71fbecb8 (diff)
downloadqtwebengine-chromium-9729c4479fe23554eae6e6dd1f30ff488f470c84.tar.gz
Add sources for push-notifications
Change-Id: Ibfaa78cdc3790b5936b2ae683328274ffc89c3bc Reviewed-by: Szabolcs David <davidsz@inf.u-szeged.hu> Reviewed-by: Peter Varga <pvarga@inf.u-szeged.hu>
Diffstat (limited to 'chromium/chrome')
-rw-r--r--chromium/chrome/browser/gcm/COMMON_METADATA4
-rw-r--r--chromium/chrome/browser/gcm/DIR_METADATA1
-rw-r--r--chromium/chrome/browser/gcm/OWNERS4
-rw-r--r--chromium/chrome/browser/gcm/gcm_product_util.cc57
-rw-r--r--chromium/chrome/browser/gcm/gcm_product_util.h30
-rw-r--r--chromium/chrome/browser/gcm/gcm_profile_service_factory.cc174
-rw-r--r--chromium/chrome/browser/gcm/gcm_profile_service_factory.h57
-rw-r--r--chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc269
-rw-r--r--chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc60
-rw-r--r--chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h44
-rw-r--r--chromium/chrome/browser/profiles/incognito_helpers.cc28
-rw-r--r--chromium/chrome/browser/profiles/incognito_helpers.h29
-rw-r--r--chromium/chrome/browser/push_messaging/DIR_METADATA1
-rw-r--r--chromium/chrome/browser/push_messaging/OWNERS1
-rw-r--r--chromium/chrome/browser/push_messaging/budget.proto30
-rw-r--r--chromium/chrome/browser/push_messaging/budget_database.cc400
-rw-r--r--chromium/chrome/browser/push_messaging/budget_database.h182
-rw-r--r--chromium/chrome/browser/push_messaging/budget_database_unittest.cc351
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc322
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h152
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc310
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc3227
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_constants.cc11
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_constants.h25
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_features.cc15
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_features.h21
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc353
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h122
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc87
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_refresher.cc121
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_refresher.h99
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc84
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc82
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_factory.h40
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc1684
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_impl.h475
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc480
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_utils.cc44
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_utils.h34
-rw-r--r--chromium/chrome/browser/signin/DEPS3
-rw-r--r--chromium/chrome/browser/signin/DIR_METADATA1
-rw-r--r--chromium/chrome/browser/signin/OWNERS5
-rw-r--r--chromium/chrome/browser/signin/about_signin_internals_factory.cc58
-rw-r--r--chromium/chrome/browser/signin/about_signin_internals_factory.h40
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager.cc208
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager.h97
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc53
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h35
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc259
-rw-r--r--chromium/chrome/browser/signin/account_id_from_account_info.cc24
-rw-r--r--chromium/chrome/browser/signin/account_id_from_account_info.h18
-rw-r--r--chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc20
-rw-r--r--chromium/chrome/browser/signin/account_investigator_factory.cc57
-rw-r--r--chromium/chrome/browser/signin/account_investigator_factory.h44
-rw-r--r--chromium/chrome/browser/signin/account_reconcilor_factory.cc212
-rw-r--r--chromium/chrome/browser/signin/account_reconcilor_factory.h57
-rw-r--r--chromium/chrome/browser/signin/chrome_device_id_helper.cc87
-rw-r--r--chromium/chrome/browser/signin/chrome_device_id_helper.h36
-rw-r--r--chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc40
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client.cc369
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client.h115
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_factory.cc34
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_factory.h37
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_test_util.cc21
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_test_util.h26
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_unittest.cc438
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_helper.cc712
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_helper.h129
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc182
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc551
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h100
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc359
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc141
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h58
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc61
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc189
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h72
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc281
-rw-r--r--chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc174
-rw-r--r--chromium/chrome/browser/signin/cookie_reminter_factory.cc37
-rw-r--r--chromium/chrome/browser/signin/cookie_reminter_factory.h30
-rw-r--r--chromium/chrome/browser/signin/dice_browsertest.cc1236
-rw-r--r--chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc179
-rw-r--r--chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h102
-rw-r--r--chromium/chrome/browser/signin/dice_response_handler.cc426
-rw-r--r--chromium/chrome/browser/signin/dice_response_handler.h187
-rw-r--r--chromium/chrome/browser/signin/dice_response_handler_unittest.cc820
-rw-r--r--chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc211
-rw-r--r--chromium/chrome/browser/signin/dice_signed_in_profile_creator.h70
-rw-r--r--chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc285
-rw-r--r--chromium/chrome/browser/signin/dice_tab_helper.cc117
-rw-r--r--chromium/chrome/browser/signin/dice_tab_helper.h97
-rw-r--r--chromium/chrome/browser/signin/dice_tab_helper_unittest.cc253
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor.cc851
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor.h411
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc1200
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc45
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h37
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc953
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc776
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/live_test.cc61
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/live_test.h33
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc75
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h46
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc100
-rw-r--r--chromium/chrome/browser/signin/force_signin_verifier.cc195
-rw-r--r--chromium/chrome/browser/signin/force_signin_verifier.h95
-rw-r--r--chromium/chrome/browser/signin/force_signin_verifier_unittest.cc416
-rw-r--r--chromium/chrome/browser/signin/header_modification_delegate.h38
-rw-r--r--chromium/chrome/browser/signin/header_modification_delegate_impl.cc154
-rw-r--r--chromium/chrome/browser/signin/header_modification_delegate_impl.h68
-rw-r--r--chromium/chrome/browser/signin/identity_manager_factory.cc171
-rw-r--r--chromium/chrome/browser/signin/identity_manager_factory.h67
-rw-r--r--chromium/chrome/browser/signin/identity_manager_provider.cc38
-rw-r--r--chromium/chrome/browser/signin/identity_manager_provider.h32
-rw-r--r--chromium/chrome/browser/signin/identity_services_provider_android.cc39
-rw-r--r--chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc103
-rw-r--r--chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h101
-rw-r--r--chromium/chrome/browser/signin/investigator_dependency_provider.cc14
-rw-r--r--chromium/chrome/browser/signin/investigator_dependency_provider.h33
-rw-r--r--chromium/chrome/browser/signin/logout_tab_helper.cc34
-rw-r--r--chromium/chrome/browser/signin/logout_tab_helper.h36
-rw-r--r--chromium/chrome/browser/signin/logout_tab_helper_unittest.cc24
-rw-r--r--chromium/chrome/browser/signin/mirror_browsertest.cc277
-rw-r--r--chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc146
-rw-r--r--chromium/chrome/browser/signin/process_dice_header_delegate_impl.h77
-rw-r--r--chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc375
-rw-r--r--chromium/chrome/browser/signin/reauth_result.h38
-rw-r--r--chromium/chrome/browser/signin/reauth_tab_helper.cc96
-rw-r--r--chromium/chrome/browser/signin/reauth_tab_helper.h66
-rw-r--r--chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc184
-rw-r--r--chromium/chrome/browser/signin/reauth_util.cc40
-rw-r--r--chromium/chrome/browser/signin/reauth_util.h24
-rw-r--r--chromium/chrome/browser/signin/reauth_util_unittest.cc33
-rw-r--r--chromium/chrome/browser/signin/remove_local_account_browsertest.cc143
-rw-r--r--chromium/chrome/browser/signin/services/DIR_METADATA1
-rw-r--r--chromium/chrome/browser/signin/services/OWNERS2
-rw-r--r--chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java120
-rw-r--r--chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java89
-rw-r--r--chromium/chrome/browser/signin/signin_error_controller_factory.cc48
-rw-r--r--chromium/chrome/browser/signin/signin_error_controller_factory.h37
-rw-r--r--chromium/chrome/browser/signin/signin_features.cc16
-rw-r--r--chromium/chrome/browser/signin/signin_features.h15
-rw-r--r--chromium/chrome/browser/signin/signin_global_error.cc170
-rw-r--r--chromium/chrome/browser/signin/signin_global_error.h64
-rw-r--r--chromium/chrome/browser/signin/signin_global_error_factory.cc46
-rw-r--r--chromium/chrome/browser/signin/signin_global_error_factory.h40
-rw-r--r--chromium/chrome/browser/signin/signin_global_error_unittest.cc166
-rw-r--r--chromium/chrome/browser/signin/signin_manager.cc200
-rw-r--r--chromium/chrome/browser/signin/signin_manager.h71
-rw-r--r--chromium/chrome/browser/signin/signin_manager_android_factory.cc41
-rw-r--r--chromium/chrome/browser/signin/signin_manager_android_factory.h32
-rw-r--r--chromium/chrome/browser/signin/signin_manager_factory.cc48
-rw-r--r--chromium/chrome/browser/signin/signin_manager_factory.h33
-rw-r--r--chromium/chrome/browser/signin/signin_manager_unittest.cc421
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater.cc72
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater.h56
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc53
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h42
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc224
-rw-r--r--chromium/chrome/browser/signin/signin_promo.cc143
-rw-r--r--chromium/chrome/browser/signin/signin_promo.h83
-rw-r--r--chromium/chrome/browser/signin/signin_promo_unittest.cc56
-rw-r--r--chromium/chrome/browser/signin/signin_promo_util.cc49
-rw-r--r--chromium/chrome/browser/signin/signin_promo_util.h18
-rw-r--r--chromium/chrome/browser/signin/signin_ui_util.cc608
-rw-r--r--chromium/chrome/browser/signin/signin_ui_util.h188
-rw-r--r--chromium/chrome/browser/signin/signin_ui_util_browsertest.cc85
-rw-r--r--chromium/chrome/browser/signin/signin_ui_util_unittest.cc736
-rw-r--r--chromium/chrome/browser/signin/signin_util.cc382
-rw-r--r--chromium/chrome/browser/signin/signin_util.h73
-rw-r--r--chromium/chrome/browser/signin/signin_util_unittest.cc127
-rw-r--r--chromium/chrome/browser/signin/signin_util_win.cc332
-rw-r--r--chromium/chrome/browser/signin/signin_util_win.h31
-rw-r--r--chromium/chrome/browser/signin/signin_util_win_browsertest.cc698
-rw-r--r--chromium/chrome/browser/signin/test_signin_client_builder.cc18
-rw-r--r--chromium/chrome/browser/signin/test_signin_client_builder.h26
-rw-r--r--chromium/chrome/browser/signin/token_revoker_test_utils.cc36
-rw-r--r--chromium/chrome/browser/signin/token_revoker_test_utils.h42
179 files changed, 32546 insertions, 0 deletions
diff --git a/chromium/chrome/browser/gcm/COMMON_METADATA b/chromium/chrome/browser/gcm/COMMON_METADATA
new file mode 100644
index 00000000000..777cdb6a192
--- /dev/null
+++ b/chromium/chrome/browser/gcm/COMMON_METADATA
@@ -0,0 +1,4 @@
+monorail: {
+ component: "Services>CloudMessaging"
+}
+team_email: "platform-capabilities@chromium.org"
diff --git a/chromium/chrome/browser/gcm/DIR_METADATA b/chromium/chrome/browser/gcm/DIR_METADATA
new file mode 100644
index 00000000000..66fe8683773
--- /dev/null
+++ b/chromium/chrome/browser/gcm/DIR_METADATA
@@ -0,0 +1 @@
+mixins: "//chrome/browser/gcm/COMMON_METADATA"
diff --git a/chromium/chrome/browser/gcm/OWNERS b/chromium/chrome/browser/gcm/OWNERS
new file mode 100644
index 00000000000..06f7d3cbe88
--- /dev/null
+++ b/chromium/chrome/browser/gcm/OWNERS
@@ -0,0 +1,4 @@
+dimich@chromium.org
+fgorski@chromium.org
+jianli@chromium.org
+peter@chromium.org
diff --git a/chromium/chrome/browser/gcm/gcm_product_util.cc b/chromium/chrome/browser/gcm/gcm_product_util.cc
new file mode 100644
index 00000000000..cb2fa486c11
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_product_util.cc
@@ -0,0 +1,57 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/gcm/gcm_product_util.h"
+
+#include "base/strings/string_piece.h"
+#include "base/strings/string_util.h"
+#include "chrome/common/chrome_version.h"
+#include "chrome/common/pref_names.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/pref_service.h"
+#include "components/version_info/version_info.h"
+
+namespace gcm {
+
+namespace {
+
+std::string ToLowerAlphaNum(base::StringPiece in) {
+ std::string out;
+ out.reserve(in.size());
+ for (char ch : in) {
+ if (base::IsAsciiAlpha(ch) || base::IsAsciiDigit(ch))
+ out.push_back(base::ToLowerASCII(ch));
+ }
+ return out;
+}
+
+} // namespace
+
+std::string GetProductCategoryForSubtypes(PrefService* prefs) {
+ std::string product_category_for_subtypes =
+ prefs->GetString(prefs::kGCMProductCategoryForSubtypes);
+ if (!product_category_for_subtypes.empty())
+ return product_category_for_subtypes;
+
+ std::string product = ToLowerAlphaNum(PRODUCT_SHORTNAME_STRING);
+ std::string ns = product == "chromium" ? "org" : "com";
+ std::string platform = ToLowerAlphaNum(version_info::GetOSType());
+ product_category_for_subtypes = ns + '.' + product + '.' + platform;
+
+ prefs->SetString(prefs::kGCMProductCategoryForSubtypes,
+ product_category_for_subtypes);
+ return product_category_for_subtypes;
+}
+
+void RegisterPrefs(PrefRegistrySimple* registry) {
+ registry->RegisterStringPref(prefs::kGCMProductCategoryForSubtypes,
+ std::string() /* default_value */);
+}
+
+void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
+ RegisterPrefs(registry);
+}
+
+} // namespace gcm
diff --git a/chromium/chrome/browser/gcm/gcm_product_util.h b/chromium/chrome/browser/gcm/gcm_product_util.h
new file mode 100644
index 00000000000..f91c869b310
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_product_util.h
@@ -0,0 +1,30 @@
+// Copyright 2016 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.
+
+#ifndef CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_
+#define CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_
+
+#include <string>
+
+class PrefRegistrySimple;
+class PrefService;
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+namespace gcm {
+
+// Returns a string like "com.chrome.macosx" that should be used as the GCM
+// category when an app_id is sent as a subtype instead of as a category. This
+// is generated once, then remains fixed forever (even if the product name
+// changes), since it must match existing Instance ID tokens.
+std::string GetProductCategoryForSubtypes(PrefService* prefs);
+
+void RegisterPrefs(PrefRegistrySimple* registry);
+void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+} // namespace gcm
+
+#endif // CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_
diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc b/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc
new file mode 100644
index 00000000000..b3207963fdb
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc
@@ -0,0 +1,174 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include <memory>
+
+#include "base/bind.h"
+#include "base/no_destructor.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/thread_pool.h"
+#include "build/build_config.h"
+#include "chrome/browser/profiles/incognito_helpers.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_key.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/offline_pages/buildflags/buildflags.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "services/network/public/mojom/network_context.mojom.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/gcm/gcm_product_util.h"
+#include "chrome/common/channel_info.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "content/public/browser/browser_context.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#endif
+
+#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
+#include "chrome/browser/offline_pages/prefetch/prefetch_service_factory.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/offline_pages/core/offline_page_feature.h"
+#include "components/offline_pages/core/prefetch/prefetch_gcm_app_handler.h"
+#include "components/offline_pages/core/prefetch/prefetch_service.h"
+#endif
+
+namespace gcm {
+
+namespace {
+
+#if !defined(OS_ANDROID)
+// Requests a ProxyResolvingSocketFactory on the UI thread. Note that a WeakPtr
+// of GCMProfileService is needed to detect when the KeyedService shuts down,
+// and avoid calling into |profile| which might have also been destroyed.
+void RequestProxyResolvingSocketFactoryOnUIThread(
+ Profile* profile,
+ base::WeakPtr<GCMProfileService> service,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>
+ receiver) {
+ if (!service)
+ return;
+ network::mojom::NetworkContext* network_context =
+ profile->GetDefaultStoragePartition()->GetNetworkContext();
+ network_context->CreateProxyResolvingSocketFactory(std::move(receiver));
+}
+
+// A thread-safe wrapper to request a ProxyResolvingSocketFactory.
+void RequestProxyResolvingSocketFactory(
+ Profile* profile,
+ base::WeakPtr<GCMProfileService> service,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>
+ receiver) {
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE,
+ base::BindOnce(&RequestProxyResolvingSocketFactoryOnUIThread, profile,
+ std::move(service), std::move(receiver)));
+}
+#endif
+
+BrowserContextKeyedServiceFactory::TestingFactory& GetTestingFactory() {
+ static base::NoDestructor<BrowserContextKeyedServiceFactory::TestingFactory>
+ testing_factory;
+ return *testing_factory;
+}
+
+} // namespace
+
+GCMProfileServiceFactory::ScopedTestingFactoryInstaller::
+ ScopedTestingFactoryInstaller(TestingFactory testing_factory) {
+ DCHECK(!GetTestingFactory());
+ GetTestingFactory() = std::move(testing_factory);
+}
+
+GCMProfileServiceFactory::ScopedTestingFactoryInstaller::
+ ~ScopedTestingFactoryInstaller() {
+ GetTestingFactory() = BrowserContextKeyedServiceFactory::TestingFactory();
+}
+
+// static
+GCMProfileService* GCMProfileServiceFactory::GetForProfile(
+ content::BrowserContext* profile) {
+ // GCM is not supported in incognito mode.
+ if (profile->IsOffTheRecord())
+ return NULL;
+
+ return static_cast<GCMProfileService*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+GCMProfileServiceFactory* GCMProfileServiceFactory::GetInstance() {
+ static base::NoDestructor<GCMProfileServiceFactory> instance;
+ return instance.get();
+}
+
+GCMProfileServiceFactory::GCMProfileServiceFactory()
+ : BrowserContextKeyedServiceFactory(
+ "GCMProfileService",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
+ DependsOn(offline_pages::PrefetchServiceFactory::GetInstance());
+#endif // BUILDFLAG(ENABLE_OFFLINE_PAGES)
+}
+
+GCMProfileServiceFactory::~GCMProfileServiceFactory() {
+}
+
+KeyedService* GCMProfileServiceFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ DCHECK(!profile->IsOffTheRecord());
+
+ TestingFactory& testing_factory = GetTestingFactory();
+ if (testing_factory)
+ return testing_factory.Run(context).release();
+
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner(
+ base::ThreadPool::CreateSequencedTaskRunner(
+ {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+ base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}));
+ std::unique_ptr<GCMProfileService> service;
+#if defined(OS_ANDROID)
+ service = std::make_unique<GCMProfileService>(profile->GetPath(),
+ blocking_task_runner);
+#else
+ service = std::make_unique<GCMProfileService>(
+ profile->GetPrefs(), profile->GetPath(),
+ base::BindRepeating(&RequestProxyResolvingSocketFactory, profile),
+ profile->GetDefaultStoragePartition()
+ ->GetURLLoaderFactoryForBrowserProcess(),
+ content::GetNetworkConnectionTracker(), chrome::GetChannel(),
+ gcm::GetProductCategoryForSubtypes(profile->GetPrefs()),
+ IdentityManagerFactory::GetForProfile(profile),
+ std::make_unique<GCMClientFactory>(), content::GetUIThreadTaskRunner({}),
+ content::GetIOThreadTaskRunner({}), blocking_task_runner);
+#endif
+#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
+ offline_pages::PrefetchService* prefetch_service =
+ offline_pages::PrefetchServiceFactory::GetForKey(
+ profile->GetProfileKey());
+ if (prefetch_service != nullptr) {
+ offline_pages::PrefetchGCMHandler* prefetch_gcm_handler =
+ prefetch_service->GetPrefetchGCMHandler();
+ service->driver()->AddAppHandler(prefetch_gcm_handler->GetAppId(),
+ prefetch_gcm_handler->AsGCMAppHandler());
+ }
+#endif // BUILDFLAG(ENABLE_OFFLINE_PAGES)
+
+ return service.release();
+}
+
+content::BrowserContext* GCMProfileServiceFactory::GetBrowserContextToUse(
+ content::BrowserContext* context) const {
+ return chrome::GetBrowserContextOwnInstanceInIncognito(context);
+}
+
+} // namespace gcm
diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_factory.h b/chromium/chrome/browser/gcm/gcm_profile_service_factory.h
new file mode 100644
index 00000000000..13511e84611
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_profile_service_factory.h
@@ -0,0 +1,57 @@
+// Copyright (c) 2013 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.
+
+#ifndef CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_
+
+#include "base/no_destructor.h"
+#include "components/gcm_driver/system_encryptor.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace gcm {
+
+class GCMProfileService;
+
+// Singleton that owns all GCMProfileService and associates them with
+// Profiles.
+class GCMProfileServiceFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static GCMProfileService* GetForProfile(content::BrowserContext* profile);
+ static GCMProfileServiceFactory* GetInstance();
+
+ // Helper registering a testing factory. Needs to be instantiated before the
+ // factory is accessed in your test, and deallocated after the last access.
+ // Usually this is achieved by putting this object as the first member in
+ // your test fixture.
+ class ScopedTestingFactoryInstaller {
+ public:
+ explicit ScopedTestingFactoryInstaller(TestingFactory testing_factory);
+
+ ScopedTestingFactoryInstaller(const ScopedTestingFactoryInstaller&) =
+ delete;
+ ScopedTestingFactoryInstaller& operator=(
+ const ScopedTestingFactoryInstaller&) = delete;
+
+ ~ScopedTestingFactoryInstaller();
+ };
+
+ GCMProfileServiceFactory(const GCMProfileServiceFactory&) = delete;
+ GCMProfileServiceFactory& operator=(const GCMProfileServiceFactory&) = delete;
+
+ private:
+ friend base::NoDestructor<GCMProfileServiceFactory>;
+
+ GCMProfileServiceFactory();
+ ~GCMProfileServiceFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ content::BrowserContext* GetBrowserContextToUse(
+ content::BrowserContext* context) const override;
+};
+
+} // namespace gcm
+
+#endif // CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_
diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc b/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc
new file mode 100644
index 00000000000..2a13932a454
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc
@@ -0,0 +1,269 @@
+// Copyright 2014 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 <memory>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/thread_pool.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/gcm/gcm_product_util.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/common/channel_info.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/gcm_driver/fake_gcm_app_handler.h"
+#include "components/gcm_driver/fake_gcm_client.h"
+#include "components/gcm_driver/fake_gcm_client_factory.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/mojom/network_context.mojom.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chromeos/dbus/concierge/concierge_client.h"
+#endif
+
+namespace gcm {
+
+namespace {
+
+const char kTestAppID[] = "TestApp";
+const char kUserID[] = "user";
+
+void RequestProxyResolvingSocketFactoryOnUIThread(
+ Profile* profile,
+ base::WeakPtr<gcm::GCMProfileService> service,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>
+ receiver) {
+ if (!service)
+ return;
+ return profile->GetDefaultStoragePartition()
+ ->GetNetworkContext()
+ ->CreateProxyResolvingSocketFactory(std::move(receiver));
+}
+
+void RequestProxyResolvingSocketFactory(
+ Profile* profile,
+ base::WeakPtr<gcm::GCMProfileService> service,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>
+ receiver) {
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(&RequestProxyResolvingSocketFactoryOnUIThread,
+ profile, service, std::move(receiver)));
+}
+
+std::unique_ptr<KeyedService> BuildGCMProfileService(
+ content::BrowserContext* context) {
+ Profile* profile = Profile::FromBrowserContext(context);
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner(
+ base::ThreadPool::CreateSequencedTaskRunner(
+ {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}));
+ return std::make_unique<gcm::GCMProfileService>(
+ profile->GetPrefs(), profile->GetPath(),
+ base::BindRepeating(&RequestProxyResolvingSocketFactory, profile),
+ profile->GetDefaultStoragePartition()
+ ->GetURLLoaderFactoryForBrowserProcess(),
+ network::TestNetworkConnectionTracker::GetInstance(),
+ chrome::GetChannel(),
+ gcm::GetProductCategoryForSubtypes(profile->GetPrefs()),
+ IdentityManagerFactory::GetForProfile(profile),
+ std::unique_ptr<gcm::GCMClientFactory>(
+ new gcm::FakeGCMClientFactory(content::GetUIThreadTaskRunner({}),
+ content::GetIOThreadTaskRunner({}))),
+ content::GetUIThreadTaskRunner({}), content::GetIOThreadTaskRunner({}),
+ blocking_task_runner);
+}
+
+} // namespace
+
+class GCMProfileServiceTest : public testing::Test {
+ public:
+ GCMProfileServiceTest(const GCMProfileServiceTest&) = delete;
+ GCMProfileServiceTest& operator=(const GCMProfileServiceTest&) = delete;
+
+ protected:
+ GCMProfileServiceTest();
+ ~GCMProfileServiceTest() override;
+
+ // testing::Test:
+ void SetUp() override;
+ void TearDown() override;
+
+ FakeGCMClient* GetGCMClient() const;
+
+ void CreateGCMProfileService();
+
+ void RegisterAndWaitForCompletion(const std::vector<std::string>& sender_ids);
+ void UnregisterAndWaitForCompletion();
+ void SendAndWaitForCompletion(const OutgoingMessage& message);
+
+ void RegisterCompleted(base::OnceClosure callback,
+ const std::string& registration_id,
+ GCMClient::Result result);
+ void UnregisterCompleted(base::OnceClosure callback,
+ GCMClient::Result result);
+ void SendCompleted(base::OnceClosure callback,
+ const std::string& message_id,
+ GCMClient::Result result);
+
+ GCMDriver* driver() const { return gcm_profile_service_->driver(); }
+ std::string registration_id() const { return registration_id_; }
+ GCMClient::Result registration_result() const { return registration_result_; }
+ GCMClient::Result unregistration_result() const {
+ return unregistration_result_;
+ }
+ std::string send_message_id() const { return send_message_id_; }
+ GCMClient::Result send_result() const { return send_result_; }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<TestingProfile> profile_;
+ raw_ptr<GCMProfileService> gcm_profile_service_;
+ std::unique_ptr<FakeGCMAppHandler> gcm_app_handler_;
+
+ std::string registration_id_;
+ GCMClient::Result registration_result_;
+ GCMClient::Result unregistration_result_;
+ std::string send_message_id_;
+ GCMClient::Result send_result_;
+};
+
+GCMProfileServiceTest::GCMProfileServiceTest()
+ : gcm_profile_service_(nullptr),
+ gcm_app_handler_(new FakeGCMAppHandler),
+ registration_result_(GCMClient::UNKNOWN_ERROR),
+ send_result_(GCMClient::UNKNOWN_ERROR) {}
+
+GCMProfileServiceTest::~GCMProfileServiceTest() {
+}
+
+FakeGCMClient* GCMProfileServiceTest::GetGCMClient() const {
+ return static_cast<FakeGCMClient*>(
+ gcm_profile_service_->driver()->GetGCMClientForTesting());
+}
+
+void GCMProfileServiceTest::SetUp() {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ chromeos::ConciergeClient::InitializeFake(/*fake_cicerone_client=*/nullptr);
+#endif
+ TestingProfile::Builder builder;
+ profile_ = builder.Build();
+}
+
+void GCMProfileServiceTest::TearDown() {
+ gcm_profile_service_->driver()->RemoveAppHandler(kTestAppID);
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ profile_.reset();
+ chromeos::ConciergeClient::Shutdown();
+#endif
+}
+
+void GCMProfileServiceTest::CreateGCMProfileService() {
+ gcm_profile_service_ = static_cast<GCMProfileService*>(
+ GCMProfileServiceFactory::GetInstance()->SetTestingFactoryAndUse(
+ profile_.get(), base::BindRepeating(&BuildGCMProfileService)));
+ gcm_profile_service_->driver()->AddAppHandler(
+ kTestAppID, gcm_app_handler_.get());
+}
+
+void GCMProfileServiceTest::RegisterAndWaitForCompletion(
+ const std::vector<std::string>& sender_ids) {
+ base::RunLoop run_loop;
+ gcm_profile_service_->driver()->Register(
+ kTestAppID, sender_ids,
+ base::BindOnce(&GCMProfileServiceTest::RegisterCompleted,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+}
+
+void GCMProfileServiceTest::UnregisterAndWaitForCompletion() {
+ base::RunLoop run_loop;
+ gcm_profile_service_->driver()->Unregister(
+ kTestAppID,
+ base::BindOnce(&GCMProfileServiceTest::UnregisterCompleted,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+}
+
+void GCMProfileServiceTest::SendAndWaitForCompletion(
+ const OutgoingMessage& message) {
+ base::RunLoop run_loop;
+ gcm_profile_service_->driver()->Send(
+ kTestAppID, kUserID, message,
+ base::BindOnce(&GCMProfileServiceTest::SendCompleted,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+}
+
+void GCMProfileServiceTest::RegisterCompleted(
+ base::OnceClosure callback,
+ const std::string& registration_id,
+ GCMClient::Result result) {
+ registration_id_ = registration_id;
+ registration_result_ = result;
+ std::move(callback).Run();
+}
+
+void GCMProfileServiceTest::UnregisterCompleted(base::OnceClosure callback,
+ GCMClient::Result result) {
+ unregistration_result_ = result;
+ std::move(callback).Run();
+}
+
+void GCMProfileServiceTest::SendCompleted(base::OnceClosure callback,
+ const std::string& message_id,
+ GCMClient::Result result) {
+ send_message_id_ = message_id;
+ send_result_ = result;
+ std::move(callback).Run();
+}
+
+TEST_F(GCMProfileServiceTest, RegisterAndUnregister) {
+ CreateGCMProfileService();
+
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender");
+ RegisterAndWaitForCompletion(sender_ids);
+
+ std::string expected_registration_id =
+ FakeGCMClient::GenerateGCMRegistrationID(sender_ids);
+ EXPECT_EQ(expected_registration_id, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+
+ UnregisterAndWaitForCompletion();
+ EXPECT_EQ(GCMClient::SUCCESS, unregistration_result());
+}
+
+TEST_F(GCMProfileServiceTest, Send) {
+ CreateGCMProfileService();
+
+ OutgoingMessage message;
+ message.id = "1";
+ message.data["key1"] = "value1";
+ SendAndWaitForCompletion(message);
+
+ EXPECT_EQ(message.id, send_message_id());
+ EXPECT_EQ(GCMClient::SUCCESS, send_result());
+}
+
+} // namespace gcm
diff --git a/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc
new file mode 100644
index 00000000000..8123d8f04c6
--- /dev/null
+++ b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc
@@ -0,0 +1,60 @@
+// Copyright (c) 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 "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
+
+#include <memory>
+
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/profiles/incognito_helpers.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+namespace instance_id {
+
+// static
+InstanceIDProfileService* InstanceIDProfileServiceFactory::GetForProfile(
+ content::BrowserContext* profile) {
+ // Instance ID is not supported in incognito mode.
+ if (profile->IsOffTheRecord())
+ return NULL;
+
+ return static_cast<InstanceIDProfileService*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+InstanceIDProfileServiceFactory*
+InstanceIDProfileServiceFactory::GetInstance() {
+ return base::Singleton<InstanceIDProfileServiceFactory>::get();
+}
+
+InstanceIDProfileServiceFactory::InstanceIDProfileServiceFactory()
+ : BrowserContextKeyedServiceFactory(
+ "InstanceIDProfileService",
+ BrowserContextDependencyManager::GetInstance()) {
+ // GCM is needed for device ID.
+ DependsOn(gcm::GCMProfileServiceFactory::GetInstance());
+}
+
+InstanceIDProfileServiceFactory::~InstanceIDProfileServiceFactory() {
+}
+
+KeyedService* InstanceIDProfileServiceFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ return new InstanceIDProfileService(
+ gcm::GCMProfileServiceFactory::GetForProfile(profile)->driver(),
+ profile->IsOffTheRecord());
+}
+
+content::BrowserContext*
+InstanceIDProfileServiceFactory::GetBrowserContextToUse(
+ content::BrowserContext* context) const {
+ return chrome::GetBrowserContextOwnInstanceInIncognito(context);
+}
+
+} // namespace instance_id
diff --git a/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h
new file mode 100644
index 00000000000..c4505760a3d
--- /dev/null
+++ b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h
@@ -0,0 +1,44 @@
+// Copyright (c) 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.
+
+#ifndef CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace instance_id {
+
+class InstanceIDProfileService;
+
+// Singleton that owns all InstanceIDProfileService and associates them with
+// profiles.
+class InstanceIDProfileServiceFactory :
+ public BrowserContextKeyedServiceFactory {
+ public:
+ static InstanceIDProfileService* GetForProfile(
+ content::BrowserContext* profile);
+ static InstanceIDProfileServiceFactory* GetInstance();
+
+ InstanceIDProfileServiceFactory(const InstanceIDProfileServiceFactory&) =
+ delete;
+ InstanceIDProfileServiceFactory& operator=(
+ const InstanceIDProfileServiceFactory&) = delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<InstanceIDProfileServiceFactory>;
+
+ InstanceIDProfileServiceFactory();
+ ~InstanceIDProfileServiceFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ content::BrowserContext* GetBrowserContextToUse(
+ content::BrowserContext* context) const override;
+};
+
+} // namespace instance_id
+
+#endif // CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_
diff --git a/chromium/chrome/browser/profiles/incognito_helpers.cc b/chromium/chrome/browser/profiles/incognito_helpers.cc
new file mode 100644
index 00000000000..a319237928b
--- /dev/null
+++ b/chromium/chrome/browser/profiles/incognito_helpers.cc
@@ -0,0 +1,28 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/profiles/incognito_helpers.h"
+
+#include "chrome/browser/profiles/profile.h"
+
+namespace chrome {
+
+content::BrowserContext* GetBrowserContextRedirectedInIncognito(
+ content::BrowserContext* context) {
+ return Profile::FromBrowserContext(context)->GetOriginalProfile();
+}
+
+const content::BrowserContext* GetBrowserContextRedirectedInIncognito(
+ const content::BrowserContext* context) {
+ const Profile* profile = Profile::FromBrowserContext(
+ const_cast<content::BrowserContext*>(context));
+ return profile->GetOriginalProfile();
+}
+
+content::BrowserContext* GetBrowserContextOwnInstanceInIncognito(
+ content::BrowserContext* context) {
+ return context;
+}
+
+} // namespace chrome
diff --git a/chromium/chrome/browser/profiles/incognito_helpers.h b/chromium/chrome/browser/profiles/incognito_helpers.h
new file mode 100644
index 00000000000..e8e76ce5b95
--- /dev/null
+++ b/chromium/chrome/browser/profiles/incognito_helpers.h
@@ -0,0 +1,29 @@
+// Copyright 2013 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.
+
+#ifndef CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_
+#define CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_
+
+namespace content {
+class BrowserContext;
+}
+
+namespace chrome {
+
+// Returns the original browser context even for Incognito contexts.
+content::BrowserContext* GetBrowserContextRedirectedInIncognito(
+ content::BrowserContext* context);
+
+// Returns the original browser context even for Incognito contexts.
+const content::BrowserContext* GetBrowserContextRedirectedInIncognito(
+ const content::BrowserContext* context);
+
+// Returns non-NULL even for Incognito contexts so that a separate
+// instance of a service is created for the Incognito context.
+content::BrowserContext* GetBrowserContextOwnInstanceInIncognito(
+ content::BrowserContext* context);
+
+} // namespace chrome
+
+#endif // CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_
diff --git a/chromium/chrome/browser/push_messaging/DIR_METADATA b/chromium/chrome/browser/push_messaging/DIR_METADATA
new file mode 100644
index 00000000000..a684b81174a
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/DIR_METADATA
@@ -0,0 +1 @@
+mixins: "//content/browser/push_messaging/COMMON_METADATA"
diff --git a/chromium/chrome/browser/push_messaging/OWNERS b/chromium/chrome/browser/push_messaging/OWNERS
new file mode 100644
index 00000000000..d09ffef01de
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/OWNERS
@@ -0,0 +1 @@
+file://content/browser/push_messaging/OWNERS
diff --git a/chromium/chrome/browser/push_messaging/budget.proto b/chromium/chrome/browser/push_messaging/budget.proto
new file mode 100644
index 00000000000..229ea5370b7
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/budget.proto
@@ -0,0 +1,30 @@
+// Copyright 2016 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.
+
+syntax = "proto2";
+
+package budget_service;
+
+// Chrome requires this.
+option optimize_for = LITE_RUNTIME;
+
+// Next available id: 3
+message Budget {
+ // The sequence of budget chunks and their expiration times.
+ repeated BudgetChunk budget = 1;
+
+ // The timestamp of the last time that new engagement budget was awarded.
+ // This stores the internal value needed to construct a base::Time object.
+ optional int64 engagement_last_updated = 2;
+}
+
+// Next available id: 3
+message BudgetChunk {
+ // The amount of budget remaining in this chunk.
+ optional double amount = 1;
+
+ // The timestamp when the budget expires. This stores the internal value
+ // needed to construct a base::Time object.
+ optional int64 expiration = 2;
+}
diff --git a/chromium/chrome/browser/push_messaging/budget_database.cc b/chromium/chrome/browser/push_messaging/budget_database.cc
new file mode 100644
index 00000000000..96b2d36d01d
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/budget_database.cc
@@ -0,0 +1,400 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/budget_database.h"
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/task/post_task.h"
+#include "base/task/thread_pool.h"
+#include "base/time/clock.h"
+#include "base/time/default_clock.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/push_messaging/budget.pb.h"
+#include "components/leveldb_proto/public/proto_database_provider.h"
+#include "components/site_engagement/content/site_engagement_score.h"
+#include "components/site_engagement/content/site_engagement_service.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/storage_partition.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+using content::BrowserThread;
+
+namespace {
+
+// The default amount of time during which a budget will be valid.
+constexpr int kBudgetDurationInDays = 4;
+
+// The amount of budget that a maximally engaged site should receive per hour.
+// For context, silent push messages cost 2 each, so this allows 6 silent push
+// messages a day for a fully engaged site. See budget_manager.cc for costs of
+// various actions.
+constexpr double kMaximumHourlyBudget = 12.0 / 24.0;
+
+} // namespace
+
+BudgetState::BudgetState() = default;
+BudgetState::BudgetState(const BudgetState& other) = default;
+BudgetState::~BudgetState() = default;
+
+BudgetState& BudgetState::operator=(const BudgetState& other) = default;
+
+BudgetDatabase::BudgetInfo::BudgetInfo() = default;
+
+BudgetDatabase::BudgetInfo::BudgetInfo(const BudgetInfo&& other)
+ : last_engagement_award(other.last_engagement_award) {
+ chunks = std::move(other.chunks);
+}
+
+BudgetDatabase::BudgetInfo::~BudgetInfo() = default;
+
+BudgetDatabase::BudgetDatabase(Profile* profile)
+ : profile_(profile), clock_(base::WrapUnique(new base::DefaultClock)) {
+ auto* protodb_provider =
+ profile->GetDefaultStoragePartition()->GetProtoDatabaseProvider();
+ // In incognito mode the provider service is not created.
+ if (!protodb_provider)
+ return;
+
+ db_ = protodb_provider->GetDB<budget_service::Budget>(
+ leveldb_proto::ProtoDbType::BUDGET_DATABASE,
+ profile->GetPath().Append(FILE_PATH_LITERAL("BudgetDatabase")),
+ base::ThreadPool::CreateSequencedTaskRunner(
+ {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+ base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}));
+ db_->Init(base::BindOnce(&BudgetDatabase::OnDatabaseInit,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+BudgetDatabase::~BudgetDatabase() = default;
+
+void BudgetDatabase::GetBudgetDetails(const url::Origin& origin,
+ GetBudgetCallback callback) {
+ SyncCache(origin, base::BindOnce(&BudgetDatabase::GetBudgetAfterSync,
+ weak_ptr_factory_.GetWeakPtr(), origin,
+ std::move(callback)));
+}
+
+void BudgetDatabase::SpendBudget(const url::Origin& origin,
+ SpendBudgetCallback callback,
+ double amount) {
+ SyncCache(origin, base::BindOnce(&BudgetDatabase::SpendBudgetAfterSync,
+ weak_ptr_factory_.GetWeakPtr(), origin,
+ amount, std::move(callback)));
+}
+
+void BudgetDatabase::SetClockForTesting(std::unique_ptr<base::Clock> clock) {
+ clock_ = std::move(clock);
+}
+
+void BudgetDatabase::OnDatabaseInit(leveldb_proto::Enums::InitStatus status) {
+ // TODO(harkness): Consider caching the budget database now?
+ if (status != leveldb_proto::Enums::InitStatus::kOK)
+ db_.reset();
+}
+
+bool BudgetDatabase::IsCached(const url::Origin& origin) const {
+ return budget_map_.find(origin) != budget_map_.end();
+}
+
+double BudgetDatabase::GetBudget(const url::Origin& origin) const {
+ double total = 0;
+ auto iter = budget_map_.find(origin);
+ if (iter == budget_map_.end())
+ return total;
+
+ const BudgetInfo& info = iter->second;
+ for (const BudgetChunk& chunk : info.chunks)
+ total += chunk.amount;
+ return total;
+}
+
+void BudgetDatabase::AddToCache(
+ const url::Origin& origin,
+ CacheCallback callback,
+ bool success,
+ std::unique_ptr<budget_service::Budget> budget_proto) {
+ // If the database read failed or there's nothing to add, just return.
+ if (!success || !budget_proto) {
+ std::move(callback).Run(success);
+ return;
+ }
+
+ // If there were two simultaneous loads, don't overwrite the cache value,
+ // which might have been updated after the previous load.
+ if (IsCached(origin)) {
+ std::move(callback).Run(success);
+ return;
+ }
+
+ // Add the data to the cache, converting from the proto format to an STL
+ // format which is better for removing things from the list.
+ BudgetInfo& info = budget_map_[origin];
+ for (const auto& chunk : budget_proto->budget()) {
+ info.chunks.emplace_back(chunk.amount(),
+ base::Time::FromInternalValue(chunk.expiration()));
+ }
+
+ info.last_engagement_award =
+ base::Time::FromInternalValue(budget_proto->engagement_last_updated());
+
+ std::move(callback).Run(success);
+}
+
+void BudgetDatabase::GetBudgetAfterSync(const url::Origin& origin,
+ GetBudgetCallback callback,
+ bool success) {
+ std::vector<BudgetState> predictions;
+
+ // If the database wasn't able to read the information, return the
+ // failure and an empty predictions array.
+ if (!success) {
+ std::move(callback).Run(std::move(predictions));
+ return;
+ }
+
+ // Now, build up the BudgetExpection. This is different from the format
+ // in which the cache stores the data. The cache stores chunks of budget and
+ // when that budget expires. The mojo array describes a set of times
+ // and the budget at those times.
+ double total = GetBudget(origin);
+
+ // Always add one entry at the front of the list for the total budget now.
+ {
+ BudgetState prediction;
+ prediction.budget_at = total;
+ prediction.time = clock_->Now().ToJsTime();
+ predictions.push_back(prediction);
+ }
+
+ // Starting with the soonest expiring chunks, add entries for the
+ // expiration times going forward.
+ const BudgetChunks& chunks = budget_map_[origin].chunks;
+ for (const auto& chunk : chunks) {
+ BudgetState prediction;
+ total -= chunk.amount;
+ prediction.budget_at = total;
+ prediction.time = chunk.expiration.ToJsTime();
+ predictions.push_back(prediction);
+ }
+
+ std::move(callback).Run(std::move(predictions));
+}
+
+void BudgetDatabase::SpendBudgetAfterSync(const url::Origin& origin,
+ double amount,
+ SpendBudgetCallback callback,
+ bool success) {
+ if (!success) {
+ std::move(callback).Run(false /* success */);
+ return;
+ }
+
+ // Get the current SES score, to generate UMA.
+ double score = GetSiteEngagementScoreForOrigin(origin);
+
+ // Walk the list of budget chunks to see if the origin has enough budget.
+ double total = 0;
+ BudgetInfo& info = budget_map_[origin];
+ for (const BudgetChunk& chunk : info.chunks)
+ total += chunk.amount;
+
+ if (total < amount) {
+ UMA_HISTOGRAM_COUNTS_100("PushMessaging.SESForNoBudgetOrigin", score);
+ std::move(callback).Run(false /* success */);
+ return;
+ } else if (total < amount * 2) {
+ UMA_HISTOGRAM_COUNTS_100("PushMessaging.SESForLowBudgetOrigin", score);
+ }
+
+ // Walk the chunks and remove enough budget to cover the needed amount.
+ double bill = amount;
+ for (auto iter = info.chunks.begin(); iter != info.chunks.end();) {
+ if (iter->amount > bill) {
+ iter->amount -= bill;
+ bill = 0;
+ break;
+ }
+ bill -= iter->amount;
+ iter = info.chunks.erase(iter);
+ }
+
+ // There should have been enough budget to cover the entire bill.
+ DCHECK_EQ(0, bill);
+
+ // Now that the cache is updated, write the data to the database.
+ WriteCachedValuesToDatabase(
+ origin,
+ base::BindOnce(&BudgetDatabase::SpendBudgetAfterWrite,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+// This converts the bool value which is returned from the database to a Mojo
+// error type.
+void BudgetDatabase::SpendBudgetAfterWrite(SpendBudgetCallback callback,
+ bool write_successful) {
+ // TODO(harkness): If the database write fails, the cache will be out of sync
+ // with the database. Consider ways to mitigate this.
+ if (!write_successful) {
+ std::move(callback).Run(false /* success */);
+ return;
+ }
+ std::move(callback).Run(true /* success */);
+}
+
+void BudgetDatabase::WriteCachedValuesToDatabase(const url::Origin& origin,
+ StoreBudgetCallback callback) {
+ if (!db_) {
+ base::SequencedTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), false));
+ return;
+ }
+
+ // Create the data structures that are passed to the ProtoDatabase.
+ std::unique_ptr<
+ leveldb_proto::ProtoDatabase<budget_service::Budget>::KeyEntryVector>
+ entries(new leveldb_proto::ProtoDatabase<
+ budget_service::Budget>::KeyEntryVector());
+ std::unique_ptr<std::vector<std::string>> keys_to_remove(
+ new std::vector<std::string>());
+
+ // Each operation can either update the existing budget or remove the origin's
+ // budget information.
+ if (IsCached(origin)) {
+ // Build the Budget proto object.
+ budget_service::Budget budget;
+ const BudgetInfo& info = budget_map_[origin];
+ for (const auto& chunk : info.chunks) {
+ budget_service::BudgetChunk* budget_chunk = budget.add_budget();
+ budget_chunk->set_amount(chunk.amount);
+ budget_chunk->set_expiration(chunk.expiration.ToInternalValue());
+ }
+ budget.set_engagement_last_updated(
+ info.last_engagement_award.ToInternalValue());
+ entries->push_back(std::make_pair(origin.Serialize(), budget));
+ } else {
+ // If the origin doesn't exist in the cache, this is a remove operation.
+ keys_to_remove->push_back(origin.Serialize());
+ }
+
+ // Send the updates to the database.
+ db_->UpdateEntries(std::move(entries), std::move(keys_to_remove),
+ std::move(callback));
+}
+
+void BudgetDatabase::SyncCache(const url::Origin& origin,
+ CacheCallback callback) {
+ // If the origin isn't already cached, add it to the cache.
+ if (!IsCached(origin)) {
+ if (!db_) {
+ base::SequencedTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), false));
+ return;
+ }
+ CacheCallback add_callback = base::BindOnce(
+ &BudgetDatabase::SyncLoadedCache, weak_ptr_factory_.GetWeakPtr(),
+ origin, std::move(callback));
+ db_->GetEntry(origin.Serialize(),
+ base::BindOnce(&BudgetDatabase::AddToCache,
+ weak_ptr_factory_.GetWeakPtr(), origin,
+ std::move(add_callback)));
+ return;
+ }
+ SyncLoadedCache(origin, std::move(callback), true /* success */);
+}
+
+void BudgetDatabase::SyncLoadedCache(const url::Origin& origin,
+ CacheCallback callback,
+ bool success) {
+ if (!success) {
+ std::move(callback).Run(false /* success */);
+ return;
+ }
+
+ // Now, cleanup any expired budget chunks for the origin.
+ bool needs_write = CleanupExpiredBudget(origin);
+
+ // Get the SES score and add engagement budget for the site.
+ AddEngagementBudget(origin);
+
+ if (needs_write)
+ WriteCachedValuesToDatabase(origin, std::move(callback));
+ else
+ std::move(callback).Run(success);
+}
+
+void BudgetDatabase::AddEngagementBudget(const url::Origin& origin) {
+ // Calculate how much budget should be awarded. The award depends on the
+ // time elapsed since the last award and the SES score.
+ // By default, give the origin kBudgetDurationInDays worth of budget, but
+ // reduce that if budget has already been given during that period.
+ base::TimeDelta elapsed = base::Days(kBudgetDurationInDays);
+ if (IsCached(origin)) {
+ elapsed = clock_->Now() - budget_map_[origin].last_engagement_award;
+ // Don't give engagement awards for periods less than an hour.
+ if (elapsed.InHours() < 1)
+ return;
+ // Cap elapsed time to the budget duration.
+ if (elapsed.InDays() > kBudgetDurationInDays)
+ elapsed = base::Days(kBudgetDurationInDays);
+ }
+
+ // Get the current SES score, and calculate the hourly budget for that score.
+ double hourly_budget = kMaximumHourlyBudget *
+ GetSiteEngagementScoreForOrigin(origin) /
+ site_engagement::SiteEngagementService::GetMaxPoints();
+
+ // Update the last_engagement_award to the current time. If the origin wasn't
+ // already in the map, this adds a new entry for it.
+ budget_map_[origin].last_engagement_award = clock_->Now();
+
+ // Add a new chunk of budget for the origin at the default expiration time.
+ base::Time expiration = clock_->Now() + base::Days(kBudgetDurationInDays);
+ budget_map_[origin].chunks.emplace_back(elapsed.InHours() * hourly_budget,
+ expiration);
+
+ // Any time we award engagement budget, which is done at most once an hour
+ // whenever any budget action is taken, record the budget.
+ double budget = GetBudget(origin);
+ UMA_HISTOGRAM_COUNTS_100("PushMessaging.BackgroundBudget", budget);
+}
+
+// Cleans up budget in the cache. Relies on the caller eventually writing the
+// cache back to the database.
+bool BudgetDatabase::CleanupExpiredBudget(const url::Origin& origin) {
+ if (!IsCached(origin))
+ return false;
+
+ base::Time now = clock_->Now();
+ BudgetChunks& chunks = budget_map_[origin].chunks;
+ auto cleanup_iter = chunks.begin();
+
+ // This relies on the list of chunks being in timestamp order.
+ while (cleanup_iter != chunks.end() && cleanup_iter->expiration <= now)
+ cleanup_iter = chunks.erase(cleanup_iter);
+
+ // If the entire budget is empty now AND there have been no engagements
+ // in the last kBudgetDurationInDays days, remove this from the cache.
+ if (chunks.empty() && budget_map_[origin].last_engagement_award <
+ clock_->Now() - base::Days(kBudgetDurationInDays)) {
+ budget_map_.erase(origin);
+ return true;
+ }
+
+ // Although some things may have expired, there are some chunks still valid.
+ // Don't write to the DB now, write either when all chunks expire or when the
+ // origin spends some budget.
+ return false;
+}
+
+double BudgetDatabase::GetSiteEngagementScoreForOrigin(
+ const url::Origin& origin) const {
+ if (profile_->IsOffTheRecord())
+ return 0;
+
+ return site_engagement::SiteEngagementService::Get(profile_)->GetScore(
+ origin.GetURL());
+}
diff --git a/chromium/chrome/browser/push_messaging/budget_database.h b/chromium/chrome/browser/push_messaging/budget_database.h
new file mode 100644
index 00000000000..0ae20374262
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/budget_database.h
@@ -0,0 +1,182 @@
+// Copyright 2016 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_
+
+#include <list>
+#include <map>
+#include <memory>
+
+#include "base/callback_forward.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "components/leveldb_proto/public/proto_database.h"
+
+namespace base {
+class Clock;
+class Time;
+} // namespace base
+
+namespace budget_service {
+class Budget;
+}
+
+namespace url {
+class Origin;
+}
+
+class Profile;
+
+// Structure representing the budget at points in time in the future.
+struct BudgetState {
+ BudgetState();
+ BudgetState(const BudgetState& other);
+ ~BudgetState();
+
+ BudgetState& operator=(const BudgetState& other);
+
+ // Amount of budget that will be available. This should be the lower bound of
+ // the budget between this time and the previous time.
+ double budget_at = 0;
+
+ // Time at which the budget is available, in milliseconds since 00:00:00 UTC
+ // on 1 January 1970, at which the budget_at will be valid.
+ double time = 0;
+};
+
+// A class used to asynchronously read and write details of the budget
+// assigned to an origin. The class uses an underlying LevelDB.
+class BudgetDatabase {
+ public:
+ // The default amount of budget that should be spent.
+ static constexpr double kDefaultAmount = 2.0;
+
+ // Callback for getting a list of all budget chunks.
+ using GetBudgetCallback = base::OnceCallback<void(std::vector<BudgetState>)>;
+
+ // This is invoked only after the spend has been written to the database.
+ using SpendBudgetCallback = base::OnceCallback<void(bool success)>;
+
+ // The database_dir specifies the location of the budget information on disk.
+ explicit BudgetDatabase(Profile* profile);
+
+ BudgetDatabase(const BudgetDatabase&) = delete;
+ BudgetDatabase& operator=(const BudgetDatabase&) = delete;
+
+ ~BudgetDatabase();
+
+ // Get the full budget expectation for the origin. This will return a
+ // sequence of time points and the expected budget at those times.
+ void GetBudgetDetails(const url::Origin& origin, GetBudgetCallback callback);
+
+ // Spend a fixed (2.0) amount of budget for an origin. The callback indicates
+ // whether the budget could be spent for the given |origin|.
+ void SpendBudget(const url::Origin& origin,
+ SpendBudgetCallback callback,
+ double amount = kDefaultAmount);
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(BudgetDatabaseTest,
+ DefaultSiteEngagementInIncognitoProfile);
+ friend class BudgetDatabaseTest;
+
+ // Used to allow tests to change time for testing.
+ void SetClockForTesting(std::unique_ptr<base::Clock> clock);
+
+ // Holds information about individual pieces of awarded budget. There is a
+ // one-to-one mapping of these to the chunks in the underlying database.
+ struct BudgetChunk {
+ BudgetChunk(double amount, base::Time expiration)
+ : amount(amount), expiration(expiration) {}
+ BudgetChunk(const BudgetChunk&) = default;
+ BudgetChunk& operator=(const BudgetChunk&) = default;
+
+ double amount;
+ base::Time expiration;
+ };
+
+ // Data structure for caching budget information.
+ using BudgetChunks = std::list<BudgetChunk>;
+
+ // Holds information about the overall budget for a site. This includes the
+ // time the budget was last incremented, as well as a list of budget chunks
+ // which have been awarded.
+ struct BudgetInfo {
+ BudgetInfo();
+
+ BudgetInfo(const BudgetInfo&) = delete;
+ BudgetInfo& operator=(const BudgetInfo&) = delete;
+
+ BudgetInfo(const BudgetInfo&& other);
+
+ ~BudgetInfo();
+
+ base::Time last_engagement_award;
+ BudgetChunks chunks;
+ };
+
+ // Callback for writing budget values to the database.
+ using StoreBudgetCallback = base::OnceCallback<void(bool success)>;
+
+ using CacheCallback = base::OnceCallback<void(bool success)>;
+
+ void OnDatabaseInit(leveldb_proto::Enums::InitStatus status);
+
+ bool IsCached(const url::Origin& origin) const;
+
+ double GetBudget(const url::Origin& origin) const;
+
+ void AddToCache(const url::Origin& origin,
+ CacheCallback callback,
+ bool success,
+ std::unique_ptr<budget_service::Budget> budget);
+
+ void GetBudgetAfterSync(const url::Origin& origin,
+ GetBudgetCallback callback,
+ bool success);
+
+ void SpendBudgetAfterSync(const url::Origin& origin,
+ double amount,
+ SpendBudgetCallback callback,
+ bool success);
+
+ void SpendBudgetAfterWrite(SpendBudgetCallback callback, bool success);
+
+ void WriteCachedValuesToDatabase(const url::Origin& origin,
+ StoreBudgetCallback callback);
+
+ void SyncCache(const url::Origin& origin, CacheCallback callback);
+ void SyncLoadedCache(const url::Origin& origin,
+ CacheCallback callback,
+ bool success);
+
+ // Add budget based on engagement with an origin. The method queries for the
+ // engagement score of the origin, and then calculates when engagement budget
+ // was last awarded and awards a portion of the score based on that.
+ // This only writes budget to the cache.
+ void AddEngagementBudget(const url::Origin& origin);
+
+ bool CleanupExpiredBudget(const url::Origin& origin);
+
+ // Gets the current Site Engagement Score for |origin|. Will return a fixed
+ // score of zero when |profile_| is off the record.
+ double GetSiteEngagementScoreForOrigin(const url::Origin& origin) const;
+
+ raw_ptr<Profile> profile_;
+
+ // The database for storing budget information.
+ std::unique_ptr<leveldb_proto::ProtoDatabase<budget_service::Budget>> db_;
+
+ // Cached data for the origins which have been loaded.
+ std::map<url::Origin, BudgetInfo> budget_map_;
+
+ // The clock used to vend times.
+ std::unique_ptr<base::Clock> clock_;
+
+ base::WeakPtrFactory<BudgetDatabase> weak_ptr_factory_{this};
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_
diff --git a/chromium/chrome/browser/push_messaging/budget_database_unittest.cc b/chromium/chrome/browser/push_messaging/budget_database_unittest.cc
new file mode 100644
index 00000000000..dbf29452424
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/budget_database_unittest.cc
@@ -0,0 +1,351 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/budget_database.h"
+
+#include <math.h>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/run_loop.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/simple_test_clock.h"
+#include "chrome/browser/push_messaging/budget.pb.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/leveldb_proto/public/proto_database.h"
+#include "components/leveldb_proto/public/proto_database_provider.h"
+#include "components/site_engagement/content/site_engagement_score.h"
+#include "components/site_engagement/content/site_engagement_service.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace {
+
+// These values mirror the defaults in budget_database.cc
+const double kDefaultExpirationInDays = 4;
+const double kMaxDailyBudget = 12;
+
+const double kEngagement = 25;
+
+const char kTestOrigin[] = "https://example.com";
+
+} // namespace
+
+class BudgetDatabaseTest : public ::testing::Test {
+ public:
+ BudgetDatabaseTest()
+ : success_(false),
+ db_(&profile_),
+ origin_(url::Origin::Create(GURL(kTestOrigin))) {}
+
+ void WriteBudgetComplete(base::OnceClosure run_loop_closure, bool success) {
+ success_ = success;
+ std::move(run_loop_closure).Run();
+ }
+
+ // Spend budget for the origin.
+ bool SpendBudget(double amount) {
+ base::RunLoop run_loop;
+ db_.SpendBudget(
+ origin(),
+ base::BindOnce(&BudgetDatabaseTest::WriteBudgetComplete,
+ base::Unretained(this), run_loop.QuitClosure()),
+ amount);
+ run_loop.Run();
+ return success_;
+ }
+
+ void GetBudgetDetailsComplete(base::OnceClosure run_loop_closure,
+ std::vector<BudgetState> predictions) {
+ success_ = !predictions.empty();
+ prediction_.swap(predictions);
+ std::move(run_loop_closure).Run();
+ }
+
+ // Get the full set of budget predictions for the origin.
+ void GetBudgetDetails() {
+ base::RunLoop run_loop;
+ db_.GetBudgetDetails(
+ origin(),
+ base::BindOnce(&BudgetDatabaseTest::GetBudgetDetailsComplete,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+ }
+
+ Profile* profile() { return &profile_; }
+ BudgetDatabase* database() { return &db_; }
+ const url::Origin& origin() const { return origin_; }
+
+ // Setup a test clock so that the tests can control time.
+ base::SimpleTestClock* SetClockForTesting() {
+ base::SimpleTestClock* clock = new base::SimpleTestClock();
+ db_.SetClockForTesting(base::WrapUnique(clock));
+ return clock;
+ }
+
+ void SetSiteEngagementScore(double score) {
+ site_engagement::SiteEngagementService* service =
+ site_engagement::SiteEngagementService::Get(&profile_);
+ service->ResetBaseScoreForURL(GURL(kTestOrigin), score);
+ }
+
+ protected:
+ base::HistogramTester* GetHistogramTester() { return &histogram_tester_; }
+ bool success_;
+ std::vector<BudgetState> prediction_;
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfile profile_;
+ BudgetDatabase db_;
+ base::HistogramTester histogram_tester_;
+ const url::Origin origin_;
+};
+
+TEST_F(BudgetDatabaseTest, GetBudgetNoBudgetOrSES) {
+ GetBudgetDetails();
+ ASSERT_TRUE(success_);
+ ASSERT_EQ(2U, prediction_.size());
+ EXPECT_EQ(0, prediction_[0].budget_at);
+}
+
+TEST_F(BudgetDatabaseTest, AddEngagementBudgetTest) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+ base::Time expiration_time =
+ clock->Now() + base::Days(kDefaultExpirationInDays);
+
+ // Set the default site engagement.
+ SetSiteEngagementScore(kEngagement);
+
+ // The budget should include kDefaultExpirationInDays days worth of
+ // engagement.
+ double daily_budget =
+ kMaxDailyBudget *
+ (kEngagement / site_engagement::SiteEngagementScore::kMaxPoints);
+ GetBudgetDetails();
+ ASSERT_TRUE(success_);
+ ASSERT_EQ(2U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget * kDefaultExpirationInDays,
+ prediction_[0].budget_at);
+ ASSERT_EQ(0, prediction_[1].budget_at);
+ ASSERT_EQ(expiration_time.ToJsTime(), prediction_[1].time);
+
+ // Advance time 1 day and add more engagement budget.
+ clock->Advance(base::Days(1));
+ GetBudgetDetails();
+
+ // The budget should now have 1 full share plus 1 daily budget.
+ ASSERT_TRUE(success_);
+ ASSERT_EQ(3U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget * (kDefaultExpirationInDays + 1),
+ prediction_[0].budget_at);
+ ASSERT_DOUBLE_EQ(daily_budget, prediction_[1].budget_at);
+ ASSERT_EQ(expiration_time.ToJsTime(), prediction_[1].time);
+ ASSERT_DOUBLE_EQ(0, prediction_[2].budget_at);
+ ASSERT_EQ((expiration_time + base::Days(1)).ToJsTime(), prediction_[2].time);
+
+ // Advance time by 59 minutes and check that no engagement budget is added
+ // since budget should only be added for > 1 hour increments.
+ clock->Advance(base::Minutes(59));
+ GetBudgetDetails();
+
+ // The budget should be the same as before the attempted add.
+ ASSERT_TRUE(success_);
+ ASSERT_EQ(3U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget * (kDefaultExpirationInDays + 1),
+ prediction_[0].budget_at);
+}
+
+TEST_F(BudgetDatabaseTest, SpendBudgetTest) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+
+ // Set the default site engagement.
+ SetSiteEngagementScore(kEngagement);
+
+ // Intialize the budget with several chunks.
+ GetBudgetDetails();
+ clock->Advance(base::Days(1));
+ GetBudgetDetails();
+ clock->Advance(base::Days(1));
+ GetBudgetDetails();
+
+ // Spend an amount of budget less than the daily budget.
+ ASSERT_TRUE(SpendBudget(1));
+ GetBudgetDetails();
+
+ // There should still be three chunks of budget of size daily_budget-1,
+ // daily_budget, and kDefaultExpirationInDays * daily_budget.
+ double daily_budget =
+ kMaxDailyBudget *
+ (kEngagement / site_engagement::SiteEngagementScore::kMaxPoints);
+ ASSERT_EQ(4U, prediction_.size());
+ ASSERT_DOUBLE_EQ((2 + kDefaultExpirationInDays) * daily_budget - 1,
+ prediction_[0].budget_at);
+ ASSERT_DOUBLE_EQ(daily_budget * 2, prediction_[1].budget_at);
+ ASSERT_DOUBLE_EQ(daily_budget, prediction_[2].budget_at);
+ ASSERT_DOUBLE_EQ(0, prediction_[3].budget_at);
+
+ // Now spend enough that it will use up the rest of the first chunk and all of
+ // the second chunk, but not all of the third chunk.
+ ASSERT_TRUE(SpendBudget((1 + kDefaultExpirationInDays) * daily_budget));
+ GetBudgetDetails();
+ ASSERT_EQ(2U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget - 1, prediction_[0].budget_at);
+
+ // Validate that the code returns false if SpendBudget tries to spend more
+ // budget than the origin has.
+ EXPECT_FALSE(SpendBudget(kEngagement));
+ GetBudgetDetails();
+ ASSERT_EQ(2U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget - 1, prediction_[0].budget_at);
+
+ // Advance time until the last remaining chunk should be expired, then query
+ // for the full engagement worth of budget.
+ clock->Advance(base::Days(kDefaultExpirationInDays + 1));
+ EXPECT_TRUE(SpendBudget(daily_budget * kDefaultExpirationInDays));
+}
+
+// There are times when a device's clock could move backwards in time, either
+// due to hardware issues or user actions. Test here to make sure that even if
+// time goes backwards and then forwards again, the origin isn't granted extra
+// budget.
+TEST_F(BudgetDatabaseTest, GetBudgetNegativeTime) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+
+ // Set the default site engagement.
+ SetSiteEngagementScore(kEngagement);
+
+ // Initialize the budget with two chunks.
+ GetBudgetDetails();
+ clock->Advance(base::Days(1));
+ GetBudgetDetails();
+
+ // Save off the budget total.
+ ASSERT_EQ(3U, prediction_.size());
+ double budget = prediction_[0].budget_at;
+
+ // Move the clock backwards in time to before the budget awards.
+ clock->SetNow(clock->Now() - base::Days(5));
+
+ // Make sure the budget is the same.
+ GetBudgetDetails();
+ ASSERT_EQ(3U, prediction_.size());
+ ASSERT_EQ(budget, prediction_[0].budget_at);
+
+ // Now move the clock back to the original time and check that no extra budget
+ // is awarded.
+ clock->SetNow(clock->Now() + base::Days(5));
+ GetBudgetDetails();
+ ASSERT_EQ(3U, prediction_.size());
+ ASSERT_EQ(budget, prediction_[0].budget_at);
+}
+
+TEST_F(BudgetDatabaseTest, CheckBackgroundBudgetHistogram) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+
+ // Set the default site engagement.
+ SetSiteEngagementScore(kEngagement);
+
+ // Initialize the budget with some interesting chunks: 30 budget (full
+ // engagement), 15 budget (half of the engagement), 0 budget (less than an
+ // hour), and then after the first two expire, another 30 budget.
+ GetBudgetDetails();
+ clock->Advance(base::Days(kDefaultExpirationInDays / 2));
+ GetBudgetDetails();
+ clock->Advance(base::Minutes(59));
+ GetBudgetDetails();
+ clock->Advance(base::Days(kDefaultExpirationInDays + 1));
+ GetBudgetDetails();
+
+ // The BackgroundBudget UMA is recorded when budget is added to the origin.
+ // This can happen a maximum of once per hour so there should be two entries.
+ std::vector<base::Bucket> buckets =
+ GetHistogramTester()->GetAllSamples("PushMessaging.BackgroundBudget");
+ ASSERT_EQ(2U, buckets.size());
+ // First bucket is for full award, which should have 2 entries.
+ double full_award = kMaxDailyBudget * kEngagement /
+ site_engagement::SiteEngagementScore::kMaxPoints *
+ kDefaultExpirationInDays;
+ EXPECT_EQ(floor(full_award), buckets[0].min);
+ EXPECT_EQ(2, buckets[0].count);
+ // Second bucket is for 1.5 * award, which should have 1 entry.
+ EXPECT_EQ(floor(full_award * 1.5), buckets[1].min);
+ EXPECT_EQ(1, buckets[1].count);
+}
+
+TEST_F(BudgetDatabaseTest, CheckEngagementHistograms) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+
+ // Manipulate the engagement so that the budget is twice the cost of an
+ // action.
+ double cost = 2;
+ double engagement = 2 * cost *
+ site_engagement::SiteEngagementScore::kMaxPoints /
+ kDefaultExpirationInDays / kMaxDailyBudget;
+ SetSiteEngagementScore(engagement);
+
+ // Get the budget, which will award a chunk of budget equal to engagement.
+ GetBudgetDetails();
+
+ // Now spend the budget to trigger the UMA recording the SES score. The first
+ // call shouldn't write any UMA. The second should write a lowSES entry, and
+ // the third should write a noSES entry.
+ ASSERT_TRUE(SpendBudget(cost));
+ ASSERT_TRUE(SpendBudget(cost));
+ ASSERT_FALSE(SpendBudget(cost));
+
+ // Advance the clock by 12 days (to guarantee a full new engagement grant)
+ // then change the SES score to get a different UMA entry, then spend the
+ // budget again.
+ clock->Advance(base::Days(12));
+ GetBudgetDetails();
+ SetSiteEngagementScore(engagement * 2);
+ ASSERT_TRUE(SpendBudget(cost));
+ ASSERT_TRUE(SpendBudget(cost));
+ ASSERT_FALSE(SpendBudget(cost));
+
+ // Now check the UMA. Both UMA should have 2 buckets with 1 entry each.
+ std::vector<base::Bucket> no_budget_buckets =
+ GetHistogramTester()->GetAllSamples("PushMessaging.SESForNoBudgetOrigin");
+ ASSERT_EQ(2U, no_budget_buckets.size());
+ EXPECT_EQ(floor(engagement), no_budget_buckets[0].min);
+ EXPECT_EQ(1, no_budget_buckets[0].count);
+ EXPECT_EQ(floor(engagement * 2), no_budget_buckets[1].min);
+ EXPECT_EQ(1, no_budget_buckets[1].count);
+
+ std::vector<base::Bucket> low_budget_buckets =
+ GetHistogramTester()->GetAllSamples(
+ "PushMessaging.SESForLowBudgetOrigin");
+ ASSERT_EQ(2U, low_budget_buckets.size());
+ EXPECT_EQ(floor(engagement), low_budget_buckets[0].min);
+ EXPECT_EQ(1, low_budget_buckets[0].count);
+ EXPECT_EQ(floor(engagement * 2), low_budget_buckets[1].min);
+ EXPECT_EQ(1, low_budget_buckets[1].count);
+}
+
+TEST_F(BudgetDatabaseTest, DefaultSiteEngagementInIncognitoProfile) {
+ TestingProfile second_profile;
+ Profile* second_profile_incognito =
+ second_profile.GetPrimaryOTRProfile(/*create_if_needed=*/true);
+
+ // Create a second BudgetDatabase instance for the off-the-record version of
+ // a second profile. This will not have been influenced by the |profile_|.
+ std::unique_ptr<BudgetDatabase> second_database =
+ std::make_unique<BudgetDatabase>(second_profile_incognito);
+
+ ASSERT_FALSE(profile()->IsOffTheRecord());
+ ASSERT_FALSE(second_profile.IsOffTheRecord());
+ ASSERT_TRUE(second_profile_incognito->IsOffTheRecord());
+
+ // The Site Engagement Score considered by an Incognito profile must be equal
+ // to the score considered in a regular profile visting a page for the first
+ // time. This may grant a small amount of budget, but does mean that Incognito
+ // mode cannot be detected through the Budget API.
+ EXPECT_EQ(database()->GetSiteEngagementScoreForOrigin(origin()),
+ second_database->GetSiteEngagementScoreForOrigin(origin()));
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc
new file mode 100644
index 00000000000..a09e96fba38
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc
@@ -0,0 +1,322 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+
+#include <string.h>
+
+#include "base/check_op.h"
+#include "base/guid.h"
+#include "base/notreached.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/values.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/common/pref_names.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
+
+constexpr char kPushMessagingAppIdentifierPrefix[] = "wp:";
+constexpr char kInstanceIDGuidSuffix[] = "-V2";
+
+namespace {
+
+// sizeof is strlen + 1 since it's null-terminated.
+constexpr size_t kPrefixLength = sizeof(kPushMessagingAppIdentifierPrefix) - 1;
+constexpr size_t kGuidSuffixLength = sizeof(kInstanceIDGuidSuffix) - 1;
+
+// Ok to use '#' as separator since only the origin of the url is used.
+constexpr char kPrefValueSeparator = '#';
+constexpr size_t kGuidLength = 36; // "%08X-%04X-%04X-%04X-%012llX"
+
+std::string FromTimeToString(base::Time time) {
+ DCHECK(!time.is_null());
+ return base::NumberToString(time.ToDeltaSinceWindowsEpoch().InMilliseconds());
+}
+
+bool FromStringToTime(const std::string& time_string,
+ absl::optional<base::Time>* time) {
+ DCHECK(!time_string.empty());
+ int64_t milliseconds;
+ if (base::StringToInt64(time_string, &milliseconds) && milliseconds > 0) {
+ *time = absl::make_optional(base::Time::FromDeltaSinceWindowsEpoch(
+ base::Milliseconds(milliseconds)));
+ return true;
+ }
+ return false;
+}
+
+std::string MakePrefValue(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt) {
+ std::string result = origin.spec() + kPrefValueSeparator +
+ base::NumberToString(service_worker_registration_id);
+ if (expiration_time)
+ result += kPrefValueSeparator + FromTimeToString(*expiration_time);
+ return result;
+}
+
+bool DisassemblePrefValue(const std::string& pref_value,
+ GURL* origin,
+ int64_t* service_worker_registration_id,
+ absl::optional<base::Time>* expiration_time) {
+ std::vector<std::string> parts =
+ base::SplitString(pref_value, std::string(1, kPrefValueSeparator),
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ if (parts.size() < 2 || parts.size() > 3)
+ return false;
+
+ if (!base::StringToInt64(parts[1], service_worker_registration_id))
+ return false;
+
+ *origin = GURL(parts[0]);
+ if (!origin->is_valid())
+ return false;
+
+ if (parts.size() == 3)
+ return FromStringToTime(parts[2], expiration_time);
+
+ return true;
+}
+
+} // namespace
+
+// static
+void PushMessagingAppIdentifier::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ // TODO(johnme): If push becomes enabled in incognito, be careful that this
+ // pref is read from the right profile, as prefs defined in a regular profile
+ // are visible in the corresponding incognito profile unless overridden.
+ // TODO(johnme): Make sure this pref doesn't get out of sync after crashes.
+ registry->RegisterDictionaryPref(prefs::kPushMessagingAppIdentifierMap);
+}
+
+// static
+bool PushMessagingAppIdentifier::UseInstanceID(const std::string& app_id) {
+ return base::EndsWith(app_id, kInstanceIDGuidSuffix,
+ base::CompareCase::SENSITIVE);
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::Generate(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time) {
+ // All new push subscriptions use Instance ID tokens.
+ return GenerateInternal(origin, service_worker_registration_id,
+ true /* use_instance_id */, expiration_time);
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::LegacyGenerateForTesting(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time) {
+ return GenerateInternal(origin, service_worker_registration_id,
+ false /* use_instance_id */, expiration_time);
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::GenerateInternal(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ bool use_instance_id,
+ const absl::optional<base::Time>& expiration_time) {
+ // Use uppercase GUID for consistency with GUIDs Push has already sent to GCM.
+ // Also allows detecting case mangling; see code commented "crbug.com/461867".
+ std::string guid = base::ToUpperASCII(base::GenerateGUID());
+ if (use_instance_id) {
+ guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength,
+ kInstanceIDGuidSuffix);
+ }
+ CHECK(!guid.empty());
+ std::string app_id = kPushMessagingAppIdentifierPrefix + origin.spec() +
+ kPrefValueSeparator + guid;
+
+ PushMessagingAppIdentifier app_identifier(
+ app_id, origin, service_worker_registration_id, expiration_time);
+ app_identifier.DCheckValid();
+ return app_identifier;
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::FindByAppId(
+ Profile* profile, const std::string& app_id) {
+ if (!base::StartsWith(app_id, kPushMessagingAppIdentifierPrefix,
+ base::CompareCase::INSENSITIVE_ASCII)) {
+ return PushMessagingAppIdentifier();
+ }
+
+ // Since we now know this is a Push Messaging app_id, check the case hasn't
+ // been mangled (crbug.com/461867).
+ DCHECK_EQ(kPushMessagingAppIdentifierPrefix, app_id.substr(0, kPrefixLength));
+ DCHECK_GE(app_id.size(), kPrefixLength + kGuidLength);
+ DCHECK_EQ(app_id.substr(app_id.size() - kGuidLength),
+ base::ToUpperASCII(app_id.substr(app_id.size() - kGuidLength)));
+
+ const base::Value* map =
+ profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap);
+
+ const std::string* map_value = map->FindStringKey(app_id);
+
+ if (!map_value || map_value->empty())
+ return PushMessagingAppIdentifier();
+
+ GURL origin;
+ int64_t service_worker_registration_id;
+ absl::optional<base::Time> expiration_time;
+ // Try disassemble the pref value, return an invalid app identifier if the
+ // pref value is corrupted
+ if (!DisassemblePrefValue(*map_value, &origin,
+ &service_worker_registration_id,
+ &expiration_time)) {
+ NOTREACHED();
+ return PushMessagingAppIdentifier();
+ }
+
+ PushMessagingAppIdentifier app_identifier(
+ app_id, origin, service_worker_registration_id, expiration_time);
+ app_identifier.DCheckValid();
+ return app_identifier;
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::FindByServiceWorker(
+ Profile* profile,
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ const std::string base_pref_value =
+ MakePrefValue(origin, service_worker_registration_id);
+
+ const base::Value* map =
+ profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap);
+ for (auto entry : map->DictItems()) {
+ if (entry.second.is_string() &&
+ base::StartsWith(entry.second.GetString(), base_pref_value,
+ base::CompareCase::SENSITIVE)) {
+ return FindByAppId(profile, entry.first);
+ }
+ }
+ return PushMessagingAppIdentifier();
+}
+
+// static
+std::vector<PushMessagingAppIdentifier> PushMessagingAppIdentifier::GetAll(
+ Profile* profile) {
+ std::vector<PushMessagingAppIdentifier> result;
+
+ const base::Value* map =
+ profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap);
+ for (auto entry : map->DictItems()) {
+ result.push_back(FindByAppId(profile, entry.first));
+ }
+
+ return result;
+}
+
+// static
+void PushMessagingAppIdentifier::DeleteAllFromPrefs(Profile* profile) {
+ DictionaryPrefUpdate update(profile->GetPrefs(),
+ prefs::kPushMessagingAppIdentifierMap);
+ base::Value* map = update.Get();
+ map->DictClear();
+}
+
+// static
+size_t PushMessagingAppIdentifier::GetCount(Profile* profile) {
+ return profile->GetPrefs()
+ ->GetDictionary(prefs::kPushMessagingAppIdentifierMap)
+ ->DictSize();
+}
+
+PushMessagingAppIdentifier::PushMessagingAppIdentifier(
+ const PushMessagingAppIdentifier& other) = default;
+
+PushMessagingAppIdentifier::PushMessagingAppIdentifier()
+ : origin_(GURL::EmptyGURL()), service_worker_registration_id_(-1) {}
+
+PushMessagingAppIdentifier::PushMessagingAppIdentifier(
+ const std::string& app_id,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time)
+ : app_id_(app_id),
+ origin_(origin),
+ service_worker_registration_id_(service_worker_registration_id),
+ expiration_time_(expiration_time) {}
+
+PushMessagingAppIdentifier::~PushMessagingAppIdentifier() {}
+
+bool PushMessagingAppIdentifier::IsExpired() const {
+ return (expiration_time_) ? *expiration_time_ < base::Time::Now() : false;
+}
+
+void PushMessagingAppIdentifier::PersistToPrefs(Profile* profile) const {
+ DCheckValid();
+
+ DictionaryPrefUpdate update(profile->GetPrefs(),
+ prefs::kPushMessagingAppIdentifierMap);
+ base::Value* map = update.Get();
+
+ // Delete any stale entry with the same origin and Service Worker
+ // registration id (hence we ensure there is a 1:1 not 1:many mapping).
+ PushMessagingAppIdentifier old =
+ FindByServiceWorker(profile, origin_, service_worker_registration_id_);
+ if (!old.is_null())
+ map->RemoveKey(old.app_id_);
+
+ map->SetKey(app_id_,
+ base::Value(MakePrefValue(
+ origin_, service_worker_registration_id_, expiration_time_)));
+}
+
+void PushMessagingAppIdentifier::DeleteFromPrefs(Profile* profile) const {
+ DCheckValid();
+
+ DictionaryPrefUpdate update(profile->GetPrefs(),
+ prefs::kPushMessagingAppIdentifierMap);
+ base::Value* map = update.Get();
+ map->RemoveKey(app_id_);
+}
+
+void PushMessagingAppIdentifier::DCheckValid() const {
+#if DCHECK_IS_ON()
+ DCHECK_GE(service_worker_registration_id_, 0);
+
+ DCHECK(origin_.is_valid());
+ DCHECK_EQ(origin_.DeprecatedGetOriginAsURL(), origin_);
+
+ // "wp:"
+ DCHECK_EQ(kPushMessagingAppIdentifierPrefix,
+ app_id_.substr(0, kPrefixLength));
+
+ // Optional (origin.spec() + '#')
+ if (app_id_.size() != kPrefixLength + kGuidLength) {
+ constexpr size_t suffix_length = 1 /* kPrefValueSeparator */ + kGuidLength;
+ DCHECK_GT(app_id_.size(), kPrefixLength + suffix_length);
+ DCHECK_EQ(origin_, GURL(app_id_.substr(
+ kPrefixLength,
+ app_id_.size() - kPrefixLength - suffix_length)));
+ DCHECK_EQ(std::string(1, kPrefValueSeparator),
+ app_id_.substr(app_id_.size() - suffix_length, 1));
+ }
+
+ // GUID. In order to distinguish them, an app_id created for an InstanceID
+ // based subscription has the last few characters of the GUID overwritten with
+ // kInstanceIDGuidSuffix (which contains non-hex characters invalid in GUIDs).
+ std::string guid = app_id_.substr(app_id_.size() - kGuidLength);
+ if (UseInstanceID(app_id_)) {
+ DCHECK(!base::IsValidGUID(guid));
+
+ // Replace suffix with valid hex so we can validate the rest of the string.
+ guid = guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength,
+ kGuidSuffixLength, 'C');
+ }
+ DCHECK(base::IsValidGUID(guid));
+#endif // DCHECK_IS_ON()
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h
new file mode 100644
index 00000000000..631976003fc
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h
@@ -0,0 +1,152 @@
+// Copyright 2014 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+#include "base/check.h"
+#include "base/gtest_prod_util.h"
+#include "base/time/time.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "url/gurl.h"
+
+class Profile;
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+// The prefix used for all push messaging application ids.
+extern const char kPushMessagingAppIdentifierPrefix[];
+
+// Type used to identify a Service Worker registration from a Push API
+// perspective. These can be persisted to prefs, in a 1:1 mapping between
+// app_id (which includes origin) and service_worker_registration_id.
+// Legacy mapped values saved by old versions of Chrome are also supported;
+// these don't contain the origin in the app_id, so instead they map from
+// app_id to pair<origin, service_worker_registration_id>.
+class PushMessagingAppIdentifier {
+ public:
+ // Register profile-specific prefs.
+ static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+ // Returns whether the modern InstanceID API should be used with this app_id
+ // (rather than legacy GCM registration).
+ static bool UseInstanceID(const std::string& app_id);
+
+ // Generates a new app identifier, with partially random app_id.
+ static PushMessagingAppIdentifier Generate(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt);
+
+ // Looks up an app identifier by app_id. If not found, is_null() will be true.
+ static PushMessagingAppIdentifier FindByAppId(Profile* profile,
+ const std::string& app_id);
+
+ // Looks up an app identifier by origin & service worker registration id.
+ // If not found, is_null() will be true.
+ static PushMessagingAppIdentifier FindByServiceWorker(
+ Profile* profile,
+ const GURL& origin,
+ int64_t service_worker_registration_id);
+
+ // Returns all the PushMessagingAppIdentifiers currently registered for the
+ // given |profile|.
+ static std::vector<PushMessagingAppIdentifier> GetAll(Profile* profile);
+
+ // Deletes all PushMessagingAppIdentifiers currently registered for the given
+ // |profile|.
+ static void DeleteAllFromPrefs(Profile* profile);
+
+ // Returns the number of PushMessagingAppIdentifiers currently registered for
+ // the given |profile|.
+ static size_t GetCount(Profile* profile);
+
+ ~PushMessagingAppIdentifier();
+
+ // Persist this app identifier to prefs.
+ void PersistToPrefs(Profile* profile) const;
+
+ // Delete this app identifier from prefs.
+ void DeleteFromPrefs(Profile* profile) const;
+
+ // Returns true if this identifier does not represent an app (i.e. this was
+ // returned by a failed Find call).
+ bool is_null() const { return service_worker_registration_id_ < 0; }
+
+ // String that should be passed to push services like GCM to identify a
+ // particular Service Worker (so we can route incoming messages). Example:
+ // wp:https://foo.example.com:8443/#9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F
+ // Legacy app_ids have no origin, e.g. wp:9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F
+ const std::string& app_id() const {
+ DCHECK(!is_null());
+ return app_id_;
+ }
+
+ const GURL& origin() const {
+ DCHECK(!is_null());
+ return origin_;
+ }
+
+ int64_t service_worker_registration_id() const {
+ DCHECK(!is_null());
+ return service_worker_registration_id_;
+ }
+
+ void set_expiration_time(const absl::optional<base::Time>& expiration_time) {
+ expiration_time_ = expiration_time;
+ }
+
+ bool IsExpired() const;
+
+ absl::optional<base::Time> expiration_time() const {
+ DCHECK(!is_null());
+ return expiration_time_;
+ }
+
+ // Copy constructor
+ PushMessagingAppIdentifier(const PushMessagingAppIdentifier& other);
+
+ private:
+ friend class PushMessagingAppIdentifierTest;
+ friend class PushMessagingBrowserTestBase;
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingAppIdentifierTest, FindLegacy);
+
+ // Generates a new app identifier for legacy GCM (not modern InstanceID).
+ static PushMessagingAppIdentifier LegacyGenerateForTesting(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt);
+
+ static PushMessagingAppIdentifier GenerateInternal(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ bool use_instance_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt);
+
+ // Constructs an invalid app identifier.
+ PushMessagingAppIdentifier();
+ // Constructs a valid app identifier.
+ PushMessagingAppIdentifier(
+ const std::string& app_id,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt);
+
+ // Validates that all the fields contain valid values.
+ void DCheckValid() const;
+
+ std::string app_id_;
+ GURL origin_;
+ int64_t service_worker_registration_id_;
+ absl::optional<base::Time> expiration_time_;
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc
new file mode 100644
index 00000000000..5b7c649c2e4
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc
@@ -0,0 +1,310 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+
+#include <stdint.h>
+
+#include "base/time/time.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+void ExpectAppIdentifiersEqual(const PushMessagingAppIdentifier& a,
+ const PushMessagingAppIdentifier& b) {
+ EXPECT_EQ(a.app_id(), b.app_id());
+ EXPECT_EQ(a.origin(), b.origin());
+ EXPECT_EQ(a.service_worker_registration_id(),
+ b.service_worker_registration_id());
+ EXPECT_EQ(a.expiration_time(), b.expiration_time());
+}
+
+base::Time kExpirationTime =
+ base::Time::FromDeltaSinceWindowsEpoch(base::Seconds(1));
+
+} // namespace
+
+class PushMessagingAppIdentifierTest : public testing::Test {
+ protected:
+ PushMessagingAppIdentifier GenerateId(
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ // To bypass DCHECK in PushMessagingAppIdentifier::Generate, we just use it
+ // to generate app_id, and then use private constructor.
+ std::string app_id = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 1)
+ .app_id();
+ return PushMessagingAppIdentifier(app_id, origin,
+ service_worker_registration_id);
+ }
+
+ void SetUp() override {
+ original_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 1);
+ same_origin_and_sw_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com"), 1);
+ different_origin_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://foobar.example.com/"), 1);
+ different_sw_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 42);
+ with_et_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 1, kExpirationTime);
+ different_et_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 1,
+ kExpirationTime + base::Seconds(100));
+ }
+
+ Profile* profile() { return &profile_; }
+
+ PushMessagingAppIdentifier original_;
+ PushMessagingAppIdentifier same_origin_and_sw_;
+ PushMessagingAppIdentifier different_origin_;
+ PushMessagingAppIdentifier different_sw_;
+ PushMessagingAppIdentifier different_et_;
+ PushMessagingAppIdentifier with_et_;
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfile profile_;
+};
+
+TEST_F(PushMessagingAppIdentifierTest, ConstructorValidity) {
+ // The following two are valid:
+ EXPECT_FALSE(GenerateId(GURL("https://www.example.com/"), 1).is_null());
+ EXPECT_FALSE(GenerateId(GURL("https://www.example.com"), 1).is_null());
+ // The following four are invalid and will DCHECK in Generate:
+ EXPECT_FALSE(GenerateId(GURL(""), 1).is_null());
+ EXPECT_FALSE(GenerateId(GURL("foo"), 1).is_null());
+ EXPECT_FALSE(GenerateId(GURL("https://www.example.com/foo"), 1).is_null());
+ EXPECT_FALSE(GenerateId(GURL("https://www.example.com/#foo"), 1).is_null());
+ // The following one is invalid and will DCHECK in Generate and be null:
+ EXPECT_TRUE(GenerateId(GURL("https://www.example.com/"), -1).is_null());
+}
+
+TEST_F(PushMessagingAppIdentifierTest, UniqueGuids) {
+ EXPECT_NE(
+ PushMessagingAppIdentifier::Generate(GURL("https://www.example.com/"), 1)
+ .app_id(),
+ PushMessagingAppIdentifier::Generate(GURL("https://www.example.com/"), 1)
+ .app_id());
+}
+
+TEST_F(PushMessagingAppIdentifierTest, FindInvalidAppId) {
+ // These calls to FindByAppId should not DCHECK.
+ EXPECT_TRUE(PushMessagingAppIdentifier::FindByAppId(profile(), "").is_null());
+ EXPECT_TRUE(PushMessagingAppIdentifier::FindByAppId(
+ profile(), "amhfneadkjmnlefnpidcijoldiibcdnd")
+ .is_null());
+}
+
+TEST_F(PushMessagingAppIdentifierTest, PersistAndFind) {
+ ASSERT_TRUE(
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id())
+ .is_null());
+
+ const auto identifier = PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+
+ ASSERT_TRUE(identifier.is_null());
+
+ // Test basic PersistToPrefs round trips.
+ original_.PersistToPrefs(profile());
+ {
+ PushMessagingAppIdentifier found_by_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_FALSE(found_by_app_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_app_id);
+ }
+ {
+ PushMessagingAppIdentifier found_by_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_FALSE(found_by_origin_and_swr_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_origin_and_swr_id);
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, FindLegacy) {
+ const std::string legacy_app_id("wp:9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F");
+ ASSERT_TRUE(PushMessagingAppIdentifier::FindByAppId(profile(), legacy_app_id)
+ .is_null());
+
+ const auto identifier = PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+
+ ASSERT_TRUE(identifier.is_null());
+
+ // Create a legacy preferences entry (the test happens to use PersistToPrefs
+ // since that currently works, but it's ok to change the behavior of
+ // PersistToPrefs; if so, this test can just do a raw DictionaryPrefUpdate).
+ original_.app_id_ = legacy_app_id;
+ original_.PersistToPrefs(profile());
+
+ // Test that legacy entries can be read back from prefs.
+ {
+ PushMessagingAppIdentifier found_by_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_FALSE(found_by_app_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_app_id);
+ }
+ {
+ PushMessagingAppIdentifier found_by_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_FALSE(found_by_origin_and_swr_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_origin_and_swr_id);
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, PersistOverwritesSameOriginAndSW) {
+ original_.PersistToPrefs(profile());
+
+ // Test that PersistToPrefs overwrites when same origin and Service Worker.
+ ASSERT_NE(original_.app_id(), same_origin_and_sw_.app_id());
+ ASSERT_EQ(original_.origin(), same_origin_and_sw_.origin());
+ ASSERT_EQ(original_.service_worker_registration_id(),
+ same_origin_and_sw_.service_worker_registration_id());
+ same_origin_and_sw_.PersistToPrefs(profile());
+ {
+ PushMessagingAppIdentifier found_by_original_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_TRUE(found_by_original_app_id.is_null());
+ }
+ {
+ PushMessagingAppIdentifier found_by_soas_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(),
+ same_origin_and_sw_.app_id());
+ EXPECT_FALSE(found_by_soas_app_id.is_null());
+ ExpectAppIdentifiersEqual(same_origin_and_sw_, found_by_soas_app_id);
+ }
+ {
+ PushMessagingAppIdentifier found_by_original_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_FALSE(found_by_original_origin_and_swr_id.is_null());
+ ExpectAppIdentifiersEqual(same_origin_and_sw_,
+ found_by_original_origin_and_swr_id);
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, PersistDoesNotOverwriteDifferent) {
+ original_.PersistToPrefs(profile());
+
+ // Test that PersistToPrefs doesn't overwrite when different origin or SW.
+ ASSERT_NE(original_.app_id(), different_origin_.app_id());
+ ASSERT_NE(original_.app_id(), different_sw_.app_id());
+ different_origin_.PersistToPrefs(profile());
+ different_sw_.PersistToPrefs(profile());
+ {
+ PushMessagingAppIdentifier found_by_original_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_FALSE(found_by_original_app_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_original_app_id);
+ }
+ {
+ PushMessagingAppIdentifier found_by_original_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_FALSE(found_by_original_origin_and_swr_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_original_origin_and_swr_id);
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, DeleteFromPrefs) {
+ original_.PersistToPrefs(profile());
+ different_origin_.PersistToPrefs(profile());
+ different_sw_.PersistToPrefs(profile());
+
+ // Test DeleteFromPrefs. Deleted app identifier should be deleted.
+ original_.DeleteFromPrefs(profile());
+ {
+ PushMessagingAppIdentifier found_by_original_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_TRUE(found_by_original_app_id.is_null());
+ }
+ {
+ PushMessagingAppIdentifier found_by_original_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_TRUE(found_by_original_origin_and_swr_id.is_null());
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, GetAll) {
+ original_.PersistToPrefs(profile());
+ different_origin_.PersistToPrefs(profile());
+ different_sw_.PersistToPrefs(profile());
+
+ original_.DeleteFromPrefs(profile());
+
+ // Test GetAll. Non-deleted app identifiers should all be listed.
+ std::vector<PushMessagingAppIdentifier> all_app_identifiers =
+ PushMessagingAppIdentifier::GetAll(profile());
+ EXPECT_EQ(2u, all_app_identifiers.size());
+ // Order is unspecified.
+ bool contained_different_origin = false;
+ bool contained_different_sw = false;
+ for (const PushMessagingAppIdentifier& app_identifier : all_app_identifiers) {
+ if (app_identifier.app_id() == different_origin_.app_id()) {
+ ExpectAppIdentifiersEqual(different_origin_, app_identifier);
+ contained_different_origin = true;
+ } else {
+ ExpectAppIdentifiersEqual(different_sw_, app_identifier);
+ contained_different_sw = true;
+ }
+ }
+ EXPECT_TRUE(contained_different_origin);
+ EXPECT_TRUE(contained_different_sw);
+}
+
+TEST_F(PushMessagingAppIdentifierTest, PersistWithExpirationTime) {
+ ASSERT_TRUE(with_et_.expiration_time());
+ ASSERT_TRUE(different_et_.expiration_time());
+ ASSERT_EQ(with_et_.origin(), different_et_.origin());
+ ASSERT_EQ(with_et_.service_worker_registration_id(),
+ different_et_.service_worker_registration_id());
+ ASSERT_FALSE(kExpirationTime.is_null());
+
+ different_et_.PersistToPrefs(profile());
+
+ // Test PersistToPrefs and FindByAppId, whether expiration time is saved
+ // properly
+ std::vector<PushMessagingAppIdentifier> all_app_identifiers =
+ PushMessagingAppIdentifier::GetAll(profile());
+ EXPECT_EQ(1u, all_app_identifiers.size());
+ {
+ PushMessagingAppIdentifier found_by_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(),
+ different_et_.app_id());
+ // Check whether expiration time was saved
+ ExpectAppIdentifiersEqual(found_by_app_id, different_et_);
+ }
+ with_et_.PersistToPrefs(profile());
+ {
+ all_app_identifiers = PushMessagingAppIdentifier::GetAll(profile());
+ EXPECT_EQ(1u, all_app_identifiers.size());
+ }
+ {
+ PushMessagingAppIdentifier found_by_with_et_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), with_et_.app_id());
+ EXPECT_FALSE(found_by_with_et_app_id.is_null());
+ EXPECT_EQ(found_by_with_et_app_id.expiration_time(), kExpirationTime);
+ ExpectAppIdentifiersEqual(found_by_with_et_app_id, with_et_);
+ }
+ {
+ PushMessagingAppIdentifier found_by_different_et_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(),
+ different_et_.app_id());
+ EXPECT_TRUE(found_by_different_et_app_id.is_null());
+ }
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc b/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc
new file mode 100644
index 00000000000..b94212aad9f
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc
@@ -0,0 +1,3227 @@
+// Copyright 2014 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 <stddef.h>
+#include <stdint.h>
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "base/barrier_closure.h"
+#include "base/base64url.h"
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
+#include "chrome/browser/chrome_notification_types.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
+#include "chrome/browser/notifications/notification_display_service_tester.h"
+#include "chrome/browser/notifications/notification_handler.h"
+#include "chrome/browser/permissions/crowd_deny_fake_safe_browsing_database_manager.h"
+#include "chrome/browser/permissions/crowd_deny_preload_data.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "chrome/browser/push_messaging/push_messaging_features.h"
+#include "chrome/browser/push_messaging/push_messaging_service_factory.h"
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/buildflags.h"
+#include "chrome/common/chrome_features.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/browsing_data/content/browsing_data_helper.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "components/content_settings/core/common/content_settings_types.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/fake_gcm_profile_service.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h"
+#include "components/gcm_driver/instance_id/instance_id_driver.h"
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "components/keep_alive_registry/keep_alive_registry.h"
+#include "components/keep_alive_registry/keep_alive_types.h"
+#include "components/network_session_configurator/common/network_switches.h"
+#include "components/permissions/permission_request_manager.h"
+#include "components/site_engagement/content/site_engagement_score.h"
+#include "components/site_engagement/content/site_engagement_service.h"
+#include "content/public/browser/browsing_data_remover.h"
+#include "content/public/browser/notification_service.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/common/content_features.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/browsing_data_remover_test_util.h"
+#include "content/public/test/prerender_test_util.h"
+#include "content/public/test/test_utils.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+#include "ui/base/window_open_disposition.h"
+#include "ui/message_center/public/cpp/notification.h"
+
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+#include "chrome/browser/background/background_mode_manager.h"
+#endif
+
+namespace {
+
+const char kManifestSenderId[] = "1234567890";
+const int32_t kApplicationServerKeyLength = 65;
+
+enum class PushSubscriptionKeyFormat { kOmitKey, kBinary, kBase64UrlEncoded };
+
+// NIST P-256 public key made available to tests. Must be an uncompressed
+// point in accordance with SEC1 2.3.3.
+const uint8_t kApplicationServerKey[kApplicationServerKeyLength] = {
+ 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36,
+ 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48,
+ 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B,
+ 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13,
+ 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76,
+ 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD};
+
+// URL-safe base64 encoded version of the |kApplicationServerKey|.
+const char kEncodedApplicationServerKey[] =
+ "BFVSaqVujqpHlzYQwWY8HmW_oXvuSMnGu78CGFNyHQx7qeMRtwNSIdNxkBOowc_tIPcf0X_ydr"
+ "YBINg1pdk8Q_0";
+
+// From chrome/browser/push_messaging/push_messaging_manager.cc
+const char* kIncognitoWarningPattern =
+ "Chrome currently does not support the Push API in incognito mode "
+ "(https://crbug.com/401439). There is deliberately no way to "
+ "feature-detect this, since incognito mode needs to be undetectable by "
+ "websites.";
+
+std::string GetTestApplicationServerKey(bool base64_url_encoded = false) {
+ std::string application_server_key;
+
+ if (base64_url_encoded) {
+ base::Base64UrlEncode(reinterpret_cast<const char*>(kApplicationServerKey),
+ base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &application_server_key);
+ } else {
+ application_server_key =
+ std::string(kApplicationServerKey,
+ kApplicationServerKey + base::size(kApplicationServerKey));
+ }
+
+ return application_server_key;
+}
+
+void LegacyRegisterCallback(base::OnceClosure done_callback,
+ std::string* out_registration_id,
+ gcm::GCMClient::Result* out_result,
+ const std::string& registration_id,
+ gcm::GCMClient::Result result) {
+ if (out_registration_id)
+ *out_registration_id = registration_id;
+ if (out_result)
+ *out_result = result;
+ std::move(done_callback).Run();
+}
+
+void DidRegister(base::OnceClosure done_callback,
+ const std::string& registration_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ EXPECT_EQ(blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE,
+ status);
+ std::move(done_callback).Run();
+}
+
+void InstanceIDResultCallback(base::OnceClosure done_callback,
+ instance_id::InstanceID::Result* out_result,
+ instance_id::InstanceID::Result result) {
+ DCHECK(out_result);
+ *out_result = result;
+ std::move(done_callback).Run();
+}
+
+} // namespace
+
+class PushMessagingBrowserTestBase : public InProcessBrowserTest {
+ public:
+ PushMessagingBrowserTestBase()
+ : scoped_testing_factory_installer_(
+ base::BindRepeating(&gcm::FakeGCMProfileService::Build)),
+ gcm_service_(nullptr),
+ gcm_driver_(nullptr) {}
+
+ ~PushMessagingBrowserTestBase() override = default;
+
+ PushMessagingBrowserTestBase(const PushMessagingBrowserTestBase&) = delete;
+ PushMessagingBrowserTestBase& operator=(const PushMessagingBrowserTestBase&) =
+ delete;
+
+ // InProcessBrowserTest:
+ void SetUp() override {
+ https_server_ = std::make_unique<net::EmbeddedTestServer>(
+ net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server_->ServeFilesFromSourceDirectory(GetChromeTestDataDir());
+ content::SetupCrossSiteRedirector(https_server_.get());
+
+ site_engagement::SiteEngagementScore::SetParamValuesForTesting();
+ InProcessBrowserTest::SetUp();
+ }
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ // Enable experimental features for subscription restrictions.
+ command_line->AppendSwitch(
+ switches::kEnableExperimentalWebPlatformFeatures);
+
+ // HTTPS server only serves a valid cert for localhost, so this is needed to
+ // load webby domains like "embedded.com" without an interstitial.
+ command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
+ }
+
+ // InProcessBrowserTest:
+ void SetUpOnMainThread() override {
+ host_resolver()->AddRule("*", "127.0.0.1");
+ ASSERT_TRUE(https_server_->Start());
+
+ KeyedService* keyed_service =
+ gcm::GCMProfileServiceFactory::GetForProfile(GetBrowser()->profile());
+ if (keyed_service) {
+ gcm_service_ = static_cast<gcm::FakeGCMProfileService*>(keyed_service);
+ gcm_driver_ = static_cast<instance_id::FakeGCMDriverForInstanceID*>(
+ gcm_service_->driver());
+ }
+
+ notification_tester_ = std::make_unique<NotificationDisplayServiceTester>(
+ GetBrowser()->profile());
+
+ push_service_ =
+ PushMessagingServiceFactory::GetForProfile(GetBrowser()->profile());
+
+ LoadTestPage();
+ }
+
+ void TearDownOnMainThread() override {
+ notification_tester_.reset();
+ InProcessBrowserTest::TearDownOnMainThread();
+ }
+
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void RestartPushService() {
+ Profile* profile = GetBrowser()->profile();
+ PushMessagingServiceFactory::GetInstance()->SetTestingFactory(
+ profile, BrowserContextKeyedServiceFactory::TestingFactory());
+ ASSERT_EQ(nullptr, PushMessagingServiceFactory::GetForProfile(profile));
+ PushMessagingServiceFactory::GetInstance()->RestoreFactoryForTests(profile);
+ PushMessagingServiceImpl::InitializeForProfile(profile);
+ push_service_ = PushMessagingServiceFactory::GetForProfile(profile);
+ }
+
+ // Helper function to test if a Keep Alive is registered while avoiding the
+ // platform checks. Returns a boolean so that assertion failures are reported
+ // at the right line.
+ // Returns true when KeepAlives are not supported by the platform, or when
+ // the registration state is equal to the expectation.
+ bool IsRegisteredKeepAliveEqualTo(bool expectation) {
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ return expectation ==
+ KeepAliveRegistry::GetInstance()->IsOriginRegistered(
+ KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE);
+#else
+ return true;
+#endif
+ }
+
+ void LoadTestPage(const std::string& path) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(),
+ https_server_->GetURL(path)));
+ }
+
+ void LoadTestPage() { LoadTestPage(GetTestURL()); }
+
+ void LoadTestPageWithoutManifest() { LoadTestPage(GetNoManifestTestURL()); }
+
+ bool RunScript(const std::string& script, std::string* result) {
+ return RunScript(script, result, nullptr);
+ }
+
+ bool RunScript(const std::string& script, std::string* result,
+ content::WebContents* web_contents) {
+ if (!web_contents)
+ web_contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
+ return content::ExecuteScriptAndExtractString(web_contents->GetMainFrame(),
+ script, result);
+ }
+
+ gcm::GCMAppHandler* GetAppHandler() {
+ return gcm_driver_->GetAppHandler(kPushMessagingAppIdentifierPrefix);
+ }
+
+ permissions::PermissionRequestManager* GetPermissionRequestManager() {
+ return permissions::PermissionRequestManager::FromWebContents(
+ GetBrowser()->tab_strip_model()->GetActiveWebContents());
+ }
+
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void RequestAndAcceptPermission();
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void RequestAndDenyPermission();
+
+ // Sets out_token to the subscription token (not including server URL).
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void SubscribeSuccessfully(
+ PushSubscriptionKeyFormat key_format = PushSubscriptionKeyFormat::kBinary,
+ std::string* out_token = nullptr);
+
+ // Sets up the state corresponding to a dangling push subscription whose
+ // service worker registration no longer exists. Some users may be left with
+ // such orphaned subscriptions due to service worker unregistrations not
+ // clearing push subscriptions in the past. This allows us to emulate that.
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void SetupOrphanedPushSubscription(std::string* out_app_id);
+
+ // Legacy subscribe path using GCMDriver rather than Instance IDs. Only
+ // for testing that we maintain support for existing stored registrations.
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void LegacySubscribeSuccessfully(std::string* out_subscription_id = nullptr);
+
+ // Strips server URL from a registration endpoint to get subscription token.
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void EndpointToToken(const std::string& endpoint,
+ bool standard_protocol = true,
+ std::string* out_token = nullptr);
+
+ blink::mojom::PushSubscriptionPtr GetSubscriptionForAppIdentifier(
+ const PushMessagingAppIdentifier& app_identifier) {
+ blink::mojom::PushSubscriptionPtr result;
+ base::RunLoop run_loop;
+ push_service_->GetPushSubscriptionFromAppIdentifier(
+ app_identifier,
+ base::BindLambdaForTesting(
+ [&](blink::mojom::PushSubscriptionPtr subscription) {
+ result = std::move(subscription);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+ return result;
+ }
+
+ // Deletes an Instance ID from the GCM Store but keeps the push subscription
+ // stored in the PushMessagingAppIdentifier map and Service Worker DB.
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void DeleteInstanceIDAsIfGCMStoreReset(const std::string& app_id);
+
+ PushMessagingAppIdentifier GetAppIdentifierForServiceWorkerRegistration(
+ int64_t service_worker_registration_id);
+
+ void SendMessageAndWaitUntilHandled(
+ const PushMessagingAppIdentifier& app_identifier,
+ const gcm::IncomingMessage& message);
+
+ net::EmbeddedTestServer* https_server() const { return https_server_.get(); }
+
+ // Returns a vector of the currently displayed Notification objects.
+ std::vector<message_center::Notification> GetDisplayedNotifications() {
+ return notification_tester_->GetDisplayedNotificationsForType(
+ NotificationHandler::Type::WEB_PERSISTENT);
+ }
+
+ // Returns the number of notifications that are currently being shown.
+ size_t GetNotificationCount() { return GetDisplayedNotifications().size(); }
+
+ // Removes all shown notifications.
+ void RemoveAllNotifications() {
+ notification_tester_->RemoveAllNotifications(
+ NotificationHandler::Type::WEB_PERSISTENT, true /* by_user */);
+ }
+
+ // To be called when delivery of a push message has finished. The |run_loop|
+ // will be told to quit after |messages_required| messages were received.
+ void OnDeliveryFinished(std::vector<size_t>* number_of_notifications_shown,
+ base::OnceClosure done_closure) {
+ DCHECK(number_of_notifications_shown);
+ number_of_notifications_shown->push_back(GetNotificationCount());
+
+ std::move(done_closure).Run();
+ }
+
+ PushMessagingServiceImpl* push_service() const { return push_service_; }
+
+ void SetSiteEngagementScore(const GURL& url, double score) {
+ site_engagement::SiteEngagementService* service =
+ site_engagement::SiteEngagementService::Get(GetBrowser()->profile());
+ service->ResetBaseScoreForURL(url, score);
+ EXPECT_EQ(score, service->GetScore(url));
+ }
+
+ // Matches |tag| against the notification's ID to see if the notification's
+ // js-provided tag could have been |tag|. This is not perfect as it might
+ // return true for a |tag| that is a substring of the original tag.
+ static bool TagEquals(const message_center::Notification& notification,
+ const std::string& tag) {
+ return std::string::npos != notification.id().find(tag);
+ }
+
+ protected:
+ virtual std::string GetTestURL() { return "/push_messaging/test.html"; }
+
+ virtual std::string GetNoManifestTestURL() {
+ return "/push_messaging/test_no_manifest.html";
+ }
+
+ virtual Browser* GetBrowser() const { return browser(); }
+
+ gcm::GCMProfileServiceFactory::ScopedTestingFactoryInstaller
+ scoped_testing_factory_installer_;
+
+ raw_ptr<gcm::FakeGCMProfileService> gcm_service_;
+ raw_ptr<instance_id::FakeGCMDriverForInstanceID> gcm_driver_;
+ base::HistogramTester histogram_tester_;
+
+ std::unique_ptr<NotificationDisplayServiceTester> notification_tester_;
+
+ private:
+ std::unique_ptr<net::EmbeddedTestServer> https_server_;
+ raw_ptr<PushMessagingServiceImpl> push_service_;
+};
+
+void PushMessagingBrowserTestBase::RequestAndAcceptPermission() {
+ std::string script_result;
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::ACCEPT_ALL);
+ ASSERT_TRUE(RunScript("requestNotificationPermission();", &script_result));
+ ASSERT_EQ("permission status - granted", script_result);
+}
+
+void PushMessagingBrowserTestBase::RequestAndDenyPermission() {
+ std::string script_result;
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::DENY_ALL);
+ ASSERT_TRUE(RunScript("requestNotificationPermission();", &script_result));
+ ASSERT_EQ("permission status - denied", script_result);
+}
+
+void PushMessagingBrowserTestBase::SubscribeSuccessfully(
+ PushSubscriptionKeyFormat key_format,
+ std::string* out_token) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ switch (key_format) {
+ case PushSubscriptionKeyFormat::kBinary:
+ ASSERT_TRUE(RunScript("removeManifest()", &script_result));
+ ASSERT_EQ("manifest removed", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, true, out_token));
+ break;
+ case PushSubscriptionKeyFormat::kBase64UrlEncoded:
+ ASSERT_TRUE(RunScript("removeManifest()", &script_result));
+ ASSERT_EQ("manifest removed", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePushWithBase64URLEncodedString()",
+ &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, true, out_token));
+ break;
+ case PushSubscriptionKeyFormat::kOmitKey:
+ // Test backwards compatibility with old ID based subscriptions.
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushWithoutKey()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, false, out_token));
+ break;
+ default:
+ NOTREACHED();
+ }
+}
+
+void PushMessagingBrowserTestBase::SetupOrphanedPushSubscription(
+ std::string* out_app_id) {
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+ GURL requesting_origin =
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ // Use 1234LL as it's unlikely to collide with an active service worker
+ // registration id (they increment from 0).
+ const int64_t service_worker_registration_id = 1234LL;
+
+ auto options = blink::mojom::PushSubscriptionOptions::New();
+ options->user_visible_only = true;
+
+ std::string test_application_server_key = GetTestApplicationServerKey();
+ options->application_server_key = std::vector<uint8_t>(
+ test_application_server_key.begin(), test_application_server_key.end());
+
+ base::RunLoop run_loop;
+ push_service()->SubscribeFromWorker(
+ requesting_origin, service_worker_registration_id, std::move(options),
+ base::BindOnce(&DidRegister, run_loop.QuitClosure()));
+ run_loop.Run();
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), requesting_origin,
+ service_worker_registration_id);
+ ASSERT_FALSE(app_identifier.is_null());
+ *out_app_id = app_identifier.app_id();
+}
+
+void PushMessagingBrowserTestBase::LegacySubscribeSuccessfully(
+ std::string* out_subscription_id) {
+ // Create a non-InstanceID GCM registration. Have to directly access
+ // GCMDriver, since this codepath has been deleted from Push.
+
+ std::string script_result;
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ GURL requesting_origin =
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ int64_t service_worker_registration_id = 0LL;
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::LegacyGenerateForTesting(
+ requesting_origin, service_worker_registration_id);
+ push_service_->IncreasePushSubscriptionCount(1, true /* is_pending */);
+
+ std::string subscription_id;
+ {
+ base::RunLoop run_loop;
+ gcm::GCMClient::Result register_result = gcm::GCMClient::UNKNOWN_ERROR;
+ gcm_driver_->Register(
+ app_identifier.app_id(), {kManifestSenderId},
+ base::BindOnce(&LegacyRegisterCallback, run_loop.QuitClosure(),
+ &subscription_id, &register_result));
+ run_loop.Run();
+ ASSERT_EQ(gcm::GCMClient::SUCCESS, register_result);
+ }
+
+ app_identifier.PersistToPrefs(GetBrowser()->profile());
+ push_service_->IncreasePushSubscriptionCount(1, false /* is_pending */);
+ push_service_->DecreasePushSubscriptionCount(1, true /* was_pending */);
+
+ {
+ base::RunLoop run_loop;
+ push_service_->StorePushSubscriptionForTesting(
+ GetBrowser()->profile(), requesting_origin,
+ service_worker_registration_id, subscription_id, kManifestSenderId,
+ run_loop.QuitClosure());
+ run_loop.Run();
+ }
+
+ if (out_subscription_id)
+ *out_subscription_id = subscription_id;
+}
+
+void PushMessagingBrowserTestBase::EndpointToToken(const std::string& endpoint,
+ bool standard_protocol,
+ std::string* out_token) {
+ size_t last_slash = endpoint.rfind('/');
+
+ ASSERT_EQ(kPushMessagingGcmEndpoint, endpoint.substr(0, last_slash + 1));
+
+ ASSERT_LT(last_slash + 1, endpoint.length()); // Token must not be empty.
+
+ if (out_token)
+ *out_token = endpoint.substr(last_slash + 1);
+}
+
+PushMessagingAppIdentifier
+PushMessagingBrowserTestBase::GetAppIdentifierForServiceWorkerRegistration(
+ int64_t service_worker_registration_id) {
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin, service_worker_registration_id);
+ EXPECT_FALSE(app_identifier.is_null());
+ return app_identifier;
+}
+
+void PushMessagingBrowserTestBase::DeleteInstanceIDAsIfGCMStoreReset(
+ const std::string& app_id) {
+ // Delete the Instance ID directly, keeping the push subscription stored in
+ // the PushMessagingAppIdentifier map and the Service Worker database. This
+ // simulates the GCM Store getting reset but failing to clear push
+ // subscriptions, either because the store got reset before
+ // 93ec793ac69a542b2213297737178a55d069fd0d (Chrome 56), or because a race
+ // condition (e.g. shutdown) prevents PushMessagingServiceImpl::OnStoreReset
+ // from clearing all subscriptions.
+ instance_id::InstanceIDProfileService* instance_id_profile_service =
+ instance_id::InstanceIDProfileServiceFactory::GetForProfile(
+ GetBrowser()->profile());
+ DCHECK(instance_id_profile_service);
+ instance_id::InstanceIDDriver* instance_id_driver =
+ instance_id_profile_service->driver();
+ DCHECK(instance_id_driver);
+ instance_id::InstanceID::Result delete_result =
+ instance_id::InstanceID::UNKNOWN_ERROR;
+ base::RunLoop run_loop;
+ instance_id_driver->GetInstanceID(app_id)->DeleteID(base::BindOnce(
+ &InstanceIDResultCallback, run_loop.QuitClosure(), &delete_result));
+ run_loop.Run();
+ ASSERT_EQ(instance_id::InstanceID::SUCCESS, delete_result);
+}
+
+void PushMessagingBrowserTestBase::SendMessageAndWaitUntilHandled(
+ const PushMessagingAppIdentifier& app_identifier,
+ const gcm::IncomingMessage& message) {
+ base::RunLoop run_loop;
+ push_service()->SetMessageCallbackForTesting(run_loop.QuitClosure());
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ run_loop.Run();
+}
+
+class PushMessagingBrowserTest : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingBrowserTest() {
+ feature_list_.InitAndDisableFeature(
+ features::kPushMessagingDisallowSenderIDs);
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeWithoutKeySuccessNotificationsGranted) {
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey));
+ EXPECT_EQ(kManifestSenderId, gcm_driver_->last_gettoken_authorized_entity());
+ EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(),
+ gcm_driver_->last_gettoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeSuccessNotificationsGranted) {
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ EXPECT_EQ(kEncodedApplicationServerKey,
+ gcm_driver_->last_gettoken_authorized_entity());
+ EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(),
+ gcm_driver_->last_gettoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeSuccessNotificationsGrantedWithBase64URLKey) {
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBase64UrlEncoded));
+ EXPECT_EQ(kEncodedApplicationServerKey,
+ gcm_driver_->last_gettoken_authorized_entity());
+ EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(),
+ gcm_driver_->last_gettoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeSuccessNotificationsPrompt) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::ACCEPT_ALL);
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ // Both of these methods EXPECT that they succeed.
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result));
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeFailureNotificationsBlocked) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndDenyPermission());
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ EXPECT_EQ("NotAllowedError - Registration failed - permission denied",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeFailureNoManifest) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ ASSERT_TRUE(RunScript("removeManifest()", &script_result));
+ ASSERT_EQ("manifest removed", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "manifest empty or missing",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeFailureNoSenderId) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ ASSERT_TRUE(RunScript("swapManifestNoSenderId()", &script_result));
+ ASSERT_EQ("sender id removed from manifest", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "gcm_sender_id not found in manifest",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ RegisterFailureEmptyPushSubscriptionOptions) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushWithEmptyOptions()", &script_result));
+ EXPECT_EQ("NotAllowedError - Registration failed - permission denied",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeWithInvalidation) {
+ std::string token1, token2, token3;
+
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token1));
+ ASSERT_FALSE(token1.empty());
+
+ // Repeated calls to |subscribe()| should yield the same token.
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2));
+ ASSERT_EQ(token1, token2);
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(),
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL(),
+ 0LL /* service_worker_registration_id */);
+
+ ASSERT_FALSE(app_identifier.is_null());
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ // Delete the InstanceID. This captures two scenarios: either the database was
+ // corrupted, or the subscription was invalidated by the server.
+ ASSERT_NO_FATAL_FAILURE(
+ DeleteInstanceIDAsIfGCMStoreReset(app_identifier.app_id()));
+
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id());
+
+ // Repeated calls to |subscribe()| will now (silently) result in a new token.
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token3));
+ ASSERT_FALSE(token3.empty());
+ EXPECT_NE(token1, token3);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeWorker) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Try to subscribe from a worker without a key. This should fail.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "gcm_sender_id not found in manifest",
+ script_result);
+
+ // Now run the subscribe with a key. This should succeed.
+ ASSERT_TRUE(RunScript("workerSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, true /* standard_protocol */));
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeWorkerWithBase64URLEncodedString) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Try to subscribe from a worker without a key. This should fail.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "gcm_sender_id not found in manifest",
+ script_result);
+
+ // Now run the subscribe with a key. This should succeed.
+ ASSERT_TRUE(RunScript("workerSubscribePushWithBase64URLEncodedString()",
+ &script_result));
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, true /* standard_protocol */));
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingWithKeyInManifest) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscription from the document without a key, this will trigger
+ // the code to read sender id from the manifest and will write it to the
+ // datastore.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token1));
+
+ ASSERT_TRUE(RunScript("removeManifest()", &script_result));
+ ASSERT_EQ("manifest removed", script_result);
+
+ // Try to resubscribe from the document without a key or manifest.
+ // This should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now run the subscribe from the service worker without a key.
+ // In this case, the sender id should be read from the datastore.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token2));
+ EXPECT_EQ(token1, token2);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, subscribe again from the worker with no key.
+ // The sender id should again be read from the datastore, so the
+ // subcribe should succeed, and we should get a new subscription token.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token3));
+ EXPECT_NE(token1, token3);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingFromDocumentWithP256Key) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPageWithoutManifest(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscription from the document with a key.
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result));
+
+ // Try to resubscribe from the document without a key - should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now try to resubscribe from the service worker without a key.
+ // This should also fail as the original key was not numeric.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and gcm_sender_id not found in manifest",
+ script_result);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, try to resubscribe again without a key.
+ // This should again fail.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and gcm_sender_id not found in manifest",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingFromWorkerWithP256Key) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPageWithoutManifest(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscribe from the service worker with a key.
+ // This should succeed.
+ ASSERT_TRUE(RunScript("workerSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, true /* standard_protocol */));
+
+ // Try to resubscribe from the document without a key - should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now try to resubscribe from the service worker without a key.
+ // This should also fail as the original key was not numeric.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "gcm_sender_id not found in manifest",
+ script_result);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, try to resubscribe again without a key.
+ // This should again fail.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and gcm_sender_id not found in manifest",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingFromDocumentWithNumber) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPageWithoutManifest(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscribe from the document with a numeric key.
+ // This should succeed.
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushWithNumericKey()", &script_result));
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token1));
+
+ // Try to resubscribe from the document without a key - should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now run the subscribe from the service worker without a key.
+ // In this case, the sender id should be read from the datastore.
+ // Note, we would rather this failed as we only really want to support
+ // no-key subscribes after subscribing with a numeric gcm sender id in the
+ // manifest, not a numeric applicationServerKey, but for code simplicity
+ // this case is allowed.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token2));
+ EXPECT_EQ(token1, token2);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, subscribe again from the worker with no key.
+ // The sender id should again be read from the datastore, so the
+ // subcribe should succeed, and we should get a new subscription token.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token3));
+ EXPECT_NE(token1, token3);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingFromWorkerWithNumber) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPageWithoutManifest(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscribe from the service worker with a numeric key.
+ // This should succeed.
+ ASSERT_TRUE(RunScript("workerSubscribePushWithNumericKey()", &script_result));
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token1));
+
+ // Try to resubscribe from the document without a key - should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now run the subscribe from the service worker without a key.
+ // In this case, the sender id should be read from the datastore.
+ // Note, we would rather this failed as we only really want to support
+ // no-key subscribes after subscribing with a numeric gcm sender id in the
+ // manifest, not a numeric applicationServerKey, but for code simplicity
+ // this case is allowed.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token2));
+ EXPECT_EQ(token1, token2);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, subscribe again from the worker with no key.
+ // The sender id should again be read from the datastore, so the
+ // subcribe should succeed, and we should get a new subscription token.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token3));
+ EXPECT_NE(token1, token3);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, ResubscribeWithMismatchedKey) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscribe from the service worker with a key.
+ // This should succeed.
+ ASSERT_TRUE(
+ RunScript("workerSubscribePushWithNumericKey('11111')", &script_result));
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token1));
+
+ // Try to resubscribe with a different key - should fail.
+ ASSERT_TRUE(
+ RunScript("workerSubscribePushWithNumericKey('22222')", &script_result));
+ EXPECT_EQ(
+ "InvalidStateError - Registration failed - A subscription with a "
+ "different applicationServerKey (or gcm_sender_id) already exists; to "
+ "change the applicationServerKey, unsubscribe then resubscribe.",
+ script_result);
+
+ // Try to resubscribe with the original key - should succeed.
+ ASSERT_TRUE(
+ RunScript("workerSubscribePushWithNumericKey('11111')", &script_result));
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token2));
+ EXPECT_EQ(token1, token2);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // Resubscribe with a different key after unsubscribing.
+ // Should succeed, and we should get a new subscription token.
+ ASSERT_TRUE(
+ RunScript("workerSubscribePushWithNumericKey('22222')", &script_result));
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token3));
+ EXPECT_NE(token1, token3);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribePersisted) {
+ std::string script_result;
+
+ // First, test that Service Worker registration IDs are assigned in order of
+ // registering the Service Workers, and the (fake) push subscription ids are
+ // assigned in order of push subscription (even when these orders are
+ // different).
+
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token1));
+ PushMessagingAppIdentifier sw0_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+ EXPECT_EQ(sw0_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ LoadTestPage("/push_messaging/subscope1/test.html");
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ LoadTestPage("/push_messaging/subscope2/test.html");
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ // Note that we need to reload the page after registering, otherwise
+ // navigator.serviceWorker.ready is going to be resolved with the parent
+ // Service Worker which still controls the page.
+ LoadTestPage("/push_messaging/subscope2/test.html");
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2));
+ EXPECT_NE(token1, token2);
+ PushMessagingAppIdentifier sw2_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(2LL);
+ EXPECT_EQ(sw2_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ LoadTestPage("/push_messaging/subscope1/test.html");
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token3));
+ EXPECT_NE(token1, token3);
+ EXPECT_NE(token2, token3);
+ PushMessagingAppIdentifier sw1_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(1LL);
+ EXPECT_EQ(sw1_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ // Now test that the Service Worker registration IDs and push subscription IDs
+ // generated above were persisted to SW storage, by checking that they are
+ // unchanged despite requesting them in a different order.
+
+ LoadTestPage("/push_messaging/subscope1/test.html");
+ std::string token4;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token4));
+ EXPECT_EQ(token3, token4);
+ EXPECT_EQ(sw1_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ LoadTestPage("/push_messaging/subscope2/test.html");
+ std::string token5;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token5));
+ EXPECT_EQ(token2, token5);
+ EXPECT_EQ(sw2_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ LoadTestPage();
+ std::string token6;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token6));
+ EXPECT_EQ(token1, token6);
+ EXPECT_EQ(sw0_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, AppHandlerOnlyIfSubscribed) {
+ // This test restarts the push service to simulate restarting the browser.
+
+ EXPECT_NE(push_service(), GetAppHandler());
+ ASSERT_NO_FATAL_FAILURE(RestartPushService());
+ EXPECT_NE(push_service(), GetAppHandler());
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ EXPECT_EQ(push_service(), GetAppHandler());
+ ASSERT_NO_FATAL_FAILURE(RestartPushService());
+ EXPECT_EQ(push_service(), GetAppHandler());
+
+ std::string script_result;
+
+ // Unsubscribe.
+ base::RunLoop run_loop;
+ push_service()->SetUnsubscribeCallbackForTesting(run_loop.QuitClosure());
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ // The app handler is only guaranteed to be unregistered once the unsubscribe
+ // callback for testing has been run (PushSubscription.unsubscribe() usually
+ // resolves before that, in order to avoid blocking on network retries etc).
+ run_loop.Run();
+
+ EXPECT_NE(push_service(), GetAppHandler());
+ ASSERT_NO_FATAL_FAILURE(RestartPushService());
+ EXPECT_NE(push_service(), GetAppHandler());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventSuccess) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true));
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("testdata", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.FindServiceWorker",
+ 0 /* SERVICE_WORKER_OK */, 1);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent",
+ 0 /* SERVICE_WORKER_OK */, 1);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::SUCCESS), 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventOnShutdown) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ push_service()->Observe(chrome::NOTIFICATION_APP_TERMINATING,
+ content::NotificationService::AllSources(),
+ content::NotificationService::NoDetails());
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventWithoutPayload) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.decrypted = false;
+
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("[NULL]", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, LegacyPushEvent) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ gcm::IncomingMessage message;
+ message.sender_id = kManifestSenderId;
+ message.decrypted = false;
+
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("[NULL]", script_result);
+}
+
+// Some users may have gotten into a state in the past where they still have
+// a subscription even though the service worker was unregistered.
+// Emulate this and test a push message triggers unsubscription.
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventNoServiceWorker) {
+ std::string app_id;
+ ASSERT_NO_FATAL_FAILURE(SetupOrphanedPushSubscription(&app_id));
+
+ // Try to send a push message.
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+
+ base::RunLoop run_loop;
+ push_service()->SetMessageCallbackForTesting(run_loop.QuitClosure());
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ push_service()->OnMessage(app_id, message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true));
+ run_loop.Run();
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+
+ // No push data should have been received.
+ std::string script_result;
+ ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.FindServiceWorker",
+ 5 /* SERVICE_WORKER_ERROR_NOT_FOUND */, 1);
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::NO_SERVICE_WORKER), 1);
+
+ // Missing Service Workers should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_id, gcm_driver_->last_deletetoken_app_id());
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::DELIVERY_NO_SERVICE_WORKER),
+ 1);
+
+ // |app_identifier| should no longer be stored in prefs.
+ PushMessagingAppIdentifier stored_app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), app_id);
+ EXPECT_TRUE(stored_app_identifier.is_null());
+}
+
+// Tests receiving messages for a subscription that no longer exists.
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, NoSubscription) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 1);
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+
+ // No push data should have been received.
+ ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.FindServiceWorker", 0);
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::UNKNOWN_APP_ID), 1);
+
+ // Missing subscriptions should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id());
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::DELIVERY_UNKNOWN_APP_ID),
+ 1);
+}
+
+// Tests receiving messages for an origin that does not have permission, but
+// somehow still has a subscription (as happened in https://crbug.com/633310).
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventWithoutPermission) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Revoke notifications permission, but first disable the
+ // PushMessagingServiceImpl's OnContentSettingChanged handler so that it
+ // doesn't automatically unsubscribe, since we want to test the case where
+ // there is still a subscription.
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->RemoveObserver(push_service());
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS);
+ base::RunLoop().RunUntilIdle();
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+
+ // No push data should have been received.
+ ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.FindServiceWorker", 0);
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::PERMISSION_DENIED), 1);
+
+ // Missing permission should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id());
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier_afterwards =
+ PushMessagingAppIdentifier::FindByServiceWorker(GetBrowser()->profile(),
+ origin, 0LL);
+ EXPECT_TRUE(app_identifier_afterwards.is_null());
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::DELIVERY_PERMISSION_DENIED),
+ 1);
+}
+
+// https://crbug.com/458160 test is flaky on all platforms; but mostly linux.
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ DISABLED_PushEventEnforcesUserVisibleNotification) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ RemoveAllNotifications();
+ ASSERT_EQ(0u, GetNotificationCount());
+
+ // We'll need to specify the web_contents in which to eval script, since we're
+ // going to run script in a background tab.
+ content::WebContents* web_contents =
+ GetBrowser()->tab_strip_model()->GetActiveWebContents();
+
+ // Set the site engagement score for the site. Setting it to 10 means it
+ // should have a budget of 4, enough for two non-shown notification, which
+ // cost 2 each.
+ SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 10.0);
+
+ // If the site is visible in an active tab, we should not force a notification
+ // to be shown. Try it twice, since we allow one mistake per 10 push events.
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.decrypted = true;
+ for (int n = 0; n < 2; n++) {
+ message.raw_data = "testdata";
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("testdata", script_result);
+ EXPECT_EQ(0u, GetNotificationCount());
+ }
+
+ // Open a blank foreground tab so site is no longer visible.
+ ui_test_utils::NavigateToURLWithDisposition(
+ GetBrowser(), GURL("about:blank"),
+ WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
+
+ // If the Service Worker push event handler shows a notification, we
+ // should not show a forced one.
+ message.raw_data = "shownotification";
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("shownotification", script_result);
+ EXPECT_EQ(1u, GetNotificationCount());
+ EXPECT_TRUE(TagEquals(GetDisplayedNotifications()[0], "push_test_tag"));
+ RemoveAllNotifications();
+
+ // If the Service Worker push event handler does not show a notification, we
+ // should show a forced one, but only once the origin is out of budget.
+ message.raw_data = "testdata";
+ for (int n = 0; n < 2; n++) {
+ // First two missed notifications shouldn't force a default one.
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+ EXPECT_EQ(0u, GetNotificationCount());
+ }
+
+ // Third missed notification should trigger a default notification, since the
+ // origin will be out of budget.
+ message.raw_data = "testdata";
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+
+ {
+ std::vector<message_center::Notification> notifications =
+ GetDisplayedNotifications();
+ ASSERT_EQ(notifications.size(), 1u);
+
+ EXPECT_TRUE(
+ TagEquals(notifications[0], kPushMessagingForcedNotificationTag));
+ EXPECT_TRUE(notifications[0].silent());
+ }
+
+ // The notification will be automatically dismissed when the developer shows
+ // a new notification themselves at a later point in time.
+ message.raw_data = "shownotification";
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("shownotification", script_result);
+
+ {
+ std::vector<message_center::Notification> notifications =
+ GetDisplayedNotifications();
+ ASSERT_EQ(notifications.size(), 1u);
+
+ EXPECT_FALSE(
+ TagEquals(notifications[0], kPushMessagingForcedNotificationTag));
+ }
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ PushEventAllowSilentPushCommandLineFlag) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+ EXPECT_EQ(kEncodedApplicationServerKey,
+ gcm_driver_->last_gettoken_authorized_entity());
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ RemoveAllNotifications();
+ ASSERT_EQ(0u, GetNotificationCount());
+
+ // We'll need to specify the web_contents in which to eval script, since we're
+ // going to run script in a background tab.
+ content::WebContents* web_contents =
+ GetBrowser()->tab_strip_model()->GetActiveWebContents();
+
+ SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 5.0);
+
+ ui_test_utils::NavigateToURLWithDisposition(
+ GetBrowser(), GURL("about:blank"),
+ WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
+
+ // Send a missed notification to use up the budget.
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+ EXPECT_EQ(0u, GetNotificationCount());
+
+ // If the Service Worker push event handler does not show a notification, we
+ // should show a forced one providing there is no foreground tab and the
+ // origin ran out of budget.
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+
+ // Because the --allow-silent-push command line flag has not been passed,
+ // this should have shown a default notification.
+ {
+ std::vector<message_center::Notification> notifications =
+ GetDisplayedNotifications();
+ ASSERT_EQ(notifications.size(), 1u);
+
+ EXPECT_TRUE(
+ TagEquals(notifications[0], kPushMessagingForcedNotificationTag));
+ EXPECT_TRUE(notifications[0].silent());
+ }
+
+ RemoveAllNotifications();
+
+ // Send the message again, but this time with the -allow-silent-push command
+ // line flag set. The default notification should *not* be shown.
+ base::CommandLine::ForCurrentProcess()->AppendSwitch(
+ switches::kAllowSilentPush);
+
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+
+ ASSERT_EQ(0u, GetNotificationCount());
+}
+
+class PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation() = default;
+
+ using SiteReputation = CrowdDenyPreloadData::SiteReputation;
+
+ void CreatedBrowserMainParts(
+ content::BrowserMainParts* browser_main_parts) override {
+ PushMessagingBrowserTestBase::CreatedBrowserMainParts(browser_main_parts);
+
+ testing_preload_data_.emplace();
+ fake_database_manager_ =
+ base::MakeRefCounted<CrowdDenyFakeSafeBrowsingDatabaseManager>();
+ test_safe_browsing_factory_ =
+ std::make_unique<safe_browsing::TestSafeBrowsingServiceFactory>();
+ test_safe_browsing_factory_->SetTestDatabaseManager(
+ fake_database_manager_.get());
+ safe_browsing::SafeBrowsingServiceInterface::RegisterFactory(
+ test_safe_browsing_factory_.get());
+ }
+
+ void AddToPreloadDataBlocklist(
+ const GURL& origin,
+ chrome_browser_crowd_deny::
+ SiteReputation_NotificationUserExperienceQuality reputation_type) {
+ SiteReputation reputation;
+ reputation.set_notification_ux_quality(reputation_type);
+ testing_preload_data_->SetOriginReputation(url::Origin::Create(origin),
+ std::move(reputation));
+ }
+
+ void AddToSafeBrowsingBlocklist(const GURL& url) {
+ safe_browsing::ThreatMetadata test_metadata;
+ test_metadata.api_permissions.emplace("NOTIFICATIONS");
+ fake_database_manager_->SetSimulatedMetadataForUrl(url, test_metadata);
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ absl::optional<testing::ScopedCrowdDenyPreloadDataOverride>
+ testing_preload_data_;
+ scoped_refptr<CrowdDenyFakeSafeBrowsingDatabaseManager>
+ fake_database_manager_;
+ std::unique_ptr<safe_browsing::TestSafeBrowsingServiceFactory>
+ test_safe_browsing_factory_;
+};
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation,
+ PushEventPermissionRevoked) {
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ std::string script_result;
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Add an origin to blocking lists after service worker is registered.
+ AddToPreloadDataBlocklist(
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL(),
+ SiteReputation::ABUSIVE_CONTENT);
+ AddToSafeBrowsingBlocklist(
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL());
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+
+ // No push data should have been received.
+ ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.FindServiceWorker", 0);
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(
+ blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE),
+ 1);
+
+ // Missing permission should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id());
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier_afterwards =
+ PushMessagingAppIdentifier::FindByServiceWorker(GetBrowser()->profile(),
+ origin, 0LL);
+ EXPECT_TRUE(app_identifier_afterwards.is_null());
+
+ // 1st event - blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED.
+ // 2nd event -
+ // blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE.
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 2);
+
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE, 1);
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED, 1);
+}
+
+// That test verifies that an origin is not revoked because it is not on
+// SafeBrowsing blocking list.
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation,
+ OriginIsNotOnSafeBrowsingBlockingList) {
+ std::string script_result;
+
+ // The origin should be marked as |ABUSIVE_CONTENT| on |CrowdDenyPreloadData|
+ // otherwise the permission revocation logic will not be triggered.
+ AddToPreloadDataBlocklist(
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL(),
+ SiteReputation::ABUSIVE_CONTENT);
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true));
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("testdata", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.FindServiceWorker",
+ 0 /* SERVICE_WORKER_OK */, 1);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent",
+ 0 /* SERVICE_WORKER_OK */, 1);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::SUCCESS), 1);
+}
+
+class PushMessagingBrowserTestWithNotificationTriggersEnabled
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingBrowserTestWithNotificationTriggersEnabled() {
+ feature_list_.InitAndEnableFeature(features::kNotificationTriggers);
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTestWithNotificationTriggersEnabled,
+ PushEventIgnoresScheduledNotificationsForEnforcement) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ RemoveAllNotifications();
+
+ // We'll need to specify the web_contents in which to eval script, since we're
+ // going to run script in a background tab.
+ content::WebContents* web_contents =
+ GetBrowser()->tab_strip_model()->GetActiveWebContents();
+
+ // Initialize site engagement score to have no budget for silent pushes.
+ SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 0);
+
+ ui_test_utils::NavigateToURLWithDisposition(
+ GetBrowser(), GURL("about:blank"),
+ WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "shownotification-with-showtrigger";
+ message.decrypted = true;
+
+ // If the Service Worker push event handler only schedules a notification, we
+ // should show a forced one providing there is no foreground tab and the
+ // origin ran out of budget.
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("shownotification-with-showtrigger", script_result);
+
+ // Because scheduled notifications do not count as displayed notifications,
+ // this should have shown a default notification.
+ std::vector<message_center::Notification> notifications =
+ GetDisplayedNotifications();
+ ASSERT_EQ(notifications.size(), 1u);
+
+ EXPECT_TRUE(TagEquals(notifications[0], kPushMessagingForcedNotificationTag));
+ EXPECT_TRUE(notifications[0].silent());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ PushEventEnforcesUserVisibleNotificationAfterQueue) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Fire off two push messages in sequence, only the second one of which will
+ // display a notification. The additional round-trip and I/O required by the
+ // second message, which shows a notification, should give us a reasonable
+ // confidence that the ordering will be maintained.
+
+ std::vector<size_t> number_of_notifications_shown;
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.decrypted = true;
+
+ {
+ base::RunLoop run_loop;
+ push_service()->SetMessageCallbackForTesting(base::BindRepeating(
+ &PushMessagingBrowserTestBase::OnDeliveryFinished,
+ base::Unretained(this), &number_of_notifications_shown,
+ base::BarrierClosure(2 /* num_closures */, run_loop.QuitClosure())));
+
+ message.raw_data = "testdata";
+ push_service()->OnMessage(app_identifier.app_id(), message);
+
+ message.raw_data = "shownotification";
+ push_service()->OnMessage(app_identifier.app_id(), message);
+
+ run_loop.Run();
+ }
+
+ ASSERT_EQ(2u, number_of_notifications_shown.size());
+ EXPECT_EQ(0u, number_of_notifications_shown[0]);
+ EXPECT_EQ(1u, number_of_notifications_shown[1]);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ PushEventNotificationWithoutEventWaitUntil) {
+ std::string script_result;
+ content::WebContents* web_contents =
+ GetBrowser()->tab_strip_model()->GetActiveWebContents();
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ base::RunLoop run_loop;
+ base::RepeatingClosure quit_barrier =
+ base::BarrierClosure(2 /* num_closures */, run_loop.QuitClosure());
+ push_service()->SetMessageCallbackForTesting(quit_barrier);
+ notification_tester_->SetNotificationAddedClosure(quit_barrier);
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "shownotification-without-waituntil";
+ message.decrypted = true;
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true));
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("immediate:shownotification-without-waituntil", script_result);
+
+ run_loop.Run();
+
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ ASSERT_EQ(1u, GetNotificationCount());
+ EXPECT_TRUE(TagEquals(GetDisplayedNotifications()[0], "push_test_tag"));
+
+ // Verify that the renderer process hasn't crashed.
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysPrompt) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ ASSERT_EQ("permission status - prompt", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysGranted) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result));
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysDenied) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndDenyPermission());
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ EXPECT_EQ("NotAllowedError - Registration failed - permission denied",
+ script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, CrossOriginFrame) {
+ const GURL kEmbedderURL = https_server()->GetURL(
+ "embedder.com", "/push_messaging/framed_test.html");
+ const GURL kRequesterURL = https_server()->GetURL("requester.com", "/");
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), kEmbedderURL));
+
+ auto* web_contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
+ LOG(ERROR) << web_contents->GetLastCommittedURL();
+ auto* subframe = content::ChildFrameAt(web_contents->GetMainFrame(), 0u);
+ ASSERT_TRUE(subframe);
+
+ // A cross-origin subframe that had not been granted the NOTIFICATIONS
+ // permission previously should see it as "denied", not be able to request it,
+ // and not be able to use the Push and Web Notification API. It is verified
+ // that no prompts are shown by auto-accepting and still expecting the
+ // permission to be denied.
+
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::ACCEPT_ALL);
+
+ std::string script_result;
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "requestNotificationPermission();", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "notificationPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "notificationPermissionAPIState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "documentSubscribePush()", &script_result));
+ EXPECT_EQ("NotAllowedError - Registration failed - permission denied",
+ script_result);
+
+ // A cross-origin subframe that had been granted the NOTIFICATIONS permission
+ // previously (in a first-party context) should see it as "granted", and be
+ // able to use the Push and Web Notifications APIs.
+
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(kRequesterURL, kRequesterURL,
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_ALLOW);
+
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::DENY_ALL);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "requestNotificationPermission();", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "notificationPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "notificationPermissionAPIState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "documentSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result));
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, UnsubscribeSuccess) {
+ std::string script_result;
+
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token1));
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+
+ // Resolves true if there was a subscription.
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 1);
+
+ // Resolves false if there was no longer a subscription.
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: false", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 2);
+
+ // TODO(johnme): Test that doesn't reject if there was a network error (should
+ // deactivate subscription locally anyway).
+ // TODO(johnme): Test that doesn't reject if there were other push service
+ // errors (should deactivate subscription locally anyway).
+
+ // Unsubscribing (with an existing reference to a PushSubscription), after
+ // replacing the Service Worker, actually still works, as the Service Worker
+ // registration is unchanged.
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token2));
+ EXPECT_NE(token1, token2);
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+ ASSERT_TRUE(RunScript("replaceServiceWorker()", &script_result));
+ EXPECT_EQ("ok - service worker replaced", script_result);
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 3);
+
+ // Unsubscribing (with an existing reference to a PushSubscription), after
+ // unregistering the Service Worker, should fail.
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token3));
+ EXPECT_NE(token1, token3);
+ EXPECT_NE(token2, token3);
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+
+ // Unregister service worker and wait for callback.
+ base::RunLoop run_loop;
+ push_service()->SetServiceWorkerUnregisteredCallbackForTesting(
+ run_loop.QuitClosure());
+ ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result));
+ EXPECT_EQ("service worker unregistration status: true", script_result);
+ run_loop.Run();
+
+ // Unregistering should have triggered an automatic unsubscribe.
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED),
+ 1);
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 4);
+
+ // Now manual unsubscribe should return false.
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: false", script_result);
+}
+
+// Push subscriptions used to be non-InstanceID GCM registrations. Still need
+// to be able to unsubscribe these, even though new ones are no longer created.
+// Flaky on some Win and Linux buildbots. See crbug.com/835382.
+#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS)
+#define MAYBE_LegacyUnsubscribeSuccess DISABLED_LegacyUnsubscribeSuccess
+#else
+#define MAYBE_LegacyUnsubscribeSuccess LegacyUnsubscribeSuccess
+#endif
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ MAYBE_LegacyUnsubscribeSuccess) {
+ std::string script_result;
+
+ std::string subscription_id1;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id1));
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+
+ // Resolves true if there was a subscription.
+ gcm_service_->AddExpectedUnregisterResponse(gcm::GCMClient::SUCCESS);
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 1);
+
+ // Resolves false if there was no longer a subscription.
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: false", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 2);
+
+ // Doesn't reject if there was a network error (deactivates subscription
+ // locally anyway).
+ std::string subscription_id2;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id2));
+ EXPECT_NE(subscription_id1, subscription_id2);
+ gcm_service_->AddExpectedUnregisterResponse(gcm::GCMClient::NETWORK_ERROR);
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 3);
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ // Doesn't reject if there were other push service errors (deactivates
+ // subscription locally anyway).
+ std::string subscription_id3;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id3));
+ EXPECT_NE(subscription_id1, subscription_id3);
+ EXPECT_NE(subscription_id2, subscription_id3);
+ gcm_service_->AddExpectedUnregisterResponse(
+ gcm::GCMClient::INVALID_PARAMETER);
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 4);
+
+ // Unsubscribing (with an existing reference to a PushSubscription), after
+ // replacing the Service Worker, actually still works, as the Service Worker
+ // registration is unchanged.
+ std::string subscription_id4;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id4));
+ EXPECT_NE(subscription_id1, subscription_id4);
+ EXPECT_NE(subscription_id2, subscription_id4);
+ EXPECT_NE(subscription_id3, subscription_id4);
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+ ASSERT_TRUE(RunScript("replaceServiceWorker()", &script_result));
+ EXPECT_EQ("ok - service worker replaced", script_result);
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 5);
+
+ // Unsubscribing (with an existing reference to a PushSubscription), after
+ // unregistering the Service Worker, should fail.
+ std::string subscription_id5;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id5));
+ EXPECT_NE(subscription_id1, subscription_id5);
+ EXPECT_NE(subscription_id2, subscription_id5);
+ EXPECT_NE(subscription_id3, subscription_id5);
+ EXPECT_NE(subscription_id4, subscription_id5);
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+
+ // Unregister service worker and wait for callback.
+ base::RunLoop run_loop;
+ push_service()->SetServiceWorkerUnregisteredCallbackForTesting(
+ run_loop.QuitClosure());
+ ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result));
+ EXPECT_EQ("service worker unregistration status: true", script_result);
+ run_loop.Run();
+
+ // Unregistering should have triggered an automatic unsubscribe.
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED),
+ 1);
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 6);
+
+ // Now manual unsubscribe should return false.
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: false", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, UnsubscribeOffline) {
+ std::string script_result;
+
+ EXPECT_NE(push_service(), GetAppHandler());
+
+ std::string token;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token));
+
+ gcm_service_->set_offline(true);
+
+ // Should quickly resolve true after deleting local state (rather than waiting
+ // until unsubscribing over the network exceeds the maximum backoff duration).
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 1);
+
+ // Since the service is offline, the network request to GCM is still being
+ // retried, so the app handler shouldn't have been unregistered yet.
+ EXPECT_EQ(push_service(), GetAppHandler());
+ // But restarting the push service will unregister the app handler, since the
+ // subscription is no longer stored in the PushMessagingAppIdentifier map.
+ ASSERT_NO_FATAL_FAILURE(RestartPushService());
+ EXPECT_NE(push_service(), GetAppHandler());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ UnregisteringServiceWorkerUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Unregister the worker, and wait for callback to complete.
+ base::RunLoop run_loop;
+ push_service()->SetServiceWorkerUnregisteredCallbackForTesting(
+ run_loop.QuitClosure());
+ ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result));
+ ASSERT_EQ("service worker unregistration status: true", script_result);
+ run_loop.Run();
+
+ // This should have unregistered the push subscription.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED),
+ 1);
+
+ // We should not be able to look up the app id.
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin,
+ 0LL /* service_worker_registration_id */);
+ EXPECT_TRUE(app_identifier.is_null());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ ServiceWorkerDatabaseDeletionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Pretend as if the Service Worker database went away, and wait for callback
+ // to complete.
+ base::RunLoop run_loop;
+ push_service()->SetServiceWorkerDatabaseWipedCallbackForTesting(
+ run_loop.QuitClosure());
+ push_service()->DidDeleteServiceWorkerDatabase();
+ run_loop.Run();
+
+ // This should have unregistered the push subscription.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::
+ SERVICE_WORKER_DATABASE_WIPED),
+ 1);
+
+ // There should not be any subscriptions left.
+ EXPECT_EQ(PushMessagingAppIdentifier::GetCount(GetBrowser()->profile()), 0u);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ InvalidGetSubscriptionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier1 =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin,
+ 0LL /* service_worker_registration_id */);
+ ASSERT_FALSE(app_identifier1.is_null());
+
+ ASSERT_NO_FATAL_FAILURE(
+ DeleteInstanceIDAsIfGCMStoreReset(app_identifier1.app_id()));
+
+ // Push messaging should not yet be aware of the InstanceID being deleted.
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0);
+ // We should still be able to look up the app id.
+ PushMessagingAppIdentifier app_identifier2 =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin,
+ 0LL /* service_worker_registration_id */);
+ EXPECT_FALSE(app_identifier2.is_null());
+ EXPECT_EQ(app_identifier1.app_id(), app_identifier2.app_id());
+
+ // Now call PushManager.getSubscription(). It should return null.
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ // This should have unsubscribed the push subscription.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::
+ GET_SUBSCRIPTION_STORAGE_CORRUPT),
+ 1);
+ // We should no longer be able to look up the app id.
+ PushMessagingAppIdentifier app_identifier3 =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin,
+ 0LL /* service_worker_registration_id */);
+ EXPECT_TRUE(app_identifier3.is_null());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ GlobalResetPushPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - prompt", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ LocalResetPushPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, origin,
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_DEFAULT);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - prompt", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ DenyPushPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, origin,
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_BLOCK);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ GlobalResetNotificationsPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - prompt", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ LocalResetNotificationsPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_DEFAULT);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - prompt", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ DenyNotificationsPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_BLOCK);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ GrantAlreadyGrantedPermissionDoesNotUnsubscribe) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_ALLOW);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0);
+}
+
+// This test is testing some non-trivial content settings rules and make sure
+// that they are respected with regards to automatic unsubscription. In other
+// words, it checks that the push service does not end up unsubscribing origins
+// that have push permission with some non-common rules.
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ AutomaticUnsubscriptionFollowsContentSettingRules) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(2, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetDefaultContentSetting(ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_ALLOW);
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_DEFAULT);
+
+ message_loop_runner->Run();
+
+ // The two first rules should give |origin| the permission to use Push even
+ // if the rules it used to have have been reset.
+ // The Push service should not unsubscribe |origin| because at no point it was
+ // left without permission to use Push.
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0);
+}
+
+// Checks automatically unsubscribing due to a revoked permission after
+// previously clearing site data, under legacy conditions (ie. when
+// unregistering a worker did not unsubscribe from push.)
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResetPushPermissionAfterClearingSiteDataUnderLegacyConditions) {
+ std::string app_id;
+ ASSERT_NO_FATAL_FAILURE(SetupOrphanedPushSubscription(&app_id));
+
+ // Simulate a user clearing site data (including Service Workers, crucially).
+ content::BrowsingDataRemover* remover =
+ GetBrowser()->profile()->GetBrowsingDataRemover();
+ content::BrowsingDataRemoverCompletionObserver observer(remover);
+ remover->RemoveAndReply(
+ base::Time(), base::Time::Max(),
+ chrome_browsing_data_remover::DATA_TYPE_SITE_DATA,
+ content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB, &observer);
+ observer.BlockUntilCompletion();
+
+ base::RunLoop run_loop;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ run_loop.QuitClosure());
+ // This shouldn't (asynchronously) cause a DCHECK.
+ // TODO(johnme): Get this test running on Android with legacy GCM
+ // registrations, which have a different codepath due to sender_id being
+ // required for unsubscribing there.
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS);
+
+ run_loop.Run();
+
+ // |app_identifier| should no longer be stored in prefs.
+ PushMessagingAppIdentifier stored_app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), app_id);
+ EXPECT_TRUE(stored_app_identifier.is_null());
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+
+ base::RunLoop().RunUntilIdle();
+
+ // Revoked permission should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_id, gcm_driver_->last_deletetoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, EncryptionKeyUniqueness) {
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token1));
+
+ std::string first_public_key;
+ ASSERT_TRUE(RunScript("GetP256dh()", &first_public_key));
+ EXPECT_GE(first_public_key.size(), 32u);
+
+ std::string script_result;
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2));
+ EXPECT_NE(token1, token2);
+
+ std::string second_public_key;
+ ASSERT_TRUE(RunScript("GetP256dh()", &second_public_key));
+ EXPECT_GE(second_public_key.size(), 32u);
+
+ EXPECT_NE(first_public_key, second_public_key);
+}
+
+class PushMessagingIncognitoBrowserTest : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingIncognitoBrowserTest()
+ : prerender_helper_(base::BindRepeating(
+ &PushMessagingIncognitoBrowserTest::web_contents,
+ base::Unretained(this))) {}
+ ~PushMessagingIncognitoBrowserTest() override = default;
+
+ // PushMessagingBrowserTest:
+ void SetUpOnMainThread() override {
+ incognito_browser_ = CreateIncognitoBrowser();
+ // We SetUp here rather than in SetUp since the https_server isn't yet
+ // created at that time.
+ prerender_helper_.SetUp(https_server());
+ PushMessagingBrowserTestBase::SetUpOnMainThread();
+ }
+ Browser* GetBrowser() const override { return incognito_browser_; }
+
+ content::WebContents* web_contents() {
+ return GetBrowser()->tab_strip_model()->GetActiveWebContents();
+ }
+
+ protected:
+ content::test::PrerenderTestHelper prerender_helper_;
+ raw_ptr<Browser> incognito_browser_ = nullptr;
+};
+
+// Regression test for https://crbug.com/476474
+IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest,
+ IncognitoGetSubscriptionDoesNotHang) {
+ ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord());
+
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ // In Incognito mode the promise returned by getSubscription should not hang,
+ // it should just fulfill with null.
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ ASSERT_EQ("false - not subscribed", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest, WarningToCorrectRFH) {
+ ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord());
+
+ content::WebContentsConsoleObserver console_observer(web_contents());
+ console_observer.SetPattern(kIncognitoWarningPattern);
+
+ // Filter out the main frame host of the currently active page.
+ content::RenderFrameHost* rfh = web_contents()->GetMainFrame();
+ console_observer.SetFilter(base::BindLambdaForTesting(
+ [&](const content::WebContentsConsoleObserver::Message& message) {
+ return message.source_frame == rfh;
+ }));
+
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ ASSERT_EQ("AbortError - Registration failed - permission denied",
+ script_result);
+
+ console_observer.Wait();
+ EXPECT_EQ(1u, console_observer.messages().size());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest,
+ WarningToCorrectRFH_Prerender) {
+ ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord());
+
+ const GURL url(https_server()->GetURL(GetTestURL()));
+
+ // Start a prerender with the push messaging test URL.
+ int host_id = prerender_helper_.AddPrerender(url);
+ content::test::PrerenderHostObserver prerender_observer(*web_contents(),
+ host_id);
+ ASSERT_NE(prerender_helper_.GetHostForUrl(url),
+ content::RenderFrameHost::kNoFrameTreeNodeId);
+
+ content::WebContentsConsoleObserver console_observer(web_contents());
+ console_observer.SetPattern(kIncognitoWarningPattern);
+
+ // Filter out the main frame host of the prerendered page.
+ content::RenderFrameHost* prerender_rfh =
+ prerender_helper_.GetPrerenderedMainFrameHost(host_id);
+ console_observer.SetFilter(base::BindLambdaForTesting(
+ [&](const content::WebContentsConsoleObserver::Message& message) {
+ return message.source_frame == prerender_rfh;
+ }));
+
+ std::string script_result;
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ prerender_rfh, "registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ // Use ExecuteScriptAsync because binding of blink::mojom::PushMessaging
+ // is deferred for the prerendered page. Script execution will finish after
+ // the activation.
+ ExecuteScriptAsync(prerender_rfh, "documentSubscribePush()");
+
+ // Activate the prerendered page and wait for a response of script execution.
+ content::DOMMessageQueue message_queue;
+ prerender_helper_.NavigatePrimaryPage(url);
+ // Make sure that the prerender was activated.
+ ASSERT_TRUE(prerender_observer.was_activated());
+ do {
+ ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
+ } while (script_result !=
+ "\"AbortError - Registration failed - permission denied\"");
+
+ console_observer.Wait();
+ EXPECT_EQ(1u, console_observer.messages().size());
+}
+
+class PushMessagingDisallowSenderIdsBrowserTest
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingDisallowSenderIdsBrowserTest() {
+ scoped_feature_list_.InitAndEnableFeature(
+ features::kPushMessagingDisallowSenderIDs);
+ }
+
+ ~PushMessagingDisallowSenderIdsBrowserTest() override = default;
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushMessagingDisallowSenderIdsBrowserTest,
+ SubscriptionWithSenderIdFails) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Attempt to create a subscription with a GCM Sender ID ("numeric key"),
+ // which should fail because the kPushMessagingDisallowSenderIDs feature has
+ // been enabled for this test.
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushWithNumericKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - GCM Sender IDs are no longer "
+ "supported, please upgrade to VAPID authentication instead",
+ script_result);
+}
+
+class PushSubscriptionWithExpirationTimeTest
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushSubscriptionWithExpirationTimeTest() {
+ scoped_feature_list_.InitAndEnableFeature(
+ features::kPushSubscriptionWithExpirationTime);
+ }
+
+ ~PushSubscriptionWithExpirationTimeTest() override = default;
+
+ // Checks whether |expiration_time| lies in the future and is in the
+ // valid format (seconds elapsed since Unix time)
+ bool IsExpirationTimeValid(const std::string& expiration_time);
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+bool PushSubscriptionWithExpirationTimeTest::IsExpirationTimeValid(
+ const std::string& expiration_time) {
+ int64_t output;
+ if (!base::StringToInt64(expiration_time, &output))
+ return false;
+ return base::Time::Now().ToJsTimeIgnoringNull() < output;
+}
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionWithExpirationTimeTest,
+ SubscribeGetSubscriptionWithExpirationTime) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Subscribe with expiration time enabled, should get a subscription with
+ // expiration time in the future back
+ std::string subscription_expiration_time;
+ ASSERT_TRUE(RunScript("documentSubscribePushGetExpirationTime()",
+ &subscription_expiration_time));
+ EXPECT_TRUE(IsExpirationTimeValid(subscription_expiration_time));
+
+ std::string get_subscription_expiration_time;
+ // Get subscription should also yield a subscription with expiration time
+ ASSERT_TRUE(RunScript("GetSubscriptionExpirationTime()",
+ &get_subscription_expiration_time));
+ EXPECT_TRUE(IsExpirationTimeValid(get_subscription_expiration_time));
+ // Both methods should return the same expiration time
+ ASSERT_EQ(subscription_expiration_time, get_subscription_expiration_time);
+}
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionWithExpirationTimeTest,
+ GetSubscriptionWithExpirationTime) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ // Get subscription should also yield a subscription with expiration time
+ ASSERT_TRUE(RunScript("GetSubscriptionExpirationTime()", &script_result));
+ EXPECT_TRUE(IsExpirationTimeValid(script_result));
+}
+
+class PushSubscriptionWithoutExpirationTimeTest
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushSubscriptionWithoutExpirationTimeTest() {
+ // Override current feature list to ensure having
+ // |kPushSubscriptionWithExpirationTime| disabled
+ scoped_feature_list_.InitAndDisableFeature(
+ features::kPushSubscriptionWithExpirationTime);
+ }
+
+ ~PushSubscriptionWithoutExpirationTimeTest() override = default;
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionWithoutExpirationTimeTest,
+ SubscribeDocumentExpirationTimeNull) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // When |features::kPushSubscriptionWithExpirationTime| is disabled,
+ // expiration time should be null
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushGetExpirationTime()", &script_result));
+ EXPECT_EQ("null", script_result);
+}
+
+class PushSubscriptionChangeEventTest : public PushMessagingBrowserTestBase {
+ public:
+ PushSubscriptionChangeEventTest() {
+ scoped_feature_list_.InitWithFeatures(
+ {features::kPushSubscriptionChangeEvent,
+ features::kPushSubscriptionWithExpirationTime},
+ {});
+ }
+
+ ~PushSubscriptionChangeEventTest() override = default;
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest,
+ PushSubscriptionChangeEventSuccess) {
+ std::string script_result;
+
+ // Create the |old_subscription| by subscribing and unsubscribing again
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ blink::mojom::PushSubscriptionPtr old_subscription =
+ GetSubscriptionForAppIdentifier(app_identifier);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // There should be no subscription since we unsubscribed
+ EXPECT_EQ(PushMessagingAppIdentifier::GetCount(GetBrowser()->profile()), 0u);
+
+ // Create a |new_subscription| by resubscribing
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ app_identifier = GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ blink::mojom::PushSubscriptionPtr new_subscription =
+ GetSubscriptionForAppIdentifier(app_identifier);
+
+ // Save the endpoints to compare with the JS result
+ GURL old_endpoint = old_subscription->endpoint;
+ GURL new_endpoint = new_subscription->endpoint;
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ base::RunLoop run_loop;
+ push_service()->FirePushSubscriptionChange(
+ app_identifier, run_loop.QuitClosure(), std::move(new_subscription),
+ std::move(old_subscription));
+ run_loop.Run();
+
+ // Compare old subscription
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ(old_endpoint.spec(), script_result);
+ // Compare new subscription
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ(new_endpoint.spec(), script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.PushSubscriptionChangeStatus",
+ blink::mojom::PushEventStatus::SUCCESS, 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest,
+ FiredAfterPermissionRevoked) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+ auto old_subscription = GetSubscriptionForAppIdentifier(app_identifier);
+
+ base::RunLoop run_loop;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ run_loop.QuitClosure());
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(app_identifier.origin(), GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_BLOCK);
+ run_loop.Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ // Check if the pushsubscriptionchangeevent arrived in the document and
+ // whether the |old_subscription| has the expected endpoint and
+ // |new_subscription| is null
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ(old_subscription->endpoint.spec(), script_result);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.PushSubscriptionChangeStatus",
+ blink::mojom::PushEventStatus::SUCCESS, 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest, OnInvalidation) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+ ASSERT_FALSE(app_identifier.is_null());
+
+ base::RunLoop run_loop;
+ push_service()->SetInvalidationCallbackForTesting(run_loop.QuitClosure());
+ push_service()->OnSubscriptionInvalidation(app_identifier.app_id());
+ run_loop.Run();
+
+ // Old subscription should be gone
+ PushMessagingAppIdentifier deleted_identifier =
+ PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(),
+ app_identifier.app_id());
+ EXPECT_TRUE(deleted_identifier.is_null());
+
+ // New subscription with a different app id should exist
+ PushMessagingAppIdentifier new_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), app_identifier.origin(),
+ app_identifier.service_worker_registration_id());
+ EXPECT_FALSE(new_identifier.is_null());
+
+ base::RunLoop().RunUntilIdle();
+
+ // Expect `pushsubscriptionchange` event that is not null
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_NE("null", script_result);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_NE("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.PushSubscriptionChangeStatus",
+ blink::mojom::PushEventStatus::SUCCESS, 1);
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_constants.cc b/chromium/chrome/browser/push_messaging/push_messaging_constants.cc
new file mode 100644
index 00000000000..bd0d33d6134
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_constants.cc
@@ -0,0 +1,11 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+
+const char kPushMessagingGcmEndpoint[] =
+ "https://fcm.googleapis.com/fcm/send/";
+
+const char kPushMessagingForcedNotificationTag[] =
+ "user_visible_auto_notification";
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_constants.h b/chromium/chrome/browser/push_messaging/push_messaging_constants.h
new file mode 100644
index 00000000000..cf0480a5f14
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_constants.h
@@ -0,0 +1,25 @@
+// Copyright 2014 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_
+
+#include "base/time/time.h"
+
+extern const char kPushMessagingGcmEndpoint[];
+
+// The tag of the notification that will be automatically shown if a webapp
+// receives a push message then fails to show a notification.
+extern const char kPushMessagingForcedNotificationTag[];
+
+// Chrome decided cadence on subscription refreshes. According to the standards:
+// https://w3c.github.io/push-api/#dfn-subscription-expiration-time it is
+// optional and set by the browser.
+constexpr base::TimeDelta kPushSubscriptionExpirationPeriodTimeDelta =
+ base::Days(90);
+
+// TimeDelta for subscription refreshes to keep old subscriptions alive
+constexpr base::TimeDelta kPushSubscriptionRefreshTimeDelta = base::Minutes(2);
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_features.cc b/chromium/chrome/browser/push_messaging/push_messaging_features.cc
new file mode 100644
index 00000000000..11ad3a97bb5
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_features.cc
@@ -0,0 +1,15 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/push_messaging_features.h"
+
+namespace features {
+
+const base::Feature kPushMessagingDisallowSenderIDs{
+ "PushMessagingDisallowSenderIDs", base::FEATURE_DISABLED_BY_DEFAULT};
+
+const base::Feature kPushSubscriptionWithExpirationTime{
+ "PushSubscriptionWithExpirationTime", base::FEATURE_DISABLED_BY_DEFAULT};
+
+} // namespace features
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_features.h b/chromium/chrome/browser/push_messaging/push_messaging_features.h
new file mode 100644
index 00000000000..1bdc9237860
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_features.h
@@ -0,0 +1,21 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_
+
+#include "base/feature_list.h"
+
+namespace features {
+
+// Feature flag to disallow creation of push messages with GCM Sender IDs.
+extern const base::Feature kPushMessagingDisallowSenderIDs;
+
+// Feature flag to enable push subscription with expiration times specified in
+// /chrome/browser/push_messaging/push_messaging_constants.h
+extern const base::Feature kPushSubscriptionWithExpirationTime;
+
+} // namespace features
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc
new file mode 100644
index 00000000000..e2a8f2adc42
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc
@@ -0,0 +1,353 @@
+// 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 "chrome/browser/push_messaging/push_messaging_notification_manager.h"
+
+#include <stddef.h>
+
+#include <bitset>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/feature_list.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/task/post_task.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/notifications/platform_notification_service_factory.h"
+#include "chrome/browser/notifications/platform_notification_service_impl.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/site_engagement/content/site_engagement_service.h"
+#include "components/url_formatter/elide_url.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/platform_notification_context.h"
+#include "content/public/browser/push_messaging_service.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/common/content_features.h"
+#include "content/public/common/page_visibility_state.h"
+#include "content/public/common/url_constants.h"
+#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
+#include "third_party/blink/public/common/notifications/notification_resources.h"
+#include "third_party/blink/public/mojom/notifications/notification.mojom-shared.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "url/gurl.h"
+
+#if defined(OS_ANDROID)
+#include "chrome/browser/ui/android/tab_model/tab_model.h"
+#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
+#else
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/android_sms/android_sms_service_factory.h"
+#include "chrome/browser/ash/android_sms/android_sms_urls.h"
+#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h"
+#endif
+
+using content::BrowserThread;
+using content::NotificationDatabaseData;
+using content::PlatformNotificationContext;
+using content::PushMessagingService;
+using content::ServiceWorkerContext;
+using content::WebContents;
+
+namespace {
+void RecordUserVisibleStatus(blink::mojom::PushUserVisibleStatus status) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.UserVisibleStatus", status);
+}
+
+content::StoragePartition* GetStoragePartition(Profile* profile,
+ const GURL& origin) {
+ return profile->GetStoragePartitionForUrl(origin);
+}
+
+NotificationDatabaseData CreateDatabaseData(
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ blink::PlatformNotificationData notification_data;
+ notification_data.title = url_formatter::FormatUrlForSecurityDisplay(
+ origin, url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS);
+ notification_data.direction =
+ blink::mojom::NotificationDirection::LEFT_TO_RIGHT;
+ notification_data.body =
+ l10n_util::GetStringUTF16(IDS_PUSH_MESSAGING_GENERIC_NOTIFICATION_BODY);
+ notification_data.tag = kPushMessagingForcedNotificationTag;
+ notification_data.icon = GURL();
+ notification_data.timestamp = base::Time::Now();
+ notification_data.silent = true;
+
+ NotificationDatabaseData database_data;
+ database_data.origin = origin;
+ database_data.service_worker_registration_id = service_worker_registration_id;
+ database_data.notification_data = notification_data;
+
+ // Make sure we don't expose this notification to the site.
+ database_data.is_shown_by_browser = true;
+
+ return database_data;
+}
+
+} // namespace
+
+PushMessagingNotificationManager::PushMessagingNotificationManager(
+ Profile* profile)
+ : profile_(profile), budget_database_(profile) {}
+
+PushMessagingNotificationManager::~PushMessagingNotificationManager() = default;
+
+void PushMessagingNotificationManager::EnforceUserVisibleOnlyRequirements(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (ShouldSkipUserVisibleOnlyRequirements(origin)) {
+ std::move(message_handled_callback)
+ .Run(/* did_show_generic_notification= */ false);
+ return;
+ }
+#endif
+
+ // TODO(johnme): Relax this heuristic slightly.
+ scoped_refptr<PlatformNotificationContext> notification_context =
+ GetStoragePartition(profile_, origin)->GetPlatformNotificationContext();
+
+ notification_context->CountVisibleNotificationsForServiceWorkerRegistration(
+ origin, service_worker_registration_id,
+ base::BindOnce(
+ &PushMessagingNotificationManager::DidCountVisibleNotifications,
+ weak_factory_.GetWeakPtr(), origin, service_worker_registration_id,
+ std::move(message_handled_callback)));
+}
+
+void PushMessagingNotificationManager::DidCountVisibleNotifications(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback,
+ bool success,
+ int notification_count) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+ // TODO(johnme): Hiding an existing notification should also count as a useful
+ // user-visible action done in response to a push message - but make sure that
+ // sending two messages in rapid succession which show then hide a
+ // notification doesn't count.
+ // TODO(crbug.com/891339): Scheduling a notification should count as a
+ // user-visible action, if it is not immediately cancelled or the |origin|
+ // schedules too many notifications too far in the future.
+ bool notification_shown = notification_count > 0;
+ bool notification_needed = true;
+
+ base::UmaHistogramCounts100("PushMessaging.VisibleNotificationCount",
+ notification_count);
+
+ // Sites with a currently visible tab don't need to show notifications.
+#if defined(OS_ANDROID)
+ for (const TabModel* model : TabModelList::models()) {
+ Profile* profile = model->GetProfile();
+ WebContents* active_web_contents = model->GetActiveWebContents();
+#else
+ for (auto* browser : *BrowserList::GetInstance()) {
+ Profile* profile = browser->profile();
+ WebContents* active_web_contents =
+ browser->tab_strip_model()->GetActiveWebContents();
+#endif
+ if (IsTabVisible(profile, active_web_contents, origin)) {
+ notification_needed = false;
+ break;
+ }
+ }
+
+ // If more than one notification is showing for this Service Worker, close
+ // the default notification if it happens to be part of this group.
+ if (notification_count >= 2) {
+ scoped_refptr<PlatformNotificationContext> notification_context =
+ GetStoragePartition(profile_, origin)->GetPlatformNotificationContext();
+ notification_context->DeleteAllNotificationDataWithTag(
+ kPushMessagingForcedNotificationTag, /*is_shown_by_browser=*/true,
+ origin, base::DoNothing());
+ }
+
+ if (notification_needed && !notification_shown) {
+ // If the worker needed to show a notification and didn't, see if a silent
+ // push was allowed.
+ budget_database_.SpendBudget(
+ url::Origin::Create(origin),
+ base::BindOnce(&PushMessagingNotificationManager::ProcessSilentPush,
+ weak_factory_.GetWeakPtr(), origin,
+ service_worker_registration_id,
+ std::move(message_handled_callback)));
+ return;
+ }
+
+ if (notification_needed && notification_shown) {
+ RecordUserVisibleStatus(
+ blink::mojom::PushUserVisibleStatus::REQUIRED_AND_SHOWN);
+ } else if (!notification_needed && !notification_shown) {
+ RecordUserVisibleStatus(
+ blink::mojom::PushUserVisibleStatus::NOT_REQUIRED_AND_NOT_SHOWN);
+ } else {
+ RecordUserVisibleStatus(
+ blink::mojom::PushUserVisibleStatus::NOT_REQUIRED_BUT_SHOWN);
+ }
+
+ std::move(message_handled_callback)
+ .Run(/* did_show_generic_notification= */ false);
+}
+
+bool PushMessagingNotificationManager::IsTabVisible(
+ Profile* profile,
+ WebContents* active_web_contents,
+ const GURL& origin) {
+ if (!active_web_contents || !active_web_contents->GetMainFrame())
+ return false;
+
+ // Don't leak information from other profiles.
+ if (profile != profile_)
+ return false;
+
+ // Ignore minimized windows.
+ switch (active_web_contents->GetMainFrame()->GetVisibilityState()) {
+ case content::PageVisibilityState::kHidden:
+ case content::PageVisibilityState::kHiddenButPainting:
+ return false;
+ case content::PageVisibilityState::kVisible:
+ break;
+ }
+
+ // Use the visible URL since that's the one the user is aware of (and it
+ // doesn't matter whether the page loaded successfully).
+ GURL visible_url = active_web_contents->GetVisibleURL();
+
+ // view-source: pages are considered to be controlled Service Worker clients
+ // and thus should be considered when checking the visible URL. However, the
+ // prefix has to be removed before the origins can be compared.
+ if (visible_url.SchemeIs(content::kViewSourceScheme))
+ visible_url = GURL(visible_url.GetContent());
+
+ return visible_url.DeprecatedGetOriginAsURL() == origin;
+}
+
+void PushMessagingNotificationManager::ProcessSilentPush(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback,
+ bool silent_push_allowed) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+ // If the origin was allowed to issue a silent push, just return.
+ if (silent_push_allowed) {
+ RecordUserVisibleStatus(
+ blink::mojom::PushUserVisibleStatus::REQUIRED_BUT_NOT_SHOWN_USED_GRACE);
+ std::move(message_handled_callback)
+ .Run(/* did_show_generic_notification= */ false);
+ return;
+ }
+
+ RecordUserVisibleStatus(blink::mojom::PushUserVisibleStatus::
+ REQUIRED_BUT_NOT_SHOWN_GRACE_EXCEEDED);
+
+ // The site failed to show a notification when one was needed, and they don't
+ // have enough budget to cover the cost of suppressing, so we will show a
+ // generic notification.
+ NotificationDatabaseData database_data =
+ CreateDatabaseData(origin, service_worker_registration_id);
+ scoped_refptr<PlatformNotificationContext> notification_context =
+ GetStoragePartition(profile_, origin)->GetPlatformNotificationContext();
+ int64_t next_persistent_notification_id =
+ PlatformNotificationServiceFactory::GetForProfile(profile_)
+ ->ReadNextPersistentNotificationId();
+
+ notification_context->WriteNotificationData(
+ next_persistent_notification_id, service_worker_registration_id, origin,
+ database_data,
+ base::BindOnce(
+ &PushMessagingNotificationManager::DidWriteNotificationData,
+ weak_factory_.GetWeakPtr(), std::move(message_handled_callback)));
+}
+
+void PushMessagingNotificationManager::DidWriteNotificationData(
+ EnforceRequirementsCallback message_handled_callback,
+ bool success,
+ const std::string& notification_id) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+ if (!success)
+ DLOG(ERROR) << "Writing forced notification to database should not fail";
+
+ std::move(message_handled_callback)
+ .Run(/* did_show_generic_notification= */ true);
+}
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+bool PushMessagingNotificationManager::ShouldSkipUserVisibleOnlyRequirements(
+ const GURL& origin) {
+ // This is a short-term exception to user visible only enforcement added
+ // to support for "Messages for Web" integration on ChromeOS.
+
+ chromeos::multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client;
+ if (test_multidevice_setup_client_) {
+ multidevice_setup_client = test_multidevice_setup_client_;
+ } else {
+ multidevice_setup_client =
+ ash::multidevice_setup::MultiDeviceSetupClientFactory::GetForProfile(
+ profile_);
+ }
+
+ if (!multidevice_setup_client)
+ return false;
+
+ // Check if messages feature is enabled
+ if (multidevice_setup_client->GetFeatureState(
+ chromeos::multidevice_setup::mojom::Feature::kMessages) !=
+ chromeos::multidevice_setup::mojom::FeatureState::kEnabledByUser) {
+ return false;
+ }
+
+ ash::android_sms::AndroidSmsAppManager* android_sms_app_manager;
+ if (test_android_sms_app_manager_) {
+ android_sms_app_manager = test_android_sms_app_manager_;
+ } else {
+ auto* android_sms_service =
+ ash::android_sms::AndroidSmsServiceFactory::GetForBrowserContext(
+ profile_);
+ if (!android_sms_service)
+ return false;
+ android_sms_app_manager = android_sms_service->android_sms_app_manager();
+ }
+
+ // Check if origin matches current messages url
+ absl::optional<GURL> app_url = android_sms_app_manager->GetCurrentAppUrl();
+ if (!app_url)
+ app_url = ash::android_sms::GetAndroidMessagesURL();
+
+ if (!origin.EqualsIgnoringRef(app_url->DeprecatedGetOriginAsURL()))
+ return false;
+
+ return true;
+}
+
+void PushMessagingNotificationManager::SetTestMultiDeviceSetupClient(
+ chromeos::multidevice_setup::MultiDeviceSetupClient*
+ multidevice_setup_client) {
+ test_multidevice_setup_client_ = multidevice_setup_client;
+}
+
+void PushMessagingNotificationManager::SetTestAndroidSmsAppManager(
+ ash::android_sms::AndroidSmsAppManager* android_sms_app_manager) {
+ test_android_sms_app_manager_ = android_sms_app_manager;
+}
+#endif
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h
new file mode 100644
index 00000000000..b2621f55c53
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h
@@ -0,0 +1,122 @@
+// 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_
+
+#include <stdint.h>
+#include <vector>
+
+#include "base/callback_forward.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/push_messaging/budget_database.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/android_sms/android_sms_app_manager.h"
+#include "chromeos/services/multidevice_setup/public/cpp/multidevice_setup_client.h"
+#endif
+
+class GURL;
+class Profile;
+
+namespace content {
+class WebContents;
+} // namespace content
+
+// Developers may be required to display a Web Notification in response to an
+// incoming push message in order to clarify to the user that something has
+// happened in the background. When they forget to do so, a default notification
+// has to be displayed on their behalf.
+//
+// This class implements the heuristics for determining whether the default
+// notification is necessary, as well as the functionality of displaying the
+// default notification when it is.
+//
+// See the following document and bug for more context:
+// https://docs.google.com/document/d/13VxFdLJbMwxHrvnpDm8RXnU41W2ZlcP0mdWWe9zXQT8/edit
+// https://crbug.com/437277
+class PushMessagingNotificationManager {
+ public:
+ using EnforceRequirementsCallback =
+ base::OnceCallback<void(bool did_show_generic_notification)>;
+
+ explicit PushMessagingNotificationManager(Profile* profile);
+
+ PushMessagingNotificationManager(const PushMessagingNotificationManager&) =
+ delete;
+ PushMessagingNotificationManager& operator=(
+ const PushMessagingNotificationManager&) = delete;
+
+ ~PushMessagingNotificationManager();
+
+ // Enforces the requirements implied for push subscriptions which must display
+ // a Web Notification in response to an incoming message.
+ void EnforceUserVisibleOnlyRequirements(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback);
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingNotificationManagerTest, IsTabVisible);
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingNotificationManagerTest,
+ IsTabVisibleViewSource);
+ FRIEND_TEST_ALL_PREFIXES(
+ PushMessagingNotificationManagerTest,
+ SkipEnforceUserVisibleOnlyRequirementsForAndroidMessages);
+
+ void DidCountVisibleNotifications(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback,
+ bool success,
+ int notification_count);
+
+ // Checks whether |profile| is the one owning this instance,
+ // |active_web_contents| exists and its main frame is visible, and the URL
+ // currently visible to the user is for |origin|.
+ bool IsTabVisible(Profile* profile,
+ content::WebContents* active_web_contents,
+ const GURL& origin);
+
+ void ProcessSilentPush(const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback,
+ bool silent_push_allowed);
+
+ void DidWriteNotificationData(
+ EnforceRequirementsCallback message_handled_callback,
+ bool success,
+ const std::string& notification_id);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ bool ShouldSkipUserVisibleOnlyRequirements(const GURL& origin);
+
+ void SetTestMultiDeviceSetupClient(
+ chromeos::multidevice_setup::MultiDeviceSetupClient*
+ multidevice_setup_client);
+
+ void SetTestAndroidSmsAppManager(
+ ash::android_sms::AndroidSmsAppManager* android_sms_app_manager);
+#endif
+
+ // Weak. This manager is owned by a keyed service on this profile.
+ raw_ptr<Profile> profile_;
+
+ BudgetDatabase budget_database_;
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ chromeos::multidevice_setup::MultiDeviceSetupClient*
+ test_multidevice_setup_client_ = nullptr;
+
+ ash::android_sms::AndroidSmsAppManager* test_android_sms_app_manager_ =
+ nullptr;
+#endif
+
+ base::WeakPtrFactory<PushMessagingNotificationManager> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc
new file mode 100644
index 00000000000..7b6b46680f5
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc
@@ -0,0 +1,87 @@
+// 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 "chrome/browser/push_messaging/push_messaging_notification_manager.h"
+
+#include "base/bind.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/test_renderer_host.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include <memory>
+
+#include "chrome/browser/ash/android_sms/fake_android_sms_app_manager.h"
+#include "chromeos/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h"
+#endif
+
+class PushMessagingNotificationManagerTest
+ : public ChromeRenderViewHostTestHarness {};
+
+TEST_F(PushMessagingNotificationManagerTest, IsTabVisible) {
+ PushMessagingNotificationManager manager(profile());
+ GURL origin("https://google.com/");
+ GURL origin_with_path = origin.Resolve("/path/");
+ NavigateAndCommit(origin_with_path);
+
+ EXPECT_FALSE(manager.IsTabVisible(profile(), nullptr, origin));
+ EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(),
+ GURL("https://chrome.com/")));
+ EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin));
+
+ content::RenderViewHostTester::For(rvh())->SimulateWasHidden();
+ EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), origin));
+
+ content::RenderViewHostTester::For(rvh())->SimulateWasShown();
+ EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin));
+}
+
+TEST_F(PushMessagingNotificationManagerTest, IsTabVisibleViewSource) {
+ PushMessagingNotificationManager manager(profile());
+
+ GURL origin("https://google.com/");
+ GURL view_source_page("view-source:https://google.com/path/");
+
+ NavigateAndCommit(view_source_page);
+
+ ASSERT_EQ(view_source_page, web_contents()->GetVisibleURL());
+ EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin));
+
+ content::RenderViewHostTester::For(rvh())->SimulateWasHidden();
+ EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), origin));
+}
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+TEST_F(PushMessagingNotificationManagerTest,
+ SkipEnforceUserVisibleOnlyRequirementsForAndroidMessages) {
+ GURL app_url("https://example.com/test/");
+ auto fake_android_sms_app_manager =
+ std::make_unique<ash::android_sms::FakeAndroidSmsAppManager>();
+ fake_android_sms_app_manager->SetInstalledAppUrl(app_url);
+
+ auto fake_multidevice_setup_client = std::make_unique<
+ chromeos::multidevice_setup::FakeMultiDeviceSetupClient>();
+ fake_multidevice_setup_client->SetFeatureState(
+ chromeos::multidevice_setup::mojom::Feature::kMessages,
+ chromeos::multidevice_setup::mojom::FeatureState::kEnabledByUser);
+
+ PushMessagingNotificationManager manager(profile());
+ manager.SetTestMultiDeviceSetupClient(fake_multidevice_setup_client.get());
+ manager.SetTestAndroidSmsAppManager(fake_android_sms_app_manager.get());
+
+ bool was_called = false;
+ manager.EnforceUserVisibleOnlyRequirements(
+ app_url.DeprecatedGetOriginAsURL(), 0l,
+ base::BindOnce(
+ [](bool* was_called, bool did_show_generic_notification) {
+ *was_called = true;
+ },
+ &was_called));
+ EXPECT_TRUE(was_called);
+}
+#endif
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc b/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc
new file mode 100644
index 00000000000..2f8bdee4b43
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc
@@ -0,0 +1,121 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/push_messaging_refresher.h"
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/feature_list.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+#include "chrome/browser/push_messaging/push_messaging_utils.h"
+#include "chrome/common/pref_names.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/push_messaging_service.h"
+#include "content/public/common/content_features.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+#include "url/gurl.h"
+
+PushMessagingRefresher::PushMessagingRefresher() = default;
+
+PushMessagingRefresher::~PushMessagingRefresher() = default;
+
+size_t PushMessagingRefresher::GetCount() const {
+ return old_subscriptions_.size();
+}
+
+void PushMessagingRefresher::Refresh(
+ PushMessagingAppIdentifier old_app_identifier,
+ const std::string& new_app_id,
+ const std::string& sender_id) {
+ RefreshObject refresh_object = {old_app_identifier, sender_id,
+ false /* is_valid */};
+ // Insert is as current started refresh
+ old_subscriptions_.emplace(new_app_id, refresh_object);
+ refresh_map_.emplace(old_app_identifier.app_id(), new_app_id);
+ // TODO(viviy): Save old_subscription in a seperate map in preferences, so
+ // that in case of a browser shutdown the subscription is remembered.
+ // Unsubscribe on next startup.
+}
+
+void PushMessagingRefresher::OnSubscriptionUpdated(
+ const std::string& new_app_id) {
+ RefreshInfo::iterator result = old_subscriptions_.find(new_app_id);
+
+ if (result == old_subscriptions_.end())
+ return;
+
+ // Schedule a unsubscription event for the old subscription
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&PushMessagingRefresher::NotifyOnOldSubscriptionExpired,
+ weak_factory_.GetWeakPtr(),
+ result->second.old_identifier.app_id(),
+ result->second.sender_id),
+ kPushSubscriptionRefreshTimeDelta);
+}
+
+void PushMessagingRefresher::NotifyOnOldSubscriptionExpired(
+ const std::string& old_app_id,
+ const std::string& sender_id) {
+ for (Observer& obs : observers_)
+ obs.OnOldSubscriptionExpired(old_app_id, sender_id);
+}
+
+void PushMessagingRefresher::OnUnsubscribed(const std::string& old_app_id) {
+ auto found_new_app_id = refresh_map_.find(old_app_id);
+ // Already unsubscribed
+ if (found_new_app_id == refresh_map_.end())
+ return;
+
+ std::string new_app_id = found_new_app_id->second;
+ refresh_map_.erase(found_new_app_id);
+
+ RefreshInfo::iterator result = old_subscriptions_.find(new_app_id);
+ DCHECK(result != old_subscriptions_.end());
+
+ PushMessagingAppIdentifier old_identifier = result->second.old_identifier;
+ old_subscriptions_.erase(result);
+
+ for (Observer& obs : observers_)
+ obs.OnRefreshFinished(old_identifier);
+}
+
+void PushMessagingRefresher::GotMessageFrom(const std::string& app_id) {
+ RefreshInfo::iterator result = old_subscriptions_.find(app_id);
+ // If a message arrives that is part of the refresh, expire the old
+ // subscription immediately
+ if (result != old_subscriptions_.end() && !result->second.is_valid) {
+ NotifyOnOldSubscriptionExpired(result->second.old_identifier.app_id(),
+ result->second.sender_id);
+ result->second.is_valid = true;
+ }
+}
+
+absl::optional<PushMessagingAppIdentifier>
+PushMessagingRefresher::FindActiveAppIdentifier(const std::string& app_id) {
+ absl::optional<PushMessagingAppIdentifier> app_identifier;
+ RefreshMap::iterator refresh_map_it = refresh_map_.find(app_id);
+ if (refresh_map_it != refresh_map_.end()) {
+ RefreshInfo::iterator result =
+ old_subscriptions_.find(refresh_map_it->second);
+ if (result != old_subscriptions_.end() && !result->second.is_valid) {
+ app_identifier = result->second.old_identifier;
+ }
+ }
+ return app_identifier;
+}
+
+base::WeakPtr<PushMessagingRefresher> PushMessagingRefresher::GetWeakPtr() {
+ return weak_factory_.GetWeakPtr();
+}
+
+void PushMessagingRefresher::AddObserver(Observer* observer) {
+ observers_.AddObserver(observer);
+}
+
+void PushMessagingRefresher::RemoveObserver(Observer* observer) {
+ observers_.RemoveObserver(observer);
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher.h b/chromium/chrome/browser/push_messaging/push_messaging_refresher.h
new file mode 100644
index 00000000000..c049a14388e
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher.h
@@ -0,0 +1,99 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_
+
+#include <map>
+#include <vector>
+
+#include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "content/public/browser/push_messaging_service.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom-forward.h"
+
+// This class enables push subscription refreshes as defined in the docs:
+// https://w3c.github.io/push-api/#subscription-refreshes
+// The idea is to keep the refresh information of both new and old subscription
+// in memory during the refresh process to be still able to receive messages
+// through the old subscription after it was replaced by the new subscription.
+class PushMessagingRefresher {
+ public:
+ PushMessagingRefresher();
+
+ PushMessagingRefresher(const PushMessagingRefresher&) = delete;
+ PushMessagingRefresher& operator=(const PushMessagingRefresher&) = delete;
+
+ ~PushMessagingRefresher();
+
+ // Return number of objects that are currently being refreshed
+ size_t GetCount() const;
+
+ // Register a new refresh pair with relevant information.
+ void Refresh(PushMessagingAppIdentifier old_app_identifier,
+ const std::string& new_app_id,
+ const std::string& sender_id);
+
+ // The subscription with the new app id was updated, new messages arriving
+ // through the new subscription should be accepted now.
+ void OnSubscriptionUpdated(const std::string& new_app_id);
+
+ // Unsubscribe event happened for the old subscription. It is deleted in
+ // the RefreshMap and notify all observers that the refresh process for
+ // |app_id| has finished
+ void OnUnsubscribed(const std::string& app_id);
+
+ // If a new message arrives through an |app_id| that is associated with a
+ // refresh, the old subscription needs to be deactivated.
+ void GotMessageFrom(const std::string& app_id);
+
+ // If a subscription was refreshed, we accept the old subscription for
+ // a moment after refresh
+ absl::optional<PushMessagingAppIdentifier> FindActiveAppIdentifier(
+ const std::string& app_id);
+
+ base::WeakPtr<PushMessagingRefresher> GetWeakPtr();
+
+ // Observer for Refresh status updates
+ class Observer : public base::CheckedObserver {
+ public:
+ virtual void OnOldSubscriptionExpired(const std::string& app_id,
+ const std::string& sender_id) = 0;
+ virtual void OnRefreshFinished(
+ const PushMessagingAppIdentifier& app_identifier) = 0;
+ };
+
+ void AddObserver(Observer* observer);
+ void RemoveObserver(Observer* observer);
+
+ private:
+ // A RefreshObject carries subscription information that is needed to receive
+ // messages and to unsubscribe from the old subscription
+ struct RefreshObject {
+ PushMessagingAppIdentifier old_identifier;
+ std::string sender_id;
+ bool is_valid;
+ };
+
+ void NotifyOnOldSubscriptionExpired(const std::string& app_id,
+ const std::string& sender_id);
+
+ base::ObserverList<Observer> observers_;
+
+ // Maps from new app id to the refresh information of the old subscription
+ // that is needed to receive messages and unsubscribe
+ using RefreshInfo = std::map<std::string, RefreshObject>;
+ RefreshInfo old_subscriptions_;
+
+ // Maps from old app id to new app id
+ using RefreshMap = std::map<std::string, std::string>;
+ RefreshMap refresh_map_;
+
+ base::WeakPtrFactory<PushMessagingRefresher> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc
new file mode 100644
index 00000000000..63570f41e83
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc
@@ -0,0 +1,84 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/push_messaging_refresher.h"
+
+#include <stdint.h>
+
+#include "base/time/time.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_refresher.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "url/gurl.h"
+namespace {
+
+void ExpectAppIdentifiersEqual(const PushMessagingAppIdentifier& a,
+ const PushMessagingAppIdentifier& b) {
+ EXPECT_EQ(a.app_id(), b.app_id());
+ EXPECT_EQ(a.origin(), b.origin());
+ EXPECT_EQ(a.service_worker_registration_id(),
+ b.service_worker_registration_id());
+ EXPECT_EQ(a.expiration_time(), b.expiration_time());
+}
+
+constexpr char kTestOrigin[] = "https://example.com";
+constexpr char kTestSenderId[] = "1234567890";
+const int64_t kTestServiceWorkerId = 42;
+
+class PushMessagingRefresherTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ old_app_identifier_ = PushMessagingAppIdentifier::Generate(
+ GURL(kTestOrigin), kTestServiceWorkerId);
+ new_app_identifier_ = PushMessagingAppIdentifier::Generate(
+ GURL(kTestOrigin), kTestServiceWorkerId);
+ }
+
+ Profile* profile() { return &profile_; }
+
+ PushMessagingRefresher* refresher() { return &refresher_; }
+
+ absl::optional<PushMessagingAppIdentifier> old_app_identifier_;
+ absl::optional<PushMessagingAppIdentifier> new_app_identifier_;
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfile profile_;
+ PushMessagingRefresher refresher_;
+};
+
+TEST_F(PushMessagingRefresherTest, GotMessageThroughNewSubscription) {
+ refresher()->Refresh(old_app_identifier_.value(),
+ new_app_identifier_.value().app_id(), kTestSenderId);
+ refresher()->GotMessageFrom(new_app_identifier_.value().app_id());
+ auto app_identifier = refresher()->FindActiveAppIdentifier(
+ old_app_identifier_.value().app_id());
+ EXPECT_FALSE(app_identifier.has_value());
+}
+
+TEST_F(PushMessagingRefresherTest, LookupOldSubscription) {
+ refresher()->Refresh(old_app_identifier_.value(),
+ new_app_identifier_.value().app_id(), kTestSenderId);
+ {
+ absl::optional<PushMessagingAppIdentifier> found_old_app_identifier =
+ refresher()->FindActiveAppIdentifier(
+ old_app_identifier_.value().app_id());
+ EXPECT_TRUE(found_old_app_identifier.has_value());
+ ExpectAppIdentifiersEqual(old_app_identifier_.value(),
+ found_old_app_identifier.value());
+ }
+ refresher()->OnUnsubscribed(old_app_identifier_.value().app_id());
+ {
+ absl::optional<PushMessagingAppIdentifier> found_after_unsubscribe =
+ refresher()->FindActiveAppIdentifier(
+ old_app_identifier_.value().app_id());
+ EXPECT_FALSE(found_after_unsubscribe.has_value());
+ }
+ EXPECT_EQ(0u, refresher()->GetCount());
+}
+
+} // namespace
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc
new file mode 100644
index 00000000000..2987c24efdf
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc
@@ -0,0 +1,82 @@
+// 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 "chrome/browser/push_messaging/push_messaging_service_factory.h"
+
+#include <memory>
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/engagement/site_engagement_service_factory.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
+#include "chrome/browser/permissions/permission_manager_factory.h"
+#include "chrome/browser/profiles/incognito_helpers.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/android_sms/android_sms_service_factory.h"
+#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h"
+#endif
+
+// static
+PushMessagingServiceImpl* PushMessagingServiceFactory::GetForProfile(
+ content::BrowserContext* context) {
+ // The Push API is not currently supported in incognito mode.
+ // See https://crbug.com/401439.
+ if (context->IsOffTheRecord())
+ return nullptr;
+
+ return static_cast<PushMessagingServiceImpl*>(
+ GetInstance()->GetServiceForBrowserContext(context, true));
+}
+
+// static
+PushMessagingServiceFactory* PushMessagingServiceFactory::GetInstance() {
+ return base::Singleton<PushMessagingServiceFactory>::get();
+}
+
+PushMessagingServiceFactory::PushMessagingServiceFactory()
+ : BrowserContextKeyedServiceFactory(
+ "PushMessagingProfileService",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(gcm::GCMProfileServiceFactory::GetInstance());
+ DependsOn(instance_id::InstanceIDProfileServiceFactory::GetInstance());
+ DependsOn(HostContentSettingsMapFactory::GetInstance());
+ DependsOn(PermissionManagerFactory::GetInstance());
+ DependsOn(site_engagement::SiteEngagementServiceFactory::GetInstance());
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ DependsOn(ash::android_sms::AndroidSmsServiceFactory::GetInstance());
+ DependsOn(
+ ash::multidevice_setup::MultiDeviceSetupClientFactory::GetInstance());
+#endif
+}
+
+PushMessagingServiceFactory::~PushMessagingServiceFactory() {}
+
+void PushMessagingServiceFactory::RestoreFactoryForTests(
+ content::BrowserContext* context) {
+ SetTestingFactory(context,
+ base::BindRepeating([](content::BrowserContext* context) {
+ return base::WrapUnique(
+ GetInstance()->BuildServiceInstanceFor(context));
+ }));
+}
+
+KeyedService* PushMessagingServiceFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ CHECK(!profile->IsOffTheRecord());
+ return new PushMessagingServiceImpl(profile);
+}
+
+content::BrowserContext* PushMessagingServiceFactory::GetBrowserContextToUse(
+ content::BrowserContext* context) const {
+ return chrome::GetBrowserContextOwnInstanceInIncognito(context);
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h
new file mode 100644
index 00000000000..5512ed8dfe7
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h
@@ -0,0 +1,40 @@
+// 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class PushMessagingServiceImpl;
+
+class PushMessagingServiceFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static PushMessagingServiceImpl* GetForProfile(
+ content::BrowserContext* profile);
+ static PushMessagingServiceFactory* GetInstance();
+
+ PushMessagingServiceFactory(const PushMessagingServiceFactory&) = delete;
+ PushMessagingServiceFactory& operator=(const PushMessagingServiceFactory&) =
+ delete;
+
+ // Undo a previous call to SetTestingFactory, such that subsequent calls to
+ // GetForProfile get a real push service.
+ void RestoreFactoryForTests(content::BrowserContext* context);
+
+ private:
+ friend struct base::DefaultSingletonTraits<PushMessagingServiceFactory>;
+
+ PushMessagingServiceFactory();
+ ~PushMessagingServiceFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ content::BrowserContext* GetBrowserContextToUse(
+ content::BrowserContext* context) const override;
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc
new file mode 100644
index 00000000000..6313d399760
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc
@@ -0,0 +1,1684 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+
+#include <map>
+#include <sstream>
+#include <vector>
+
+#include "base/barrier_closure.h"
+#include "base/base64url.h"
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/command_line.h"
+#include "base/feature_list.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/string_util.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/chrome_notification_types.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
+#include "chrome/browser/permissions/abusive_origin_permission_revocation_request.h"
+#include "chrome/browser/permissions/permission_manager_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_keep_alive_types.h"
+#include "chrome/browser/profiles/scoped_profile_keep_alive.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "chrome/browser/push_messaging/push_messaging_features.h"
+#include "chrome/browser/push_messaging/push_messaging_service_factory.h"
+#include "chrome/browser/push_messaging/push_messaging_utils.h"
+#include "chrome/browser/ui/chrome_pages.h"
+#include "chrome/common/buildflags.h"
+#include "chrome/common/chrome_features.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/gcm_driver/instance_id/instance_id.h"
+#include "components/gcm_driver/instance_id/instance_id_driver.h"
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "components/permissions/permission_manager.h"
+#include "components/permissions/permission_result.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/devtools_background_services_context.h"
+#include "content/public/browser/notification_service.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/service_worker_context.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/common/child_process_host.h"
+#include "content/public/common/content_features.h"
+#include "content/public/common/content_switches.h"
+#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
+#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+#include "chrome/browser/background/background_mode_manager.h"
+#include "components/keep_alive_registry/keep_alive_types.h"
+#include "components/keep_alive_registry/scoped_keep_alive.h"
+#endif
+
+#if defined(OS_ANDROID)
+#include "base/android/jni_android.h"
+#include "chrome/android/chrome_jni_headers/PushMessagingServiceObserver_jni.h"
+#endif
+
+using instance_id::InstanceID;
+
+namespace {
+
+// Scope passed to getToken to obtain GCM registration tokens.
+// Must match Java GoogleCloudMessaging.INSTANCE_ID_SCOPE.
+const char kGCMScope[] = "GCM";
+
+const int kMaxRegistrations = 1000000;
+
+// Chrome does not yet support silent push messages, and requires websites to
+// indicate that they will only send user-visible messages.
+const char kSilentPushUnsupportedMessage[] =
+ "Chrome currently only supports the Push API for subscriptions that will "
+ "result in user-visible messages. You can indicate this by calling "
+ "pushManager.subscribe({userVisibleOnly: true}) instead. See "
+ "https://goo.gl/yqv4Q4 for more details.";
+
+// Message displayed in the console (as an error) when a GCM Sender ID is used
+// to create a subscription, which is unsupported. The subscription request will
+// have been blocked, and an exception will be thrown as well.
+const char kSenderIdRegistrationDisallowedMessage[] =
+ "The provided application server key is not a VAPID key. Only VAPID keys "
+ "are supported. For more information check https://crbug.com/979235.";
+
+// Message displayed in the console (as a warning) when a GCM Sender ID is used
+// to create a subscription, which will soon be unsupported.
+const char kSenderIdRegistrationDeprecatedMessage[] =
+ "The provided application server key is not a VAPID key. Only VAPID keys "
+ "will be supported in the future. For more information check "
+ "https://crbug.com/979235.";
+
+void RecordDeliveryStatus(blink::mojom::PushEventStatus status) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.DeliveryStatus", status);
+}
+
+void RecordPushSubcriptionChangeStatus(blink::mojom::PushEventStatus status) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.PushSubscriptionChangeStatus",
+ status);
+}
+void RecordUnsubscribeReason(blink::mojom::PushUnregistrationReason reason) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationReason", reason);
+}
+
+void RecordUnsubscribeGCMResult(gcm::GCMClient::Result result) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationGCMResult", result);
+}
+
+void RecordUnsubscribeIIDResult(InstanceID::Result result) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationIIDResult", result);
+}
+
+blink::mojom::PermissionStatus ToPermissionStatus(
+ ContentSetting content_setting) {
+ switch (content_setting) {
+ case CONTENT_SETTING_ALLOW:
+ return blink::mojom::PermissionStatus::GRANTED;
+ case CONTENT_SETTING_BLOCK:
+ return blink::mojom::PermissionStatus::DENIED;
+ case CONTENT_SETTING_ASK:
+ return blink::mojom::PermissionStatus::ASK;
+ default:
+ break;
+ }
+ NOTREACHED();
+ return blink::mojom::PermissionStatus::DENIED;
+}
+
+void UnregisterCallbackToClosure(
+ base::OnceClosure closure,
+ blink::mojom::PushUnregistrationStatus status) {
+ DCHECK(closure);
+ std::move(closure).Run();
+}
+
+void LogMessageReceivedEventToDevTools(
+ content::DevToolsBackgroundServicesContext* devtools_context,
+ const PushMessagingAppIdentifier& app_identifier,
+ const std::string& message_id,
+ bool was_encrypted,
+ const std::string& error_message,
+ const std::string& payload) {
+ if (!devtools_context)
+ return;
+
+ std::map<std::string, std::string> event_metadata = {
+ {"Success", error_message.empty() ? "Yes" : "No"},
+ {"Was Encrypted", was_encrypted ? "Yes" : "No"}};
+
+ if (!error_message.empty())
+ event_metadata["Error Reason"] = error_message;
+ else if (was_encrypted)
+ event_metadata["Payload"] = payload;
+
+ devtools_context->LogBackgroundServiceEvent(
+ app_identifier.service_worker_registration_id(),
+ url::Origin::Create(app_identifier.origin()),
+ content::DevToolsBackgroundService::kPushMessaging,
+ "Push message received" /* event_name */, message_id, event_metadata);
+}
+
+PendingMessage::PendingMessage(std::string app_id, gcm::IncomingMessage message)
+ : app_id(std::move(app_id)),
+ message(std::move(message)),
+ received_time(base::Time::Now()) {}
+PendingMessage::PendingMessage(const PendingMessage& other) = default;
+PendingMessage::PendingMessage(PendingMessage&& other) = default;
+PendingMessage& PendingMessage::operator=(PendingMessage&& other) = default;
+PendingMessage::~PendingMessage() = default;
+
+} // namespace
+
+// static
+void PushMessagingServiceImpl::InitializeForProfile(Profile* profile) {
+ // TODO(johnme): Consider whether push should be enabled in incognito.
+ if (!profile || profile->IsOffTheRecord())
+ return;
+
+ int count = PushMessagingAppIdentifier::GetCount(profile);
+ if (count <= 0)
+ return;
+
+ PushMessagingServiceImpl* push_service =
+ PushMessagingServiceFactory::GetForProfile(profile);
+ if (push_service) {
+ push_service->IncreasePushSubscriptionCount(count, false /* is_pending */);
+ push_service->RemoveExpiredSubscriptions();
+ }
+}
+
+void PushMessagingServiceImpl::RemoveExpiredSubscriptions() {
+ if (!base::FeatureList::IsEnabled(
+ features::kPushSubscriptionWithExpirationTime)) {
+ return;
+ }
+
+ base::RepeatingClosure barrier_closure = base::BarrierClosure(
+ PushMessagingAppIdentifier::GetCount(profile_),
+ remove_expired_subscriptions_callback_for_testing_.is_null()
+ ? base::DoNothing()
+ : std::move(remove_expired_subscriptions_callback_for_testing_));
+
+ for (const auto& identifier : PushMessagingAppIdentifier::GetAll(profile_)) {
+ if (!identifier.IsExpired()) {
+ base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, barrier_closure);
+ continue;
+ }
+ content::BrowserThread::PostBestEffortTask(
+ FROM_HERE, base::ThreadTaskRunnerHandle::Get(),
+ base::BindOnce(
+ &PushMessagingServiceImpl::UnexpectedChange,
+ weak_factory_.GetWeakPtr(), identifier,
+ blink::mojom::PushUnregistrationReason::SUBSCRIPTION_EXPIRED,
+ barrier_closure));
+ }
+}
+
+void PushMessagingServiceImpl::UnexpectedChange(
+ PushMessagingAppIdentifier identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ base::OnceClosure completed_closure) {
+ auto unsubscribe_closure =
+ base::BindOnce(&PushMessagingServiceImpl::UnexpectedUnsubscribe,
+ weak_factory_.GetWeakPtr(), identifier, reason,
+ base::BindOnce(&UnregisterCallbackToClosure,
+ std::move(completed_closure)));
+ if (base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent)) {
+ // Find old subscription and fire a `pushsubscriptionchange` event
+ GetPushSubscriptionFromAppIdentifier(
+ identifier,
+ base::BindOnce(&PushMessagingServiceImpl::FirePushSubscriptionChange,
+ weak_factory_.GetWeakPtr(), identifier,
+ std::move(unsubscribe_closure),
+ nullptr /* new_subscription */));
+ } else {
+ std::move(unsubscribe_closure).Run();
+ }
+}
+
+PushMessagingServiceImpl::PushMessagingServiceImpl(Profile* profile)
+ : profile_(profile),
+ push_subscription_count_(0),
+ pending_push_subscription_count_(0),
+ notification_manager_(profile) {
+ DCHECK(profile);
+ HostContentSettingsMapFactory::GetForProfile(profile_)->AddObserver(this);
+
+ registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING,
+ content::NotificationService::AllSources());
+ refresh_observation_.Observe(&refresher_);
+}
+
+PushMessagingServiceImpl::~PushMessagingServiceImpl() = default;
+
+void PushMessagingServiceImpl::IncreasePushSubscriptionCount(int add,
+ bool is_pending) {
+ DCHECK_GT(add, 0);
+ if (push_subscription_count_ + pending_push_subscription_count_ == 0)
+ GetGCMDriver()->AddAppHandler(kPushMessagingAppIdentifierPrefix, this);
+
+ if (is_pending)
+ pending_push_subscription_count_ += add;
+ else
+ push_subscription_count_ += add;
+}
+
+void PushMessagingServiceImpl::DecreasePushSubscriptionCount(int subtract,
+ bool was_pending) {
+ DCHECK_GT(subtract, 0);
+ if (was_pending) {
+ pending_push_subscription_count_ -= subtract;
+ DCHECK_GE(pending_push_subscription_count_, 0);
+ } else {
+ push_subscription_count_ -= subtract;
+ DCHECK_GE(push_subscription_count_, 0);
+ }
+
+ if (push_subscription_count_ + pending_push_subscription_count_ == 0)
+ GetGCMDriver()->RemoveAppHandler(kPushMessagingAppIdentifierPrefix);
+}
+
+bool PushMessagingServiceImpl::CanHandle(const std::string& app_id) const {
+ return base::StartsWith(app_id, kPushMessagingAppIdentifierPrefix,
+ base::CompareCase::INSENSITIVE_ASCII);
+}
+
+void PushMessagingServiceImpl::ShutdownHandler() {
+ // Shutdown() should come before and it removes us from the list of app
+ // handlers of gcm::GCMDriver so this shouldn't ever been called.
+ NOTREACHED();
+}
+
+void PushMessagingServiceImpl::OnStoreReset() {
+ // Delete all cached subscriptions, since they are now invalid.
+ for (const auto& identifier : PushMessagingAppIdentifier::GetAll(profile_)) {
+ RecordUnsubscribeReason(
+ blink::mojom::PushUnregistrationReason::GCM_STORE_RESET);
+ // Clear all the subscriptions in parallel, to reduce risk that shutdown
+ // occurs before we finish clearing them.
+ ClearPushSubscriptionId(profile_, identifier.origin(),
+ identifier.service_worker_registration_id(),
+ base::DoNothing());
+ // TODO(johnme): Fire pushsubscriptionchange/pushsubscriptionlost SW event.
+ }
+ PushMessagingAppIdentifier::DeleteAllFromPrefs(profile_);
+}
+
+// OnMessage methods -----------------------------------------------------------
+
+void PushMessagingServiceImpl::OnMessage(const std::string& app_id,
+ const gcm::IncomingMessage& message) {
+ // We won't have time to process and act on the message.
+ // TODO(peter) This should be checked at the level of the GCMDriver, so that
+ // the message is not consumed. See https://crbug.com/612815
+ if (g_browser_process->IsShuttingDown() || shutdown_started_)
+ return;
+
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ if (g_browser_process->background_mode_manager()) {
+ UMA_HISTOGRAM_BOOLEAN("PushMessaging.ReceivedMessageInBackground",
+ g_browser_process->background_mode_manager()
+ ->IsBackgroundWithoutWindows());
+ }
+
+ if (!in_flight_keep_alive_) {
+ in_flight_keep_alive_ = std::make_unique<ScopedKeepAlive>(
+ KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE,
+ KeepAliveRestartOption::DISABLED);
+ in_flight_profile_keep_alive_ = std::make_unique<ScopedProfileKeepAlive>(
+ profile_, ProfileKeepAliveOrigin::kInFlightPushMessage);
+ }
+#endif
+
+ refresher_.GotMessageFrom(app_id);
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+ // Drop message and unregister if app_id was unknown (maybe recently deleted).
+ if (app_identifier.is_null()) {
+ absl::optional<PushMessagingAppIdentifier> refresh_identifier =
+ refresher_.FindActiveAppIdentifier(app_id);
+ if (!refresh_identifier) {
+ DeliverMessageCallback(app_id, GURL::EmptyGURL(),
+ -1 /* kInvalidServiceWorkerRegistrationId */,
+ message, false /* did_enqueue_message */,
+ blink::mojom::PushEventStatus::UNKNOWN_APP_ID);
+ return;
+ }
+ app_identifier = std::move(*refresh_identifier);
+ }
+
+ LogMessageReceivedEventToDevTools(
+ GetDevToolsContext(app_identifier.origin()), app_identifier,
+ message.message_id,
+ /* was_encrypted= */ message.decrypted, std::string() /* error_message */,
+ message.decrypted ? message.raw_data : std::string());
+
+ if (IsPermissionSet(app_identifier.origin())) {
+ messages_pending_permission_check_.emplace(app_id, message);
+ // Start abusive origin verification only if no other verification is in
+ // progress.
+ if (!abusive_origin_revocation_request_)
+ CheckOriginForAbuseAndDispatchNextMessage();
+ } else {
+ // Drop message and unregister if origin has lost push permission.
+ DeliverMessageCallback(app_id, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ message, false /* did_enqueue_message */,
+ blink::mojom::PushEventStatus::PERMISSION_DENIED);
+ }
+}
+
+void PushMessagingServiceImpl::CheckOriginForAbuseAndDispatchNextMessage() {
+ if (messages_pending_permission_check_.empty())
+ return;
+
+ PendingMessage message =
+ std::move(messages_pending_permission_check_.front());
+ messages_pending_permission_check_.pop();
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, message.app_id);
+
+ if (app_identifier.is_null()) {
+ CheckOriginForAbuseAndDispatchNextMessage();
+ return;
+ }
+
+ DCHECK(!abusive_origin_revocation_request_)
+ << "Create one Abusive Origin Revocation instance per request.";
+ abusive_origin_revocation_request_ =
+ std::make_unique<AbusiveOriginPermissionRevocationRequest>(
+ profile_, app_identifier.origin(),
+ base::BindOnce(&PushMessagingServiceImpl::OnCheckedOriginForAbuse,
+ weak_factory_.GetWeakPtr(), std::move(message)));
+}
+
+void PushMessagingServiceImpl::OnCheckedOriginForAbuse(
+ PendingMessage message,
+ AbusiveOriginPermissionRevocationRequest::Outcome outcome) {
+ abusive_origin_revocation_request_.reset();
+
+ base::UmaHistogramLongTimes("PushMessaging.CheckOriginForAbuseTime",
+ base::Time::Now() - message.received_time);
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, message.app_id);
+
+ if (app_identifier.is_null()) {
+ CheckOriginForAbuseAndDispatchNextMessage();
+ return;
+ }
+
+ const GURL& origin = app_identifier.origin();
+ int64_t service_worker_registration_id =
+ app_identifier.service_worker_registration_id();
+
+ // It is possible that Notifications permission has been revoked by an user
+ // during abusive origin verification.
+ if (outcome == AbusiveOriginPermissionRevocationRequest::Outcome::
+ PERMISSION_NOT_REVOKED &&
+ IsPermissionSet(origin)) {
+ std::queue<PendingMessage>& delivery_queue =
+ message_delivery_queue_[{origin, service_worker_registration_id}];
+ delivery_queue.push(std::move(message));
+
+ // Start delivering push messages to this service worker if this was the
+ // first message. Otherwise just enqueue the message to be delivered once
+ // all previous messages have been handled.
+ if (delivery_queue.size() == 1) {
+ DeliverNextQueuedMessageForServiceWorkerRegistration(
+ origin, service_worker_registration_id);
+ }
+ } else {
+ // Drop message and unregister if origin has lost push permission.
+ DeliverMessageCallback(
+ message.app_id, origin, service_worker_registration_id, message.message,
+ false /* did_enqueue_message */,
+ outcome == AbusiveOriginPermissionRevocationRequest::Outcome::
+ PERMISSION_NOT_REVOKED
+ ? blink::mojom::PushEventStatus::PERMISSION_DENIED
+ : blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE);
+ }
+
+ // Verify the next message in the queue.
+ CheckOriginForAbuseAndDispatchNextMessage();
+}
+
+void PushMessagingServiceImpl::
+ DeliverNextQueuedMessageForServiceWorkerRegistration(
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ MessageDeliveryQueueKey key{origin, service_worker_registration_id};
+ auto iter = message_delivery_queue_.find(key);
+ if (iter == message_delivery_queue_.end())
+ return;
+
+ const std::queue<PendingMessage>& delivery_queue = iter->second;
+ CHECK(!delivery_queue.empty());
+ const PendingMessage& next_message = delivery_queue.front();
+
+ const std::string& app_id = next_message.app_id;
+ const gcm::IncomingMessage& message = next_message.message;
+
+ auto deliver_message_callback = base::BindOnce(
+ &PushMessagingServiceImpl::DeliverMessageCallback,
+ weak_factory_.GetWeakPtr(), app_id, origin,
+ service_worker_registration_id, message, true /* did_enqueue_message */);
+
+ // It is possible that Notification permissions have been revoked by a user
+ // while handling previous messages for |origin|.
+ if (!IsPermissionSet(origin)) {
+ std::move(deliver_message_callback)
+ .Run(blink::mojom::PushEventStatus::PERMISSION_DENIED);
+ return;
+ }
+
+ // The payload of a push message can be valid with content, valid with empty
+ // content, or null.
+ absl::optional<std::string> payload;
+ if (message.decrypted)
+ payload = message.raw_data;
+
+ base::UmaHistogramLongTimes("PushMessaging.DeliverQueuedMessageTime",
+ base::Time::Now() - next_message.received_time);
+
+ // Inform tests observing message dispatching about the event.
+ if (message_dispatched_callback_for_testing_) {
+ message_dispatched_callback_for_testing_.Run(
+ app_id, origin, service_worker_registration_id, std::move(payload),
+ std::move(deliver_message_callback));
+ return;
+ }
+
+ // Dispatch the message to the appropriate Service Worker.
+ profile_->DeliverPushMessage(origin, service_worker_registration_id,
+ message.message_id, payload,
+ std::move(deliver_message_callback));
+}
+
+void PushMessagingServiceImpl::DeliverMessageCallback(
+ const std::string& app_id,
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ const gcm::IncomingMessage& message,
+ bool did_enqueue_message,
+ blink::mojom::PushEventStatus status) {
+ RecordDeliveryStatus(status);
+
+ // Note: It's important that |message_handled_callback| is run or passed to
+ // another function before this function returns.
+ auto message_handled_callback =
+ base::BindOnce(&PushMessagingServiceImpl::DidHandleMessage,
+ weak_factory_.GetWeakPtr(), app_id, message.message_id);
+
+ if (did_enqueue_message) {
+ message_handled_callback = base::BindOnce(
+ &PushMessagingServiceImpl::DidHandleEnqueuedMessage,
+ weak_factory_.GetWeakPtr(), requesting_origin,
+ service_worker_registration_id, std::move(message_handled_callback));
+ }
+
+ // A reason to automatically unsubscribe. UNKNOWN means do not unsubscribe.
+ blink::mojom::PushUnregistrationReason unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::UNKNOWN;
+
+ // TODO(mvanouwerkerk): Show a warning in the developer console of the
+ // Service Worker corresponding to app_id (and/or on an internals page).
+ // See https://crbug.com/508516 for options.
+ switch (status) {
+ // Call EnforceUserVisibleOnlyRequirements if the message was delivered to
+ // the Service Worker JavaScript, even if the website's event handler failed
+ // (to prevent sites deliberately failing in order to avoid having to show
+ // notifications).
+ case blink::mojom::PushEventStatus::SUCCESS:
+ case blink::mojom::PushEventStatus::EVENT_WAITUNTIL_REJECTED:
+ case blink::mojom::PushEventStatus::TIMEOUT:
+ // Only enforce the user visible requirements if silent push has not been
+ // enabled through a command line flag.
+ if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kAllowSilentPush)) {
+ notification_manager_.EnforceUserVisibleOnlyRequirements(
+ requesting_origin, service_worker_registration_id,
+ std::move(message_handled_callback));
+ message_handled_callback = base::OnceCallback<void(bool)>();
+ }
+ break;
+ case blink::mojom::PushEventStatus::SERVICE_WORKER_ERROR:
+ // Do nothing, and hope the error is transient.
+ break;
+ case blink::mojom::PushEventStatus::UNKNOWN_APP_ID:
+ unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::DELIVERY_UNKNOWN_APP_ID;
+ break;
+ case blink::mojom::PushEventStatus::PERMISSION_DENIED:
+ unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::DELIVERY_PERMISSION_DENIED;
+ break;
+ case blink::mojom::PushEventStatus::NO_SERVICE_WORKER:
+ unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::DELIVERY_NO_SERVICE_WORKER;
+ break;
+ case blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE:
+ unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE;
+ break;
+ }
+
+ // If |message_handled_callback| was not yet used, make a
+ // |completion_closure_runner| which should run by default at the end of this
+ // function, unless it is explicitly passed to another function or disabled.
+ base::ScopedClosureRunner completion_closure_runner(
+ message_handled_callback
+ ? base::BindOnce(std::move(message_handled_callback),
+ false /* did_show_generic_notification */)
+ : base::DoNothing());
+
+ if (unsubscribe_reason != blink::mojom::PushUnregistrationReason::UNKNOWN) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+ UnsubscribeInternal(
+ unsubscribe_reason,
+ app_identifier.is_null() ? GURL::EmptyGURL() : app_identifier.origin(),
+ app_identifier.is_null()
+ ? -1 /* kInvalidServiceWorkerRegistrationId */
+ : app_identifier.service_worker_registration_id(),
+ app_id, message.sender_id,
+ base::BindOnce(&UnregisterCallbackToClosure,
+ completion_closure_runner.Release()));
+
+ if (app_identifier.is_null())
+ return;
+
+ if (auto* devtools_context = GetDevToolsContext(app_identifier.origin())) {
+ std::stringstream ss;
+ ss << unsubscribe_reason;
+ devtools_context->LogBackgroundServiceEvent(
+ app_identifier.service_worker_registration_id(),
+ url::Origin::Create(app_identifier.origin()),
+ content::DevToolsBackgroundService::kPushMessaging,
+ "Unsubscribed due to error" /* event_name */, message.message_id,
+ {{"Reason", ss.str()}});
+ }
+ }
+}
+
+void PushMessagingServiceImpl::DidHandleEnqueuedMessage(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ base::OnceCallback<void(bool)> message_handled_callback,
+ bool did_show_generic_notification) {
+ // Lookup the message queue for the correct service worker.
+ MessageDeliveryQueueKey key{origin, service_worker_registration_id};
+ auto iter = message_delivery_queue_.find(key);
+ CHECK(iter != message_delivery_queue_.end());
+
+ // Remove the delivered message from the queue.
+ std::queue<PendingMessage>& delivery_queue = iter->second;
+ CHECK(!delivery_queue.empty());
+
+ base::UmaHistogramLongTimes(
+ "PushMessaging.MessageHandledTime",
+ base::Time::Now() - delivery_queue.front().received_time);
+
+ delivery_queue.pop();
+ if (delivery_queue.empty())
+ message_delivery_queue_.erase(key);
+
+ // This will call PushMessagingServiceImpl::DidHandleMessage().
+ std::move(message_handled_callback).Run(did_show_generic_notification);
+
+ // Deliver next message to this service worker now. We deliver them in series
+ // so we can check the visibility requirements after each message.
+ DeliverNextQueuedMessageForServiceWorkerRegistration(
+ origin, service_worker_registration_id);
+}
+
+void PushMessagingServiceImpl::DidHandleMessage(
+ const std::string& app_id,
+ const std::string& push_message_id,
+ bool did_show_generic_notification) {
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ // Reset before running callbacks below, so tests can verify keep-alive reset.
+ if (message_delivery_queue_.empty()) {
+ in_flight_keep_alive_.reset();
+ in_flight_profile_keep_alive_.reset();
+ }
+#endif
+
+ if (message_callback_for_testing_)
+ message_callback_for_testing_.Run();
+
+#if defined(OS_ANDROID)
+ chrome::android::Java_PushMessagingServiceObserver_onMessageHandled(
+ base::android::AttachCurrentThread());
+#endif
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+
+ if (app_identifier.is_null() || !did_show_generic_notification)
+ return;
+
+ if (auto* devtools_context = GetDevToolsContext(app_identifier.origin())) {
+ devtools_context->LogBackgroundServiceEvent(
+ app_identifier.service_worker_registration_id(),
+ url::Origin::Create(app_identifier.origin()),
+ content::DevToolsBackgroundService::kPushMessaging,
+ "Generic notification shown" /* event_name */, push_message_id,
+ {} /* event_metadata */);
+ }
+}
+
+void PushMessagingServiceImpl::SetMessageCallbackForTesting(
+ const base::RepeatingClosure& callback) {
+ message_callback_for_testing_ = callback;
+}
+
+// Other gcm::GCMAppHandler methods --------------------------------------------
+
+void PushMessagingServiceImpl::OnMessagesDeleted(const std::string& app_id) {
+ // TODO(mvanouwerkerk): Consider firing an event on the Service Worker
+ // corresponding to |app_id| to inform the app about deleted messages.
+}
+
+void PushMessagingServiceImpl::OnSendError(
+ const std::string& app_id,
+ const gcm::GCMClient::SendErrorDetails& send_error_details) {
+ NOTREACHED() << "The Push API shouldn't have sent messages upstream";
+}
+
+void PushMessagingServiceImpl::OnSendAcknowledged(
+ const std::string& app_id,
+ const std::string& message_id) {
+ NOTREACHED() << "The Push API shouldn't have sent messages upstream";
+}
+
+void PushMessagingServiceImpl::OnMessageDecryptionFailed(
+ const std::string& app_id,
+ const std::string& message_id,
+ const std::string& error_message) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+
+ if (app_identifier.is_null())
+ return;
+
+ LogMessageReceivedEventToDevTools(
+ GetDevToolsContext(app_identifier.origin()), app_identifier, message_id,
+ /* was_encrypted= */ true, error_message, "" /* payload */);
+}
+
+// Subscribe and GetPermissionStatus methods -----------------------------------
+
+void PushMessagingServiceImpl::SubscribeFromDocument(
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ int render_process_id,
+ int render_frame_id,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ bool user_gesture,
+ RegisterCallback callback) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, requesting_origin, service_worker_registration_id);
+
+ // If there is no existing app identifier for the given Service Worker,
+ // generate a new one. This will create a new subscription on the server.
+ if (app_identifier.is_null()) {
+ app_identifier = PushMessagingAppIdentifier::Generate(
+ requesting_origin, service_worker_registration_id);
+ }
+
+ if (push_subscription_count_ + pending_push_subscription_count_ >=
+ kMaxRegistrations) {
+ SubscribeEndWithError(std::move(callback),
+ blink::mojom::PushRegistrationStatus::LIMIT_REACHED);
+ return;
+ }
+
+ content::RenderFrameHost* render_frame_host =
+ content::RenderFrameHost::FromID(render_process_id, render_frame_id);
+
+ if (!render_frame_host) {
+ // It is possible for `render_frame_host` to be nullptr here due to a race
+ // (crbug.com/1057981).
+ SubscribeEndWithError(
+ std::move(callback),
+ blink::mojom::PushRegistrationStatus::RENDERER_SHUTDOWN);
+ return;
+ }
+
+ if (!options->user_visible_only) {
+ render_frame_host->AddMessageToConsole(
+ blink::mojom::ConsoleMessageLevel::kError,
+ kSilentPushUnsupportedMessage);
+
+ SubscribeEndWithError(
+ std::move(callback),
+ blink::mojom::PushRegistrationStatus::PERMISSION_DENIED);
+ return;
+ }
+
+ // Push does not allow permission requests from iframes.
+ PermissionManagerFactory::GetForProfile(profile_)->RequestPermission(
+ ContentSettingsType::NOTIFICATIONS, render_frame_host, requesting_origin,
+ user_gesture,
+ base::BindOnce(&PushMessagingServiceImpl::DoSubscribe,
+ weak_factory_.GetWeakPtr(), std::move(app_identifier),
+ std::move(options), std::move(callback), render_process_id,
+ render_frame_id));
+}
+
+void PushMessagingServiceImpl::SubscribeFromWorker(
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback register_callback) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, requesting_origin, service_worker_registration_id);
+
+ // If there is no existing app identifier for the given Service Worker,
+ // generate a new one. This will create a new subscription on the server.
+ if (app_identifier.is_null()) {
+ app_identifier = PushMessagingAppIdentifier::Generate(
+ requesting_origin, service_worker_registration_id);
+ }
+
+ if (push_subscription_count_ + pending_push_subscription_count_ >=
+ kMaxRegistrations) {
+ SubscribeEndWithError(std::move(register_callback),
+ blink::mojom::PushRegistrationStatus::LIMIT_REACHED);
+ return;
+ }
+
+ if (!IsPermissionSet(requesting_origin, options->user_visible_only)) {
+ SubscribeEndWithError(
+ std::move(register_callback),
+ blink::mojom::PushRegistrationStatus::PERMISSION_DENIED);
+ return;
+ }
+
+ DoSubscribe(std::move(app_identifier), std::move(options),
+ std::move(register_callback),
+ /* render_process_id= */ -1, /* render_frame_id= */ -1,
+ CONTENT_SETTING_ALLOW);
+}
+
+blink::mojom::PermissionStatus PushMessagingServiceImpl::GetPermissionStatus(
+ const GURL& origin,
+ bool user_visible) {
+ if (!user_visible)
+ return blink::mojom::PermissionStatus::DENIED;
+
+ // Because the Push API is tied to Service Workers, many usages of the API
+ // won't have an embedding origin at all. Only consider the requesting
+ // |origin| when checking whether permission to use the API has been granted.
+ return ToPermissionStatus(
+ PermissionManagerFactory::GetForProfile(profile_)
+ ->GetPermissionStatus(ContentSettingsType::NOTIFICATIONS, origin,
+ origin)
+ .content_setting);
+}
+
+bool PushMessagingServiceImpl::SupportNonVisibleMessages() {
+ return false;
+}
+
+void PushMessagingServiceImpl::DoSubscribe(
+ PushMessagingAppIdentifier app_identifier,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback register_callback,
+ int render_process_id,
+ int render_frame_id,
+ ContentSetting content_setting) {
+ if (content_setting != CONTENT_SETTING_ALLOW) {
+ SubscribeEndWithError(
+ std::move(register_callback),
+ blink::mojom::PushRegistrationStatus::PERMISSION_DENIED);
+ return;
+ }
+
+ std::string application_server_key_string(
+ options->application_server_key.begin(),
+ options->application_server_key.end());
+
+ // TODO(peter): Move this check to the renderer process & Mojo message
+ // validation once the flag is always enabled, and remove the
+ // |render_process_id| and |render_frame_id| parameters from this method.
+ if (!push_messaging::IsVapidKey(application_server_key_string)) {
+ content::RenderFrameHost* render_frame_host =
+ content::RenderFrameHost::FromID(render_process_id, render_frame_id);
+ if (base::FeatureList::IsEnabled(
+ features::kPushMessagingDisallowSenderIDs)) {
+ if (render_frame_host) {
+ render_frame_host->AddMessageToConsole(
+ blink::mojom::ConsoleMessageLevel::kError,
+ kSenderIdRegistrationDisallowedMessage);
+ }
+ SubscribeEndWithError(
+ std::move(register_callback),
+ blink::mojom::PushRegistrationStatus::UNSUPPORTED_GCM_SENDER_ID);
+ return;
+ } else if (render_frame_host) {
+ render_frame_host->AddMessageToConsole(
+ blink::mojom::ConsoleMessageLevel::kWarning,
+ kSenderIdRegistrationDeprecatedMessage);
+ }
+ }
+
+ IncreasePushSubscriptionCount(1, true /* is_pending */);
+
+ // Set time to live for GCM registration
+ base::TimeDelta ttl = base::TimeDelta();
+
+ if (base::FeatureList::IsEnabled(
+ features::kPushSubscriptionWithExpirationTime)) {
+ app_identifier.set_expiration_time(
+ base::Time::Now() + kPushSubscriptionExpirationPeriodTimeDelta);
+ DCHECK(app_identifier.expiration_time());
+ ttl = kPushSubscriptionExpirationPeriodTimeDelta;
+ }
+
+ GetInstanceIDDriver()
+ ->GetInstanceID(app_identifier.app_id())
+ ->GetToken(
+ push_messaging::NormalizeSenderInfo(application_server_key_string),
+ kGCMScope, ttl, {} /* flags */,
+ base::BindOnce(&PushMessagingServiceImpl::DidSubscribe,
+ weak_factory_.GetWeakPtr(), app_identifier,
+ application_server_key_string,
+ std::move(register_callback)));
+}
+
+void PushMessagingServiceImpl::SubscribeEnd(
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ std::move(callback).Run(subscription_id, endpoint, expiration_time, p256dh,
+ auth, status);
+}
+
+void PushMessagingServiceImpl::SubscribeEndWithError(
+ RegisterCallback callback,
+ blink::mojom::PushRegistrationStatus status) {
+ SubscribeEnd(std::move(callback), std::string() /* subscription_id */,
+ GURL::EmptyGURL() /* endpoint */,
+ absl::nullopt /* expiration_time */,
+ std::vector<uint8_t>() /* p256dh */,
+ std::vector<uint8_t>() /* auth */, status);
+}
+
+void PushMessagingServiceImpl::DidSubscribe(
+ const PushMessagingAppIdentifier& app_identifier,
+ const std::string& sender_id,
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ InstanceID::Result result) {
+ DecreasePushSubscriptionCount(1, true /* was_pending */);
+
+ blink::mojom::PushRegistrationStatus status =
+ blink::mojom::PushRegistrationStatus::SERVICE_ERROR;
+
+ switch (result) {
+ case InstanceID::SUCCESS: {
+ const GURL endpoint = push_messaging::CreateEndpoint(subscription_id);
+
+ // Make sure that this subscription has associated encryption keys prior
+ // to returning it to the developer - they'll need this information in
+ // order to send payloads to the user.
+ GetEncryptionInfoForAppId(
+ app_identifier.app_id(), sender_id,
+ base::BindOnce(
+ &PushMessagingServiceImpl::DidSubscribeWithEncryptionInfo,
+ weak_factory_.GetWeakPtr(), app_identifier, std::move(callback),
+ subscription_id, endpoint));
+ return;
+ }
+ case InstanceID::INVALID_PARAMETER:
+ case InstanceID::DISABLED:
+ case InstanceID::ASYNC_OPERATION_PENDING:
+ case InstanceID::SERVER_ERROR:
+ case InstanceID::UNKNOWN_ERROR:
+ DLOG(ERROR) << "Push messaging subscription failed; InstanceID::Result = "
+ << result;
+ status = blink::mojom::PushRegistrationStatus::SERVICE_ERROR;
+ break;
+ case InstanceID::NETWORK_ERROR:
+ status = blink::mojom::PushRegistrationStatus::NETWORK_ERROR;
+ break;
+ }
+
+ SubscribeEndWithError(std::move(callback), status);
+}
+
+void PushMessagingServiceImpl::DidSubscribeWithEncryptionInfo(
+ const PushMessagingAppIdentifier& app_identifier,
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ const GURL& endpoint,
+ std::string p256dh,
+ std::string auth_secret) {
+ if (p256dh.empty()) {
+ SubscribeEndWithError(
+ std::move(callback),
+ blink::mojom::PushRegistrationStatus::PUBLIC_KEY_UNAVAILABLE);
+ return;
+ }
+
+ app_identifier.PersistToPrefs(profile_);
+
+ IncreasePushSubscriptionCount(1, false /* is_pending */);
+
+ SubscribeEnd(std::move(callback), subscription_id, endpoint,
+ app_identifier.expiration_time(),
+ std::vector<uint8_t>(p256dh.begin(), p256dh.end()),
+ std::vector<uint8_t>(auth_secret.begin(), auth_secret.end()),
+ blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE);
+}
+
+// GetSubscriptionInfo methods -------------------------------------------------
+
+void PushMessagingServiceImpl::GetSubscriptionInfo(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const std::string& sender_id,
+ const std::string& subscription_id,
+ SubscriptionInfoCallback callback) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, origin, service_worker_registration_id);
+
+ if (app_identifier.is_null()) {
+ std::move(callback).Run(
+ false /* is_valid */, GURL::EmptyGURL() /*endpoint*/,
+ absl::nullopt /* expiration_time */,
+ std::vector<uint8_t>() /* p256dh */, std::vector<uint8_t>() /* auth */);
+ return;
+ }
+
+ const GURL endpoint = push_messaging::CreateEndpoint(subscription_id);
+ const std::string& app_id = app_identifier.app_id();
+ absl::optional<base::Time> expiration_time = app_identifier.expiration_time();
+
+ base::OnceCallback<void(bool)> validate_cb =
+ base::BindOnce(&PushMessagingServiceImpl::DidValidateSubscription,
+ weak_factory_.GetWeakPtr(), app_id, sender_id, endpoint,
+ expiration_time, std::move(callback));
+
+ if (PushMessagingAppIdentifier::UseInstanceID(app_id)) {
+ GetInstanceIDDriver()->GetInstanceID(app_id)->ValidateToken(
+ push_messaging::NormalizeSenderInfo(sender_id), kGCMScope,
+ subscription_id, std::move(validate_cb));
+ } else {
+ GetGCMDriver()->ValidateRegistration(
+ app_id, {push_messaging::NormalizeSenderInfo(sender_id)},
+ subscription_id, std::move(validate_cb));
+ }
+}
+
+void PushMessagingServiceImpl::DidValidateSubscription(
+ const std::string& app_id,
+ const std::string& sender_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ SubscriptionInfoCallback callback,
+ bool is_valid) {
+ if (!is_valid) {
+ std::move(callback).Run(
+ false /* is_valid */, GURL::EmptyGURL() /* endpoint */,
+ absl::nullopt /* expiration_time */,
+ std::vector<uint8_t>() /* p256dh */, std::vector<uint8_t>() /* auth */);
+ return;
+ }
+
+ GetEncryptionInfoForAppId(
+ app_id, sender_id,
+ base::BindOnce(&PushMessagingServiceImpl::DidGetEncryptionInfo,
+ weak_factory_.GetWeakPtr(), endpoint, expiration_time,
+ std::move(callback)));
+}
+
+void PushMessagingServiceImpl::DidGetEncryptionInfo(
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ SubscriptionInfoCallback callback,
+ std::string p256dh,
+ std::string auth_secret) const {
+ // I/O errors might prevent the GCM Driver from retrieving a key-pair.
+ bool is_valid = !p256dh.empty();
+ std::move(callback).Run(
+ is_valid, endpoint, expiration_time,
+ std::vector<uint8_t>(p256dh.begin(), p256dh.end()),
+ std::vector<uint8_t>(auth_secret.begin(), auth_secret.end()));
+}
+
+// Unsubscribe methods ---------------------------------------------------------
+
+void PushMessagingServiceImpl::Unsubscribe(
+ blink::mojom::PushUnregistrationReason reason,
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ const std::string& sender_id,
+ UnregisterCallback callback) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, requesting_origin, service_worker_registration_id);
+
+ UnsubscribeInternal(
+ reason, requesting_origin, service_worker_registration_id,
+ app_identifier.is_null() ? std::string() : app_identifier.app_id(),
+ sender_id, std::move(callback));
+}
+
+void PushMessagingServiceImpl::UnsubscribeInternal(
+ blink::mojom::PushUnregistrationReason reason,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback) {
+ DCHECK(!app_id.empty() || (!origin.is_empty() &&
+ service_worker_registration_id !=
+ -1 /* kInvalidServiceWorkerRegistrationId */))
+ << "Need an app_id and/or origin+service_worker_registration_id";
+
+ RecordUnsubscribeReason(reason);
+
+ if (origin.is_empty() ||
+ service_worker_registration_id ==
+ -1 /* kInvalidServiceWorkerRegistrationId */) {
+ // Can't clear Service Worker database.
+ DidClearPushSubscriptionId(reason, app_id, sender_id, std::move(callback));
+ return;
+ }
+ ClearPushSubscriptionId(
+ profile_, origin, service_worker_registration_id,
+ base::BindOnce(&PushMessagingServiceImpl::DidClearPushSubscriptionId,
+ weak_factory_.GetWeakPtr(), reason, app_id, sender_id,
+ std::move(callback)));
+}
+
+void PushMessagingServiceImpl::DidClearPushSubscriptionId(
+ blink::mojom::PushUnregistrationReason reason,
+ const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback) {
+ if (app_id.empty()) {
+ // Without an |app_id|, we can neither delete the subscription from the
+ // PushMessagingAppIdentifier map, nor unsubscribe with the GCM Driver.
+ std::move(callback).Run(
+ blink::mojom::PushUnregistrationStatus::SUCCESS_WAS_NOT_REGISTERED);
+ return;
+ }
+
+ // Delete the mapping for this app_id, to guarantee that no messages get
+ // delivered in future (even if unregistration fails).
+ // TODO(johnme): Instead of deleting these app ids, store them elsewhere, and
+ // retry unregistration if it fails due to network errors (crbug.com/465399).
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+ bool was_subscribed = !app_identifier.is_null();
+ if (was_subscribed)
+ app_identifier.DeleteFromPrefs(profile_);
+
+ // Run the unsubscribe callback *before* asking the InstanceIDDriver/GCMDriver
+ // to unsubscribe, since that's a slow process involving network retries, and
+ // by this point enough local state has been deleted that the subscription is
+ // inactive. Note that DeliverMessageCallback automatically unsubscribes if
+ // messages are later received for a subscription that was locally deleted,
+ // so as long as messages keep getting sent to it, the unsubscription should
+ // eventually reach GCM servers even if this particular attempt fails.
+ std::move(callback).Run(
+ was_subscribed
+ ? blink::mojom::PushUnregistrationStatus::SUCCESS_UNREGISTERED
+ : blink::mojom::PushUnregistrationStatus::SUCCESS_WAS_NOT_REGISTERED);
+
+ if (PushMessagingAppIdentifier::UseInstanceID(app_id)) {
+ GetInstanceIDDriver()->GetInstanceID(app_id)->DeleteID(
+ base::BindOnce(&PushMessagingServiceImpl::DidDeleteID,
+ weak_factory_.GetWeakPtr(), app_id, was_subscribed));
+
+ } else {
+ auto unregister_callback =
+ base::BindOnce(&PushMessagingServiceImpl::DidUnregister,
+ weak_factory_.GetWeakPtr(), was_subscribed);
+#if defined(OS_ANDROID)
+ // On Android the backend is different, and requires the original sender_id.
+ // DidGetSenderIdUnexpectedUnsubscribe and
+ // DidDeleteServiceWorkerRegistration sometimes call us with an empty one.
+ if (sender_id.empty()) {
+ std::move(unregister_callback).Run(gcm::GCMClient::INVALID_PARAMETER);
+ } else {
+ GetGCMDriver()->UnregisterWithSenderId(
+ app_id, push_messaging::NormalizeSenderInfo(sender_id),
+ std::move(unregister_callback));
+ }
+#else
+ GetGCMDriver()->Unregister(app_id, std::move(unregister_callback));
+#endif
+ }
+}
+
+void PushMessagingServiceImpl::DidUnregister(bool was_subscribed,
+ gcm::GCMClient::Result result) {
+ RecordUnsubscribeGCMResult(result);
+ DidUnsubscribe(std::string() /* app_id_when_instance_id */, was_subscribed);
+}
+
+void PushMessagingServiceImpl::DidDeleteID(const std::string& app_id,
+ bool was_subscribed,
+ InstanceID::Result result) {
+ RecordUnsubscribeIIDResult(result);
+ // DidUnsubscribe must be run asynchronously when passing a non-empty
+ // |app_id_when_instance_id|, since it calls
+ // InstanceIDDriver::RemoveInstanceID which deletes the InstanceID itself.
+ // Calling that immediately would cause a use-after-free in our caller.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&PushMessagingServiceImpl::DidUnsubscribe,
+ weak_factory_.GetWeakPtr(), app_id, was_subscribed));
+}
+
+void PushMessagingServiceImpl::DidUnsubscribe(
+ const std::string& app_id_when_instance_id,
+ bool was_subscribed) {
+ if (!app_id_when_instance_id.empty())
+ GetInstanceIDDriver()->RemoveInstanceID(app_id_when_instance_id);
+
+ if (was_subscribed)
+ DecreasePushSubscriptionCount(1, false /* was_pending */);
+
+ if (!unsubscribe_callback_for_testing_.is_null())
+ std::move(unsubscribe_callback_for_testing_).Run();
+}
+
+void PushMessagingServiceImpl::SetUnsubscribeCallbackForTesting(
+ base::OnceClosure callback) {
+ unsubscribe_callback_for_testing_ = std::move(callback);
+}
+
+// DidDeleteServiceWorkerRegistration methods ----------------------------------
+
+void PushMessagingServiceImpl::DidDeleteServiceWorkerRegistration(
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ const PushMessagingAppIdentifier& app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, origin, service_worker_registration_id);
+ if (app_identifier.is_null()) {
+ if (!service_worker_unregistered_callback_for_testing_.is_null())
+ service_worker_unregistered_callback_for_testing_.Run();
+ return;
+ }
+ // Note this will not fully unsubscribe pre-InstanceID subscriptions on
+ // Android from GCM, as that requires a sender_id. (Ideally we'd fetch it
+ // from the SWDB in some "before_unregistered" SWObserver event.)
+ UnsubscribeInternal(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED,
+ origin, service_worker_registration_id, app_identifier.app_id(),
+ std::string() /* sender_id */,
+ base::BindOnce(&UnregisterCallbackToClosure,
+ service_worker_unregistered_callback_for_testing_.is_null()
+ ? base::DoNothing()
+ : service_worker_unregistered_callback_for_testing_));
+}
+
+void PushMessagingServiceImpl::SetServiceWorkerUnregisteredCallbackForTesting(
+ base::RepeatingClosure callback) {
+ service_worker_unregistered_callback_for_testing_ = std::move(callback);
+}
+
+// DidDeleteServiceWorkerDatabase methods --------------------------------------
+
+void PushMessagingServiceImpl::DidDeleteServiceWorkerDatabase() {
+ std::vector<PushMessagingAppIdentifier> app_identifiers =
+ PushMessagingAppIdentifier::GetAll(profile_);
+
+ base::RepeatingClosure completed_closure = base::BarrierClosure(
+ app_identifiers.size(),
+ service_worker_database_wiped_callback_for_testing_.is_null()
+ ? base::DoNothing()
+ : service_worker_database_wiped_callback_for_testing_);
+
+ for (const PushMessagingAppIdentifier& app_identifier : app_identifiers) {
+ // Note this will not fully unsubscribe pre-InstanceID subscriptions on
+ // Android from GCM, as that requires a sender_id. We can't fetch those from
+ // the Service Worker database anymore as it's been deleted.
+ UnsubscribeInternal(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_DATABASE_WIPED,
+ app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ app_identifier.app_id(), std::string() /* sender_id */,
+ base::BindOnce(&UnregisterCallbackToClosure, completed_closure));
+ }
+}
+
+void PushMessagingServiceImpl::SetServiceWorkerDatabaseWipedCallbackForTesting(
+ base::RepeatingClosure callback) {
+ service_worker_database_wiped_callback_for_testing_ = std::move(callback);
+}
+
+// OnContentSettingChanged methods ---------------------------------------------
+
+void PushMessagingServiceImpl::OnContentSettingChanged(
+ const ContentSettingsPattern& primary_pattern,
+ const ContentSettingsPattern& secondary_pattern,
+ ContentSettingsTypeSet content_type_set) {
+ DCHECK(primary_pattern.IsValid());
+ if (!content_type_set.Contains(ContentSettingsType::NOTIFICATIONS))
+ return;
+
+ std::vector<PushMessagingAppIdentifier> all_app_identifiers =
+ PushMessagingAppIdentifier::GetAll(profile_);
+
+ base::RepeatingClosure barrier_closure = base::BarrierClosure(
+ all_app_identifiers.size(),
+ content_setting_changed_callback_for_testing_.is_null()
+ ? base::DoNothing()
+ : content_setting_changed_callback_for_testing_);
+
+ for (const PushMessagingAppIdentifier& app_identifier : all_app_identifiers) {
+ if (!primary_pattern.Matches(app_identifier.origin())) {
+ barrier_closure.Run();
+ continue;
+ }
+
+ if (IsPermissionSet(app_identifier.origin())) {
+ barrier_closure.Run();
+ continue;
+ }
+
+ UnexpectedChange(app_identifier,
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED,
+ barrier_closure);
+ }
+}
+
+void PushMessagingServiceImpl::UnexpectedUnsubscribe(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ UnregisterCallback unregister_callback) {
+ // When `pushsubscriptionchange` is supported by default, get |sender_id| from
+ // GetPushSubscriptionFromAppIdentifier callback and do not get the info from
+ // IO twice
+ bool need_sender_id = false;
+#if defined(OS_ANDROID)
+ need_sender_id =
+ !PushMessagingAppIdentifier::UseInstanceID(app_identifier.app_id());
+#endif
+ if (need_sender_id) {
+ GetSenderId(
+ profile_, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ base::BindOnce(
+ &PushMessagingServiceImpl::DidGetSenderIdUnexpectedUnsubscribe,
+ weak_factory_.GetWeakPtr(), app_identifier, reason,
+ std::move(unregister_callback)));
+ } else {
+ UnsubscribeInternal(reason, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ app_identifier.app_id(),
+ std::string() /* sender_id */,
+ std::move(unregister_callback));
+ }
+}
+
+void PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifier(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)>
+ subscription_cb) {
+ GetSWData(profile_, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ base::BindOnce(&PushMessagingServiceImpl::DidGetSWData,
+ weak_factory_.GetWeakPtr(), app_identifier,
+ std::move(subscription_cb)));
+}
+
+void PushMessagingServiceImpl::DidGetSWData(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> subscription_cb,
+ const std::string& sender_id,
+ const std::string& subscription_id) {
+ // SW Database was corrupted, return immediately
+ if (sender_id.empty() || subscription_id.empty()) {
+ std::move(subscription_cb).Run(nullptr /* subscription */);
+ return;
+ }
+ GetSubscriptionInfo(
+ app_identifier.origin(), app_identifier.service_worker_registration_id(),
+ sender_id, subscription_id,
+ base::BindOnce(
+ &PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifierEnd,
+ weak_factory_.GetWeakPtr(), std::move(subscription_cb), sender_id));
+}
+
+void PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifierEnd(
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback,
+ const std::string& sender_id,
+ bool is_valid,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth) {
+ if (!is_valid) {
+ // TODO(viviy): Log error in UMA
+ std::move(callback).Run(nullptr /* subscription */);
+ return;
+ }
+
+ std::move(callback).Run(blink::mojom::PushSubscription::New(
+ endpoint, expiration_time, push_messaging::MakeOptions(sender_id), p256dh,
+ auth));
+}
+
+void PushMessagingServiceImpl::FirePushSubscriptionChange(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceClosure completed_closure,
+ blink::mojom::PushSubscriptionPtr new_subscription,
+ blink::mojom::PushSubscriptionPtr old_subscription) {
+ // Ensure |completed_closure| is run after this function
+ base::ScopedClosureRunner scoped_closure(std::move(completed_closure));
+
+ if (!base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent))
+ return;
+
+ if (app_identifier.is_null()) {
+ FirePushSubscriptionChangeCallback(
+ app_identifier, blink::mojom::PushEventStatus::UNKNOWN_APP_ID);
+ return;
+ }
+
+ profile_->FirePushSubscriptionChangeEvent(
+ app_identifier.origin(), app_identifier.service_worker_registration_id(),
+ std::move(new_subscription), std::move(old_subscription),
+ base::BindOnce(
+ &PushMessagingServiceImpl::FirePushSubscriptionChangeCallback,
+ weak_factory_.GetWeakPtr(), app_identifier));
+}
+
+void PushMessagingServiceImpl::FirePushSubscriptionChangeCallback(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushEventStatus status) {
+ // Log Data in UMA
+ RecordPushSubcriptionChangeStatus(status);
+}
+
+void PushMessagingServiceImpl::DidGetSenderIdUnexpectedUnsubscribe(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ UnregisterCallback callback,
+ const std::string& sender_id) {
+ // Unsubscribe the PushMessagingAppIdentifier with the push service.
+ // It's possible for GetSenderId to have failed and sender_id to be empty, if
+ // cookies (and the SW database) for an origin got cleared before permissions
+ // are cleared for the origin. In that case for legacy GCM registrations on
+ // Android, Unsubscribe will just delete the app identifier to block future
+ // messages.
+ // TODO(johnme): Auto-unregister before SW DB is cleared (crbug.com/402458).
+ UnsubscribeInternal(reason, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ app_identifier.app_id(), sender_id, std::move(callback));
+}
+
+void PushMessagingServiceImpl::SetContentSettingChangedCallbackForTesting(
+ base::RepeatingClosure callback) {
+ content_setting_changed_callback_for_testing_ = std::move(callback);
+}
+
+// KeyedService methods -------------------------------------------------------
+
+void PushMessagingServiceImpl::Shutdown() {
+ GetGCMDriver()->RemoveAppHandler(kPushMessagingAppIdentifierPrefix);
+ HostContentSettingsMapFactory::GetForProfile(profile_)->RemoveObserver(this);
+}
+
+// content::NotificationObserver methods ---------------------------------------
+
+void PushMessagingServiceImpl::Observe(
+ int type,
+ const content::NotificationSource& source,
+ const content::NotificationDetails& details) {
+ DCHECK_EQ(chrome::NOTIFICATION_APP_TERMINATING, type);
+ shutdown_started_ = true;
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ in_flight_keep_alive_.reset();
+ in_flight_profile_keep_alive_.reset();
+#endif // BUILDFLAG(ENABLE_BACKGROUND_MODE)
+}
+
+// OnSubscriptionInvalidation methods ------------------------------------------
+
+void PushMessagingServiceImpl::OnSubscriptionInvalidation(
+ const std::string& app_id) {
+ DCHECK(base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent))
+ << "It is not allowed to call this method when "
+ "features::kPushSubscriptionChangeEvent is disabled.";
+ PushMessagingAppIdentifier old_app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+ if (old_app_identifier.is_null())
+ return;
+
+ GetSenderId(profile_, old_app_identifier.origin(),
+ old_app_identifier.service_worker_registration_id(),
+ base::BindOnce(&PushMessagingServiceImpl::GetOldSubscription,
+ weak_factory_.GetWeakPtr(), old_app_identifier));
+}
+
+void PushMessagingServiceImpl::GetOldSubscription(
+ PushMessagingAppIdentifier old_app_identifier,
+ const std::string& sender_id) {
+ GetPushSubscriptionFromAppIdentifier(
+ old_app_identifier,
+ base::BindOnce(&PushMessagingServiceImpl::StartRefresh,
+ weak_factory_.GetWeakPtr(), old_app_identifier,
+ sender_id));
+}
+
+void PushMessagingServiceImpl::StartRefresh(
+ PushMessagingAppIdentifier old_app_identifier,
+ const std::string& sender_id,
+ blink::mojom::PushSubscriptionPtr old_subscription) {
+ // Generate a new app_identifier with the same information, but a different
+ // app_id. Expiration time will be overwritten by DoSubscribe, if the flag
+ // features::kPushSubscriptionWithExpiration time is enabled
+ PushMessagingAppIdentifier new_app_identifier =
+ PushMessagingAppIdentifier::Generate(
+ old_app_identifier.origin(),
+ old_app_identifier.service_worker_registration_id(),
+ absl::nullopt /* expiration_time */);
+
+ refresher_.Refresh(old_app_identifier, new_app_identifier.app_id(),
+ sender_id);
+
+ UpdateSubscription(
+ new_app_identifier, push_messaging::MakeOptions(sender_id),
+ base::BindOnce(&PushMessagingServiceImpl::DidUpdateSubscription,
+ weak_factory_.GetWeakPtr(), new_app_identifier.app_id(),
+ old_app_identifier.app_id(), std::move(old_subscription),
+ sender_id));
+}
+
+void PushMessagingServiceImpl::UpdateSubscription(
+ PushMessagingAppIdentifier app_identifier,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback callback) {
+ // After getting a new GCM registration, update the |subscription_id| in SW
+ // database before running the callback
+ auto register_callback = base::BindOnce(
+ [](RegisterCallback cb, Profile* profile, PushMessagingAppIdentifier ai,
+ const std::string& registration_id, const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh, const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ base::OnceClosure closure =
+ base::BindOnce(std::move(cb), registration_id, endpoint,
+ expiration_time, p256dh, auth, status);
+ base::ScopedClosureRunner closure_runner(std::move(closure));
+ if (status ==
+ blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE) {
+ UpdatePushSubscriptionId(profile, ai.origin(),
+ ai.service_worker_registration_id(),
+ registration_id, closure_runner.Release());
+ }
+ },
+ std::move(callback), profile_, app_identifier);
+ // Subscribe using the new subscription information, this will overwrite
+ // the expiration time of |app_identifier|
+ DoSubscribe(app_identifier, std::move(options), std::move(register_callback),
+ -1 /* render_process_id */, -1 /* render_frame_id */,
+ CONTENT_SETTING_ALLOW);
+}
+
+void PushMessagingServiceImpl::DidUpdateSubscription(
+ const std::string& new_app_id,
+ const std::string& old_app_id,
+ blink::mojom::PushSubscriptionPtr old_subscription,
+ const std::string& sender_id,
+ const std::string& registration_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ // TODO(crbug.com/1122545): Currently, if |status| is unsuccessful, the old
+ // subscription remains in SW database and preferences and the refresh is
+ // aborted. Instead, one should abort the refresh and retry to refresh
+ // periodically.
+ if (status !=
+ blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE) {
+ return;
+ }
+
+ // Old subscription is now replaced locally by the new subscription
+ refresher_.OnSubscriptionUpdated(new_app_id);
+
+ PushMessagingAppIdentifier new_app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, new_app_id);
+
+ // Callback for testing
+ base::OnceClosure callback =
+ (invalidation_callback_for_testing_)
+ ? std::move(invalidation_callback_for_testing_)
+ : base::DoNothing();
+
+ FirePushSubscriptionChange(
+ new_app_identifier, std::move(callback),
+ blink::mojom::PushSubscription::New(
+ endpoint, expiration_time, push_messaging::MakeOptions(sender_id),
+ p256dh, auth),
+ std::move(old_subscription));
+}
+
+// PushMessagingRefresher::Observer methods ------------------------------------
+
+void PushMessagingServiceImpl::OnOldSubscriptionExpired(
+ const std::string& app_id,
+ const std::string& sender_id) {
+ // Unsubscribe without clearing SW database, since values of the new
+ // subscription are already saved there.
+ // After unsubscribing, the refresher will get notified.
+ UnsubscribeInternal(
+ blink::mojom::PushUnregistrationReason::REFRESH_FINISHED,
+ GURL::EmptyGURL() /* origin */, -1 /* service_worker_registration_id */,
+ app_id, sender_id,
+ base::BindOnce(&UnregisterCallbackToClosure,
+ base::BindOnce(&PushMessagingRefresher::OnUnsubscribed,
+ refresher_.GetWeakPtr(), app_id)));
+}
+
+void PushMessagingServiceImpl::OnRefreshFinished(
+ const PushMessagingAppIdentifier& app_identifier) {
+ // TODO(viviy): Log data in UMA
+}
+
+void PushMessagingServiceImpl::SetInvalidationCallbackForTesting(
+ base::OnceClosure callback) {
+ invalidation_callback_for_testing_ = std::move(callback);
+}
+
+// Helper methods --------------------------------------------------------------
+
+void PushMessagingServiceImpl::SetRemoveExpiredSubscriptionsCallbackForTesting(
+ base::OnceClosure closure) {
+ remove_expired_subscriptions_callback_for_testing_ = std::move(closure);
+}
+
+// Assumes user_visible always since this is just meant to check
+// if the permission was previously granted and not revoked.
+bool PushMessagingServiceImpl::IsPermissionSet(const GURL& origin,
+ bool user_visible) {
+ return GetPermissionStatus(origin, user_visible) ==
+ blink::mojom::PermissionStatus::GRANTED;
+}
+
+void PushMessagingServiceImpl::GetEncryptionInfoForAppId(
+ const std::string& app_id,
+ const std::string& sender_id,
+ gcm::GCMEncryptionProvider::EncryptionInfoCallback callback) {
+ if (PushMessagingAppIdentifier::UseInstanceID(app_id)) {
+ GetInstanceIDDriver()->GetInstanceID(app_id)->GetEncryptionInfo(
+ push_messaging::NormalizeSenderInfo(sender_id), std::move(callback));
+ } else {
+ GetGCMDriver()->GetEncryptionInfo(app_id, std::move(callback));
+ }
+}
+
+gcm::GCMDriver* PushMessagingServiceImpl::GetGCMDriver() const {
+ gcm::GCMProfileService* gcm_profile_service =
+ gcm::GCMProfileServiceFactory::GetForProfile(profile_);
+ CHECK(gcm_profile_service);
+ CHECK(gcm_profile_service->driver());
+ return gcm_profile_service->driver();
+}
+
+instance_id::InstanceIDDriver* PushMessagingServiceImpl::GetInstanceIDDriver()
+ const {
+ instance_id::InstanceIDProfileService* instance_id_profile_service =
+ instance_id::InstanceIDProfileServiceFactory::GetForProfile(profile_);
+ CHECK(instance_id_profile_service);
+ CHECK(instance_id_profile_service->driver());
+ return instance_id_profile_service->driver();
+}
+
+content::DevToolsBackgroundServicesContext*
+PushMessagingServiceImpl::GetDevToolsContext(const GURL& origin) const {
+ auto* storage_partition = profile_->GetStoragePartitionForUrl(origin);
+ if (!storage_partition)
+ return nullptr;
+
+ auto* devtools_context =
+ storage_partition->GetDevToolsBackgroundServicesContext();
+
+ if (!devtools_context->IsRecording(
+ content::DevToolsBackgroundService::kPushMessaging)) {
+ return nullptr;
+ }
+
+ return devtools_context;
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h
new file mode 100644
index 00000000000..a99e6e578f4
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h
@@ -0,0 +1,475 @@
+// Copyright 2014 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_
+
+#include <stdint.h>
+#include <memory>
+#include <queue>
+#include <utility>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/containers/flat_map.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
+#include "base/time/time.h"
+#include "chrome/browser/permissions/abusive_origin_permission_revocation_request.h"
+#include "chrome/browser/push_messaging/push_messaging_notification_manager.h"
+#include "chrome/browser/push_messaging/push_messaging_refresher.h"
+#include "chrome/common/buildflags.h"
+#include "components/content_settings/core/browser/content_settings_observer.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "components/content_settings/core/common/content_settings_types.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+#include "components/gcm_driver/gcm_app_handler.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/instance_id/instance_id.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "content/public/browser/notification_observer.h"
+#include "content/public/browser/notification_registrar.h"
+#include "content/public/browser/push_messaging_service.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom-forward.h"
+
+class GURL;
+class Profile;
+class PushMessagingAppIdentifier;
+class PushMessagingServiceTest;
+class ScopedKeepAlive;
+class ScopedProfileKeepAlive;
+
+namespace blink {
+namespace mojom {
+enum class PushEventStatus;
+enum class PushRegistrationStatus;
+} // namespace mojom
+} // namespace blink
+
+namespace content {
+class DevToolsBackgroundServicesContext;
+} // namespace content
+
+namespace gcm {
+class GCMDriver;
+} // namespace gcm
+
+namespace instance_id {
+class InstanceIDDriver;
+} // namespace instance_id
+
+namespace {
+struct PendingMessage {
+ PendingMessage(std::string app_id, gcm::IncomingMessage message);
+ PendingMessage(const PendingMessage& other);
+ PendingMessage(PendingMessage&& other);
+ ~PendingMessage();
+
+ PendingMessage& operator=(PendingMessage&& other);
+
+ std::string app_id;
+ gcm::IncomingMessage message;
+ base::Time received_time;
+};
+} // namespace
+
+class PushMessagingServiceImpl : public content::PushMessagingService,
+ public gcm::GCMAppHandler,
+ public content_settings::Observer,
+ public KeyedService,
+ public content::NotificationObserver,
+ public PushMessagingRefresher::Observer {
+ public:
+ // If any Service Workers are using push, starts GCM and adds an app handler.
+ static void InitializeForProfile(Profile* profile);
+
+ explicit PushMessagingServiceImpl(Profile* profile);
+
+ PushMessagingServiceImpl(const PushMessagingServiceImpl&) = delete;
+ PushMessagingServiceImpl& operator=(const PushMessagingServiceImpl&) = delete;
+
+ ~PushMessagingServiceImpl() override;
+
+ // Check and remove subscriptions that are expired when |this| is initialized
+ void RemoveExpiredSubscriptions();
+
+ // Gets the permission status for the given |origin|.
+ blink::mojom::PermissionStatus GetPermissionStatus(const GURL& origin,
+ bool user_visible);
+
+ // gcm::GCMAppHandler implementation.
+ void ShutdownHandler() override;
+ void OnStoreReset() override;
+ void OnMessage(const std::string& app_id,
+ const gcm::IncomingMessage& message) override;
+ void OnMessagesDeleted(const std::string& app_id) override;
+ void OnSendError(
+ const std::string& app_id,
+ const gcm::GCMClient::SendErrorDetails& send_error_details) override;
+ void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) override;
+ void OnMessageDecryptionFailed(const std::string& app_id,
+ const std::string& message_id,
+ const std::string& error_message) override;
+ bool CanHandle(const std::string& app_id) const override;
+
+ // content::PushMessagingService implementation:
+ void SubscribeFromDocument(const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ int render_process_id,
+ int render_frame_id,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ bool user_gesture,
+ RegisterCallback callback) override;
+ void SubscribeFromWorker(const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback callback) override;
+ void GetSubscriptionInfo(const GURL& origin,
+ int64_t service_worker_registration_id,
+ const std::string& sender_id,
+ const std::string& subscription_id,
+ SubscriptionInfoCallback callback) override;
+ void Unsubscribe(blink::mojom::PushUnregistrationReason reason,
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ const std::string& sender_id,
+ UnregisterCallback) override;
+ bool SupportNonVisibleMessages() override;
+ void DidDeleteServiceWorkerRegistration(
+ const GURL& origin,
+ int64_t service_worker_registration_id) override;
+ void DidDeleteServiceWorkerDatabase() override;
+
+ // content_settings::Observer implementation.
+ void OnContentSettingChanged(
+ const ContentSettingsPattern& primary_pattern,
+ const ContentSettingsPattern& secondary_pattern,
+ ContentSettingsTypeSet content_type_set) override;
+
+ // Fires the `pushsubscriptionchange` event to the associated service worker
+ // of |app_identifier|, which is the app identifier for |old_subscription|
+ // whereas |new_subscription| can be either null e.g. when a subscription is
+ // lost due to permission changes or a new subscription when it was refreshed.
+ void FirePushSubscriptionChange(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceClosure completed_closure,
+ blink::mojom::PushSubscriptionPtr new_subscription,
+ blink::mojom::PushSubscriptionPtr old_subscription);
+
+ // KeyedService implementation.
+ void Shutdown() override;
+
+ // content::NotificationObserver implementation
+ void Observe(int type,
+ const content::NotificationSource& source,
+ const content::NotificationDetails& details) override;
+
+ // WARNING: Only call this function if features::kPushSubscriptionChangeEvent
+ // is enabled, will be later used by the Push Service to trigger subscription
+ // refreshes
+ void OnSubscriptionInvalidation(const std::string& app_id);
+
+ // PushMessagingRefresher::Observer implementation
+ // Initiate unsubscribe task when old subscription becomes invalid
+ void OnOldSubscriptionExpired(const std::string& app_id,
+ const std::string& sender_id) override;
+ void OnRefreshFinished(
+ const PushMessagingAppIdentifier& app_identifier) override;
+
+ void SetMessageCallbackForTesting(const base::RepeatingClosure& callback);
+ void SetUnsubscribeCallbackForTesting(base::OnceClosure callback);
+ void SetInvalidationCallbackForTesting(base::OnceClosure callback);
+ void SetContentSettingChangedCallbackForTesting(
+ base::RepeatingClosure callback);
+ void SetServiceWorkerUnregisteredCallbackForTesting(
+ base::RepeatingClosure callback);
+ void SetServiceWorkerDatabaseWipedCallbackForTesting(
+ base::RepeatingClosure callback);
+ void SetRemoveExpiredSubscriptionsCallbackForTesting(
+ base::OnceClosure closure);
+
+ private:
+ friend class PushMessagingBrowserTestBase;
+ friend class PushMessagingServiceTest;
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, NormalizeSenderInfo);
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, PayloadEncryptionTest);
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest,
+ TestMultipleIncomingPushMessages);
+
+ // A subscription is pending until it has succeeded or failed.
+ void IncreasePushSubscriptionCount(int add, bool is_pending);
+ void DecreasePushSubscriptionCount(int subtract, bool was_pending);
+
+ // OnMessage methods ---------------------------------------------------------
+
+ void DeliverMessageCallback(const std::string& app_id,
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ const gcm::IncomingMessage& message,
+ bool did_enqueue_message,
+ blink::mojom::PushEventStatus status);
+
+ void DidHandleEnqueuedMessage(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ base::OnceCallback<void(bool)> message_handled_callback,
+ bool did_show_generic_notification);
+
+ void DidHandleMessage(const std::string& app_id,
+ const std::string& push_message_id,
+ bool did_show_generic_notification);
+
+ void OnCheckedOriginForAbuse(
+ PendingMessage message,
+ AbusiveOriginPermissionRevocationRequest::Outcome outcome);
+
+ void DeliverNextQueuedMessageForServiceWorkerRegistration(
+ const GURL& origin,
+ int64_t service_worker_registration_id);
+
+ void CheckOriginForAbuseAndDispatchNextMessage();
+
+ // Subscribe methods ---------------------------------------------------------
+
+ void DoSubscribe(PushMessagingAppIdentifier app_identifier,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback callback,
+ int render_process_id,
+ int render_frame_id,
+ ContentSetting permission_status);
+
+ void SubscribeEnd(RegisterCallback callback,
+ const std::string& subscription_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status);
+
+ void SubscribeEndWithError(RegisterCallback callback,
+ blink::mojom::PushRegistrationStatus status);
+
+ void DidSubscribe(const PushMessagingAppIdentifier& app_identifier,
+ const std::string& sender_id,
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ instance_id::InstanceID::Result result);
+
+ void DidSubscribeWithEncryptionInfo(
+ const PushMessagingAppIdentifier& app_identifier,
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ const GURL& endpoint,
+ std::string p256dh,
+ std::string auth_secret);
+
+ // GetSubscriptionInfo methods -----------------------------------------------
+
+ void DidValidateSubscription(
+ const std::string& app_id,
+ const std::string& sender_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ SubscriptionInfoCallback callback,
+ bool is_valid);
+
+ void DidGetEncryptionInfo(const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ SubscriptionInfoCallback callback,
+ std::string p256dh,
+ std::string auth_secret) const;
+
+ // Unsubscribe methods -------------------------------------------------------
+
+ // |origin|, |service_worker_registration_id| and |app_id| should be provided
+ // whenever they can be obtained. It's valid for |origin| to be empty and
+ // |service_worker_registration_id| to be kInvalidServiceWorkerRegistrationId,
+ // or for app_id to be empty, but not both at once.
+ void UnsubscribeInternal(blink::mojom::PushUnregistrationReason reason,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback);
+
+ void DidClearPushSubscriptionId(blink::mojom::PushUnregistrationReason reason,
+ const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback);
+
+ void DidUnregister(bool was_subscribed, gcm::GCMClient::Result result);
+ void DidDeleteID(const std::string& app_id,
+ bool was_subscribed,
+ instance_id::InstanceID::Result result);
+ void DidUnsubscribe(const std::string& app_id_when_instance_id,
+ bool was_subscribed);
+
+ // OnContentSettingChanged methods -------------------------------------------
+
+ void GetPushSubscriptionFromAppIdentifier(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback);
+
+ void DidGetSWData(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback,
+ const std::string& sender_id,
+ const std::string& subscription_id);
+
+ void GetPushSubscriptionFromAppIdentifierEnd(
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback,
+ const std::string& sender_id,
+ bool is_valid,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth);
+
+ // OnSubscriptionInvalidation methods-----------------------------------------
+
+ void GetOldSubscription(PushMessagingAppIdentifier old_app_identifier,
+ const std::string& sender_id);
+
+ // After gathering all relavent information to start the refresh,
+ // generate a new app id and initiate refresh
+ void StartRefresh(PushMessagingAppIdentifier old_app_identifier,
+ const std::string& sender_id,
+ blink::mojom::PushSubscriptionPtr old_subscription);
+
+ // Makes a new susbcription and replaces the old subscription by new
+ // subscription in preferences and service worker database
+ void UpdateSubscription(PushMessagingAppIdentifier app_identifier,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback callback);
+
+ // After the subscription is updated, fire a `pushsubscriptionchange` event
+ // and notify the |refresher_|
+ void DidUpdateSubscription(const std::string& new_app_id,
+ const std::string& old_app_id,
+ blink::mojom::PushSubscriptionPtr old_subscription,
+ const std::string& sender_id,
+ const std::string& registration_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status);
+ // Helper methods ------------------------------------------------------------
+
+ // The subscription given in |identifier| will be unsubscribed (and a
+ // `pushsubscriptionchange` event fires if
+ // features::kPushSubscriptionChangeEvent is enabled)
+ void UnexpectedChange(PushMessagingAppIdentifier identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ base::OnceClosure completed_closure);
+
+ void UnexpectedUnsubscribe(const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ UnregisterCallback unregister_callback);
+
+ void DidGetSenderIdUnexpectedUnsubscribe(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ UnregisterCallback callback,
+ const std::string& sender_id);
+
+ void FirePushSubscriptionChangeCallback(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushEventStatus status);
+
+ // Checks if a given origin is allowed to use Push.
+ bool IsPermissionSet(const GURL& origin, bool user_visible = true);
+
+ // Wrapper around {GCMDriver, InstanceID}::GetEncryptionInfo.
+ void GetEncryptionInfoForAppId(
+ const std::string& app_id,
+ const std::string& sender_id,
+ gcm::GCMEncryptionProvider::EncryptionInfoCallback callback);
+
+ gcm::GCMDriver* GetGCMDriver() const;
+
+ instance_id::InstanceIDDriver* GetInstanceIDDriver() const;
+
+ content::DevToolsBackgroundServicesContext* GetDevToolsContext(
+ const GURL& origin) const;
+
+ // Testing methods -----------------------------------------------------------
+
+ using PushEventCallback =
+ base::OnceCallback<void(blink::mojom::PushEventStatus)>;
+ using MessageDispatchedCallback =
+ base::RepeatingCallback<void(const std::string& app_id,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ absl::optional<std::string> payload,
+ PushEventCallback callback)>;
+
+ // Callback to be invoked when a message has been dispatched. Enables tests to
+ // observe message delivery instead of delivering it to the Service Worker.
+ void SetMessageDispatchedCallbackForTesting(
+ const MessageDispatchedCallback& callback) {
+ message_dispatched_callback_for_testing_ = callback;
+ }
+
+ raw_ptr<Profile> profile_;
+ std::unique_ptr<AbusiveOriginPermissionRevocationRequest>
+ abusive_origin_revocation_request_;
+ std::queue<PendingMessage> messages_pending_permission_check_;
+
+ // {Origin, ServiceWokerRegistratonId} key for message delivery queue. This
+ // ensures that we only deliver one message at a time per ServiceWorker.
+ using MessageDeliveryQueueKey = std::pair<GURL, int64_t>;
+
+ // Queue of pending messages per ServiceWorkerRegstration to be delivered one
+ // at a time. This allows us to enforce visibility requirements.
+ base::flat_map<MessageDeliveryQueueKey, std::queue<PendingMessage>>
+ message_delivery_queue_;
+
+ int push_subscription_count_;
+ int pending_push_subscription_count_;
+
+ base::RepeatingClosure message_callback_for_testing_;
+ base::OnceClosure unsubscribe_callback_for_testing_;
+ base::RepeatingClosure content_setting_changed_callback_for_testing_;
+ base::RepeatingClosure service_worker_unregistered_callback_for_testing_;
+ base::RepeatingClosure service_worker_database_wiped_callback_for_testing_;
+ base::OnceClosure remove_expired_subscriptions_callback_for_testing_;
+ base::OnceClosure invalidation_callback_for_testing_;
+
+ PushMessagingNotificationManager notification_manager_;
+
+ PushMessagingRefresher refresher_;
+
+ base::ScopedObservation<PushMessagingRefresher,
+ PushMessagingRefresher::Observer>
+ refresh_observation_{this};
+
+ MessageDispatchedCallback message_dispatched_callback_for_testing_;
+
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ // KeepAlive registered while we have in-flight push messages, to make sure
+ // we can finish processing them without being interrupted by BrowserProcess
+ // teardown.
+ std::unique_ptr<ScopedKeepAlive> in_flight_keep_alive_;
+
+ // Same as ScopedKeepAlive, but prevents |profile_| from getting deleted.
+ std::unique_ptr<ScopedProfileKeepAlive> in_flight_profile_keep_alive_;
+#endif
+
+ content::NotificationRegistrar registrar_;
+
+ // True when shutdown has started. Do not allow processing of incoming
+ // messages when this is true.
+ bool shutdown_started_ = false;
+
+ base::WeakPtrFactory<PushMessagingServiceImpl> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc
new file mode 100644
index 00000000000..612589a5107
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc
@@ -0,0 +1,480 @@
+// 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 "content/public/browser/push_messaging_service.h"
+
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/cxx17_backports.h"
+#include "base/run_loop.h"
+#include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/permissions/permission_manager_factory.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_features.h"
+#include "chrome/browser/push_messaging/push_messaging_service_factory.h"
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+#include "chrome/browser/push_messaging/push_messaging_utils.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/gcm_driver/crypto/gcm_crypto_test_helpers.h"
+#include "components/gcm_driver/fake_gcm_client_factory.h"
+#include "components/gcm_driver/fake_gcm_profile_service.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/permissions/permission_manager.h"
+#include "content/public/common/content_features.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+
+#if defined(OS_ANDROID)
+#include "components/gcm_driver/instance_id/instance_id_android.h"
+#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h"
+#endif // OS_ANDROID
+
+namespace {
+
+const char kTestOrigin[] = "https://example.com";
+const char kTestSenderId[] = "1234567890";
+const int64_t kTestServiceWorkerId = 42;
+const char kTestPayload[] = "Hello, world!";
+
+// NIST P-256 public key in uncompressed format per SEC1 2.3.3.
+const uint8_t kTestP256Key[] = {
+ 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36,
+ 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48,
+ 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B,
+ 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13,
+ 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76,
+ 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD};
+
+static_assert(sizeof(kTestP256Key) == 65,
+ "The fake public key must be a valid P-256 uncompressed point.");
+
+// URL-safe base64 encoded version of the |kTestP256Key|.
+const char kTestEncodedP256Key[] =
+ "BFVSaqVujqpHlzYQwWY8HmW_oXvuSMnGu78CGFNyHQx7qeMRtwNSIdNxkBOowc_tIPcf0X_ydr"
+ "YBINg1pdk8Q_0";
+
+// Implementation of the TestingProfile that provides the Push Messaging Service
+// and the Permission Manager, both of which are required for the tests.
+class PushMessagingTestingProfile : public TestingProfile {
+ public:
+ PushMessagingTestingProfile() = default;
+
+ PushMessagingTestingProfile(const PushMessagingTestingProfile&) = delete;
+ PushMessagingTestingProfile& operator=(const PushMessagingTestingProfile&) =
+ delete;
+
+ ~PushMessagingTestingProfile() override = default;
+
+ PushMessagingServiceImpl* GetPushMessagingService() override {
+ return PushMessagingServiceFactory::GetForProfile(this);
+ }
+
+ permissions::PermissionManager* GetPermissionControllerDelegate() override {
+ return PermissionManagerFactory::GetForProfile(this);
+ }
+};
+
+std::unique_ptr<KeyedService> BuildFakeGCMProfileService(
+ content::BrowserContext* context) {
+ return gcm::FakeGCMProfileService::Build(static_cast<Profile*>(context));
+}
+
+constexpr base::TimeDelta kPushEventHandleTime = base::Seconds(10);
+
+} // namespace
+
+class PushMessagingServiceTest : public ::testing::Test {
+ public:
+ PushMessagingServiceTest() {
+ // Always allow push notifications in the profile.
+ HostContentSettingsMap* host_content_settings_map =
+ HostContentSettingsMapFactory::GetForProfile(&profile_);
+ host_content_settings_map->SetDefaultContentSetting(
+ ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
+
+ // Override the GCM Profile service so that we can send fake messages.
+ gcm::GCMProfileServiceFactory::GetInstance()->SetTestingFactory(
+ &profile_, base::BindRepeating(&BuildFakeGCMProfileService));
+ }
+
+ ~PushMessagingServiceTest() override = default;
+
+ // Callback to use when the subscription may have been subscribed.
+ void DidRegister(std::string* subscription_id_out,
+ GURL* endpoint_out,
+ absl::optional<base::Time>* expiration_time_out,
+ std::vector<uint8_t>* p256dh_out,
+ std::vector<uint8_t>* auth_out,
+ base::OnceClosure done_callback,
+ const std::string& registration_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ EXPECT_EQ(blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE,
+ status);
+
+ *subscription_id_out = registration_id;
+ *expiration_time_out = expiration_time;
+ *endpoint_out = endpoint;
+ *p256dh_out = p256dh;
+ *auth_out = auth;
+
+ std::move(done_callback).Run();
+ }
+
+ // Callback to use when observing messages dispatched by the push service.
+ void DidDispatchMessage(
+ std::string* app_id_out,
+ GURL* origin_out,
+ int64_t* service_worker_registration_id_out,
+ absl::optional<std::string>* payload_out,
+ const std::string& app_id,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ absl::optional<std::string> payload,
+ PushMessagingServiceImpl::PushEventCallback callback) {
+ *app_id_out = app_id;
+ *origin_out = origin;
+ *service_worker_registration_id_out = service_worker_registration_id;
+ *payload_out = std::move(payload);
+ }
+
+ class TestPushSubscription {
+ public:
+ std::string subscription_id_;
+ GURL endpoint_;
+ absl::optional<base::Time> expiration_time_;
+ std::vector<uint8_t> p256dh_;
+ std::vector<uint8_t> auth_;
+ TestPushSubscription(const std::string& subscription_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth)
+ : subscription_id_(subscription_id),
+ endpoint_(endpoint),
+ expiration_time_(expiration_time),
+ p256dh_(p256dh),
+ auth_(auth) {}
+ TestPushSubscription() = default;
+ };
+
+ void Subscribe(PushMessagingServiceImpl* push_service,
+ const GURL& origin,
+ TestPushSubscription* subscription = nullptr) {
+ std::string subscription_id;
+ GURL endpoint;
+ absl::optional<base::Time> expiration_time;
+ std::vector<uint8_t> p256dh, auth;
+
+ base::RunLoop run_loop;
+
+ auto options = blink::mojom::PushSubscriptionOptions::New();
+ options->user_visible_only = true;
+ options->application_server_key = std::vector<uint8_t>(
+ kTestSenderId,
+ kTestSenderId + sizeof(kTestSenderId) / sizeof(char) - 1);
+
+ push_service->SubscribeFromWorker(
+ origin, kTestServiceWorkerId, std::move(options),
+ base::BindOnce(&PushMessagingServiceTest::DidRegister,
+ base::Unretained(this), &subscription_id, &endpoint,
+ &expiration_time, &p256dh, &auth,
+ run_loop.QuitClosure()));
+
+ EXPECT_EQ(0u, subscription_id.size()); // this must be asynchronous
+
+ run_loop.Run();
+
+ ASSERT_GT(subscription_id.size(), 0u);
+ ASSERT_TRUE(endpoint.is_valid());
+ ASSERT_GT(endpoint.spec().size(), 0u);
+ ASSERT_GT(p256dh.size(), 0u);
+ ASSERT_GT(auth.size(), 0u);
+
+ if (subscription) {
+ subscription->subscription_id_ = subscription_id;
+ subscription->endpoint_ = endpoint;
+ subscription->p256dh_ = p256dh;
+ subscription->auth_ = auth;
+ }
+ }
+
+ protected:
+ PushMessagingTestingProfile* profile() { return &profile_; }
+
+ content::BrowserTaskEnvironment& task_environment() {
+ return task_environment_;
+ }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_{
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+ PushMessagingTestingProfile profile_;
+
+#if defined(OS_ANDROID)
+ instance_id::InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting
+ block_async_;
+#endif // OS_ANDROID
+};
+
+// Fails too often on Linux TSAN builder: http://crbug.com/1211350.
+#if defined(OS_LINUX) && defined(THREAD_SANITIZER)
+#define MAYBE_PayloadEncryptionTest DISABLED_PayloadEncryptionTest
+#else
+#define MAYBE_PayloadEncryptionTest PayloadEncryptionTest
+#endif
+TEST_F(PushMessagingServiceTest, MAYBE_PayloadEncryptionTest) {
+ PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService();
+ ASSERT_TRUE(push_service);
+
+ const GURL origin(kTestOrigin);
+
+ // (1) Make sure that |kExampleOrigin| has access to use Push Messaging.
+ ASSERT_EQ(blink::mojom::PermissionStatus::GRANTED,
+ push_service->GetPermissionStatus(origin, true /* user_visible */));
+
+ // (2) Subscribe for Push Messaging, and verify that we've got the required
+ // information in order to be able to create encrypted messages.
+ TestPushSubscription subscription;
+ Subscribe(push_service, origin, &subscription);
+
+ // (3) Encrypt a message using the public key and authentication secret that
+ // are associated with the subscription.
+
+ gcm::IncomingMessage message;
+ message.sender_id = kTestSenderId;
+
+ ASSERT_TRUE(gcm::CreateEncryptedPayloadForTesting(
+ kTestPayload,
+ base::StringPiece(reinterpret_cast<char*>(subscription.p256dh_.data()),
+ subscription.p256dh_.size()),
+ base::StringPiece(reinterpret_cast<char*>(subscription.auth_.data()),
+ subscription.auth_.size()),
+ &message));
+
+ ASSERT_GT(message.raw_data.size(), 0u);
+ ASSERT_NE(kTestPayload, message.raw_data);
+ ASSERT_FALSE(message.decrypted);
+
+ // (4) Find the app_id that has been associated with the subscription.
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin,
+ kTestServiceWorkerId);
+
+ ASSERT_FALSE(app_identifier.is_null());
+
+ std::string app_id;
+ GURL dispatched_origin;
+ int64_t service_worker_registration_id;
+ absl::optional<std::string> payload;
+
+ // (5) Observe message dispatchings from the Push Messaging service, and
+ // then dispatch the |message| on the GCM driver as if it had actually
+ // been received by Google Cloud Messaging.
+ push_service->SetMessageDispatchedCallbackForTesting(base::BindRepeating(
+ &PushMessagingServiceTest::DidDispatchMessage, base::Unretained(this),
+ &app_id, &dispatched_origin, &service_worker_registration_id, &payload));
+
+ gcm::FakeGCMProfileService* fake_profile_service =
+ static_cast<gcm::FakeGCMProfileService*>(
+ gcm::GCMProfileServiceFactory::GetForProfile(profile()));
+
+ fake_profile_service->DispatchMessage(app_identifier.app_id(), message);
+
+ base::RunLoop().RunUntilIdle();
+
+ // (6) Verify that the message, as received by the Push Messaging Service, has
+ // indeed been decrypted by the GCM Driver, and has been forwarded to the
+ // Service Worker that has been associated with the subscription.
+ EXPECT_EQ(app_identifier.app_id(), app_id);
+ EXPECT_EQ(origin, dispatched_origin);
+ EXPECT_EQ(service_worker_registration_id, kTestServiceWorkerId);
+
+ EXPECT_TRUE(payload);
+ EXPECT_EQ(kTestPayload, *payload);
+}
+
+TEST_F(PushMessagingServiceTest, NormalizeSenderInfo) {
+ PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService();
+ ASSERT_TRUE(push_service);
+
+ std::string p256dh(kTestP256Key, kTestP256Key + base::size(kTestP256Key));
+ ASSERT_EQ(65u, p256dh.size());
+
+ // NIST P-256 public keys in uncompressed format will be encoded using the
+ // URL-safe base64 encoding by the normalization function.
+ EXPECT_EQ(kTestEncodedP256Key, push_messaging::NormalizeSenderInfo(p256dh));
+
+ // Any other value, binary or not, will be passed through as-is.
+ EXPECT_EQ("1234567890", push_messaging::NormalizeSenderInfo("1234567890"));
+ EXPECT_EQ("foo@bar.com", push_messaging::NormalizeSenderInfo("foo@bar.com"));
+
+ p256dh[0] = 0x05; // invalidate |p256dh| as a public key.
+
+ EXPECT_EQ(p256dh, push_messaging::NormalizeSenderInfo(p256dh));
+}
+
+// Fails too often on Linux TSAN builder: http://crbug.com/1211350.
+#if defined(OS_LINUX) && defined(THREAD_SANITIZER)
+#define MAYBE_RemoveExpiredSubscriptions DISABLED_RemoveExpiredSubscriptions
+#else
+#define MAYBE_RemoveExpiredSubscriptions RemoveExpiredSubscriptions
+#endif
+TEST_F(PushMessagingServiceTest, MAYBE_RemoveExpiredSubscriptions) {
+ // (1) Enable push subscriptions with expiration time and
+ // `pushsubscriptionchange` events
+ base::test::ScopedFeatureList scoped_feature_list_;
+ scoped_feature_list_.InitWithFeatures(
+ /* enabled features */
+ {features::kPushSubscriptionWithExpirationTime,
+ features::kPushSubscriptionChangeEvent},
+ /* disabled features */
+ {});
+
+ // (2) Set up push service and test origin
+ PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService();
+ ASSERT_TRUE(push_service);
+ const GURL origin(kTestOrigin);
+
+ // (3) Subscribe origin to push service and find corresponding
+ // |app_identifier|
+ Subscribe(push_service, origin);
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin,
+ kTestServiceWorkerId);
+ ASSERT_FALSE(app_identifier.is_null());
+
+ // (4) Manually set the time as expired, save the time in preferences
+ app_identifier.set_expiration_time(base::Time::UnixEpoch());
+ app_identifier.PersistToPrefs(profile());
+ ASSERT_EQ(1u, PushMessagingAppIdentifier::GetCount(profile()));
+
+ // (3) Remove all expired subscriptions
+ base::RunLoop run_loop;
+ push_service->SetRemoveExpiredSubscriptionsCallbackForTesting(
+ run_loop.QuitClosure());
+ push_service->RemoveExpiredSubscriptions();
+ run_loop.Run();
+
+ // (5) We expect the subscription to be deleted
+ ASSERT_EQ(0u, PushMessagingAppIdentifier::GetCount(profile()));
+ PushMessagingAppIdentifier deleted_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile(),
+ app_identifier.app_id());
+ EXPECT_TRUE(deleted_identifier.is_null());
+}
+
+TEST_F(PushMessagingServiceTest, TestMultipleIncomingPushMessages) {
+ base::HistogramTester histograms;
+ PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService();
+ ASSERT_TRUE(push_service);
+
+ // Subscribe |origin| to push service.
+ const GURL origin(kTestOrigin);
+ Subscribe(push_service, origin);
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin,
+ kTestServiceWorkerId);
+ ASSERT_FALSE(app_identifier.is_null());
+
+ // Setup decrypted test message.
+ gcm::IncomingMessage message;
+ message.sender_id = kTestSenderId;
+ message.raw_data = "testdata";
+ message.decrypted = true;
+
+ // Setup callbacks for dispatch and handled push events.
+ auto dispatched_run_loop = std::make_unique<base::RunLoop>();
+ auto handled_run_loop = std::make_unique<base::RunLoop>();
+ PushMessagingServiceImpl::PushEventCallback handle_push_event;
+
+ push_service->SetMessageDispatchedCallbackForTesting(
+ base::BindLambdaForTesting(
+ [&](const std::string& app_id, const GURL& origin,
+ int64_t service_worker_registration_id,
+ absl::optional<std::string> payload,
+ PushMessagingServiceImpl::PushEventCallback callback) {
+ handle_push_event = std::move(callback);
+ dispatched_run_loop->Quit();
+ }));
+
+ push_service->SetMessageCallbackForTesting(
+ base::BindLambdaForTesting([&]() { handled_run_loop->Quit(); }));
+
+ // Simulate two incoming push messages at the same time.
+ push_service->OnMessage(app_identifier.app_id(), message);
+ push_service->OnMessage(app_identifier.app_id(), message);
+
+ // First wait until we dispatched the first push message.
+ dispatched_run_loop->Run();
+ dispatched_run_loop = std::make_unique<base::RunLoop>();
+ auto handled_first = std::move(handle_push_event);
+ handle_push_event = PushMessagingServiceImpl::PushEventCallback();
+
+ histograms.ExpectUniqueTimeSample("PushMessaging.CheckOriginForAbuseTime",
+ base::Seconds(0),
+ /*expected_bucket_count=*/1);
+ histograms.ExpectUniqueTimeSample("PushMessaging.DeliverQueuedMessageTime",
+ base::Seconds(0),
+ /*expected_bucket_count=*/1);
+
+ // Run all tasks until idle so we can verify that we don't dispatch the second
+ // push message until the first one is handled.
+ base::RunLoop().RunUntilIdle();
+ EXPECT_FALSE(handle_push_event);
+
+ // Simulate handling the first push event takes some time.
+ task_environment().FastForwardBy(kPushEventHandleTime);
+
+ // Now signal that the first push event has been handled and wait until we
+ // checked for visibility requirements.
+ std::move(handled_first).Run(blink::mojom::PushEventStatus::SUCCESS);
+ handled_run_loop->Run();
+ handled_run_loop = std::make_unique<base::RunLoop>();
+
+ histograms.ExpectUniqueTimeSample("PushMessaging.MessageHandledTime",
+ kPushEventHandleTime,
+ /*expected_bucket_count=*/1);
+
+ // Simulate handling the second push event takes some time.
+ task_environment().FastForwardBy(kPushEventHandleTime);
+
+ // Now wait until we dispatched the second push message and handle it too.
+ dispatched_run_loop->Run();
+ std::move(handle_push_event).Run(blink::mojom::PushEventStatus::SUCCESS);
+ handled_run_loop->Run();
+
+ // Checking origins for abuse happens immediately on receiving a push message
+ // one at a time. Both messages do that instantly in this test.
+ histograms.ExpectTimeBucketCount("PushMessaging.CheckOriginForAbuseTime",
+ base::Seconds(0),
+ /*count=*/2);
+ // Delivering messages should be done in series so the second message should
+ // have waited for the first one to be handled.
+ histograms.ExpectTimeBucketCount("PushMessaging.DeliverQueuedMessageTime",
+ kPushEventHandleTime,
+ /*count=*/1);
+ // The total time from receiving until handling of the second message.
+ histograms.ExpectTimeBucketCount("PushMessaging.MessageHandledTime",
+ kPushEventHandleTime * 2,
+ /*count=*/1);
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_utils.cc b/chromium/chrome/browser/push_messaging/push_messaging_utils.cc
new file mode 100644
index 00000000000..b54bf14acf3
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_utils.cc
@@ -0,0 +1,44 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/push_messaging/push_messaging_utils.h"
+#include "base/base64url.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "url/gurl.h"
+
+namespace push_messaging {
+
+GURL CreateEndpoint(const std::string& subscription_id) {
+ const GURL endpoint(kPushMessagingGcmEndpoint + subscription_id);
+ DCHECK(endpoint.is_valid());
+ return endpoint;
+}
+
+blink::mojom::PushSubscriptionOptionsPtr MakeOptions(
+ const std::string& sender_id) {
+ return blink::mojom::PushSubscriptionOptions::New(
+ /*user_visible_only=*/true,
+ std::vector<uint8_t>(sender_id.begin(), sender_id.end()));
+}
+
+bool IsVapidKey(const std::string& application_server_key) {
+ // VAPID keys are NIST P-256 public keys in uncompressed format (64 bytes),
+ // verified through its length and the 0x04 prefix.
+ return application_server_key.size() == 65 &&
+ application_server_key[0] == 0x04;
+}
+
+std::string NormalizeSenderInfo(const std::string& application_server_key) {
+ if (!IsVapidKey(application_server_key))
+ return application_server_key;
+
+ std::string encoded_application_server_key;
+ base::Base64UrlEncode(application_server_key,
+ base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &encoded_application_server_key);
+
+ return encoded_application_server_key;
+}
+
+} // namespace push_messaging
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_utils.h b/chromium/chrome/browser/push_messaging/push_messaging_utils.h
new file mode 100644
index 00000000000..805deeab47e
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_utils.h
@@ -0,0 +1,34 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_
+
+#include <string>
+#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom.h"
+
+class GURL;
+
+namespace push_messaging {
+
+// Returns the URL used to send push messages to the subscription identified
+// by |subscription_id|.
+GURL CreateEndpoint(const std::string& subscription_id);
+
+// Checks size and prefix to determine whether it is a VAPID key
+bool IsVapidKey(const std::string& application_server_key);
+
+// Normalizes the |sender_info|. In most cases the |sender_info| will be
+// passed through to the GCM Driver as-is, but NIST P-256 application server
+// keys have to be encoded using the URL-safe variant of the base64 encoding.
+std::string NormalizeSenderInfo(const std::string& sender_info);
+
+// Currently |user_visible_only| is always true, once silent pushes are
+// enabled, get this information from SW database.
+blink::mojom::PushSubscriptionOptionsPtr MakeOptions(
+ const std::string& sender_id);
+
+} // namespace push_messaging
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_
diff --git a/chromium/chrome/browser/signin/DEPS b/chromium/chrome/browser/signin/DEPS
new file mode 100644
index 00000000000..e2cc84fd222
--- /dev/null
+++ b/chromium/chrome/browser/signin/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+ "+ash/components/account_manager",
+]
diff --git a/chromium/chrome/browser/signin/DIR_METADATA b/chromium/chrome/browser/signin/DIR_METADATA
new file mode 100644
index 00000000000..95ad5b66d16
--- /dev/null
+++ b/chromium/chrome/browser/signin/DIR_METADATA
@@ -0,0 +1 @@
+mixins: "//components/signin/COMMON_METADATA"
diff --git a/chromium/chrome/browser/signin/OWNERS b/chromium/chrome/browser/signin/OWNERS
new file mode 100644
index 00000000000..017d5a003db
--- /dev/null
+++ b/chromium/chrome/browser/signin/OWNERS
@@ -0,0 +1,5 @@
+file://components/signin/OWNERS
+
+per-file chrome_proximity_auth_*=xiyuan@chromium.org
+per-file easy_unlock_*=xiyuan@chromium.org
+per-file signin_profile_attributes_updater*=msalama@chromium.org
diff --git a/chromium/chrome/browser/signin/about_signin_internals_factory.cc b/chromium/chrome/browser/signin/about_signin_internals_factory.cc
new file mode 100644
index 00000000000..7e62dc93ebf
--- /dev/null
+++ b/chromium/chrome/browser/signin/about_signin_internals_factory.cc
@@ -0,0 +1,58 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/about_signin_internals_factory.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_error_controller_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/signin/core/browser/about_signin_internals.h"
+
+AboutSigninInternalsFactory::AboutSigninInternalsFactory()
+ : BrowserContextKeyedServiceFactory(
+ "AboutSigninInternals",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(ChromeSigninClientFactory::GetInstance());
+ DependsOn(SigninErrorControllerFactory::GetInstance());
+ DependsOn(AccountReconcilorFactory::GetInstance());
+ DependsOn(IdentityManagerFactory::GetInstance());
+ DependsOn(AccountConsistencyModeManagerFactory::GetInstance());
+}
+
+AboutSigninInternalsFactory::~AboutSigninInternalsFactory() {}
+
+// static
+AboutSigninInternals* AboutSigninInternalsFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<AboutSigninInternals*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+AboutSigninInternalsFactory* AboutSigninInternalsFactory::GetInstance() {
+ return base::Singleton<AboutSigninInternalsFactory>::get();
+}
+
+void AboutSigninInternalsFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* user_prefs) {
+ AboutSigninInternals::RegisterPrefs(user_prefs);
+}
+
+KeyedService* AboutSigninInternalsFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ AboutSigninInternals* service = new AboutSigninInternals(
+ IdentityManagerFactory::GetForProfile(profile),
+ SigninErrorControllerFactory::GetForProfile(profile),
+ AccountConsistencyModeManager::GetMethodForProfile(profile),
+ ChromeSigninClientFactory::GetForProfile(profile),
+ AccountReconcilorFactory::GetForProfile(profile));
+ return service;
+}
diff --git a/chromium/chrome/browser/signin/about_signin_internals_factory.h b/chromium/chrome/browser/signin/about_signin_internals_factory.h
new file mode 100644
index 00000000000..c1b7cb5a71d
--- /dev/null
+++ b/chromium/chrome/browser/signin/about_signin_internals_factory.h
@@ -0,0 +1,40 @@
+// Copyright (c) 2012 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class AboutSigninInternals;
+class Profile;
+
+// Singleton that owns all AboutSigninInternals and associates them with
+// Profiles.
+class AboutSigninInternalsFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of AboutSigninInternals associated with this profile,
+ // creating one if none exists.
+ static AboutSigninInternals* GetForProfile(Profile* profile);
+
+ // Returns an instance of the AboutSigninInternalsFactory singleton.
+ static AboutSigninInternalsFactory* GetInstance();
+
+ // Implementation of BrowserContextKeyedServiceFactory.
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+
+ private:
+ friend struct base::DefaultSingletonTraits<AboutSigninInternalsFactory>;
+
+ AboutSigninInternalsFactory();
+ ~AboutSigninInternalsFactory() override;
+
+ // BrowserContextKeyedServiceFactory
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager.cc
new file mode 100644
index 00000000000..6c1f0f04720
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager.cc
@@ -0,0 +1,208 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+
+#include <string>
+
+#include "base/command_line.h"
+#include "base/logging.h"
+#include "base/metrics/field_trial_params.h"
+#include "base/metrics/histogram_macros.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/common/pref_names.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "google_apis/google_api_keys.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/account_manager/account_manager_util.h"
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT) && BUILDFLAG(ENABLE_MIRROR)
+#error "Dice and Mirror cannot be both enabled."
+#endif
+
+#if !BUILDFLAG(ENABLE_DICE_SUPPORT) && !BUILDFLAG(ENABLE_MIRROR)
+#error "Either Dice or Mirror should be enabled."
+#endif
+
+using signin::AccountConsistencyMethod;
+
+namespace {
+
+// By default, DICE is not enabled in builds lacking an API key. May be set to
+// true for tests.
+bool g_ignore_missing_oauth_client_for_testing = false;
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+const char kAllowBrowserSigninArgument[] = "allow-browser-signin";
+
+bool IsBrowserSigninAllowedByCommandLine() {
+ base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+ if (command_line->HasSwitch(kAllowBrowserSigninArgument)) {
+ std::string allowBrowserSignin =
+ command_line->GetSwitchValueASCII(kAllowBrowserSigninArgument);
+ return base::ToLowerASCII(allowBrowserSignin) == "true";
+ }
+ // If the commandline flag is not provided, the default is true.
+ return true;
+}
+
+// Returns true if Desktop Identity Consistency can be enabled for this build
+// (i.e. if OAuth client ID and client secret are configured).
+bool CanEnableDiceForBuild() {
+ if (g_ignore_missing_oauth_client_for_testing ||
+ google_apis::HasOAuthClientConfigured()) {
+ return true;
+ }
+
+ // Only log this once.
+ static bool logged_warning = []() {
+ LOG(WARNING) << "Desktop Identity Consistency cannot be enabled as no "
+ "OAuth client ID and client secret have been configured.";
+ return true;
+ }();
+ ALLOW_UNUSED_LOCAL(logged_warning);
+
+ return false;
+}
+#endif
+
+} // namespace
+
+// static
+AccountConsistencyModeManager* AccountConsistencyModeManager::GetForProfile(
+ Profile* profile) {
+ return AccountConsistencyModeManagerFactory::GetForProfile(profile);
+}
+
+AccountConsistencyModeManager::AccountConsistencyModeManager(Profile* profile)
+ : profile_(profile),
+ account_consistency_(signin::AccountConsistencyMethod::kDisabled),
+ account_consistency_initialized_(false) {
+ DCHECK(profile_);
+ DCHECK(ShouldBuildServiceForProfile(profile));
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ PrefService* prefs = profile->GetPrefs();
+ // Propagate settings changes from the previous launch to the signin-allowed
+ // pref.
+ bool signin_allowed = IsDiceSignInAllowed() &&
+ prefs->GetBoolean(prefs::kSigninAllowedOnNextStartup);
+ prefs->SetBoolean(prefs::kSigninAllowed, signin_allowed);
+
+ UMA_HISTOGRAM_BOOLEAN("Signin.SigninAllowed", signin_allowed);
+#endif
+
+ account_consistency_ = ComputeAccountConsistencyMethod(profile_);
+ DCHECK_EQ(account_consistency_, ComputeAccountConsistencyMethod(profile_));
+ account_consistency_initialized_ = true;
+}
+
+AccountConsistencyModeManager::~AccountConsistencyModeManager() {}
+
+// static
+void AccountConsistencyModeManager::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ registry->RegisterBooleanPref(prefs::kSigninAllowedOnNextStartup, true);
+}
+
+// static
+AccountConsistencyMethod AccountConsistencyModeManager::GetMethodForProfile(
+ Profile* profile) {
+ if (!ShouldBuildServiceForProfile(profile))
+ return AccountConsistencyMethod::kDisabled;
+
+ return AccountConsistencyModeManager::GetForProfile(profile)
+ ->GetAccountConsistencyMethod();
+}
+
+// static
+bool AccountConsistencyModeManager::IsDiceEnabledForProfile(Profile* profile) {
+ return GetMethodForProfile(profile) == AccountConsistencyMethod::kDice;
+}
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+// static
+bool AccountConsistencyModeManager::IsDiceSignInAllowed() {
+ return CanEnableDiceForBuild() && IsBrowserSigninAllowedByCommandLine();
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+// static
+bool AccountConsistencyModeManager::IsMirrorEnabledForProfile(
+ Profile* profile) {
+ return GetMethodForProfile(profile) == AccountConsistencyMethod::kMirror;
+}
+
+// static
+void AccountConsistencyModeManager::SetIgnoreMissingOAuthClientForTesting() {
+ g_ignore_missing_oauth_client_for_testing = true;
+}
+
+// static
+bool AccountConsistencyModeManager::ShouldBuildServiceForProfile(
+ Profile* profile) {
+ return profile->IsRegularProfile();
+}
+
+AccountConsistencyMethod
+AccountConsistencyModeManager::GetAccountConsistencyMethod() {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // TODO(https://crbug.com/860671): ChromeOS should use the cached value.
+ // Changing the value dynamically is not supported.
+ return ComputeAccountConsistencyMethod(profile_);
+#else
+ // The account consistency method should not change during the lifetime of a
+ // profile. We always return the cached value, but still check that it did not
+ // change, in order to detect inconsisent states. See https://crbug.com/860471
+ CHECK(account_consistency_initialized_);
+ CHECK_EQ(ComputeAccountConsistencyMethod(profile_), account_consistency_);
+ return account_consistency_;
+#endif
+}
+
+// static
+signin::AccountConsistencyMethod
+AccountConsistencyModeManager::ComputeAccountConsistencyMethod(
+ Profile* profile) {
+ DCHECK(ShouldBuildServiceForProfile(profile));
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (!ash::IsAccountManagerAvailable(profile))
+ return AccountConsistencyMethod::kDisabled;
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ // Account consistency is unavailable on Managed Guest Sessions and Public
+ // Sessions.
+ if (profiles::IsPublicSession())
+ return AccountConsistencyMethod::kDisabled;
+#endif
+
+#if BUILDFLAG(ENABLE_MIRROR)
+ return AccountConsistencyMethod::kMirror;
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ if (!profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)) {
+ VLOG(1) << "Desktop Identity Consistency disabled as sign-in to Chrome"
+ "is not allowed";
+ return AccountConsistencyMethod::kDisabled;
+ }
+
+ return AccountConsistencyMethod::kDice;
+#endif
+
+ NOTREACHED();
+ return AccountConsistencyMethod::kDisabled;
+}
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager.h b/chromium/chrome/browser/signin/account_consistency_mode_manager.h
new file mode 100644
index 00000000000..8ca66aae9b9
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager.h
@@ -0,0 +1,97 @@
+// Copyright 2017 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_
+
+#include "base/feature_list.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "build/buildflag.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/prefs/pref_member.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_buildflags.h"
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+class Profile;
+
+// Manages the account consistency mode for each profile.
+class AccountConsistencyModeManager : public KeyedService {
+ public:
+ // Returns the AccountConsistencyModeManager associated with this profile.
+ // May return nullptr if there is none (e.g. in incognito).
+ static AccountConsistencyModeManager* GetForProfile(Profile* profile);
+
+ explicit AccountConsistencyModeManager(Profile* profile);
+
+ AccountConsistencyModeManager(const AccountConsistencyModeManager&) = delete;
+ AccountConsistencyModeManager& operator=(
+ const AccountConsistencyModeManager&) = delete;
+
+ ~AccountConsistencyModeManager() override;
+
+ static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+ // Helper method, shorthand for calling GetAccountConsistencyMethod().
+ // TODO(crbug.com/1232361): Migrate usages to
+ // `IdentityManager::GetAccountConsistency`.
+ static signin::AccountConsistencyMethod GetMethodForProfile(Profile* profile);
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ // This is a pre-requisite of IsDiceEnabledForProfile(), independent of
+ // particular profile type or profile prefs.
+ static bool IsDiceSignInAllowed();
+#endif
+
+ // If true, then account management is done through Gaia webpages.
+ // Can only be used on the UI thread.
+ // Returns false if |profile| is in Guest or Incognito mode.
+ // A given |profile| will have only one of Mirror or Dice consistency
+ // behaviour enabled.
+ static bool IsDiceEnabledForProfile(Profile* profile);
+
+ // Returns |true| if Mirror account consistency is enabled for |profile|.
+ // Can only be used on the UI thread.
+ // A given |profile| will have only one of Mirror or Dice consistency
+ // behaviour enabled.
+ static bool IsMirrorEnabledForProfile(Profile* profile);
+
+ // By default, Desktop Identity Consistency (aka Dice) is not enabled in
+ // builds lacking an API key. For testing, set to have Dice enabled in tests.
+ static void SetIgnoreMissingOAuthClientForTesting();
+
+ // Returns true is the AccountConsistencyModeManager should be instantiated
+ // for the profile. Guest, incognito and system sessions do not instantiate
+ // the service.
+ static bool ShouldBuildServiceForProfile(Profile* profile);
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
+ MigrateAtCreation);
+ FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
+ SigninAllowedChangesDiceState);
+ FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
+ AllowBrowserSigninSwitch);
+ FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
+ DiceEnabledForNewProfiles);
+
+ // Returns the account consistency method for the current profile.
+ signin::AccountConsistencyMethod GetAccountConsistencyMethod();
+
+ // Computes the account consistency method for the current profile. This is
+ // only called from the constructor, the account consistency method cannot
+ // change during the lifetime of a profile.
+ static signin::AccountConsistencyMethod ComputeAccountConsistencyMethod(
+ Profile* profile);
+
+ raw_ptr<Profile> profile_;
+ signin::AccountConsistencyMethod account_consistency_;
+ bool account_consistency_initialized_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc
new file mode 100644
index 00000000000..10178f34732
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc
@@ -0,0 +1,53 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
+
+#include "base/check.h"
+#include "build/build_config.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+// static
+AccountConsistencyModeManagerFactory*
+AccountConsistencyModeManagerFactory::GetInstance() {
+ return base::Singleton<AccountConsistencyModeManagerFactory>::get();
+}
+
+// static
+AccountConsistencyModeManager*
+AccountConsistencyModeManagerFactory::GetForProfile(Profile* profile) {
+ DCHECK(profile);
+ return static_cast<AccountConsistencyModeManager*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+AccountConsistencyModeManagerFactory::AccountConsistencyModeManagerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "AccountConsistencyModeManager",
+ BrowserContextDependencyManager::GetInstance()) {}
+
+AccountConsistencyModeManagerFactory::~AccountConsistencyModeManagerFactory() =
+ default;
+
+KeyedService* AccountConsistencyModeManagerFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ DCHECK(!context->IsOffTheRecord());
+ Profile* profile = Profile::FromBrowserContext(context);
+
+ return AccountConsistencyModeManager::ShouldBuildServiceForProfile(profile)
+ ? new AccountConsistencyModeManager(profile)
+ : nullptr;
+}
+
+void AccountConsistencyModeManagerFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ AccountConsistencyModeManager::RegisterProfilePrefs(registry);
+}
+
+bool AccountConsistencyModeManagerFactory::ServiceIsCreatedWithBrowserContext()
+ const {
+ return true;
+}
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h
new file mode 100644
index 00000000000..7990d84bad5
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h
@@ -0,0 +1,35 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class AccountConsistencyModeManagerFactory
+ : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns an instance of the factory singleton.
+ static AccountConsistencyModeManagerFactory* GetInstance();
+
+ static AccountConsistencyModeManager* GetForProfile(Profile* profile);
+
+ private:
+ friend struct base::DefaultSingletonTraits<
+ AccountConsistencyModeManagerFactory>;
+
+ AccountConsistencyModeManagerFactory();
+ ~AccountConsistencyModeManagerFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+ bool ServiceIsCreatedWithBrowserContext() const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc
new file mode 100644
index 00000000000..d0a4ee97b83
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc
@@ -0,0 +1,259 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/command_line.h"
+#include "base/test/scoped_command_line.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/prefs/browser_prefs.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/prefs/pref_notifier_impl.h"
+#include "components/prefs/testing_pref_store.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+std::unique_ptr<TestingProfile> BuildTestingProfile(bool is_new_profile) {
+ TestingProfile::Builder profile_builder;
+ profile_builder.SetIsNewProfile(is_new_profile);
+ std::unique_ptr<TestingProfile> profile = profile_builder.Build();
+ EXPECT_EQ(is_new_profile, profile->IsNewProfile());
+ return profile;
+}
+
+} // namespace
+
+// Check the default account consistency method.
+TEST(AccountConsistencyModeManagerTest, DefaultValue) {
+ content::BrowserTaskEnvironment task_environment;
+ std::unique_ptr<TestingProfile> profile =
+ BuildTestingProfile(/*is_new_profile=*/false);
+
+ signin::AccountConsistencyMethod method =
+#if BUILDFLAG(ENABLE_MIRROR)
+ signin::AccountConsistencyMethod::kMirror;
+#elif BUILDFLAG(ENABLE_DICE_SUPPORT)
+ signin::AccountConsistencyMethod::kDice;
+#else
+#error Either Dice or Mirror should be enabled
+#endif
+
+ EXPECT_EQ(method,
+ AccountConsistencyModeManager::GetMethodForProfile(profile.get()));
+ EXPECT_EQ(
+ method == signin::AccountConsistencyMethod::kMirror,
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile.get()));
+ EXPECT_EQ(
+ method == signin::AccountConsistencyMethod::kDice,
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get()));
+}
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+// Checks that changing the signin-allowed pref changes the Dice state on next
+// startup.
+TEST(AccountConsistencyModeManagerTest, SigninAllowedChangesDiceState) {
+ content::BrowserTaskEnvironment task_environment;
+ std::unique_ptr<TestingProfile> profile =
+ BuildTestingProfile(/*is_new_profile=*/false);
+
+ {
+ // First startup.
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_TRUE(
+ profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+
+ // User changes their settings.
+ profile->GetPrefs()->SetBoolean(prefs::kSigninAllowedOnNextStartup, false);
+ // Dice should remain in the same state until restart.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+ }
+
+ {
+ // Second startup.
+ AccountConsistencyModeManager manager(profile.get());
+ // The signin-allowed pref should be disabled.
+ EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_FALSE(
+ profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+ // Dice should be disabled.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ manager.GetAccountConsistencyMethod());
+ }
+}
+
+TEST(AccountConsistencyModeManagerTest, AllowBrowserSigninSwitch) {
+ content::BrowserTaskEnvironment task_environment;
+ std::unique_ptr<TestingProfile> profile =
+ BuildTestingProfile(/*is_new_profile=*/false);
+ {
+ base::test::ScopedCommandLine scoped_command_line;
+ scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII(
+ "allow-browser-signin", "false");
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ // Dice should be disabled.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ manager.GetAccountConsistencyMethod());
+ }
+
+ {
+ base::test::ScopedCommandLine scoped_command_line;
+ scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII(
+ "allow-browser-signin", "true");
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ // Dice should be enabled.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+ }
+
+ {
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_TRUE(
+ profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+ // Dice should be enabled.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+ }
+}
+
+// Checks that Dice is enabled for new profiles.
+TEST(AccountConsistencyModeManagerTest, DiceEnabledForNewProfiles) {
+ content::BrowserTaskEnvironment task_environment;
+ std::unique_ptr<TestingProfile> profile =
+ BuildTestingProfile(/*is_new_profile=*/false);
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+}
+
+TEST(AccountConsistencyModeManagerTest, DiceOnlyForRegularProfile) {
+ content::BrowserTaskEnvironment task_environment;
+
+ {
+ // Regular profile.
+ TestingProfile profile;
+ EXPECT_TRUE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ AccountConsistencyModeManager::GetMethodForProfile(&profile));
+ EXPECT_TRUE(
+ AccountConsistencyModeManager::ShouldBuildServiceForProfile(&profile));
+
+ // Incognito profile.
+ Profile* incognito_profile =
+ profile.GetPrimaryOTRProfile(/*create_if_needed=*/true);
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ incognito_profile));
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::GetForProfile(incognito_profile));
+ EXPECT_EQ(
+ signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(incognito_profile));
+ EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile(
+ incognito_profile));
+
+ // Non-primary off-the-record profile.
+ Profile* otr_profile = profile.GetOffTheRecordProfile(
+ Profile::OTRProfileID::CreateUniqueForTesting(),
+ /*create_if_needed=*/true);
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(otr_profile));
+ EXPECT_FALSE(AccountConsistencyModeManager::GetForProfile(otr_profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(otr_profile));
+ EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile(
+ otr_profile));
+ }
+
+ // Guest profile.
+ {
+ TestingProfile::Builder profile_builder;
+ profile_builder.SetGuestSession();
+ std::unique_ptr<Profile> profile = profile_builder.Build();
+ ASSERT_TRUE(profile->IsGuestSession());
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get()));
+ EXPECT_EQ(
+ signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(profile.get()));
+ EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile(
+ profile.get()));
+ }
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(ENABLE_MIRROR)
+// Mirror is enabled by default on Chrome OS, unless specified otherwise.
+TEST(AccountConsistencyModeManagerTest, MirrorEnabledByDefault) {
+ // Creation of this object sets the current thread's id as UI thread.
+ content::BrowserTaskEnvironment task_environment;
+
+ TestingProfile profile;
+ EXPECT_TRUE(
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile));
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kMirror,
+ AccountConsistencyModeManager::GetMethodForProfile(&profile));
+}
+
+TEST(AccountConsistencyModeManagerTest, MirrorDisabledForGuestSession) {
+ // Creation of this object sets the current thread's id as UI thread.
+ content::BrowserTaskEnvironment task_environment;
+
+ TestingProfile profile;
+ profile.SetGuestSession(true);
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile));
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(&profile));
+}
+
+TEST(AccountConsistencyModeManagerTest, MirrorDisabledForOffTheRecordProfile) {
+ // Creation of this object sets the current thread's id as UI thread.
+ content::BrowserTaskEnvironment task_environment;
+
+ TestingProfile profile;
+ Profile* incognito_profile =
+ profile.GetPrimaryOTRProfile(/*create_if_needed=*/true);
+ EXPECT_FALSE(AccountConsistencyModeManager::IsMirrorEnabledForProfile(
+ incognito_profile));
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ incognito_profile));
+ EXPECT_EQ(
+ signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(incognito_profile));
+
+ Profile* otr_profile = profile.GetOffTheRecordProfile(
+ Profile::OTRProfileID::CreateUniqueForTesting(),
+ /*create_if_needed=*/true);
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(otr_profile));
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(otr_profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(otr_profile));
+}
+
+#endif // BUILDFLAG(ENABLE_MIRROR)
diff --git a/chromium/chrome/browser/signin/account_id_from_account_info.cc b/chromium/chrome/browser/signin/account_id_from_account_info.cc
new file mode 100644
index 00000000000..c801c7ac983
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_id_from_account_info.cc
@@ -0,0 +1,24 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/account_id_from_account_info.h"
+#include "build/chromeos_buildflags.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "components/user_manager/known_user.h"
+#endif
+
+AccountId AccountIdFromAccountInfo(const CoreAccountInfo& account_info) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ return user_manager::known_user::GetAccountId(
+ account_info.email, account_info.gaia, AccountType::GOOGLE);
+#else
+ if (account_info.email.empty() || account_info.gaia.empty())
+ return EmptyAccountId();
+
+ return AccountId::FromUserEmailGaiaId(
+ gaia::CanonicalizeEmail(account_info.email), account_info.gaia);
+#endif
+}
diff --git a/chromium/chrome/browser/signin/account_id_from_account_info.h b/chromium/chrome/browser/signin/account_id_from_account_info.h
new file mode 100644
index 00000000000..cdd6836878d
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_id_from_account_info.h
@@ -0,0 +1,18 @@
+// Copyright 2019 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_
+
+#include "components/account_id/account_id.h"
+#include "components/signin/public/identity_manager/account_info.h"
+
+// Returns AccountID populated from |account_info|.
+// NOTE: This utility is in //chrome rather than being part of
+// //components/signin/public because it is only //chrome that needs to go back
+// and forth between AccountId and AccountInfo, and it is outside the scope of
+// //components/signin/public to have knowledge about AccountId.
+AccountId AccountIdFromAccountInfo(const CoreAccountInfo& account_info);
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_
diff --git a/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc b/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc
new file mode 100644
index 00000000000..356e3385e88
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc
@@ -0,0 +1,20 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/account_id_from_account_info.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class AccountIdFromAccountInfoTest : public testing::Test {};
+
+// Tests that AccountIdFromAccountInfo() passes along a canonicalized email to
+// AccountId.
+TEST(AccountIdFromAccountInfoTest,
+ AccountIdFromAccountInfo_CanonicalizesRawEmail) {
+ AccountInfo info;
+ info.email = "test.email@gmail.com";
+ info.gaia = "test_id";
+
+ EXPECT_EQ("testemail@gmail.com",
+ AccountIdFromAccountInfo(info).GetUserEmail());
+}
diff --git a/chromium/chrome/browser/signin/account_investigator_factory.cc b/chromium/chrome/browser/signin/account_investigator_factory.cc
new file mode 100644
index 00000000000..731c8b45a8b
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_investigator_factory.cc
@@ -0,0 +1,57 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/account_investigator_factory.h"
+
+#include "base/memory/singleton.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service_factory.h"
+#include "components/signin/core/browser/account_investigator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+// static
+AccountInvestigatorFactory* AccountInvestigatorFactory::GetInstance() {
+ return base::Singleton<AccountInvestigatorFactory>::get();
+}
+
+// static
+AccountInvestigator* AccountInvestigatorFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<AccountInvestigator*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+AccountInvestigatorFactory::AccountInvestigatorFactory()
+ : BrowserContextKeyedServiceFactory(
+ "AccountInvestigator",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+AccountInvestigatorFactory::~AccountInvestigatorFactory() {}
+
+KeyedService* AccountInvestigatorFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile(Profile::FromBrowserContext(context));
+ AccountInvestigator* investigator = new AccountInvestigator(
+ profile->GetPrefs(), IdentityManagerFactory::GetForProfile(profile));
+ investigator->Initialize();
+ return investigator;
+}
+
+void AccountInvestigatorFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ AccountInvestigator::RegisterPrefs(registry);
+}
+
+bool AccountInvestigatorFactory::ServiceIsCreatedWithBrowserContext() const {
+ return true;
+}
+
+bool AccountInvestigatorFactory::ServiceIsNULLWhileTesting() const {
+ return true;
+}
diff --git a/chromium/chrome/browser/signin/account_investigator_factory.h b/chromium/chrome/browser/signin/account_investigator_factory.h
new file mode 100644
index 00000000000..6f1d56caec5
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_investigator_factory.h
@@ -0,0 +1,44 @@
+// Copyright 2016 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_
+
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class Profile;
+class AccountInvestigator;
+
+namespace base {
+template <typename T>
+struct DefaultSingletonTraits;
+} // namespace base
+
+// Factory for BrowserKeyedService AccountInvestigator.
+class AccountInvestigatorFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static AccountInvestigator* GetForProfile(Profile* profile);
+
+ static AccountInvestigatorFactory* GetInstance();
+
+ AccountInvestigatorFactory(const AccountInvestigatorFactory&) = delete;
+ AccountInvestigatorFactory& operator=(const AccountInvestigatorFactory&) =
+ delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<AccountInvestigatorFactory>;
+
+ AccountInvestigatorFactory();
+ ~AccountInvestigatorFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+ bool ServiceIsCreatedWithBrowserContext() const override;
+ bool ServiceIsNULLWhileTesting() const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/account_reconcilor_factory.cc b/chromium/chrome/browser/signin/account_reconcilor_factory.cc
new file mode 100644
index 00000000000..9b47faae104
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_reconcilor_factory.cc
@@ -0,0 +1,212 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/check.h"
+#include "base/feature_list.h"
+#include "base/notreached.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/account_reconcilor_delegate.h"
+#include "components/signin/core/browser/mirror_account_reconcilor_delegate.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_buildflags.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "base/metrics/histogram_macros.h"
+#include "base/time/time.h"
+#include "chrome/browser/ash/account_manager/account_manager_util.h"
+#include "chrome/browser/lifetime/application_lifetime.h"
+#include "chromeos/tpm/install_attributes.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/active_directory_account_reconcilor_delegate.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/user_manager/user_manager.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "components/signin/core/browser/dice_account_reconcilor_delegate.h"
+#endif
+
+namespace {
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+class ChromeOSLimitedAccessAccountReconcilorDelegate
+ : public signin::MirrorAccountReconcilorDelegate {
+ public:
+ enum class ReconcilorBehavior {
+ kChild,
+ kEnterprise,
+ };
+
+ ChromeOSLimitedAccessAccountReconcilorDelegate(
+ ReconcilorBehavior reconcilor_behavior,
+ signin::IdentityManager* identity_manager)
+ : signin::MirrorAccountReconcilorDelegate(identity_manager),
+ reconcilor_behavior_(reconcilor_behavior) {}
+
+ ChromeOSLimitedAccessAccountReconcilorDelegate(
+ const ChromeOSLimitedAccessAccountReconcilorDelegate&) = delete;
+ ChromeOSLimitedAccessAccountReconcilorDelegate& operator=(
+ const ChromeOSLimitedAccessAccountReconcilorDelegate&) = delete;
+
+ base::TimeDelta GetReconcileTimeout() const override {
+ switch (reconcilor_behavior_) {
+ case ReconcilorBehavior::kChild:
+ return base::Seconds(10);
+ case ReconcilorBehavior::kEnterprise:
+ // 60 seconds is enough to cover about 99% of all reconcile cases.
+ return base::Seconds(60);
+ default:
+ NOTREACHED();
+ return MirrorAccountReconcilorDelegate::GetReconcileTimeout();
+ }
+ }
+
+ void OnReconcileError(const GoogleServiceAuthError& error) override {
+ // If |error| is |GoogleServiceAuthError::State::NONE| or a transient error.
+ if (!error.IsPersistentError()) {
+ return;
+ }
+
+ if (!GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSignin))) {
+ return;
+ }
+
+ // Mark the account to require an online sign in.
+ const user_manager::User* primary_user =
+ user_manager::UserManager::Get()->GetPrimaryUser();
+ DCHECK(primary_user);
+ user_manager::UserManager::Get()->SaveForceOnlineSignin(
+ primary_user->GetAccountId(), true /* force_online_signin */);
+
+ if (reconcilor_behavior_ == ReconcilorBehavior::kChild) {
+ UMA_HISTOGRAM_BOOLEAN(
+ "ChildAccountReconcilor.ForcedUserExitOnReconcileError", true);
+ }
+ // Force a logout.
+ chrome::AttemptUserExit();
+ }
+
+ private:
+ const ReconcilorBehavior reconcilor_behavior_;
+};
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+
+} // namespace
+
+AccountReconcilorFactory::AccountReconcilorFactory()
+ : BrowserContextKeyedServiceFactory(
+ "AccountReconcilor",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(ChromeSigninClientFactory::GetInstance());
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+AccountReconcilorFactory::~AccountReconcilorFactory() {}
+
+// static
+AccountReconcilor* AccountReconcilorFactory::GetForProfile(Profile* profile) {
+ return static_cast<AccountReconcilor*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+AccountReconcilorFactory* AccountReconcilorFactory::GetInstance() {
+ return base::Singleton<AccountReconcilorFactory>::get();
+}
+
+KeyedService* AccountReconcilorFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ SigninClient* signin_client =
+ ChromeSigninClientFactory::GetForProfile(profile);
+ AccountReconcilor* reconcilor =
+ new AccountReconcilor(identity_manager, signin_client,
+ CreateAccountReconcilorDelegate(profile));
+ reconcilor->Initialize(true /* start_reconcile_if_tokens_available */);
+ return reconcilor;
+}
+
+void AccountReconcilorFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ registry->RegisterBooleanPref(prefs::kForceLogoutUnauthenticatedUserEnabled,
+ false);
+#endif
+}
+
+// static
+std::unique_ptr<signin::AccountReconcilorDelegate>
+AccountReconcilorFactory::CreateAccountReconcilorDelegate(Profile* profile) {
+ signin::AccountConsistencyMethod account_consistency =
+ AccountConsistencyModeManager::GetMethodForProfile(profile);
+ switch (account_consistency) {
+ case signin::AccountConsistencyMethod::kMirror:
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // Only for child accounts on Chrome OS, use the specialized Mirror
+ // delegate.
+ if (profile->IsChild()) {
+ return std::make_unique<ChromeOSLimitedAccessAccountReconcilorDelegate>(
+ ChromeOSLimitedAccessAccountReconcilorDelegate::ReconcilorBehavior::
+ kChild,
+ IdentityManagerFactory::GetForProfile(profile));
+ }
+
+ // Only for Active Directory accounts on Chrome OS.
+ // TODO(https://crbug.com/993317): Remove the check for
+ // |IsAccountManagerAvailable| after fixing https://crbug.com/1008349 and
+ // https://crbug.com/993317.
+ if (ash::IsAccountManagerAvailable(profile) &&
+ chromeos::InstallAttributes::Get()->IsActiveDirectoryManaged()) {
+ return std::make_unique<
+ signin::ActiveDirectoryAccountReconcilorDelegate>();
+ }
+
+ if (profile->GetPrefs()->GetBoolean(
+ prefs::kForceLogoutUnauthenticatedUserEnabled)) {
+ return std::make_unique<ChromeOSLimitedAccessAccountReconcilorDelegate>(
+ ChromeOSLimitedAccessAccountReconcilorDelegate::ReconcilorBehavior::
+ kEnterprise,
+ IdentityManagerFactory::GetForProfile(profile));
+ }
+
+ return std::make_unique<signin::MirrorAccountReconcilorDelegate>(
+ IdentityManagerFactory::GetForProfile(profile));
+#else
+ return std::make_unique<signin::MirrorAccountReconcilorDelegate>(
+ IdentityManagerFactory::GetForProfile(profile));
+#endif
+
+ case signin::AccountConsistencyMethod::kDisabled:
+ return std::make_unique<signin::AccountReconcilorDelegate>();
+
+ case signin::AccountConsistencyMethod::kDice:
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ return std::make_unique<signin::DiceAccountReconcilorDelegate>();
+#else
+ NOTREACHED();
+ return nullptr;
+#endif
+ }
+
+ NOTREACHED();
+ return nullptr;
+}
diff --git a/chromium/chrome/browser/signin/account_reconcilor_factory.h b/chromium/chrome/browser/signin/account_reconcilor_factory.h
new file mode 100644
index 00000000000..1358fa13cac
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_reconcilor_factory.h
@@ -0,0 +1,57 @@
+// Copyright 2013 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_
+
+#include <memory>
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace signin {
+class IdentityManager;
+}
+
+namespace signin {
+class AccountReconcilorDelegate;
+}
+
+class AccountReconcilor;
+class Profile;
+class SigninClient;
+
+// Singleton that owns all AccountReconcilors and associates them with
+// Profiles. Listens for the Profile's destruction notification and cleans up.
+class AccountReconcilorFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of AccountReconcilor associated with this profile
+ // (creating one if none exists). Returns NULL if this profile cannot have an
+ // AccountReconcilor (for example, if |profile| is incognito).
+ static AccountReconcilor* GetForProfile(Profile* profile);
+
+ // Returns an instance of the factory singleton.
+ static AccountReconcilorFactory* GetInstance();
+
+ // BrowserContextKeyedServiceFactory:
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+
+ private:
+ friend struct base::DefaultSingletonTraits<AccountReconcilorFactory>;
+ friend class DummyAccountReconcilorWithDelegate; // For testing.
+
+ AccountReconcilorFactory();
+ ~AccountReconcilorFactory() override;
+
+ // Creates the AccountReconcilorDelegate.
+ static std::unique_ptr<signin::AccountReconcilorDelegate>
+ CreateAccountReconcilorDelegate(Profile* profile);
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper.cc b/chromium/chrome/browser/signin/chrome_device_id_helper.cc
new file mode 100644
index 00000000000..7b28246d24a
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_device_id_helper.cc
@@ -0,0 +1,87 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_device_id_helper.h"
+
+#include <string>
+
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/signin/public/base/device_id_helper.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "base/command_line.h"
+#include "base/guid.h"
+#include "base/logging.h"
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/base/signin_switches.h"
+#include "components/user_manager/known_user.h"
+#include "components/user_manager/user_manager.h"
+#endif
+
+std::string GetSigninScopedDeviceIdForProfile(Profile* profile) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (base::CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kDisableSigninScopedDeviceId)) {
+ return std::string();
+ }
+
+ // UserManager may not exist in unit_tests.
+ if (!user_manager::UserManager::IsInitialized())
+ return std::string();
+
+ const user_manager::User* user =
+ chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
+ if (!user)
+ return std::string();
+
+ const std::string signin_scoped_device_id =
+ user_manager::known_user::GetDeviceId(user->GetAccountId());
+ LOG_IF(ERROR, signin_scoped_device_id.empty())
+ << "Device ID is not set for user.";
+ return signin_scoped_device_id;
+#else
+ return signin::GetSigninScopedDeviceId(profile->GetPrefs());
+#endif
+}
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+
+std::string GenerateSigninScopedDeviceId(bool for_ephemeral) {
+ constexpr char kEphemeralUserDeviceIDPrefix[] = "t_";
+ std::string guid = base::GenerateGUID();
+ return for_ephemeral ? kEphemeralUserDeviceIDPrefix + guid : guid;
+}
+
+void MigrateSigninScopedDeviceId(Profile* profile) {
+ // UserManager may not exist in unit_tests.
+ if (!user_manager::UserManager::IsInitialized())
+ return;
+
+ const user_manager::User* user =
+ chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
+ if (!user)
+ return;
+ const AccountId account_id = user->GetAccountId();
+ if (user_manager::known_user::GetDeviceId(account_id).empty()) {
+ const std::string legacy_device_id = profile->GetPrefs()->GetString(
+ prefs::kGoogleServicesSigninScopedDeviceId);
+ if (!legacy_device_id.empty()) {
+ // Need to move device ID from the old location to the new one, if it has
+ // not been done yet.
+ user_manager::known_user::SetDeviceId(account_id, legacy_device_id);
+ } else {
+ user_manager::known_user::SetDeviceId(
+ account_id, GenerateSigninScopedDeviceId(
+ user_manager::UserManager::Get()
+ ->IsUserNonCryptohomeDataEphemeral(account_id)));
+ }
+ }
+ profile->GetPrefs()->SetString(prefs::kGoogleServicesSigninScopedDeviceId,
+ std::string());
+}
+
+#endif
diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper.h b/chromium/chrome/browser/signin/chrome_device_id_helper.h
new file mode 100644
index 00000000000..9e84d416528
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_device_id_helper.h
@@ -0,0 +1,36 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_
+
+#include <string>
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+
+class Profile;
+
+// Returns the device ID that is scoped to single signin.
+// All refresh tokens for |profile| are annotated with this device ID when they
+// are requested.
+// On non-ChromeOS platforms, this is equivalent to:
+// signin::GetSigninScopedDeviceId(profile->GetPrefs());
+std::string GetSigninScopedDeviceIdForProfile(Profile* profile);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+
+// Helper method. The device ID should generally be obtained through
+// GetSigninScopedDeviceIdForProfile().
+// If |for_ephemeral| is true, special kind of device ID for ephemeral users is
+// generated.
+std::string GenerateSigninScopedDeviceId(bool for_ephemeral);
+
+// Moves any existing device ID out of the pref service into the UserManager,
+// and creates a new ID if it is empty.
+void MigrateSigninScopedDeviceId(Profile* profile);
+
+#endif
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_
diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc b/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc
new file mode 100644
index 00000000000..3e2f2e9517b
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc
@@ -0,0 +1,40 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_device_id_helper.h"
+
+#include <string>
+
+#include "base/strings/string_util.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+const char kEpehemeralPrefix[] = "t_";
+
+TEST(DeviceIdHelper, NotEphemeral) {
+ std::string device_id =
+ GenerateSigninScopedDeviceId(false /* for_ephemeral */);
+ // Not empty.
+ EXPECT_FALSE(device_id.empty());
+ // No ephemeral prefix.
+ EXPECT_FALSE(base::StartsWith(device_id, kEpehemeralPrefix,
+ base::CompareCase::SENSITIVE));
+ // ID is unique.
+ EXPECT_NE(device_id, GenerateSigninScopedDeviceId(false /* for_ephemeral */));
+}
+
+TEST(DeviceIdHelper, Ephemeral) {
+ std::string device_id =
+ GenerateSigninScopedDeviceId(true /* for_ephemeral */);
+ // Ephemeral prefix.
+ EXPECT_TRUE(base::StartsWith(device_id, kEpehemeralPrefix,
+ base::CompareCase::SENSITIVE));
+ // Not empty.
+ EXPECT_NE(device_id, kEpehemeralPrefix);
+ // ID is unique.
+ EXPECT_NE(device_id, GenerateSigninScopedDeviceId(true /* for_ephemeral */));
+}
+#endif
diff --git a/chromium/chrome/browser/signin/chrome_signin_client.cc b/chromium/chrome/browser/signin/chrome_signin_client.cc
new file mode 100644
index 00000000000..3a5ea106adb
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client.cc
@@ -0,0 +1,369 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_client.h"
+
+#include <stddef.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/content_settings/cookie_settings_factory.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_metrics.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/chrome_device_id_helper.h"
+#include "chrome/browser/signin/force_signin_verifier.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/common/buildflags.h"
+#include "chrome/common/channel_info.h"
+#include "chrome/common/pref_names.h"
+#include "components/content_settings/core/browser/cookie_settings.h"
+#include "components/metrics/metrics_service.h"
+#include "components/policy/core/browser/browser_policy_connector.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/cookie_settings_util.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/access_token_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/scope_set.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "google_apis/gaia/gaia_constants.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "url/gurl.h"
+
+#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
+#include "chrome/browser/supervised_user/supervised_user_constants.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/net/delay_network_call.h"
+#include "chromeos/network/network_handler.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "chrome/browser/lacros/account_manager/account_manager_util.h"
+#include "chromeos/crosapi/mojom/account_manager.mojom.h"
+#include "chromeos/lacros/lacros_service.h"
+#include "components/account_manager_core/account.h"
+#include "components/account_manager_core/account_manager_util.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#endif
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/profiles/profile_window.h"
+#endif
+
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/profile_picker.h"
+#endif
+
+namespace {
+
+// List of sources for which sign out is always allowed.
+signin_metrics::ProfileSignout kAlwaysAllowedSignoutSources[] = {
+ // Allowed, because data has not been synced yet.
+ signin_metrics::ProfileSignout::ABORT_SIGNIN,
+ // Allowed, because only used on Android and the primary account must be
+ // cleared when the account is removed from device
+ signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE,
+ // Allowed to force finish the account id migration.
+ signin_metrics::ACCOUNT_ID_MIGRATION,
+ // Allowed, for tests.
+ signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST};
+
+SigninClient::SignoutDecision IsSignoutAllowed(
+ Profile* profile,
+ const signin_metrics::ProfileSignout signout_source) {
+ if (signin_util::IsUserSignoutAllowedForProfile(profile))
+ return SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+
+ auto* identity_manager =
+ IdentityManagerFactory::GetForProfileIfExists(profile);
+ if (identity_manager &&
+ !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ return SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ }
+
+ for (const auto& always_allowed_source : kAlwaysAllowedSignoutSources) {
+ if (signout_source == always_allowed_source)
+ return SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ }
+
+ return SigninClient::SignoutDecision::DISALLOW_SIGNOUT;
+}
+
+} // namespace
+
+ChromeSigninClient::ChromeSigninClient(Profile* profile) : profile_(profile) {
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
+#endif
+}
+
+ChromeSigninClient::~ChromeSigninClient() {
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);
+#endif
+}
+
+void ChromeSigninClient::DoFinalInit() {
+ VerifySyncToken();
+}
+
+// static
+bool ChromeSigninClient::ProfileAllowsSigninCookies(Profile* profile) {
+ content_settings::CookieSettings* cookie_settings =
+ CookieSettingsFactory::GetForProfile(profile).get();
+ return signin::SettingsAllowSigninCookies(cookie_settings);
+}
+
+PrefService* ChromeSigninClient::GetPrefs() { return profile_->GetPrefs(); }
+
+scoped_refptr<network::SharedURLLoaderFactory>
+ChromeSigninClient::GetURLLoaderFactory() {
+ if (url_loader_factory_for_testing_)
+ return url_loader_factory_for_testing_;
+
+ return profile_->GetDefaultStoragePartition()
+ ->GetURLLoaderFactoryForBrowserProcess();
+}
+
+network::mojom::CookieManager* ChromeSigninClient::GetCookieManager() {
+ return profile_->GetDefaultStoragePartition()
+ ->GetCookieManagerForBrowserProcess();
+}
+
+bool ChromeSigninClient::AreSigninCookiesAllowed() {
+ return ProfileAllowsSigninCookies(profile_);
+}
+
+bool ChromeSigninClient::AreSigninCookiesDeletedOnExit() {
+ content_settings::CookieSettings* cookie_settings =
+ CookieSettingsFactory::GetForProfile(profile_).get();
+ return signin::SettingsDeleteSigninCookiesOnExit(cookie_settings);
+}
+
+void ChromeSigninClient::AddContentSettingsObserver(
+ content_settings::Observer* observer) {
+ HostContentSettingsMapFactory::GetForProfile(profile_)
+ ->AddObserver(observer);
+}
+
+void ChromeSigninClient::RemoveContentSettingsObserver(
+ content_settings::Observer* observer) {
+ HostContentSettingsMapFactory::GetForProfile(profile_)
+ ->RemoveObserver(observer);
+}
+
+void ChromeSigninClient::PreSignOut(
+ base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached,
+ signin_metrics::ProfileSignout signout_source_metric) {
+ DCHECK(on_signout_decision_reached);
+ DCHECK(!on_signout_decision_reached_) << "SignOut already in-progress!";
+ on_signout_decision_reached_ = std::move(on_signout_decision_reached);
+
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ // `signout_source_metric` is `signin_metrics::ABORT_SIGNIN` if the user
+ // declines sync in the signin process. In case the user accepts the managed
+ // account but declines sync, we should keep the window open.
+ bool user_declines_sync_after_consenting_to_management =
+ signout_source_metric == signin_metrics::ABORT_SIGNIN &&
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile_);
+ // These sign out won't remove the policy cache, keep the window opened.
+ bool keep_window_opened =
+ signout_source_metric ==
+ signin_metrics::GOOGLE_SERVICE_NAME_PATTERN_CHANGED ||
+ signout_source_metric == signin_metrics::SERVER_FORCED_DISABLE ||
+ signout_source_metric == signin_metrics::SIGNOUT_PREF_CHANGED ||
+ user_declines_sync_after_consenting_to_management;
+ if (signin_util::IsForceSigninEnabled() && !profile_->IsSystemProfile() &&
+ !profile_->IsGuestSession() && !profile_->IsChild() &&
+ !keep_window_opened) {
+ if (signout_source_metric ==
+ signin_metrics::SIGNIN_PREF_CHANGED_DURING_SIGNIN) {
+ // SIGNIN_PREF_CHANGED_DURING_SIGNIN will be triggered when
+ // IdentityManager is initialized before window opening, there is no need
+ // to close window. Call OnCloseBrowsersSuccess to continue sign out and
+ // show UserManager afterwards.
+ should_display_user_manager_ = false; // Don't show UserManager twice.
+ OnCloseBrowsersSuccess(signout_source_metric, profile_->GetPath());
+ } else {
+ BrowserList::CloseAllBrowsersWithProfile(
+ profile_,
+ base::BindRepeating(&ChromeSigninClient::OnCloseBrowsersSuccess,
+ base::Unretained(this), signout_source_metric),
+ base::BindRepeating(&ChromeSigninClient::OnCloseBrowsersAborted,
+ base::Unretained(this)),
+ signout_source_metric == signin_metrics::ABORT_SIGNIN ||
+ signout_source_metric ==
+ signin_metrics::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN ||
+ signout_source_metric == signin_metrics::TRANSFER_CREDENTIALS);
+ }
+ } else {
+#else
+ {
+#endif
+ std::move(on_signout_decision_reached_)
+ .Run(IsSignoutAllowed(profile_, signout_source_metric));
+ }
+}
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+void ChromeSigninClient::OnConnectionChanged(
+ network::mojom::ConnectionType type) {
+ if (type == network::mojom::ConnectionType::CONNECTION_NONE)
+ return;
+
+ for (base::OnceClosure& callback : delayed_callbacks_)
+ std::move(callback).Run();
+
+ delayed_callbacks_.clear();
+}
+#endif
+
+void ChromeSigninClient::DelayNetworkCall(base::OnceClosure callback) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // Do not make network requests in unit tests. chromeos::NetworkHandler should
+ // not be used and is not expected to have been initialized in unit tests.
+ if (url_loader_factory_for_testing_ &&
+ !chromeos::NetworkHandler::IsInitialized()) {
+ std::move(callback).Run();
+ return;
+ }
+ chromeos::DelayNetworkCall(
+ base::Milliseconds(chromeos::kDefaultNetworkRetryDelayMS),
+ std::move(callback));
+ return;
+#else
+ // Don't bother if we don't have any kind of network connection.
+ network::mojom::ConnectionType type;
+ bool sync = content::GetNetworkConnectionTracker()->GetConnectionType(
+ &type, base::BindOnce(&ChromeSigninClient::OnConnectionChanged,
+ weak_ptr_factory_.GetWeakPtr()));
+ if (!sync || type == network::mojom::ConnectionType::CONNECTION_NONE) {
+ // Connection type cannot be retrieved synchronously so delay the callback.
+ delayed_callbacks_.push_back(std::move(callback));
+ } else {
+ std::move(callback).Run();
+ }
+#endif
+}
+
+std::unique_ptr<GaiaAuthFetcher> ChromeSigninClient::CreateGaiaAuthFetcher(
+ GaiaAuthConsumer* consumer,
+ gaia::GaiaSource source) {
+ return std::make_unique<GaiaAuthFetcher>(consumer, source,
+ GetURLLoaderFactory());
+}
+
+void ChromeSigninClient::VerifySyncToken() {
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ // We only verifiy the token once when Profile is just created.
+ if (signin_util::IsForceSigninEnabled() && !force_signin_verifier_)
+ force_signin_verifier_ = std::make_unique<ForceSigninVerifier>(
+ profile_, IdentityManagerFactory::GetForProfile(profile_));
+#endif
+}
+
+bool ChromeSigninClient::IsNonEnterpriseUser(const std::string& username) {
+ return policy::BrowserPolicyConnector::IsNonEnterpriseUser(username);
+}
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+// Returns the account that must be auto-signed-in to the Main Profile in
+// Lacros.
+// This is, when available, the account used to sign into the Chrome OS
+// session. This may be a Gaia account or a Microsoft Active Directory
+// account. This field will be null for Guest sessions, Managed Guest
+// sessions, Demo mode, and Kiosks. Note that this is different from the
+// concept of a Primary Account in the browser. A user may not be signed into
+// a Lacros browser Profile, or may be signed into a browser Profile with an
+// account which is different from the account which they used to sign into
+// the device - aka Device Account.
+// Also note that this will be null for Secondary / non-Main Profiles in
+// Lacros, because they do not start with the Chrome OS Device Account
+// signed-in by default.
+absl::optional<account_manager::Account>
+ChromeSigninClient::GetInitialPrimaryAccount() {
+ if (!profile_->IsMainProfile())
+ return absl::nullopt;
+
+ const crosapi::mojom::AccountPtr& device_account =
+ chromeos::LacrosService::Get()->init_params()->device_account;
+ if (!device_account)
+ return absl::nullopt;
+
+ return account_manager::FromMojoAccount(device_account);
+}
+#endif
+
+void ChromeSigninClient::SetURLLoaderFactoryForTest(
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
+ url_loader_factory_for_testing_ = url_loader_factory;
+}
+
+void ChromeSigninClient::OnCloseBrowsersSuccess(
+ const signin_metrics::ProfileSignout signout_source_metric,
+ const base::FilePath& profile_path) {
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ if (signin_util::IsForceSigninEnabled() && force_signin_verifier_.get()) {
+ force_signin_verifier_->Cancel();
+ }
+#endif
+
+ std::move(on_signout_decision_reached_)
+ .Run(IsSignoutAllowed(profile_, signout_source_metric));
+
+ LockForceSigninProfile(profile_path);
+ // After sign out, lock the profile and show UserManager if necessary.
+ if (should_display_user_manager_) {
+ ShowUserManager(profile_path);
+ } else {
+ should_display_user_manager_ = true;
+ }
+}
+
+void ChromeSigninClient::OnCloseBrowsersAborted(
+ const base::FilePath& profile_path) {
+ should_display_user_manager_ = true;
+
+ // Disallow sign-out (aborted).
+ std::move(on_signout_decision_reached_)
+ .Run(SignoutDecision::DISALLOW_SIGNOUT);
+}
+
+void ChromeSigninClient::LockForceSigninProfile(
+ const base::FilePath& profile_path) {
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile_->GetPath());
+ if (!entry)
+ return;
+ entry->LockForceSigninProfile(true);
+}
+
+void ChromeSigninClient::ShowUserManager(const base::FilePath& profile_path) {
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ ProfilePicker::Show(ProfilePicker::EntryPoint::kProfileLocked);
+#endif
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_client.h b/chromium/chrome/browser/signin/chrome_signin_client.h
new file mode 100644
index 00000000000..cef8fd3c32e
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client.h
@@ -0,0 +1,115 @@
+// Copyright 2014 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_
+
+#include <list>
+#include <memory>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "components/signin/public/base/signin_client.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/mojom/network_change_manager.mojom-forward.h"
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+#include "services/network/public/cpp/network_connection_tracker.h"
+#endif
+
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+class ForceSigninVerifier;
+#endif
+class Profile;
+
+class ChromeSigninClient
+ : public SigninClient
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ ,
+ public network::NetworkConnectionTracker::NetworkConnectionObserver
+#endif
+{
+ public:
+ explicit ChromeSigninClient(Profile* profile);
+
+ ChromeSigninClient(const ChromeSigninClient&) = delete;
+ ChromeSigninClient& operator=(const ChromeSigninClient&) = delete;
+
+ ~ChromeSigninClient() override;
+
+ void DoFinalInit() override;
+
+ // Utility method.
+ static bool ProfileAllowsSigninCookies(Profile* profile);
+
+ // SigninClient implementation.
+ PrefService* GetPrefs() override;
+ void PreSignOut(
+ base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached,
+ signin_metrics::ProfileSignout signout_source_metric) override;
+ scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory() override;
+ network::mojom::CookieManager* GetCookieManager() override;
+ bool AreSigninCookiesAllowed() override;
+ bool AreSigninCookiesDeletedOnExit() override;
+ void AddContentSettingsObserver(
+ content_settings::Observer* observer) override;
+ void RemoveContentSettingsObserver(
+ content_settings::Observer* observer) override;
+ void DelayNetworkCall(base::OnceClosure callback) override;
+ std::unique_ptr<GaiaAuthFetcher> CreateGaiaAuthFetcher(
+ GaiaAuthConsumer* consumer,
+ gaia::GaiaSource source) override;
+ bool IsNonEnterpriseUser(const std::string& username) override;
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ // network::NetworkConnectionTracker::NetworkConnectionObserver
+ // implementation.
+ void OnConnectionChanged(network::mojom::ConnectionType type) override;
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ absl::optional<account_manager::Account> GetInitialPrimaryAccount() override;
+#endif
+
+ // Used in tests to override the URLLoaderFactory returned by
+ // GetURLLoaderFactory().
+ void SetURLLoaderFactoryForTest(
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
+
+ protected:
+ virtual void ShowUserManager(const base::FilePath& profile_path);
+ virtual void LockForceSigninProfile(const base::FilePath& profile_path);
+
+ private:
+ void VerifySyncToken();
+ void OnCloseBrowsersSuccess(
+ const signin_metrics::ProfileSignout signout_source_metric,
+ const base::FilePath& profile_path);
+ void OnCloseBrowsersAborted(const base::FilePath& profile_path);
+
+ raw_ptr<Profile> profile_;
+
+ // Stored callback from PreSignOut();
+ base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached_;
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ std::list<base::OnceClosure> delayed_callbacks_;
+#endif
+
+ bool should_display_user_manager_ = true;
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ std::unique_ptr<ForceSigninVerifier> force_signin_verifier_;
+#endif
+
+ scoped_refptr<network::SharedURLLoaderFactory>
+ url_loader_factory_for_testing_;
+
+ base::WeakPtrFactory<ChromeSigninClient> weak_ptr_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_factory.cc b/chromium/chrome/browser/signin/chrome_signin_client_factory.cc
new file mode 100644
index 00000000000..62c0d638cbd
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_factory.cc
@@ -0,0 +1,34 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+
+#include "chrome/browser/net/profile_network_context_service_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+ChromeSigninClientFactory::ChromeSigninClientFactory()
+ : BrowserContextKeyedServiceFactory(
+ "ChromeSigninClient",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(ProfileNetworkContextServiceFactory::GetInstance());
+}
+
+ChromeSigninClientFactory::~ChromeSigninClientFactory() {}
+
+// static
+SigninClient* ChromeSigninClientFactory::GetForProfile(Profile* profile) {
+ return static_cast<SigninClient*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+ChromeSigninClientFactory* ChromeSigninClientFactory::GetInstance() {
+ return base::Singleton<ChromeSigninClientFactory>::get();
+}
+
+KeyedService* ChromeSigninClientFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ return new ChromeSigninClient(Profile::FromBrowserContext(context));
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_factory.h b/chromium/chrome/browser/signin/chrome_signin_client_factory.h
new file mode 100644
index 00000000000..e88d3f23b00
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_factory.h
@@ -0,0 +1,37 @@
+// Copyright 2014 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class Profile;
+
+// Singleton that owns all ChromeSigninClients and associates them with
+// Profiles.
+class ChromeSigninClientFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of SigninClient associated with this profile
+ // (creating one if none exists). Returns NULL if this profile cannot have an
+ // SigninClient (for example, if |profile| is incognito).
+ static SigninClient* GetForProfile(Profile* profile);
+
+ // Returns an instance of the factory singleton.
+ static ChromeSigninClientFactory* GetInstance();
+
+ private:
+ friend struct base::DefaultSingletonTraits<ChromeSigninClientFactory>;
+
+ ChromeSigninClientFactory();
+ ~ChromeSigninClientFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc b/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc
new file mode 100644
index 00000000000..e0412419de1
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc
@@ -0,0 +1,21 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_client_test_util.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/test/test_url_loader_factory.h"
+
+std::unique_ptr<KeyedService> BuildChromeSigninClientWithURLLoader(
+ network::TestURLLoaderFactory* test_url_loader_factory,
+ content::BrowserContext* context) {
+ Profile* profile = Profile::FromBrowserContext(context);
+ auto signin_client = std::make_unique<ChromeSigninClient>(profile);
+ signin_client->SetURLLoaderFactoryForTest(
+ test_url_loader_factory->GetSafeWeakWrapper());
+ return signin_client;
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_test_util.h b/chromium/chrome/browser/signin/chrome_signin_client_test_util.h
new file mode 100644
index 00000000000..5c76977980f
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_test_util.h
@@ -0,0 +1,26 @@
+// Copyright 2016 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_
+
+#include <memory>
+
+class KeyedService;
+
+namespace content {
+class BrowserContext;
+}
+
+namespace network {
+class TestURLLoaderFactory;
+}
+
+// Creates a ChromeSigninClient using the supplied
+// |test_url_loader_factory| and |context|.
+std::unique_ptr<KeyedService> BuildChromeSigninClientWithURLLoader(
+ network::TestURLLoaderFactory* test_url_loader_factory,
+ content::BrowserContext* context);
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc
new file mode 100644
index 00000000000..ed2ac0db988
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc
@@ -0,0 +1,438 @@
+// 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 "chrome/browser/signin/chrome_signin_client.h"
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/cxx17_backports.h"
+#include "base/feature_list.h"
+#include "base/memory/raw_ptr.h"
+#include "base/notreached.h"
+#include "base/run_loop.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/test/browser_task_environment.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/test/base/browser_with_test_window_test.h"
+#endif
+
+// ChromeOS has its own network delay logic.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+
+namespace {
+
+class CallbackTester {
+ public:
+ CallbackTester() : called_(0) {}
+
+ void Increment();
+ void IncrementAndUnblock(base::RunLoop* run_loop);
+ bool WasCalledExactlyOnce();
+
+ private:
+ int called_;
+};
+
+void CallbackTester::Increment() {
+ called_++;
+}
+
+void CallbackTester::IncrementAndUnblock(base::RunLoop* run_loop) {
+ Increment();
+ run_loop->QuitWhenIdle();
+}
+
+bool CallbackTester::WasCalledExactlyOnce() {
+ return called_ == 1;
+}
+
+} // namespace
+
+class ChromeSigninClientTest : public testing::Test {
+ public:
+ ChromeSigninClientTest() {
+ // Create a signed-in profile.
+ TestingProfile::Builder builder;
+ profile_ = builder.Build();
+
+ signin_client_ = ChromeSigninClientFactory::GetForProfile(profile());
+ }
+
+ protected:
+ void SetUpNetworkConnection(bool respond_synchronously,
+ network::mojom::ConnectionType connection_type) {
+ auto* tracker = network::TestNetworkConnectionTracker::GetInstance();
+ tracker->SetRespondSynchronously(respond_synchronously);
+ tracker->SetConnectionType(connection_type);
+ }
+
+ void SetConnectionType(network::mojom::ConnectionType connection_type) {
+ network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
+ connection_type);
+ }
+
+ Profile* profile() { return profile_.get(); }
+ SigninClient* signin_client() { return signin_client_; }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<Profile> profile_;
+ raw_ptr<SigninClient> signin_client_;
+};
+
+TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsImmediatelyWithNetwork) {
+ SetUpNetworkConnection(true, network::mojom::ConnectionType::CONNECTION_3G);
+ CallbackTester tester;
+ signin_client()->DelayNetworkCall(
+ base::BindOnce(&CallbackTester::Increment, base::Unretained(&tester)));
+ ASSERT_TRUE(tester.WasCalledExactlyOnce());
+}
+
+TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsAfterGetConnectionType) {
+ SetUpNetworkConnection(false, network::mojom::ConnectionType::CONNECTION_3G);
+
+ base::RunLoop run_loop;
+ CallbackTester tester;
+ signin_client()->DelayNetworkCall(
+ base::BindOnce(&CallbackTester::IncrementAndUnblock,
+ base::Unretained(&tester), &run_loop));
+ ASSERT_FALSE(tester.WasCalledExactlyOnce());
+ run_loop.Run(); // Wait for IncrementAndUnblock().
+ ASSERT_TRUE(tester.WasCalledExactlyOnce());
+}
+
+TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsAfterNetworkChange) {
+ SetUpNetworkConnection(true, network::mojom::ConnectionType::CONNECTION_NONE);
+
+ base::RunLoop run_loop;
+ CallbackTester tester;
+ signin_client()->DelayNetworkCall(
+ base::BindOnce(&CallbackTester::IncrementAndUnblock,
+ base::Unretained(&tester), &run_loop));
+
+ ASSERT_FALSE(tester.WasCalledExactlyOnce());
+ SetConnectionType(network::mojom::ConnectionType::CONNECTION_3G);
+ run_loop.Run(); // Wait for IncrementAndUnblock().
+ ASSERT_TRUE(tester.WasCalledExactlyOnce());
+}
+
+#if !defined(OS_ANDROID)
+
+class MockChromeSigninClient : public ChromeSigninClient {
+ public:
+ explicit MockChromeSigninClient(Profile* profile)
+ : ChromeSigninClient(profile) {}
+
+ MOCK_METHOD1(ShowUserManager, void(const base::FilePath&));
+ MOCK_METHOD1(LockForceSigninProfile, void(const base::FilePath&));
+
+ MOCK_METHOD3(SignOutCallback,
+ void(signin_metrics::ProfileSignout,
+ signin_metrics::SignoutDelete,
+ SigninClient::SignoutDecision signout_decision));
+};
+
+class ChromeSigninClientSignoutTest : public BrowserWithTestWindowTest {
+ public:
+ ChromeSigninClientSignoutTest() : forced_signin_setter_(true) {}
+ void SetUp() override {
+ BrowserWithTestWindowTest::SetUp();
+ CreateClient(browser()->profile());
+ }
+
+ void TearDown() override {
+ BrowserWithTestWindowTest::TearDown();
+ TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
+ }
+
+ void CreateClient(Profile* profile) {
+ client_ = std::make_unique<MockChromeSigninClient>(profile);
+ }
+
+ void PreSignOut(signin_metrics::ProfileSignout source_metric,
+ signin_metrics::SignoutDelete delete_metric) {
+ client_->PreSignOut(base::BindOnce(&MockChromeSigninClient::SignOutCallback,
+ base::Unretained(client_.get()),
+ source_metric, delete_metric),
+ source_metric);
+ }
+
+ signin_util::ScopedForceSigninSetterForTesting forced_signin_setter_;
+ std::unique_ptr<MockChromeSigninClient> client_;
+};
+
+TEST_F(ChromeSigninClientSignoutTest, SignOut) {
+ signin_metrics::ProfileSignout source_metric =
+ signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+
+ EXPECT_CALL(*client_, ShowUserManager(browser()->profile()->GetPath()))
+ .Times(1);
+ EXPECT_CALL(*client_, LockForceSigninProfile(browser()->profile()->GetPath()))
+ .Times(1);
+ EXPECT_CALL(
+ *client_,
+ SignOutCallback(source_metric, delete_metric,
+ SigninClient::SignoutDecision::ALLOW_SIGNOUT))
+ .Times(1);
+
+ PreSignOut(source_metric, delete_metric);
+}
+
+TEST_F(ChromeSigninClientSignoutTest, SignOutWithoutForceSignin) {
+ signin_util::ScopedForceSigninSetterForTesting signin_setter(false);
+ CreateClient(browser()->profile());
+
+ signin_metrics::ProfileSignout source_metric =
+ signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+
+ EXPECT_CALL(*client_, ShowUserManager(browser()->profile()->GetPath()))
+ .Times(0);
+ EXPECT_CALL(*client_, LockForceSigninProfile(browser()->profile()->GetPath()))
+ .Times(0);
+ EXPECT_CALL(
+ *client_,
+ SignOutCallback(source_metric, delete_metric,
+ SigninClient::SignoutDecision::ALLOW_SIGNOUT))
+ .Times(1);
+ PreSignOut(source_metric, delete_metric);
+}
+
+class ChromeSigninClientSignoutSourceTest
+ : public ::testing::WithParamInterface<signin_metrics::ProfileSignout>,
+ public ChromeSigninClientSignoutTest {
+ protected:
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return &identity_test_env_;
+ }
+
+ private:
+ signin::IdentityTestEnvironment identity_test_env_;
+};
+
+// Returns true if signout can be disallowed by policy for the given source.
+bool IsSignoutDisallowedByPolicy(
+ Profile* profile,
+ signin_metrics::ProfileSignout signout_source) {
+ auto* identity_manager =
+ IdentityManagerFactory::GetForProfileIfExists(profile);
+ if (identity_manager &&
+ !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ return false;
+ }
+
+ switch (signout_source) {
+ // NOTE: SIGNOUT_TEST == SIGNOUT_PREF_CHANGED.
+ case signin_metrics::ProfileSignout::SIGNOUT_PREF_CHANGED:
+ case signin_metrics::ProfileSignout::GOOGLE_SERVICE_NAME_PATTERN_CHANGED:
+ case signin_metrics::ProfileSignout::SIGNIN_PREF_CHANGED_DURING_SIGNIN:
+ case signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS:
+ case signin_metrics::ProfileSignout::SERVER_FORCED_DISABLE:
+ case signin_metrics::ProfileSignout::TRANSFER_CREDENTIALS:
+ case signin_metrics::ProfileSignout::
+ AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN:
+ case signin_metrics::ProfileSignout::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT:
+ case signin_metrics::ProfileSignout::USER_TUNED_OFF_SYNC_FROM_DICE_UI:
+ return true;
+ case signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE:
+ case signin_metrics::ProfileSignout::
+ IOS_ACCOUNT_REMOVED_FROM_DEVICE_AFTER_RESTORE:
+ // TODO(msarda): Add more of the above cases to this "false" branch.
+ // For now only ACCOUNT_REMOVED_FROM_DEVICE is here to preserve the status
+ // quo. Additional internal sources of sign-out will be moved here in a
+ // follow up CL.
+ return false;
+ case signin_metrics::ProfileSignout::ABORT_SIGNIN:
+ // Allow signout because data has not been synced yet.
+ return false;
+ case signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST:
+ // Allow signout for tests that want to force it.
+ return false;
+ case signin_metrics::ProfileSignout::ACCOUNT_ID_MIGRATION:
+ // Allowed to force finish the account id migration.
+ return false;
+ case signin_metrics::ProfileSignout::USER_DELETED_ACCOUNT_COOKIES:
+ case signin_metrics::ProfileSignout::MOBILE_IDENTITY_CONSISTENCY_ROLLBACK:
+ // There's no special-casing for these in ChromeSigninClient, as they only
+ // happen when there's no sync account and policies aren't enforced.
+ // PrimaryAccountManager won't actually invoke PreSignOut in this case,
+ // thus it is fine for ChromeSigninClient to not have any special-casing.
+ return true;
+ case signin_metrics::ProfileSignout::NUM_PROFILE_SIGNOUT_METRICS:
+ NOTREACHED();
+ return false;
+ }
+}
+
+TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutAllowed) {
+ signin_metrics::ProfileSignout signout_source = GetParam();
+
+ TestingProfile::Builder builder;
+ builder.SetGuestSession();
+ std::unique_ptr<TestingProfile> profile = builder.Build();
+
+ CreateClient(profile.get());
+ ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+
+ // Verify IdentityManager gets callback indicating sign-out is always allowed.
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+ EXPECT_CALL(
+ *client_,
+ SignOutCallback(signout_source, delete_metric,
+ SigninClient::SignoutDecision::ALLOW_SIGNOUT))
+ .Times(1);
+
+ PreSignOut(signout_source, delete_metric);
+}
+
+#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \
+ defined(OS_MAC)
+TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutDisallowed) {
+ signin_metrics::ProfileSignout signout_source = GetParam();
+
+ TestingProfile::Builder builder;
+ builder.SetGuestSession();
+ std::unique_ptr<TestingProfile> profile = builder.Build();
+
+ CreateClient(profile.get());
+
+ ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+ signin_util::SetUserSignoutAllowedForProfile(profile.get(), false);
+ ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+
+ // Verify IdentityManager gets callback indicating sign-out is disallowed iff
+ // the source of the sign-out is a user-action.
+ SigninClient::SignoutDecision signout_decision =
+ IsSignoutDisallowedByPolicy(profile.get(), signout_source)
+ ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT
+ : SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+ EXPECT_CALL(*client_,
+ SignOutCallback(signout_source, delete_metric, signout_decision))
+ .Times(1);
+
+ PreSignOut(signout_source, delete_metric);
+}
+
+TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutDisallowedWithSync) {
+ signin_metrics::ProfileSignout signout_source = GetParam();
+
+ TestingProfile::Builder builder;
+ builder.SetGuestSession();
+ std::unique_ptr<TestingProfile> profile = builder.Build();
+
+ CreateClient(profile.get());
+
+ ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+ signin_util::SetUserSignoutAllowedForProfile(profile.get(), false);
+ ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+
+ // Verify IdentityManager gets callback indicating sign-out is disallowed iff
+ // the source of the sign-out is a user-action.
+ SigninClient::SignoutDecision signout_decision =
+ IsSignoutDisallowedByPolicy(profile.get(), signout_source)
+ ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT
+ : SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+ identity_test_env()->MakePrimaryAccountAvailable("bob@example.com",
+ signin::ConsentLevel::kSync);
+ EXPECT_CALL(*client_,
+ SignOutCallback(signout_source, delete_metric, signout_decision))
+ .Times(1);
+
+ PreSignOut(signout_source, delete_metric);
+}
+
+TEST_P(ChromeSigninClientSignoutSourceTest,
+ UserSignoutDisallowedAccountManagementAccepted) {
+ base::test::ScopedFeatureList features(kAccountPoliciesLoadedWithoutSync);
+ signin_metrics::ProfileSignout signout_source = GetParam();
+
+ TestingProfile::Builder builder;
+ builder.SetGuestSession();
+ std::unique_ptr<TestingProfile> profile = builder.Build();
+
+ CreateClient(profile.get());
+
+ ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+ signin_util::SetUserSignoutAllowedForProfile(profile.get(), false);
+ ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+
+ // Verify IdentityManager gets callback indicating sign-out is disallowed iff
+ // the source of the sign-out is a user-action.
+ SigninClient::SignoutDecision signout_decision =
+ IsSignoutDisallowedByPolicy(profile.get(), signout_source)
+ ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT
+ : SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+ EXPECT_CALL(*client_,
+ SignOutCallback(signout_source, delete_metric, signout_decision))
+ .Times(1);
+
+ PreSignOut(signout_source, delete_metric);
+}
+#endif
+
+const signin_metrics::ProfileSignout kSignoutSources[] = {
+ signin_metrics::ProfileSignout::SIGNOUT_PREF_CHANGED,
+ signin_metrics::ProfileSignout::GOOGLE_SERVICE_NAME_PATTERN_CHANGED,
+ signin_metrics::ProfileSignout::SIGNIN_PREF_CHANGED_DURING_SIGNIN,
+ signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS,
+ signin_metrics::ProfileSignout::ABORT_SIGNIN,
+ signin_metrics::ProfileSignout::SERVER_FORCED_DISABLE,
+ signin_metrics::ProfileSignout::TRANSFER_CREDENTIALS,
+ signin_metrics::ProfileSignout::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN,
+ signin_metrics::ProfileSignout::USER_TUNED_OFF_SYNC_FROM_DICE_UI,
+ signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE,
+ signin_metrics::ProfileSignout::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT,
+ signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST,
+ signin_metrics::ProfileSignout::USER_DELETED_ACCOUNT_COOKIES,
+ signin_metrics::ProfileSignout::MOBILE_IDENTITY_CONSISTENCY_ROLLBACK,
+ signin_metrics::ProfileSignout::ACCOUNT_ID_MIGRATION,
+ signin_metrics::ProfileSignout::
+ IOS_ACCOUNT_REMOVED_FROM_DEVICE_AFTER_RESTORE,
+};
+static_assert(base::size(kSignoutSources) ==
+ signin_metrics::ProfileSignout::NUM_PROFILE_SIGNOUT_METRICS,
+ "kSignoutSources should enumerate all ProfileSignout values");
+
+INSTANTIATE_TEST_SUITE_P(AllSignoutSources,
+ ChromeSigninClientSignoutSourceTest,
+ testing::ValuesIn(kSignoutSources));
+
+#endif // !defined(OS_ANDROID)
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chromium/chrome/browser/signin/chrome_signin_helper.cc b/chromium/chrome/browser/signin/chrome_signin_helper.cc
new file mode 100644
index 00000000000..b85dde43783
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_helper.cc
@@ -0,0 +1,712 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_helper.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/logging.h"
+#include "base/memory/ref_counted.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/supports_user_data.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/prefs/incognito_mode_prefs.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_io_data.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/cookie_reminter_factory.h"
+#include "chrome/browser/signin/dice_response_handler.h"
+#include "chrome/browser/signin/header_modification_delegate_impl.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/process_dice_header_delegate_impl.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/tab_contents/tab_util.h"
+#include "chrome/browser/ui/profile_picker.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
+#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
+#include "chrome/common/url_constants.h"
+#include "components/account_manager_core/account_manager_facade.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/cookie_reminter.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/identity_manager/accounts_cookie_mutator.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "net/http/http_response_headers.h"
+
+#if defined(OS_ANDROID)
+#include "chrome/browser/android/signin/signin_bridge.h"
+#include "ui/android/view_android.h"
+#else
+#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
+#endif // defined(OS_ANDROID)
+
+#if defined(OS_CHROMEOS)
+#include "chrome/browser/profiles/profile_manager.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif // defined(OS_CHROMEOS)
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/supervised_user/supervised_user_service.h"
+#include "chrome/browser/supervised_user/supervised_user_service_factory.h"
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "chrome/browser/lacros/account_manager/account_manager_util.h"
+#include "chrome/browser/lacros/account_manager/account_profile_mapper.h"
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#endif
+
+namespace signin {
+
+const void* const kManageAccountsHeaderReceivedUserDataKey =
+ &kManageAccountsHeaderReceivedUserDataKey;
+
+const char kChromeMirrorHeaderSource[] = "Chrome";
+
+namespace {
+
+// Key for RequestDestructionObserverUserData.
+const void* const kRequestDestructionObserverUserDataKey =
+ &kRequestDestructionObserverUserDataKey;
+
+const char kGoogleRemoveLocalAccountResponseHeader[] =
+ "Google-Accounts-RemoveLocalAccount";
+
+const char kRemoveLocalAccountObfuscatedIDAttrName[] = "obfuscatedid";
+
+// TODO(droger): Remove this delay when the Dice implementation is finished on
+// the server side.
+int g_dice_account_reconcilor_blocked_delay_ms = 1000;
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+const char kGoogleSignoutResponseHeader[] = "Google-Accounts-SignOut";
+
+// Refcounted wrapper that facilitates creating and deleting a
+// AccountReconcilor::Lock.
+class AccountReconcilorLockWrapper
+ : public base::RefCountedThreadSafe<AccountReconcilorLockWrapper> {
+ public:
+ explicit AccountReconcilorLockWrapper(
+ const content::WebContents::Getter& web_contents_getter) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (!web_contents)
+ return;
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ AccountReconcilor* account_reconcilor =
+ AccountReconcilorFactory::GetForProfile(profile);
+ account_reconcilor_lock_ =
+ std::make_unique<AccountReconcilor::Lock>(account_reconcilor);
+ }
+
+ AccountReconcilorLockWrapper(const AccountReconcilorLockWrapper&) = delete;
+ AccountReconcilorLockWrapper& operator=(const AccountReconcilorLockWrapper&) =
+ delete;
+
+ void DestroyAfterDelay() {
+ // TODO(dcheng): Should ReleaseSoon() support this use case?
+ content::GetUIThreadTaskRunner({})->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce([](scoped_refptr<AccountReconcilorLockWrapper>) {},
+ base::RetainedRef(this)),
+ base::Milliseconds(g_dice_account_reconcilor_blocked_delay_ms));
+ }
+
+ private:
+ friend class base::RefCountedThreadSafe<AccountReconcilorLockWrapper>;
+ ~AccountReconcilorLockWrapper() {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ }
+
+ std::unique_ptr<AccountReconcilor::Lock> account_reconcilor_lock_;
+};
+
+// Returns true if the account reconcilor needs be be blocked while a Gaia
+// sign-in request is in progress.
+//
+// The account reconcilor must be blocked on all request that may change the
+// Gaia authentication cookies. This includes:
+// * Main frame requests.
+// * XHR requests having Gaia URL as referrer.
+bool ShouldBlockReconcilorForRequest(ChromeRequestAdapter* request) {
+ if (request->GetRequestDestination() ==
+ network::mojom::RequestDestination::kDocument) {
+ return true;
+ }
+
+ return request->IsFetchLikeAPI() &&
+ gaia::IsGaiaSignonRealm(request->GetReferrerOrigin());
+}
+
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+void OnLacrosAccountsAvailableAsSecondaryFetched(
+ AccountProfileMapper* mapper,
+ const base::FilePath& profile_path,
+ const std::vector<account_manager::Account>& accounts) {
+ if (!accounts.empty()) {
+ // Pass in the current profile to signal that the user wants to select a
+ // _secondary_ account for this particular profile.
+ ProfilePicker::Show(
+ ProfilePicker::EntryPoint::kLacrosSelectAvailableAccount, GURL(),
+ profile_path);
+ return;
+ }
+ mapper->ShowAddAccountDialog(profile_path,
+ account_manager::AccountManagerFacade::
+ AccountAdditionSource::kOgbAddAccount,
+ AccountProfileMapper::AddAccountCallback());
+}
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+class RequestDestructionObserverUserData : public base::SupportsUserData::Data {
+ public:
+ explicit RequestDestructionObserverUserData(base::OnceClosure closure)
+ : closure_(std::move(closure)) {}
+
+ RequestDestructionObserverUserData(
+ const RequestDestructionObserverUserData&) = delete;
+ RequestDestructionObserverUserData& operator=(
+ const RequestDestructionObserverUserData&) = delete;
+
+ ~RequestDestructionObserverUserData() override { std::move(closure_).Run(); }
+
+ private:
+ base::OnceClosure closure_;
+};
+
+// This user data is used as a marker that a Mirror header was found on the
+// redirect chain. It does not contain any data, its presence is enough to
+// indicate that a header has already be found on the request.
+class ManageAccountsHeaderReceivedUserData
+ : public base::SupportsUserData::Data {};
+
+#if BUILDFLAG(ENABLE_MIRROR)
+// Processes the mirror response header on the UI thread. Currently depending
+// on the value of |header_value|, it either shows the profile avatar menu, or
+// opens an incognito window/tab.
+void ProcessMirrorHeader(
+ ManageAccountsParams manage_accounts_params,
+ const content::WebContents::Getter& web_contents_getter) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ GAIAServiceType service_type = manage_accounts_params.service_type;
+ DCHECK_NE(GAIA_SERVICE_TYPE_NONE, service_type);
+
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (!web_contents)
+ return;
+
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ DCHECK(AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile))
+ << "Gaia should not send the X-Chrome-Manage-Accounts header "
+ << "when Mirror is disabled.";
+ AccountReconcilor* account_reconcilor =
+ AccountReconcilorFactory::GetForProfile(profile);
+ account_reconcilor->OnReceivedManageAccountsResponse(service_type);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS)
+ signin_metrics::LogAccountReconcilorStateOnGaiaResponse(
+ account_reconcilor->GetState());
+
+ bool should_ignore_guest_webview = true;
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ // The mirror headers from some guest web views need to be processed.
+ should_ignore_guest_webview =
+ HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest(
+ web_contents);
+#endif
+
+ Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
+ // Do not do anything if the navigation happened in the "background".
+ if ((!browser || !browser->window()->IsActive()) &&
+ should_ignore_guest_webview) {
+ return;
+ }
+
+ // Record the service type.
+ base::UmaHistogramEnumeration("AccountManager.ManageAccountsServiceType",
+ service_type);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // Ignore response to background request from another profile, so dialogs are
+ // not displayed in the wrong profile when using ChromeOS multiprofile mode.
+ if (profile != ProfileManager::GetActiveUserProfile())
+ return;
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+
+ // The only allowed operations are:
+ // 1. Going Incognito.
+ // 2. Displaying a reauthentication window: Enterprise GSuite Accounts could
+ // have been forced through an online in-browser sign-in for sensitive
+ // webpages, thereby decreasing their session validity. After their session
+ // expires, they will receive a "Mirror" re-authentication request for all
+ // Google web properties. Another case when this can be triggered is
+ // https://crbug.com/1012649.
+ // 3. Displaying an account addition window: when user clicks "Add another
+ // account" in One Google Bar.
+ // 4. Displaying the Account Manager for managing accounts.
+
+ // 1. Going incognito.
+ if (service_type == GAIA_SERVICE_TYPE_INCOGNITO) {
+ chrome::NewIncognitoWindow(profile);
+ return;
+ }
+
+ // 2. Displaying a reauthentication window
+ if (!manage_accounts_params.email.empty()) {
+ // TODO(https://crbug.com/1226055): enable this for lacros.
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // Do not display the re-authentication dialog if this event was triggered
+ // by supervision being enabled for an account. In this situation, a
+ // complete signout is required.
+ SupervisedUserService* service =
+ SupervisedUserServiceFactory::GetForProfile(profile);
+ if (service && service->signout_required_after_supervision_enabled()) {
+ return;
+ }
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+ // Child users shouldn't get the re-authentication dialog for primary
+ // account. Log out all accounts to re-mint the cookies.
+ // (See the reason below.)
+ signin::IdentityManager* const identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ CoreAccountInfo primary_account =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ if (profile->IsChild() &&
+ gaia::AreEmailsSame(primary_account.email,
+ manage_accounts_params.email)) {
+ identity_manager->GetAccountsCookieMutator()->LogOutAllAccounts(
+ gaia::GaiaSource::kChromeOS, base::DoNothing());
+ return;
+ }
+
+ // The account's cookie is invalid but the cookie has not been removed by
+ // |AccountReconcilor|. Ideally, this should not happen. At this point,
+ // |AccountReconcilor| cannot detect this state because its source of truth
+ // (/ListAccounts) is giving us false positives (claiming an invalid account
+ // to be valid). We need to store that this account's cookie is actually
+ // invalid, so that if/when this account is re-authenticated, we can force a
+ // reconciliation for this account instead of treating it as a no-op.
+ // See https://crbug.com/1012649 for details.
+ AccountInfo maybe_account_info =
+ identity_manager->FindExtendedAccountInfoByEmailAddress(
+ manage_accounts_params.email);
+ if (!maybe_account_info.IsEmpty()) {
+ CookieReminter* const cookie_reminter =
+ CookieReminterFactory::GetForProfile(profile);
+ cookie_reminter->ForceCookieRemintingOnNextTokenUpdate(
+ maybe_account_info);
+ }
+
+ // Display a re-authentication dialog.
+ ::GetAccountManagerFacade(profile->GetPath().value())
+ ->ShowReauthAccountDialog(account_manager::AccountManagerFacade::
+ AccountAdditionSource::kContentAreaReauth,
+ manage_accounts_params.email);
+ return;
+ }
+
+ // 3. Displaying an account addition window.
+ if (service_type == GAIA_SERVICE_TYPE_ADDSESSION) {
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ AccountProfileMapper* mapper =
+ g_browser_process->profile_manager()->GetAccountProfileMapper();
+ GetAccountsAvailableAsSecondary(
+ mapper, profile->GetPath(),
+ // It's safe to bind raw `mapper`, the callback gets called iff
+ // `mapper` is still valid.
+ base::BindOnce(&OnLacrosAccountsAvailableAsSecondaryFetched, mapper,
+ profile->GetPath()));
+#else
+ ::GetAccountManagerFacade(profile->GetPath().value())
+ ->ShowAddAccountDialog(account_manager::AccountManagerFacade::
+ AccountAdditionSource::kOgbAddAccount);
+#endif
+ return;
+ }
+
+ // 4. Displaying the Account Manager for managing accounts.
+ ::GetAccountManagerFacade(profile->GetPath().value())
+ ->ShowManageAccountsSettings();
+ return;
+
+#elif defined(OS_ANDROID)
+ if (manage_accounts_params.show_consistency_promo) {
+ auto* window = web_contents->GetNativeView()->GetWindowAndroid();
+ if (!window) {
+ // The page is prefetched in the background, ignore the header.
+ // See https://crbug.com/1145031#c5 for details.
+ return;
+ }
+ SigninBridge::OpenAccountPickerBottomSheet(
+ window, manage_accounts_params.continue_url.empty()
+ ? chrome::kChromeUINativeNewTabURL
+ : manage_accounts_params.continue_url);
+ return;
+ }
+ if (service_type == signin::GAIA_SERVICE_TYPE_INCOGNITO) {
+ GURL url(manage_accounts_params.continue_url.empty()
+ ? chrome::kChromeUINativeNewTabURL
+ : manage_accounts_params.continue_url);
+ web_contents->OpenURL(content::OpenURLParams(
+ url, content::Referrer(), WindowOpenDisposition::OFF_THE_RECORD,
+ ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false));
+ } else {
+ signin_metrics::LogAccountReconcilorStateOnGaiaResponse(
+ account_reconcilor->GetState());
+ auto* window = web_contents->GetNativeView()->GetWindowAndroid();
+ if (!window)
+ return;
+ SigninBridge::OpenAccountManagementScreen(window, service_type);
+ }
+#endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS)
+}
+#endif // BUILDFLAG(ENABLE_MIRROR)
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+// Creates a DiceTurnOnSyncHelper.
+void CreateDiceTurnOnSyncHelper(Profile* profile,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::PromoAction promo_action,
+ signin_metrics::Reason reason,
+ content::WebContents* web_contents,
+ const CoreAccountId& account_id) {
+ DCHECK(profile);
+ Browser* browser = web_contents
+ ? chrome::FindBrowserWithWebContents(web_contents)
+ : chrome::FindBrowserWithProfile(profile);
+ // DiceTurnSyncOnHelper is suicidal (it will kill itself once it finishes
+ // enabling sync).
+ new DiceTurnSyncOnHelper(
+ profile, browser, access_point, promo_action, reason, account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode::REMOVE_ACCOUNT);
+}
+
+// Shows UI for signin errors.
+void ShowDiceSigninError(Profile* profile,
+ content::WebContents* web_contents,
+ const SigninUIError& error) {
+ DCHECK(profile);
+ Browser* browser = web_contents
+ ? chrome::FindBrowserWithWebContents(web_contents)
+ : chrome::FindBrowserWithProfile(profile);
+ LoginUIServiceFactory::GetForProfile(profile)->DisplayLoginResult(browser,
+ error);
+}
+
+void ProcessDiceHeader(
+ const DiceResponseParams& dice_params,
+ const content::WebContents::Getter& web_contents_getter) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (!web_contents)
+ return;
+
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ DCHECK(!profile->IsOffTheRecord());
+
+ // Ignore Dice response headers if Dice is not enabled.
+ if (!AccountConsistencyModeManager::IsDiceEnabledForProfile(profile))
+ return;
+
+ DiceResponseHandler* dice_response_handler =
+ DiceResponseHandler::GetForProfile(profile);
+ dice_response_handler->ProcessDiceHeader(
+ dice_params,
+ std::make_unique<ProcessDiceHeaderDelegateImpl>(
+ web_contents, base::BindOnce(&CreateDiceTurnOnSyncHelper),
+ base::BindOnce(&ShowDiceSigninError)));
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(ENABLE_MIRROR)
+// Looks for the X-Chrome-Manage-Accounts response header, and if found,
+// tries to show the avatar bubble in the browser identified by the
+// child/route id. Must be called on IO thread.
+void ProcessMirrorResponseHeaderIfExists(ResponseAdapter* response,
+ bool is_off_the_record) {
+ CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin()));
+
+ if (!response->IsMainFrame())
+ return;
+
+ const net::HttpResponseHeaders* response_headers = response->GetHeaders();
+ if (!response_headers)
+ return;
+
+ std::string header_value;
+ if (!response_headers->GetNormalizedHeader(kChromeManageAccountsHeader,
+ &header_value)) {
+ return;
+ }
+
+ if (is_off_the_record) {
+ NOTREACHED() << "Gaia should not send the X-Chrome-Manage-Accounts header "
+ << "in incognito.";
+ return;
+ }
+
+ ManageAccountsParams params = BuildManageAccountsParams(header_value);
+ // If the request does not have a response header or if the header contains
+ // garbage, then |service_type| is set to |GAIA_SERVICE_TYPE_NONE|.
+ if (params.service_type == GAIA_SERVICE_TYPE_NONE)
+ return;
+
+ // Only process one mirror header per request (multiple headers on the same
+ // redirect chain are ignored).
+ if (response->GetUserData(kManageAccountsHeaderReceivedUserDataKey)) {
+ LOG(ERROR) << "Multiple X-Chrome-Manage-Accounts headers on a redirect "
+ << "chain, ignoring";
+ return;
+ }
+
+ response->SetUserData(
+ kManageAccountsHeaderReceivedUserDataKey,
+ std::make_unique<ManageAccountsHeaderReceivedUserData>());
+
+ // Post a task even if we are already on the UI thread to avoid making any
+ // requests while processing a throttle event.
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(ProcessMirrorHeader, params,
+ response->GetWebContentsGetter()));
+}
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+void ProcessDiceResponseHeaderIfExists(ResponseAdapter* response,
+ bool is_off_the_record) {
+ CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin()));
+
+ if (is_off_the_record)
+ return;
+
+ const net::HttpResponseHeaders* response_headers = response->GetHeaders();
+ if (!response_headers)
+ return;
+
+ std::string header_value;
+ DiceResponseParams params;
+ if (response_headers->GetNormalizedHeader(kDiceResponseHeader,
+ &header_value)) {
+ params = BuildDiceSigninResponseParams(header_value);
+ // The header must be removed for privacy reasons, so that renderers never
+ // have access to the authorization code.
+ response->RemoveHeader(kDiceResponseHeader);
+ } else if (response_headers->GetNormalizedHeader(kGoogleSignoutResponseHeader,
+ &header_value)) {
+ params = BuildDiceSignoutResponseParams(header_value);
+ }
+
+ // If the request does not have a response header or if the header contains
+ // garbage, then |user_intention| is set to |NONE|.
+ if (params.user_intention == DiceAction::NONE)
+ return;
+
+ // Post a task even if we are already on the UI thread to avoid making any
+ // requests while processing a throttle event.
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(ProcessDiceHeader, std::move(params),
+ response->GetWebContentsGetter()));
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+std::string ParseGaiaIdFromRemoveLocalAccountResponseHeader(
+ const net::HttpResponseHeaders* response_headers) {
+ if (!response_headers)
+ return std::string();
+
+ std::string header_value;
+ if (!response_headers->GetNormalizedHeader(
+ kGoogleRemoveLocalAccountResponseHeader, &header_value)) {
+ return std::string();
+ }
+
+ const SigninHeaderHelper::ResponseHeaderDictionary header_dictionary =
+ SigninHeaderHelper::ParseAccountConsistencyResponseHeader(header_value);
+
+ std::string gaia_id;
+ const auto it =
+ header_dictionary.find(kRemoveLocalAccountObfuscatedIDAttrName);
+ if (it != header_dictionary.end()) {
+ // The Gaia ID is wrapped in quotes.
+ base::TrimString(it->second, "\"", &gaia_id);
+ }
+ return gaia_id;
+}
+
+void ProcessRemoveLocalAccountResponseHeaderIfExists(ResponseAdapter* response,
+ bool is_off_the_record) {
+ CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin()));
+
+ if (is_off_the_record)
+ return;
+
+ const std::string gaia_id =
+ ParseGaiaIdFromRemoveLocalAccountResponseHeader(response->GetHeaders());
+
+ if (gaia_id.empty())
+ return;
+
+ content::WebContents* web_contents = response->GetWebContentsGetter().Run();
+ // The tab could have just closed. Technically, it would be possible to
+ // refactor the code to pass around the profile by other means, but this
+ // should be rare enough to be worth supporting.
+ if (!web_contents)
+ return;
+
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ DCHECK(!profile->IsOffTheRecord());
+
+ IdentityManagerFactory::GetForProfile(profile)
+ ->GetAccountsCookieMutator()
+ ->RemoveLoggedOutAccountByGaiaId(gaia_id);
+}
+
+} // namespace
+
+ChromeRequestAdapter::ChromeRequestAdapter(
+ const GURL& url,
+ const net::HttpRequestHeaders& original_headers,
+ net::HttpRequestHeaders* modified_headers,
+ std::vector<std::string>* headers_to_remove)
+ : RequestAdapter(url,
+ original_headers,
+ modified_headers,
+ headers_to_remove) {}
+
+ChromeRequestAdapter::~ChromeRequestAdapter() = default;
+
+ResponseAdapter::ResponseAdapter() = default;
+
+ResponseAdapter::~ResponseAdapter() = default;
+
+void SetDiceAccountReconcilorBlockDelayForTesting(int delay_ms) {
+ g_dice_account_reconcilor_blocked_delay_ms = delay_ms;
+}
+
+void FixAccountConsistencyRequestHeader(
+ ChromeRequestAdapter* request,
+ const GURL& redirect_url,
+ bool is_off_the_record,
+ int incognito_availibility,
+ AccountConsistencyMethod account_consistency,
+ const std::string& gaia_id,
+ signin::Tribool is_child_account,
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ bool is_secondary_account_addition_allowed,
+#endif
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ bool is_sync_enabled,
+ const std::string& signin_scoped_device_id,
+#endif
+ content_settings::CookieSettings* cookie_settings) {
+ if (is_off_the_record)
+ return; // Account consistency is disabled in incognito.
+
+ // If new url is eligible to have the header, add it, otherwise remove it.
+
+// Mirror header:
+#if BUILDFLAG(ENABLE_MIRROR)
+ int profile_mode_mask = PROFILE_MODE_DEFAULT;
+ if (incognito_availibility ==
+ static_cast<int>(IncognitoModePrefs::Availability::kDisabled) ||
+ IncognitoModePrefs::ArePlatformParentalControlsEnabled()) {
+ profile_mode_mask |= PROFILE_MODE_INCOGNITO_DISABLED;
+ }
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (!is_secondary_account_addition_allowed) {
+ account_consistency = AccountConsistencyMethod::kMirror;
+ // Can't add new accounts.
+ profile_mode_mask |= PROFILE_MODE_ADD_ACCOUNT_DISABLED;
+ }
+#endif
+
+ AppendOrRemoveMirrorRequestHeader(
+ request, redirect_url, gaia_id, is_child_account, account_consistency,
+ cookie_settings, profile_mode_mask, kChromeMirrorHeaderSource,
+ /*force_account_consistency=*/false);
+#endif // BUILDFLAG(ENABLE_MIRROR)
+
+// Dice header:
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ bool dice_header_added = AppendOrRemoveDiceRequestHeader(
+ request, redirect_url, gaia_id, is_sync_enabled, account_consistency,
+ cookie_settings, signin_scoped_device_id);
+
+ // Block the AccountReconcilor while the Dice requests are in flight. This
+ // allows the DiceReponseHandler to process the response before the reconcilor
+ // starts.
+ if (dice_header_added && ShouldBlockReconcilorForRequest(request)) {
+ auto lock_wrapper = base::MakeRefCounted<AccountReconcilorLockWrapper>(
+ request->GetWebContentsGetter());
+ // On destruction of the request |lock_wrapper| will be released.
+ request->SetDestructionCallback(base::BindOnce(
+ &AccountReconcilorLockWrapper::DestroyAfterDelay, lock_wrapper));
+ }
+#endif
+}
+
+void ProcessAccountConsistencyResponseHeaders(ResponseAdapter* response,
+ const GURL& redirect_url,
+ bool is_off_the_record) {
+ if (!gaia::IsGaiaSignonRealm(response->GetOrigin()))
+ return;
+
+#if BUILDFLAG(ENABLE_MIRROR)
+ // See if the response contains the X-Chrome-Manage-Accounts header. If so
+ // show the profile avatar bubble so that user can complete signin/out
+ // action the native UI.
+ ProcessMirrorResponseHeaderIfExists(response, is_off_the_record);
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ // Process the Dice header: on sign-in, exchange the authorization code for a
+ // refresh token, on sign-out just follow the sign-out URL.
+ ProcessDiceResponseHeaderIfExists(response, is_off_the_record);
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+ if (base::FeatureList::IsEnabled(kProcessGaiaRemoveLocalAccountHeader)) {
+ ProcessRemoveLocalAccountResponseHeaderIfExists(response,
+ is_off_the_record);
+ }
+}
+
+std::string ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ const net::HttpResponseHeaders* response_headers) {
+ return ParseGaiaIdFromRemoveLocalAccountResponseHeader(response_headers);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chrome_signin_helper.h b/chromium/chrome/browser/signin/chrome_signin_helper.h
new file mode 100644
index 00000000000..7d6ea870458
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_helper.h
@@ -0,0 +1,129 @@
+// Copyright 2013 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_
+
+#include <memory>
+#include <string>
+
+#include "base/supports_user_data.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/prefs/incognito_mode_prefs.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "content/public/browser/web_contents.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+
+namespace content_settings {
+class CookieSettings;
+}
+
+namespace net {
+class HttpResponseHeaders;
+}
+
+class GURL;
+
+// Utility functions for handling Chrome/Gaia headers during signin process.
+// Chrome identity should always stay in sync with Gaia identity. Therefore
+// Chrome needs to send Gaia special header for requests from a connected
+// profile, so that Gaia can modify its response accordingly and let Chrome
+// handle signin accordingly.
+namespace signin {
+
+enum class Tribool;
+
+// Key for ManageAccountsHeaderReceivedUserData. Exposed for testing.
+extern const void* const kManageAccountsHeaderReceivedUserDataKey;
+
+// The source to use when constructing the Mirror header.
+extern const char kChromeMirrorHeaderSource[];
+
+class ChromeRequestAdapter : public RequestAdapter {
+ public:
+ ChromeRequestAdapter(const GURL& url,
+ const net::HttpRequestHeaders& original_headers,
+ net::HttpRequestHeaders* modified_headers,
+ std::vector<std::string>* headers_to_remove);
+
+ ChromeRequestAdapter(const ChromeRequestAdapter&) = delete;
+ ChromeRequestAdapter& operator=(const ChromeRequestAdapter&) = delete;
+
+ ~ChromeRequestAdapter() override;
+
+ virtual content::WebContents::Getter GetWebContentsGetter() const = 0;
+
+ virtual network::mojom::RequestDestination GetRequestDestination() const = 0;
+
+ virtual bool IsFetchLikeAPI() const = 0;
+
+ virtual GURL GetReferrerOrigin() const = 0;
+
+ // Associate a callback with this request which will be executed when the
+ // request is complete (including any redirects). If a callback was already
+ // registered this function does nothing.
+ virtual void SetDestructionCallback(base::OnceClosure closure) = 0;
+};
+
+class ResponseAdapter {
+ public:
+ ResponseAdapter();
+
+ ResponseAdapter(const ResponseAdapter&) = delete;
+ ResponseAdapter& operator=(const ResponseAdapter&) = delete;
+
+ virtual ~ResponseAdapter();
+
+ virtual content::WebContents::Getter GetWebContentsGetter() const = 0;
+ virtual bool IsMainFrame() const = 0;
+ virtual GURL GetOrigin() const = 0;
+ virtual const net::HttpResponseHeaders* GetHeaders() const = 0;
+ virtual void RemoveHeader(const std::string& name) = 0;
+
+ virtual base::SupportsUserData::Data* GetUserData(const void* key) const = 0;
+ virtual void SetUserData(
+ const void* key,
+ std::unique_ptr<base::SupportsUserData::Data> data) = 0;
+};
+
+// When Dice is enabled, the AccountReconcilor is blocked for a short delay
+// after sending requests to Gaia. Exposed for testing.
+void SetDiceAccountReconcilorBlockDelayForTesting(int delay_ms);
+
+// Adds an account consistency header to Gaia requests from a connected profile,
+// with the exception of requests from gaia webview.
+// Removes the header if it is already in the headers but should not be there.
+void FixAccountConsistencyRequestHeader(
+ ChromeRequestAdapter* request,
+ const GURL& redirect_url,
+ bool is_off_the_record,
+ int incognito_availibility,
+ AccountConsistencyMethod account_consistency,
+ const std::string& gaia_id,
+ signin::Tribool is_child_account,
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ bool is_secondary_account_addition_allowed,
+#endif
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ bool is_sync_enabled,
+ const std::string& signin_scoped_device_id,
+#endif
+ content_settings::CookieSettings* cookie_settings);
+
+// Processes account consistency response headers (X-Chrome-Manage-Accounts and
+// Dice). |redirect_url| is empty if the request is not a redirect.
+void ProcessAccountConsistencyResponseHeaders(ResponseAdapter* response,
+ const GURL& redirect_url,
+ bool is_off_the_record);
+
+// Parses and returns an account ID (Gaia ID) from HTTP response header
+// Google-Accounts-RemoveLocalAccount. Returns an empty string if parsing
+// failed. Exposed for testing purposes.
+std::string ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ const net::HttpResponseHeaders* response_headers);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc
new file mode 100644
index 00000000000..8a5757aabe3
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc
@@ -0,0 +1,182 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_helper.h"
+
+#include <memory>
+#include <string>
+
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "build/chromeos_buildflags.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "content/public/test/browser_task_environment.h"
+#include "net/http/http_response_headers.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "net/url_request/url_request.h"
+#include "net/url_request/url_request_context.h"
+#include "net/url_request/url_request_filter.h"
+#include "net/url_request/url_request_interceptor.h"
+#include "net/url_request/url_request_test_job.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace {
+
+#if BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS)
+const char kChromeManageAccountsHeader[] = "X-Chrome-Manage-Accounts";
+const char kMirrorAction[] = "action=ADDSESSION";
+#endif
+
+// URLRequestInterceptor adding a account consistency response header to Gaia
+// responses.
+class TestRequestInterceptor : public net::URLRequestInterceptor {
+ public:
+ explicit TestRequestInterceptor(const std::string& header_name,
+ const std::string& header_value)
+ : header_name_(header_name), header_value_(header_value) {}
+ ~TestRequestInterceptor() override = default;
+
+ private:
+ std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest(
+ net::URLRequest* request) const override {
+ std::string response_headers =
+ base::StringPrintf("HTTP/1.1 200 OK\n\n%s: %s\n", header_name_.c_str(),
+ header_value_.c_str());
+ return std::make_unique<net::URLRequestTestJob>(request, response_headers,
+ "", true);
+ }
+
+ const std::string header_name_;
+ const std::string header_value_;
+};
+
+class TestResponseAdapter : public signin::ResponseAdapter,
+ public base::SupportsUserData {
+ public:
+ TestResponseAdapter(const std::string& header_name,
+ const std::string& header_value,
+ bool is_main_frame)
+ : is_main_frame_(is_main_frame),
+ headers_(new net::HttpResponseHeaders(std::string())) {
+ headers_->SetHeader(header_name, header_value);
+ }
+
+ TestResponseAdapter(const TestResponseAdapter&) = delete;
+ TestResponseAdapter& operator=(const TestResponseAdapter&) = delete;
+
+ ~TestResponseAdapter() override {}
+
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return base::BindRepeating(
+ []() -> content::WebContents* { return nullptr; });
+ }
+ bool IsMainFrame() const override { return is_main_frame_; }
+ GURL GetOrigin() const override {
+ return GURL("https://accounts.google.com");
+ }
+ const net::HttpResponseHeaders* GetHeaders() const override {
+ return headers_.get();
+ }
+
+ void RemoveHeader(const std::string& name) override {
+ headers_->RemoveHeader(name);
+ }
+
+ base::SupportsUserData::Data* GetUserData(const void* key) const override {
+ return base::SupportsUserData::GetUserData(key);
+ }
+
+ void SetUserData(
+ const void* key,
+ std::unique_ptr<base::SupportsUserData::Data> data) override {
+ return base::SupportsUserData::SetUserData(key, std::move(data));
+ }
+
+ private:
+ bool is_main_frame_;
+ scoped_refptr<net::HttpResponseHeaders> headers_;
+};
+
+} // namespace
+
+class ChromeSigninHelperTest : public testing::Test {
+ protected:
+ ChromeSigninHelperTest()
+ : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {}
+
+ ~ChromeSigninHelperTest() override = default;
+
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<net::TestDelegate> test_request_delegate_;
+};
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+// Tests that Dice response headers are removed after being processed.
+TEST_F(ChromeSigninHelperTest, RemoveDiceSigninHeader) {
+ // Process the header.
+ TestResponseAdapter adapter(signin::kDiceResponseHeader, "Foo",
+ /*is_main_frame=*/false);
+ signin::ProcessAccountConsistencyResponseHeaders(&adapter, GURL(),
+ false /* is_incognito */);
+
+ // Check that the header has been removed.
+ EXPECT_FALSE(adapter.GetHeaders()->HasHeader(signin::kDiceResponseHeader));
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS)
+// Tests that user data is set on Mirror requests.
+TEST_F(ChromeSigninHelperTest, MirrorMainFrame) {
+ // Process the header.
+ TestResponseAdapter response_adapter(kChromeManageAccountsHeader,
+ kMirrorAction,
+ /*is_main_frame=*/true);
+ signin::ProcessAccountConsistencyResponseHeaders(&response_adapter, GURL(),
+ false /* is_incognito */);
+ // Check that the header has not been removed.
+ EXPECT_TRUE(
+ response_adapter.GetHeaders()->HasHeader(kChromeManageAccountsHeader));
+ // Request was flagged with the user data.
+ EXPECT_TRUE(response_adapter.GetUserData(
+ signin::kManageAccountsHeaderReceivedUserDataKey));
+}
+
+// Tests that user data is not set on Mirror requests for sub frames.
+TEST_F(ChromeSigninHelperTest, MirrorSubFrame) {
+ // Process the header.
+ TestResponseAdapter response_adapter(kChromeManageAccountsHeader,
+ kMirrorAction,
+ /*is_main_frame=*/false);
+ signin::ProcessAccountConsistencyResponseHeaders(&response_adapter, GURL(),
+ false /* is_incognito */);
+ // Request was not flagged with the user data.
+ EXPECT_FALSE(response_adapter.GetUserData(
+ signin::kManageAccountsHeaderReceivedUserDataKey));
+}
+#endif // BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS)
+
+TEST_F(ChromeSigninHelperTest,
+ ParseGaiaIdFromRemoveLocalAccountResponseHeader) {
+ EXPECT_EQ("123456",
+ signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ TestResponseAdapter("Google-Accounts-RemoveLocalAccount",
+ "obfuscatedid=\"123456\"",
+ /*is_main_frame=*/false)
+ .GetHeaders()));
+ EXPECT_EQ("123456",
+ signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ TestResponseAdapter("Google-Accounts-RemoveLocalAccount",
+ "obfuscatedid=\"123456\",foo=\"bar\"",
+ /*is_main_frame=*/false)
+ .GetHeaders()));
+ EXPECT_EQ(
+ "",
+ signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ TestResponseAdapter("Google-Accounts-RemoveLocalAccount", "malformed",
+ /*is_main_frame=*/false)
+ .GetHeaders()));
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc
new file mode 100644
index 00000000000..d4b8d911a9e
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc
@@ -0,0 +1,551 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h"
+
+#include "base/barrier_closure.h"
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/supports_user_data.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "chrome/browser/signin/header_modification_delegate_impl.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
+#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
+#include "extensions/buildflags/buildflags.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "net/base/net_errors.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/early_hints.mojom.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+#include "services/network/public/mojom/url_loader.mojom.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+
+#if defined(OS_ANDROID)
+#include "chrome/browser/android/tab_android.h"
+#include "chrome/browser/android/tab_web_contents_delegate_android.h"
+#endif
+
+namespace signin {
+
+namespace {
+
+// User data key for BrowserContextData.
+const void* const kBrowserContextUserDataKey = &kBrowserContextUserDataKey;
+
+// Owns all of the ProxyingURLLoaderFactorys for a given Profile.
+class BrowserContextData : public base::SupportsUserData::Data {
+ public:
+ BrowserContextData(const BrowserContextData&) = delete;
+ BrowserContextData& operator=(const BrowserContextData&) = delete;
+
+ ~BrowserContextData() override {}
+
+ static void StartProxying(
+ Profile* profile,
+ content::WebContents::Getter web_contents_getter,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver,
+ mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory) {
+ auto* self = static_cast<BrowserContextData*>(
+ profile->GetUserData(kBrowserContextUserDataKey));
+ if (!self) {
+ self = new BrowserContextData();
+ profile->SetUserData(kBrowserContextUserDataKey, base::WrapUnique(self));
+ }
+
+#if defined(OS_ANDROID)
+ bool is_custom_tab = false;
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (web_contents) {
+ auto* delegate =
+ TabAndroid::FromWebContents(web_contents)
+ ? static_cast<android::TabWebContentsDelegateAndroid*>(
+ web_contents->GetDelegate())
+ : nullptr;
+ is_custom_tab = delegate && delegate->IsCustomTab();
+ }
+ auto delegate = std::make_unique<HeaderModificationDelegateImpl>(
+ profile, /*incognito_enabled=*/!is_custom_tab);
+#else
+ auto delegate = std::make_unique<HeaderModificationDelegateImpl>(profile);
+#endif
+ auto proxy = std::make_unique<ProxyingURLLoaderFactory>(
+ std::move(delegate), std::move(web_contents_getter),
+ std::move(receiver), std::move(target_factory),
+ base::BindOnce(&BrowserContextData::RemoveProxy,
+ self->weak_factory_.GetWeakPtr()));
+ self->proxies_.emplace(std::move(proxy));
+ }
+
+ void RemoveProxy(ProxyingURLLoaderFactory* proxy) {
+ auto it = proxies_.find(proxy);
+ DCHECK(it != proxies_.end());
+ proxies_.erase(it);
+ }
+
+ private:
+ BrowserContextData() {}
+
+ std::set<std::unique_ptr<ProxyingURLLoaderFactory>, base::UniquePtrComparator>
+ proxies_;
+
+ base::WeakPtrFactory<BrowserContextData> weak_factory_{this};
+};
+
+} // namespace
+
+class ProxyingURLLoaderFactory::InProgressRequest
+ : public network::mojom::URLLoader,
+ public network::mojom::URLLoaderClient,
+ public base::SupportsUserData {
+ public:
+ InProgressRequest(
+ ProxyingURLLoaderFactory* factory,
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation);
+
+ InProgressRequest(const InProgressRequest&) = delete;
+ InProgressRequest& operator=(const InProgressRequest&) = delete;
+
+ ~InProgressRequest() override {
+ if (destruction_callback_)
+ std::move(destruction_callback_).Run();
+ }
+
+ // network::mojom::URLLoader:
+ void FollowRedirect(
+ const std::vector<std::string>& removed_headers,
+ const net::HttpRequestHeaders& modified_headers,
+ const net::HttpRequestHeaders& modified_cors_exempt_headers,
+ const absl::optional<GURL>& new_url) override;
+
+ void SetPriority(net::RequestPriority priority,
+ int32_t intra_priority_value) override {
+ target_loader_->SetPriority(priority, intra_priority_value);
+ }
+
+ void PauseReadingBodyFromNet() override {
+ target_loader_->PauseReadingBodyFromNet();
+ }
+
+ void ResumeReadingBodyFromNet() override {
+ target_loader_->ResumeReadingBodyFromNet();
+ }
+
+ // network::mojom::URLLoaderClient:
+ void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override {
+ target_client_->OnReceiveEarlyHints(std::move(early_hints));
+ }
+ void OnReceiveResponse(network::mojom::URLResponseHeadPtr head) override;
+ void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
+ network::mojom::URLResponseHeadPtr head) override;
+
+ void OnUploadProgress(int64_t current_position,
+ int64_t total_size,
+ OnUploadProgressCallback callback) override {
+ target_client_->OnUploadProgress(current_position, total_size,
+ std::move(callback));
+ }
+
+ void OnReceiveCachedMetadata(mojo_base::BigBuffer data) override {
+ target_client_->OnReceiveCachedMetadata(std::move(data));
+ }
+
+ void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
+ target_client_->OnTransferSizeUpdated(transfer_size_diff);
+ }
+
+ void OnStartLoadingResponseBody(
+ mojo::ScopedDataPipeConsumerHandle body) override {
+ target_client_->OnStartLoadingResponseBody(std::move(body));
+ }
+
+ void OnComplete(const network::URLLoaderCompletionStatus& status) override {
+ target_client_->OnComplete(status);
+ }
+
+ private:
+ class ProxyRequestAdapter;
+ class ProxyResponseAdapter;
+
+ void OnBindingsClosed() {
+ // Destroys |this|.
+ factory_->RemoveRequest(this);
+ }
+
+ // Back pointer to the factory which owns this class.
+ const raw_ptr<ProxyingURLLoaderFactory> factory_;
+
+ // Information about the current request.
+ GURL request_url_;
+ GURL response_url_;
+ GURL referrer_origin_;
+ net::HttpRequestHeaders headers_;
+ net::HttpRequestHeaders cors_exempt_headers_;
+ net::RedirectInfo redirect_info_;
+ const network::mojom::RequestDestination request_destination_;
+ const bool is_main_frame_;
+ const bool is_fetch_like_api_;
+
+ base::OnceClosure destruction_callback_;
+
+ // Messages received by |client_receiver_| are forwarded to |target_client_|.
+ mojo::Receiver<network::mojom::URLLoaderClient> client_receiver_{this};
+ mojo::Remote<network::mojom::URLLoaderClient> target_client_;
+
+ // Messages received by |loader_receiver_| are forwarded to |target_loader_|.
+ mojo::Receiver<network::mojom::URLLoader> loader_receiver_;
+ mojo::Remote<network::mojom::URLLoader> target_loader_;
+};
+
+class ProxyingURLLoaderFactory::InProgressRequest::ProxyRequestAdapter
+ : public ChromeRequestAdapter {
+ public:
+ // Does not take |modified_cors_exempt_headers| just because we don't have a
+ // use-case to modify it in this class now.
+ ProxyRequestAdapter(InProgressRequest* in_progress_request,
+ const net::HttpRequestHeaders& original_headers,
+ net::HttpRequestHeaders* modified_headers,
+ std::vector<std::string>* removed_headers)
+ : ChromeRequestAdapter(in_progress_request->request_url_,
+ original_headers,
+ modified_headers,
+ removed_headers),
+ in_progress_request_(in_progress_request) {
+ DCHECK(in_progress_request_);
+ }
+
+ ProxyRequestAdapter(const ProxyRequestAdapter&) = delete;
+ ProxyRequestAdapter& operator=(const ProxyRequestAdapter&) = delete;
+
+ ~ProxyRequestAdapter() override = default;
+
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return in_progress_request_->factory_->web_contents_getter_;
+ }
+
+ network::mojom::RequestDestination GetRequestDestination() const override {
+ return in_progress_request_->request_destination_;
+ }
+
+ bool IsFetchLikeAPI() const override {
+ return in_progress_request_->is_fetch_like_api_;
+ }
+
+ GURL GetReferrerOrigin() const override {
+ return in_progress_request_->referrer_origin_;
+ }
+
+ void SetDestructionCallback(base::OnceClosure closure) override {
+ if (!in_progress_request_->destruction_callback_)
+ in_progress_request_->destruction_callback_ = std::move(closure);
+ }
+
+ private:
+ const raw_ptr<InProgressRequest> in_progress_request_;
+};
+
+class ProxyingURLLoaderFactory::InProgressRequest::ProxyResponseAdapter
+ : public ResponseAdapter {
+ public:
+ ProxyResponseAdapter(InProgressRequest* in_progress_request,
+ net::HttpResponseHeaders* headers)
+ : in_progress_request_(in_progress_request), headers_(headers) {
+ DCHECK(in_progress_request_);
+ DCHECK(headers_);
+ }
+
+ ProxyResponseAdapter(const ProxyResponseAdapter&) = delete;
+ ProxyResponseAdapter& operator=(const ProxyResponseAdapter&) = delete;
+
+ ~ProxyResponseAdapter() override = default;
+
+ // signin::ResponseAdapter
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return in_progress_request_->factory_->web_contents_getter_;
+ }
+
+ bool IsMainFrame() const override {
+ return in_progress_request_->is_main_frame_;
+ }
+
+ GURL GetOrigin() const override {
+ return in_progress_request_->response_url_.DeprecatedGetOriginAsURL();
+ }
+
+ const net::HttpResponseHeaders* GetHeaders() const override {
+ return headers_;
+ }
+
+ void RemoveHeader(const std::string& name) override {
+ headers_->RemoveHeader(name);
+ }
+
+ base::SupportsUserData::Data* GetUserData(const void* key) const override {
+ return in_progress_request_->GetUserData(key);
+ }
+
+ void SetUserData(
+ const void* key,
+ std::unique_ptr<base::SupportsUserData::Data> data) override {
+ in_progress_request_->SetUserData(key, std::move(data));
+ }
+
+ private:
+ const raw_ptr<InProgressRequest> in_progress_request_;
+ const raw_ptr<net::HttpResponseHeaders> headers_;
+};
+
+ProxyingURLLoaderFactory::InProgressRequest::InProgressRequest(
+ ProxyingURLLoaderFactory* factory,
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
+ : factory_(factory),
+ request_url_(request.url),
+ response_url_(request.url),
+ referrer_origin_(request.referrer.DeprecatedGetOriginAsURL()),
+ request_destination_(request.destination),
+ is_main_frame_(request.is_main_frame),
+ is_fetch_like_api_(request.is_fetch_like_api),
+ target_client_(std::move(client)),
+ loader_receiver_(this, std::move(loader_receiver)) {
+ mojo::PendingRemote<network::mojom::URLLoaderClient> proxy_client =
+ client_receiver_.BindNewPipeAndPassRemote();
+
+ net::HttpRequestHeaders modified_headers;
+ std::vector<std::string> removed_headers;
+ ProxyRequestAdapter adapter(this, request.headers, &modified_headers,
+ &removed_headers);
+ factory_->delegate_->ProcessRequest(&adapter, GURL() /* redirect_url */);
+
+ if (modified_headers.IsEmpty() && removed_headers.empty()) {
+ factory_->target_factory_->CreateLoaderAndStart(
+ target_loader_.BindNewPipeAndPassReceiver(), request_id, options,
+ request, std::move(proxy_client), traffic_annotation);
+
+ // We need to keep a full copy of the request headers in case there is a
+ // redirect and the request headers need to be modified again.
+ headers_.CopyFrom(request.headers);
+ cors_exempt_headers_.CopyFrom(request.cors_exempt_headers);
+ } else {
+ network::ResourceRequest request_copy = request;
+ request_copy.headers.MergeFrom(modified_headers);
+ for (const std::string& name : removed_headers) {
+ request_copy.headers.RemoveHeader(name);
+ request_copy.cors_exempt_headers.RemoveHeader(name);
+ }
+
+ factory_->target_factory_->CreateLoaderAndStart(
+ target_loader_.BindNewPipeAndPassReceiver(), request_id, options,
+ request_copy, std::move(proxy_client), traffic_annotation);
+
+ headers_.Swap(&request_copy.headers);
+ cors_exempt_headers_.Swap(&request_copy.cors_exempt_headers);
+ }
+
+ base::RepeatingClosure closure = base::BarrierClosure(
+ 2, base::BindOnce(&InProgressRequest::OnBindingsClosed,
+ base::Unretained(this)));
+ loader_receiver_.set_disconnect_handler(closure);
+ client_receiver_.set_disconnect_handler(closure);
+}
+
+void ProxyingURLLoaderFactory::InProgressRequest::FollowRedirect(
+ const std::vector<std::string>& removed_headers_ext,
+ const net::HttpRequestHeaders& modified_headers_ext,
+ const net::HttpRequestHeaders& modified_cors_exempt_headers_ext,
+ const absl::optional<GURL>& opt_new_url) {
+ std::vector<std::string> removed_headers = removed_headers_ext;
+ net::HttpRequestHeaders modified_headers = modified_headers_ext;
+ net::HttpRequestHeaders modified_cors_exempt_headers =
+ modified_cors_exempt_headers_ext;
+ ProxyRequestAdapter adapter(this, headers_, &modified_headers,
+ &removed_headers);
+ factory_->delegate_->ProcessRequest(&adapter, redirect_info_.new_url);
+
+ headers_.MergeFrom(modified_headers);
+ cors_exempt_headers_.MergeFrom(modified_cors_exempt_headers);
+ for (const std::string& name : removed_headers) {
+ headers_.RemoveHeader(name);
+ cors_exempt_headers_.RemoveHeader(name);
+ }
+
+ target_loader_->FollowRedirect(removed_headers, modified_headers,
+ modified_cors_exempt_headers, opt_new_url);
+
+ request_url_ = redirect_info_.new_url;
+ referrer_origin_ =
+ GURL(redirect_info_.new_referrer).DeprecatedGetOriginAsURL();
+}
+
+void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveResponse(
+ network::mojom::URLResponseHeadPtr head) {
+ // Even though |head| is const we can get a non-const pointer to the headers
+ // and modifications we made are passed to the target client.
+ ProxyResponseAdapter adapter(this, head->headers.get());
+ factory_->delegate_->ProcessResponse(&adapter, GURL() /* redirect_url */);
+ target_client_->OnReceiveResponse(std::move(head));
+}
+
+void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveRedirect(
+ const net::RedirectInfo& redirect_info,
+ network::mojom::URLResponseHeadPtr head) {
+ // Even though |head| is const we can get a non-const pointer to the headers
+ // and modifications we made are passed to the target client.
+ ProxyResponseAdapter adapter(this, head->headers.get());
+ factory_->delegate_->ProcessResponse(&adapter, redirect_info.new_url);
+ target_client_->OnReceiveRedirect(redirect_info, std::move(head));
+
+ // The request URL returned by ProxyResponseAdapter::GetOrigin() is updated
+ // immediately but the URL and referrer
+ redirect_info_ = redirect_info;
+ response_url_ = redirect_info.new_url;
+}
+
+ProxyingURLLoaderFactory::ProxyingURLLoaderFactory(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver,
+ mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
+ DisconnectCallback on_disconnect) {
+ DCHECK(proxy_receivers_.empty());
+ DCHECK(!target_factory_.is_bound());
+ DCHECK(!delegate_);
+ DCHECK(!web_contents_getter_);
+ DCHECK(!on_disconnect_);
+
+ delegate_ = std::move(delegate);
+ web_contents_getter_ = std::move(web_contents_getter);
+ on_disconnect_ = std::move(on_disconnect);
+
+ target_factory_.Bind(std::move(target_factory));
+ target_factory_.set_disconnect_handler(base::BindOnce(
+ &ProxyingURLLoaderFactory::OnTargetFactoryError, base::Unretained(this)));
+
+ proxy_receivers_.Add(this, std::move(loader_receiver));
+ proxy_receivers_.set_disconnect_handler(base::BindRepeating(
+ &ProxyingURLLoaderFactory::OnProxyBindingError, base::Unretained(this)));
+}
+
+ProxyingURLLoaderFactory::~ProxyingURLLoaderFactory() = default;
+
+// static
+bool ProxyingURLLoaderFactory::MaybeProxyRequest(
+ content::RenderFrameHost* render_frame_host,
+ bool is_navigation,
+ const url::Origin& request_initiator,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory>* factory_receiver) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ // Navigation requests are handled using signin::URLLoaderThrottle.
+ if (is_navigation)
+ return false;
+
+ if (!render_frame_host)
+ return false;
+
+ // This proxy should only be installed for subresource requests from a frame
+ // that is rendering the GAIA signon realm.
+ if (!gaia::IsGaiaSignonRealm(request_initiator.GetURL()))
+ return false;
+
+ auto* web_contents =
+ content::WebContents::FromRenderFrameHost(render_frame_host);
+ auto* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ if (profile->IsOffTheRecord())
+ return false;
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ // Most requests from guest web views are ignored.
+ if (HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest(
+ web_contents)) {
+ return false;
+ }
+#endif
+
+ auto proxied_receiver = std::move(*factory_receiver);
+ // TODO(crbug.com/955171): Replace this with PendingRemote.
+ mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory_remote;
+ *factory_receiver = target_factory_remote.InitWithNewPipeAndPassReceiver();
+
+ auto web_contents_getter =
+ base::BindRepeating(&content::WebContents::FromFrameTreeNodeId,
+ render_frame_host->GetFrameTreeNodeId());
+
+ BrowserContextData::StartProxying(profile, std::move(web_contents_getter),
+ std::move(proxied_receiver),
+ std::move(target_factory_remote));
+ return true;
+}
+
+void ProxyingURLLoaderFactory::CreateLoaderAndStart(
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
+ requests_.insert(std::make_unique<InProgressRequest>(
+ this, std::move(loader_receiver), request_id, options, request,
+ std::move(client), traffic_annotation));
+}
+
+void ProxyingURLLoaderFactory::Clone(
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver) {
+ proxy_receivers_.Add(this, std::move(loader_receiver));
+}
+
+void ProxyingURLLoaderFactory::OnTargetFactoryError() {
+ // Stop calls to CreateLoaderAndStart() when |target_factory_| is invalid.
+ target_factory_.reset();
+ proxy_receivers_.Clear();
+
+ MaybeDestroySelf();
+}
+
+void ProxyingURLLoaderFactory::OnProxyBindingError() {
+ if (proxy_receivers_.empty())
+ target_factory_.reset();
+
+ MaybeDestroySelf();
+}
+
+void ProxyingURLLoaderFactory::RemoveRequest(InProgressRequest* request) {
+ auto it = requests_.find(request);
+ DCHECK(it != requests_.end());
+ requests_.erase(it);
+
+ MaybeDestroySelf();
+}
+
+void ProxyingURLLoaderFactory::MaybeDestroySelf() {
+ // Even if all URLLoaderFactory pipes connected to this object have been
+ // closed it has to stay alive until all active requests have completed.
+ if (target_factory_.is_bound() || !requests_.empty())
+ return;
+
+ // Deletes |this|.
+ std::move(on_disconnect_).Run(this);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h
new file mode 100644
index 00000000000..28589803472
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h
@@ -0,0 +1,100 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_
+
+#include "base/callback.h"
+#include "base/containers/unique_ptr_adapters.h"
+#include "base/memory/ref_counted_delete_on_sequence.h"
+#include "content/public/browser/web_contents.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+
+#include <memory>
+#include <set>
+
+namespace content {
+class RenderFrameHost;
+}
+
+namespace signin {
+
+class HeaderModificationDelegate;
+
+// This class is used to modify sub-resource requests made by the renderer
+// that is displaying the GAIA signin realm, to the GAIA signin realm. When
+// such a request is made a proxy is inserted between the renderer and the
+// Network Service to modify request and response headers.
+class ProxyingURLLoaderFactory : public network::mojom::URLLoaderFactory {
+ public:
+ using DisconnectCallback =
+ base::OnceCallback<void(ProxyingURLLoaderFactory*)>;
+
+ // Constructor public for testing purposes. New instances should be created
+ // by calling MaybeProxyRequest().
+ ProxyingURLLoaderFactory(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver,
+ mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
+ DisconnectCallback on_disconnect);
+
+ ProxyingURLLoaderFactory(const ProxyingURLLoaderFactory&) = delete;
+ ProxyingURLLoaderFactory& operator=(const ProxyingURLLoaderFactory&) = delete;
+
+ ~ProxyingURLLoaderFactory() override;
+
+ // Called when a renderer needs a URLLoaderFactory to give this module the
+ // opportunity to install a proxy. This is only done when
+ // https://accounts.google.com is loaded in non-incognito mode. Returns true
+ // when |factory_request| has been proxied.
+ static bool MaybeProxyRequest(
+ content::RenderFrameHost* render_frame_host,
+ bool is_navigation,
+ const url::Origin& request_initiator,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory>*
+ factory_receiver);
+
+ // network::mojom::URLLoaderFactory:
+ void CreateLoaderAndStart(
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
+ override;
+ void Clone(mojo::PendingReceiver<network::mojom::URLLoaderFactory>
+ loader_receiver) override;
+
+ private:
+ friend class base::DeleteHelper<ProxyingURLLoaderFactory>;
+ friend class base::RefCountedDeleteOnSequence<ProxyingURLLoaderFactory>;
+
+ class InProgressRequest;
+ class ProxyRequestAdapter;
+ class ProxyResponseAdapter;
+
+ void OnTargetFactoryError();
+ void OnProxyBindingError();
+ void RemoveRequest(InProgressRequest* request);
+ void MaybeDestroySelf();
+
+ std::unique_ptr<HeaderModificationDelegate> delegate_;
+ content::WebContents::Getter web_contents_getter_;
+
+ mojo::ReceiverSet<network::mojom::URLLoaderFactory> proxy_receivers_;
+ std::set<std::unique_ptr<InProgressRequest>, base::UniquePtrComparator>
+ requests_;
+ mojo::Remote<network::mojom::URLLoaderFactory> target_factory_;
+ DisconnectCallback on_disconnect_;
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc
new file mode 100644
index 00000000000..2d9772cc070
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc
@@ -0,0 +1,359 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h"
+
+#include <algorithm>
+#include <memory>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/run_loop.h"
+#include "base/test/mock_callback.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/simple_url_loader.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::Invoke;
+using testing::_;
+
+namespace signin {
+
+namespace {
+
+class MockDelegate : public HeaderModificationDelegate {
+ public:
+ MockDelegate() = default;
+
+ MockDelegate(const MockDelegate&) = delete;
+ MockDelegate& operator=(const MockDelegate&) = delete;
+
+ ~MockDelegate() override = default;
+
+ MOCK_METHOD1(ShouldInterceptNavigation, bool(content::WebContents* contents));
+ MOCK_METHOD2(ProcessRequest,
+ void(ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url));
+ MOCK_METHOD2(ProcessResponse,
+ void(ResponseAdapter* response_adapter,
+ const GURL& redirect_url));
+
+ base::WeakPtr<MockDelegate> GetWeakPtr() {
+ return weak_factory_.GetWeakPtr();
+ }
+
+ private:
+ base::WeakPtrFactory<MockDelegate> weak_factory_{this};
+};
+
+content::WebContents::Getter NullWebContentsGetter() {
+ return base::BindRepeating([]() -> content::WebContents* { return nullptr; });
+}
+
+} // namespace
+
+class ChromeSigninProxyingURLLoaderFactoryTest : public testing::Test {
+ public:
+ ChromeSigninProxyingURLLoaderFactoryTest()
+ : test_factory_receiver_(&test_factory_) {}
+
+ ChromeSigninProxyingURLLoaderFactoryTest(
+ const ChromeSigninProxyingURLLoaderFactoryTest&) = delete;
+ ChromeSigninProxyingURLLoaderFactoryTest& operator=(
+ const ChromeSigninProxyingURLLoaderFactoryTest&) = delete;
+
+ ~ChromeSigninProxyingURLLoaderFactoryTest() override {}
+
+ base::WeakPtr<MockDelegate> StartRequest(
+ std::unique_ptr<network::ResourceRequest> request) {
+ loader_ = network::SimpleURLLoader::Create(std::move(request),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+
+ mojo::Remote<network::mojom::URLLoaderFactory> factory_remote;
+ auto factory_request = factory_remote.BindNewPipeAndPassReceiver();
+ loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
+ factory_remote.get(),
+ base::BindOnce(
+ &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete,
+ base::Unretained(this)));
+
+ auto delegate = std::make_unique<MockDelegate>();
+ base::WeakPtr<MockDelegate> delegate_weak = delegate->GetWeakPtr();
+
+ proxying_factory_ = std::make_unique<ProxyingURLLoaderFactory>(
+ std::move(delegate), NullWebContentsGetter(),
+ std::move(factory_request),
+ test_factory_receiver_.BindNewPipeAndPassRemote(),
+ base::BindOnce(&ChromeSigninProxyingURLLoaderFactoryTest::OnDisconnect,
+ base::Unretained(this)));
+
+ return delegate_weak;
+ }
+
+ void CloseFactoryReceiver() { test_factory_receiver_.reset(); }
+
+ network::TestURLLoaderFactory* factory() { return &test_factory_; }
+ network::SimpleURLLoader* loader() { return loader_.get(); }
+ std::string* response_body() { return response_body_.get(); }
+
+ void OnDownloadComplete(std::unique_ptr<std::string> body) {
+ response_body_ = std::move(body);
+ }
+
+ private:
+ void OnDisconnect(ProxyingURLLoaderFactory* factory) {
+ EXPECT_EQ(factory, proxying_factory_.get());
+ proxying_factory_.reset();
+ }
+
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<network::SimpleURLLoader> loader_;
+ std::unique_ptr<ProxyingURLLoaderFactory> proxying_factory_;
+ network::TestURLLoaderFactory test_factory_;
+ mojo::Receiver<network::mojom::URLLoaderFactory> test_factory_receiver_;
+ std::unique_ptr<std::string> response_body_;
+};
+
+TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, NoModification) {
+ auto request = std::make_unique<network::ResourceRequest>();
+ request->url = GURL("https://google.com/");
+
+ factory()->AddResponse("https://google.com/", "Hello.");
+ base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request));
+
+ base::RunLoop().RunUntilIdle();
+ EXPECT_EQ(net::OK, loader()->NetError());
+ ASSERT_TRUE(response_body());
+ EXPECT_EQ("Hello.", *response_body());
+}
+
+TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, ModifyHeaders) {
+ const GURL kTestURL("https://google.com/index.html");
+ const GURL kTestReferrer("https://chrome.com/referrer.html");
+ const GURL kTestRedirectURL("https://youtube.com/index.html");
+
+ // Set up the request.
+ auto request = std::make_unique<network::ResourceRequest>();
+ request->url = kTestURL;
+ request->referrer = kTestReferrer;
+ request->destination = network::mojom::RequestDestination::kDocument;
+ request->is_main_frame = true;
+ request->headers.SetHeader("X-Request-1", "Foo");
+
+ base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request));
+
+ // The first destruction callback added by ProcessRequest is expected to be
+ // called. The second (added after a redirect) will not be.
+ base::MockCallback<base::OnceClosure> destruction_callback;
+ EXPECT_CALL(destruction_callback, Run()).Times(1);
+ base::MockCallback<base::OnceClosure> ignored_destruction_callback;
+ EXPECT_CALL(ignored_destruction_callback, Run()).Times(0);
+
+ // The delegate will be called twice to process a request, first when the
+ // request is started and again when the request is redirected.
+ EXPECT_CALL(*delegate, ProcessRequest(_, _))
+ .WillOnce(
+ Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(kTestURL, adapter->GetUrl());
+ EXPECT_EQ(network::mojom::RequestDestination::kDocument,
+ adapter->GetRequestDestination());
+ EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin());
+
+ EXPECT_TRUE(adapter->HasHeader("X-Request-1"));
+ adapter->RemoveRequestHeaderByName("X-Request-1");
+ EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
+
+ adapter->SetExtraHeaderByName("X-Request-2", "Bar");
+ EXPECT_TRUE(adapter->HasHeader("X-Request-2"));
+
+ EXPECT_EQ(GURL(), redirect_url);
+
+ adapter->SetDestructionCallback(destruction_callback.Get());
+ }))
+ .WillOnce(
+ Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(network::mojom::RequestDestination::kDocument,
+ adapter->GetRequestDestination());
+
+ // Changes to the URL and referrer take effect after the redirect
+ // is followed.
+ EXPECT_EQ(kTestURL, adapter->GetUrl());
+ EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin());
+
+ // X-Request-1 and X-Request-2 were modified in the previous call to
+ // ProcessRequest(). These changes should still be present.
+ EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
+ EXPECT_TRUE(adapter->HasHeader("X-Request-2"));
+
+ adapter->RemoveRequestHeaderByName("X-Request-2");
+ EXPECT_FALSE(adapter->HasHeader("X-Request-2"));
+
+ adapter->SetExtraHeaderByName("X-Request-3", "Baz");
+ EXPECT_TRUE(adapter->HasHeader("X-Request-3"));
+
+ EXPECT_EQ(kTestRedirectURL, redirect_url);
+
+ adapter->SetDestructionCallback(ignored_destruction_callback.Get());
+ }));
+
+ const void* const kResponseUserDataKey = &kResponseUserDataKey;
+ std::unique_ptr<base::SupportsUserData::Data> response_user_data =
+ std::make_unique<base::SupportsUserData::Data>();
+ base::SupportsUserData::Data* response_user_data_ptr =
+ response_user_data.get();
+
+ // The delegate will also be called twice to process a response, first when
+ // the redirect is received and again for the redirect response.
+ EXPECT_CALL(*delegate, ProcessResponse(_, _))
+ .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin());
+ EXPECT_TRUE(adapter->IsMainFrame());
+
+ adapter->SetUserData(kResponseUserDataKey,
+ std::move(response_user_data));
+ EXPECT_EQ(response_user_data_ptr,
+ adapter->GetUserData(kResponseUserDataKey));
+
+ const net::HttpResponseHeaders* headers = adapter->GetHeaders();
+ EXPECT_TRUE(headers->HasHeader("X-Response-1"));
+ EXPECT_TRUE(headers->HasHeader("X-Response-2"));
+ adapter->RemoveHeader("X-Response-2");
+
+ EXPECT_EQ(kTestRedirectURL, redirect_url);
+ }))
+ .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin());
+ EXPECT_TRUE(adapter->IsMainFrame());
+
+ EXPECT_EQ(response_user_data_ptr,
+ adapter->GetUserData(kResponseUserDataKey));
+
+ const net::HttpResponseHeaders* headers = adapter->GetHeaders();
+ // This is a new response and so previous headers should not carry over.
+ EXPECT_FALSE(headers->HasHeader("X-Response-1"));
+ EXPECT_FALSE(headers->HasHeader("X-Response-2"));
+
+ EXPECT_TRUE(headers->HasHeader("X-Response-3"));
+ EXPECT_TRUE(headers->HasHeader("X-Response-4"));
+ adapter->RemoveHeader("X-Response-3");
+
+ EXPECT_EQ(GURL(), redirect_url);
+ }));
+
+ // Set up a redirect and final response.
+ {
+ net::RedirectInfo redirect_info;
+ redirect_info.new_url = kTestRedirectURL;
+ // An HTTPS to HTTPS redirect such as this wouldn't normally change the
+ // referrer but we do for testing purposes.
+ redirect_info.new_referrer = kTestURL.spec();
+
+ auto redirect_head = network::mojom::URLResponseHead::New();
+ redirect_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
+ redirect_head->headers->SetHeader("X-Response-1", "Foo");
+ redirect_head->headers->SetHeader("X-Response-2", "Bar");
+
+ auto response_head = network::mojom::URLResponseHead::New();
+ response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
+ response_head->headers->SetHeader("X-Response-3", "Foo");
+ response_head->headers->SetHeader("X-Response-4", "Bar");
+ std::string body("Hello.");
+ network::URLLoaderCompletionStatus status;
+ status.decoded_body_length = body.size();
+
+ network::TestURLLoaderFactory::Redirects redirects;
+ redirects.push_back({redirect_info, std::move(redirect_head)});
+
+ factory()->AddResponse(kTestURL, std::move(response_head), body, status,
+ std::move(redirects));
+ }
+
+ // Wait for the request to complete and check the response.
+ base::RunLoop().RunUntilIdle();
+ EXPECT_EQ(net::OK, loader()->NetError());
+ const network::mojom::URLResponseHead* response_head =
+ loader()->ResponseInfo();
+ ASSERT_TRUE(response_head && response_head->headers);
+ EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3"));
+ EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4"));
+ ASSERT_TRUE(response_body());
+ EXPECT_EQ("Hello.", *response_body());
+
+ // NOTE: TestURLLoaderFactory currently does not expose modifications to
+ // request headers and so we cannot verify that the modifications have been
+ // passed to the target URLLoader.
+}
+
+TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, TargetFactoryFailure) {
+ mojo::Remote<network::mojom::URLLoaderFactory> factory_remote;
+ mojo::PendingRemote<network::mojom::URLLoaderFactory>
+ pending_target_factory_remote;
+ auto target_factory_receiver =
+ pending_target_factory_remote.InitWithNewPipeAndPassReceiver();
+
+ // Without a target factory the proxy will process no requests.
+ auto delegate = std::make_unique<MockDelegate>();
+ EXPECT_CALL(*delegate, ProcessRequest(_, _)).Times(0);
+
+ auto proxying_factory = std::make_unique<ProxyingURLLoaderFactory>(
+ std::move(delegate), NullWebContentsGetter(),
+ factory_remote.BindNewPipeAndPassReceiver(),
+ std::move(pending_target_factory_remote), base::DoNothing());
+
+ // Close |target_factory_receiver| instead of binding it to a
+ // URLLoaderFactory. Spin the message loop so that the connection error
+ // handler can run.
+ target_factory_receiver = mojo::NullReceiver();
+ base::RunLoop().RunUntilIdle();
+
+ auto request = std::make_unique<network::ResourceRequest>();
+ request->url = GURL("https://google.com");
+ auto loader = network::SimpleURLLoader::Create(std::move(request),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+ loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
+ factory_remote.get(),
+ base::BindOnce(
+ &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete,
+ base::Unretained(this)));
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(response_body());
+ EXPECT_EQ(net::ERR_FAILED, loader->NetError());
+}
+
+TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, RequestKeepAlive) {
+ // Start the request.
+ auto request = std::make_unique<network::ResourceRequest>();
+ request->url = GURL("https://google.com");
+ base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request));
+ base::RunLoop().RunUntilIdle();
+
+ // Close the factory receiver and spin the message loop again to allow the
+ // connection error handler to be called.
+ CloseFactoryReceiver();
+ base::RunLoop().RunUntilIdle();
+
+ // The ProxyingURLLoaderFactory should not have been destroyed yet because
+ // there is still an in progress request that has not been completed.
+ EXPECT_TRUE(delegate);
+
+ // Complete the request.
+ factory()->AddResponse("https://google.com", "Hello.");
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(delegate);
+ EXPECT_EQ(net::OK, loader()->NetError());
+ ASSERT_TRUE(response_body());
+ EXPECT_EQ("Hello.", *response_body());
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc
new file mode 100644
index 00000000000..63fe0ae0a6b
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc
@@ -0,0 +1,141 @@
+// 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 "chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h"
+
+#include <string>
+#include <vector>
+
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/signin/core/browser/signin_status_metrics_provider.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+#endif
+
+ChromeSigninStatusMetricsProviderDelegate::
+ ChromeSigninStatusMetricsProviderDelegate() {}
+
+ChromeSigninStatusMetricsProviderDelegate::
+ ~ChromeSigninStatusMetricsProviderDelegate() {
+#if !defined(OS_ANDROID)
+ BrowserList::RemoveObserver(this);
+#endif
+
+ auto* factory = IdentityManagerFactory::GetInstance();
+ if (factory)
+ factory->RemoveObserver(this);
+}
+
+void ChromeSigninStatusMetricsProviderDelegate::Initialize() {
+#if !defined(OS_ANDROID)
+ // On Android, there is always only one profile in any situation, opening new
+ // windows (which is possible with only some Android devices) will not change
+ // the opened profiles signin status.
+ BrowserList::AddObserver(this);
+#endif
+
+ auto* factory = IdentityManagerFactory::GetInstance();
+ if (factory)
+ factory->AddObserver(this);
+}
+
+AccountsStatus
+ChromeSigninStatusMetricsProviderDelegate::GetStatusOfAllAccounts() {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ std::vector<Profile*> profile_list = profile_manager->GetLoadedProfiles();
+
+ AccountsStatus accounts_status;
+ accounts_status.num_accounts = profile_list.size();
+ for (Profile* profile : profile_list) {
+#if !defined(OS_ANDROID)
+ if (chrome::GetBrowserCount(profile) == 0) {
+ // The profile is loaded, but there's no opened browser for this profile.
+ continue;
+ }
+#endif
+ accounts_status.num_opened_accounts++;
+
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile->GetOriginalProfile());
+ if (identity_manager &&
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ accounts_status.num_signed_in_accounts++;
+ }
+ }
+
+ return accounts_status;
+}
+
+std::vector<signin::IdentityManager*>
+ChromeSigninStatusMetricsProviderDelegate::GetIdentityManagersForAllAccounts() {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ std::vector<Profile*> profiles = profile_manager->GetLoadedProfiles();
+
+ std::vector<signin::IdentityManager*> managers;
+ for (Profile* profile : profiles) {
+ auto* identity_manager =
+ IdentityManagerFactory::GetForProfileIfExists(profile);
+ if (identity_manager)
+ managers.push_back(identity_manager);
+ }
+
+ return managers;
+}
+
+#if !defined(OS_ANDROID)
+void ChromeSigninStatusMetricsProviderDelegate::OnBrowserAdded(
+ Browser* browser) {
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(browser->profile());
+
+ // Nothing will change if the opened browser is in incognito mode.
+ if (!identity_manager)
+ return;
+
+ const bool signed_in =
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync);
+ UpdateStatusWhenBrowserAdded(signed_in);
+}
+#endif
+
+void ChromeSigninStatusMetricsProviderDelegate::IdentityManagerCreated(
+ signin::IdentityManager* identity_manager) {
+ owner()->OnIdentityManagerCreated(identity_manager);
+}
+
+void ChromeSigninStatusMetricsProviderDelegate::UpdateStatusWhenBrowserAdded(
+ bool signed_in) {
+#if !defined(OS_ANDROID)
+ SigninStatusMetricsProviderBase::SigninStatus status =
+ owner()->signin_status();
+
+ // NOTE: If |status| is MIXED_SIGNIN_STATUS, this method
+ // intentionally does not update it.
+ if ((status == SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN &&
+ signed_in) ||
+ (status == SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN &&
+ !signed_in)) {
+ owner()->UpdateSigninStatus(
+ SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS);
+ } else if (status == SigninStatusMetricsProviderBase::UNKNOWN_SIGNIN_STATUS) {
+ // If when function ProvideCurrentSessionData() is called, Chrome is
+ // running in the background with no browser window opened, |signin_status_|
+ // will be reset to |UNKNOWN_SIGNIN_STATUS|. Then this newly added browser
+ // is the only opened browser/profile and its signin status represents
+ // the whole status.
+ owner()->UpdateSigninStatus(
+ signed_in
+ ? SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN
+ : SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN);
+ }
+#endif
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h
new file mode 100644
index 00000000000..800d5f8e3f5
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h
@@ -0,0 +1,58 @@
+// 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_
+
+#include <vector>
+
+#include "base/gtest_prod_util.h"
+#include "build/build_config.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/signin/core/browser/signin_status_metrics_provider_delegate.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/ui/browser_list_observer.h"
+#endif // !defined(OS_ANDROID)
+
+class ChromeSigninStatusMetricsProviderDelegate
+ : public SigninStatusMetricsProviderDelegate,
+#if !defined(OS_ANDROID)
+ public BrowserListObserver,
+#endif
+ public IdentityManagerFactory::Observer {
+ public:
+ ChromeSigninStatusMetricsProviderDelegate();
+
+ ChromeSigninStatusMetricsProviderDelegate(
+ const ChromeSigninStatusMetricsProviderDelegate&) = delete;
+ ChromeSigninStatusMetricsProviderDelegate& operator=(
+ const ChromeSigninStatusMetricsProviderDelegate&) = delete;
+
+ ~ChromeSigninStatusMetricsProviderDelegate() override;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(ChromeSigninStatusMetricsProviderDelegateTest,
+ UpdateStatusWhenBrowserAdded);
+
+ // SigninStatusMetricsProviderDelegate:
+ void Initialize() override;
+ AccountsStatus GetStatusOfAllAccounts() override;
+ std::vector<signin::IdentityManager*> GetIdentityManagersForAllAccounts()
+ override;
+
+#if !defined(OS_ANDROID)
+ // BrowserListObserver:
+ void OnBrowserAdded(Browser* browser) override;
+#endif
+
+ // IdentityManagerFactoryObserver:
+ void IdentityManagerCreated(
+ signin::IdentityManager* identity_manager) override;
+
+ // Updates the sign-in status right after a new browser is opened.
+ void UpdateStatusWhenBrowserAdded(bool signed_in);
+};
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc
new file mode 100644
index 00000000000..fd83b3aada5
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc
@@ -0,0 +1,61 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h"
+
+#include <utility>
+
+#include "build/build_config.h"
+#include "components/signin/core/browser/signin_status_metrics_provider.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if !defined(OS_ANDROID)
+TEST(ChromeSigninStatusMetricsProviderDelegateTest,
+ UpdateStatusWhenBrowserAdded) {
+ content::BrowserTaskEnvironment task_environment;
+
+ std::unique_ptr<ChromeSigninStatusMetricsProviderDelegate> delegate(
+ new ChromeSigninStatusMetricsProviderDelegate);
+ ChromeSigninStatusMetricsProviderDelegate* raw_delegate = delegate.get();
+ std::unique_ptr<SigninStatusMetricsProvider> metrics_provider =
+ SigninStatusMetricsProvider::CreateInstance(std::move(delegate));
+
+ // Initial status is all signed in and then a signed-in browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 2);
+ raw_delegate->UpdateStatusWhenBrowserAdded(true);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is all signed in and then a signed-out browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 2);
+ raw_delegate->UpdateStatusWhenBrowserAdded(false);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is all signed out and then a signed-in browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 0);
+ raw_delegate->UpdateStatusWhenBrowserAdded(true);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is all signed out and then a signed-out browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 0);
+ raw_delegate->UpdateStatusWhenBrowserAdded(false);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is mixed and then a signed-in browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 1);
+ raw_delegate->UpdateStatusWhenBrowserAdded(true);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is mixed and then a signed-out browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 1);
+ raw_delegate->UpdateStatusWhenBrowserAdded(false);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS,
+ metrics_provider->GetSigninStatusForTesting());
+}
+#endif
diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc
new file mode 100644
index 00000000000..528fc7b6d44
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc
@@ -0,0 +1,189 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_url_loader_throttle.h"
+
+#include "base/memory/ptr_util.h"
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+
+namespace signin {
+
+class URLLoaderThrottle::ThrottleRequestAdapter : public ChromeRequestAdapter {
+ public:
+ ThrottleRequestAdapter(URLLoaderThrottle* throttle,
+ const net::HttpRequestHeaders& original_headers,
+ net::HttpRequestHeaders* modified_headers,
+ std::vector<std::string>* headers_to_remove)
+ : ChromeRequestAdapter(throttle->request_url_,
+ original_headers,
+ modified_headers,
+ headers_to_remove),
+ throttle_(throttle) {}
+
+ ThrottleRequestAdapter(const ThrottleRequestAdapter&) = delete;
+ ThrottleRequestAdapter& operator=(const ThrottleRequestAdapter&) = delete;
+
+ ~ThrottleRequestAdapter() override = default;
+
+ // ChromeRequestAdapter
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return throttle_->web_contents_getter_;
+ }
+
+ network::mojom::RequestDestination GetRequestDestination() const override {
+ return throttle_->request_destination_;
+ }
+
+ bool IsFetchLikeAPI() const override {
+ return throttle_->request_is_fetch_like_api_;
+ }
+
+ GURL GetReferrerOrigin() const override {
+ return throttle_->request_referrer_.DeprecatedGetOriginAsURL();
+ }
+
+ void SetDestructionCallback(base::OnceClosure closure) override {
+ if (!throttle_->destruction_callback_)
+ throttle_->destruction_callback_ = std::move(closure);
+ }
+
+ private:
+ const raw_ptr<URLLoaderThrottle> throttle_;
+};
+
+class URLLoaderThrottle::ThrottleResponseAdapter : public ResponseAdapter {
+ public:
+ ThrottleResponseAdapter(URLLoaderThrottle* throttle,
+ net::HttpResponseHeaders* headers)
+ : throttle_(throttle), headers_(headers) {}
+
+ ThrottleResponseAdapter(const ThrottleResponseAdapter&) = delete;
+ ThrottleResponseAdapter& operator=(const ThrottleResponseAdapter&) = delete;
+
+ ~ThrottleResponseAdapter() override = default;
+
+ // ResponseAdapter
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return throttle_->web_contents_getter_;
+ }
+
+ bool IsMainFrame() const override {
+ return throttle_->request_destination_ ==
+ network::mojom::RequestDestination::kDocument;
+ }
+
+ GURL GetOrigin() const override {
+ return throttle_->request_url_.DeprecatedGetOriginAsURL();
+ }
+
+ const net::HttpResponseHeaders* GetHeaders() const override {
+ return headers_;
+ }
+
+ void RemoveHeader(const std::string& name) override {
+ headers_->RemoveHeader(name);
+ }
+
+ base::SupportsUserData::Data* GetUserData(const void* key) const override {
+ return throttle_->GetUserData(key);
+ }
+
+ void SetUserData(
+ const void* key,
+ std::unique_ptr<base::SupportsUserData::Data> data) override {
+ throttle_->SetUserData(key, std::move(data));
+ }
+
+ private:
+ const raw_ptr<URLLoaderThrottle> throttle_;
+ raw_ptr<net::HttpResponseHeaders> headers_;
+};
+
+// static
+std::unique_ptr<URLLoaderThrottle> URLLoaderThrottle::MaybeCreate(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter) {
+ if (!delegate->ShouldInterceptNavigation(web_contents_getter.Run()))
+ return nullptr;
+
+ return base::WrapUnique(new URLLoaderThrottle(
+ std::move(delegate), std::move(web_contents_getter)));
+}
+
+URLLoaderThrottle::~URLLoaderThrottle() {
+ if (destruction_callback_)
+ std::move(destruction_callback_).Run();
+}
+
+void URLLoaderThrottle::WillStartRequest(network::ResourceRequest* request,
+ bool* defer) {
+ request_url_ = request->url;
+ request_referrer_ = request->referrer;
+ request_destination_ = request->destination;
+ request_is_fetch_like_api_ = request->is_fetch_like_api;
+
+ net::HttpRequestHeaders modified_request_headers;
+ std::vector<std::string> to_be_removed_request_headers;
+
+ ThrottleRequestAdapter adapter(this, request->headers,
+ &modified_request_headers,
+ &to_be_removed_request_headers);
+ delegate_->ProcessRequest(&adapter, GURL() /* redirect_url */);
+
+ request->headers.MergeFrom(modified_request_headers);
+ for (const std::string& name : to_be_removed_request_headers)
+ request->headers.RemoveHeader(name);
+
+ // We need to keep a full copy of the request headers for later calls to
+ // FixAccountConsistencyRequestHeader. Perhaps this could be replaced with
+ // more specific per-request state.
+ request_headers_.CopyFrom(request->headers);
+ request_cors_exempt_headers_.CopyFrom(request->cors_exempt_headers);
+}
+
+void URLLoaderThrottle::WillRedirectRequest(
+ net::RedirectInfo* redirect_info,
+ const network::mojom::URLResponseHead& response_head,
+ bool* /* defer */,
+ std::vector<std::string>* to_be_removed_request_headers,
+ net::HttpRequestHeaders* modified_request_headers,
+ net::HttpRequestHeaders* modified_cors_exempt_request_headers) {
+ ThrottleRequestAdapter request_adapter(this, request_headers_,
+ modified_request_headers,
+ to_be_removed_request_headers);
+ delegate_->ProcessRequest(&request_adapter, redirect_info->new_url);
+
+ request_headers_.MergeFrom(*modified_request_headers);
+ for (const std::string& name : *to_be_removed_request_headers)
+ request_headers_.RemoveHeader(name);
+
+ // Modifications to |response_head.headers| will be passed to the
+ // URLLoaderClient even though |response_head| is const.
+ ThrottleResponseAdapter response_adapter(this, response_head.headers.get());
+ delegate_->ProcessResponse(&response_adapter, redirect_info->new_url);
+
+ request_url_ = redirect_info->new_url;
+ request_referrer_ = GURL(redirect_info->new_referrer);
+}
+
+void URLLoaderThrottle::WillProcessResponse(
+ const GURL& response_url,
+ network::mojom::URLResponseHead* response_head,
+ bool* defer) {
+ ThrottleResponseAdapter adapter(this, response_head->headers.get());
+ delegate_->ProcessResponse(&adapter, GURL() /* redirect_url */);
+}
+
+URLLoaderThrottle::URLLoaderThrottle(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter)
+ : delegate_(std::move(delegate)),
+ web_contents_getter_(std::move(web_contents_getter)) {}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h
new file mode 100644
index 00000000000..352a84291d7
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h
@@ -0,0 +1,72 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_
+
+#include "base/supports_user_data.h"
+#include "content/public/browser/web_contents.h"
+#include "net/http/http_request_headers.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+#include "third_party/blink/public/common/loader/url_loader_throttle.h"
+
+namespace signin {
+
+class HeaderModificationDelegate;
+
+// This class is used to modify the main frame request made when loading the
+// GAIA signin realm.
+class URLLoaderThrottle : public blink::URLLoaderThrottle,
+ public base::SupportsUserData {
+ public:
+ // Creates a new throttle if |delegate| says that this request should be
+ // intercepted.
+ static std::unique_ptr<URLLoaderThrottle> MaybeCreate(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter);
+
+ URLLoaderThrottle(const URLLoaderThrottle&) = delete;
+ URLLoaderThrottle& operator=(const URLLoaderThrottle&) = delete;
+
+ ~URLLoaderThrottle() override;
+
+ // blink::URLLoaderThrottle
+ void WillStartRequest(network::ResourceRequest* request,
+ bool* defer) override;
+ void WillRedirectRequest(
+ net::RedirectInfo* redirect_info,
+ const network::mojom::URLResponseHead& response_head,
+ bool* defer,
+ std::vector<std::string>* headers_to_remove,
+ net::HttpRequestHeaders* modified_headers,
+ net::HttpRequestHeaders* modified_cors_exempt_headers) override;
+ void WillProcessResponse(const GURL& response_url,
+ network::mojom::URLResponseHead* response_head,
+ bool* defer) override;
+
+ private:
+ class ThrottleRequestAdapter;
+ class ThrottleResponseAdapter;
+
+ URLLoaderThrottle(std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter);
+
+ const std::unique_ptr<HeaderModificationDelegate> delegate_;
+ const content::WebContents::Getter web_contents_getter_;
+
+ // Information about the current request.
+ GURL request_url_;
+ GURL request_referrer_;
+ net::HttpRequestHeaders request_headers_;
+ net::HttpRequestHeaders request_cors_exempt_headers_;
+ network::mojom::RequestDestination request_destination_ =
+ network::mojom::RequestDestination::kEmpty;
+ bool request_is_fetch_like_api_ = false;
+
+ base::OnceClosure destruction_callback_;
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc
new file mode 100644
index 00000000000..f28486ee18a
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc
@@ -0,0 +1,281 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/chrome_signin_url_loader_throttle.h"
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/test/mock_callback.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::ElementsAre;
+using testing::Invoke;
+using testing::Return;
+using testing::_;
+
+namespace signin {
+
+namespace {
+
+class MockDelegate : public HeaderModificationDelegate {
+ public:
+ MockDelegate() = default;
+
+ MockDelegate(const MockDelegate&) = delete;
+ MockDelegate& operator=(const MockDelegate&) = delete;
+
+ ~MockDelegate() override = default;
+
+ MOCK_METHOD1(ShouldInterceptNavigation, bool(content::WebContents* contents));
+ MOCK_METHOD2(ProcessRequest,
+ void(ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url));
+ MOCK_METHOD2(ProcessResponse,
+ void(ResponseAdapter* response_adapter,
+ const GURL& redirect_url));
+};
+
+content::WebContents::Getter NullWebContentsGetter() {
+ return base::BindRepeating([]() -> content::WebContents* { return nullptr; });
+}
+
+} // namespace
+
+TEST(ChromeSigninURLLoaderThrottleTest, NoIntercept) {
+ auto* delegate = new MockDelegate();
+
+ EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(false));
+ EXPECT_FALSE(URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
+ NullWebContentsGetter()));
+}
+
+TEST(ChromeSigninURLLoaderThrottleTest, Intercept) {
+ auto* delegate = new MockDelegate();
+ EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(true));
+ auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
+ NullWebContentsGetter());
+ ASSERT_TRUE(throttle);
+
+ // Phase 1: Start the request.
+
+ const GURL kTestURL("https://google.com/index.html");
+ const GURL kTestReferrer("https://chrome.com/referrer.html");
+ base::MockCallback<base::OnceClosure> destruction_callback;
+ EXPECT_CALL(*delegate, ProcessRequest(_, _))
+ .WillOnce(
+ Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(kTestURL, adapter->GetUrl());
+ EXPECT_EQ(network::mojom::RequestDestination::kDocument,
+ adapter->GetRequestDestination());
+ EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin());
+
+ EXPECT_TRUE(adapter->HasHeader("X-Request-1"));
+ adapter->RemoveRequestHeaderByName("X-Request-1");
+ EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
+
+ adapter->SetExtraHeaderByName("X-Request-2", "Bar");
+ EXPECT_TRUE(adapter->HasHeader("X-Request-2"));
+
+ EXPECT_EQ(GURL(), redirect_url);
+
+ adapter->SetDestructionCallback(destruction_callback.Get());
+ }));
+
+ network::ResourceRequest request;
+ request.url = kTestURL;
+ request.referrer = kTestReferrer;
+ request.destination = network::mojom::RequestDestination::kDocument;
+ request.headers.SetHeader("X-Request-1", "Foo");
+ bool defer = false;
+ throttle->WillStartRequest(&request, &defer);
+
+ EXPECT_FALSE(request.headers.HasHeader("X-Request-1"));
+ std::string value;
+ EXPECT_TRUE(request.headers.GetHeader("X-Request-2", &value));
+ EXPECT_EQ("Bar", value);
+
+ EXPECT_FALSE(defer);
+
+ testing::Mock::VerifyAndClearExpectations(delegate);
+
+ // Phase 2: Redirect the request.
+
+ const GURL kTestRedirectURL("https://youtube.com/index.html");
+ const void* const kResponseUserDataKey = &kResponseUserDataKey;
+ std::unique_ptr<base::SupportsUserData::Data> response_user_data =
+ std::make_unique<base::SupportsUserData::Data>();
+ base::SupportsUserData::Data* response_user_data_ptr =
+ response_user_data.get();
+
+ EXPECT_CALL(*delegate, ProcessResponse(_, _))
+ .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin());
+ EXPECT_TRUE(adapter->IsMainFrame());
+
+ adapter->SetUserData(kResponseUserDataKey,
+ std::move(response_user_data));
+ EXPECT_EQ(response_user_data_ptr,
+ adapter->GetUserData(kResponseUserDataKey));
+
+ const net::HttpResponseHeaders* headers = adapter->GetHeaders();
+ EXPECT_TRUE(headers->HasHeader("X-Response-1"));
+ EXPECT_TRUE(headers->HasHeader("X-Response-2"));
+ adapter->RemoveHeader("X-Response-2");
+
+ EXPECT_EQ(kTestRedirectURL, redirect_url);
+ }));
+
+ base::MockCallback<base::OnceClosure> ignored_destruction_callback;
+ EXPECT_CALL(*delegate, ProcessRequest(_, _))
+ .WillOnce(
+ Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(network::mojom::RequestDestination::kDocument,
+ adapter->GetRequestDestination());
+
+ // Changes to the URL and referrer take effect after the redirect
+ // is followed.
+ EXPECT_EQ(kTestURL, adapter->GetUrl());
+ EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin());
+
+ // X-Request-1 and X-Request-2 were modified in the previous call to
+ // ProcessRequest(). These changes should still be present.
+ EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
+ EXPECT_TRUE(adapter->HasHeader("X-Request-2"));
+
+ adapter->RemoveRequestHeaderByName("X-Request-2");
+ EXPECT_FALSE(adapter->HasHeader("X-Request-2"));
+
+ adapter->SetExtraHeaderByName("X-Request-3", "Baz");
+ EXPECT_TRUE(adapter->HasHeader("X-Request-3"));
+
+ EXPECT_EQ(kTestRedirectURL, redirect_url);
+
+ adapter->SetDestructionCallback(ignored_destruction_callback.Get());
+ }));
+
+ net::RedirectInfo redirect_info;
+ redirect_info.new_url = kTestRedirectURL;
+ // An HTTPS to HTTPS redirect such as this wouldn't normally change the
+ // referrer but we do for testing purposes.
+ redirect_info.new_referrer = kTestURL.spec();
+
+ auto response_head = network::mojom::URLResponseHead::New();
+ response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
+ response_head->headers->SetHeader("X-Response-1", "Foo");
+ response_head->headers->SetHeader("X-Response-2", "Bar");
+
+ std::vector<std::string> request_headers_to_remove;
+ net::HttpRequestHeaders modified_request_headers;
+ net::HttpRequestHeaders modified_cors_exempt_request_headers;
+ throttle->WillRedirectRequest(
+ &redirect_info, *response_head, &defer, &request_headers_to_remove,
+ &modified_request_headers, &modified_cors_exempt_request_headers);
+
+ EXPECT_FALSE(defer);
+
+ EXPECT_TRUE(response_head->headers->HasHeader("X-Response-1"));
+ EXPECT_FALSE(response_head->headers->HasHeader("X-Response-2"));
+
+ EXPECT_THAT(request_headers_to_remove, ElementsAre("X-Request-2"));
+ EXPECT_TRUE(modified_request_headers.GetHeader("X-Request-3", &value));
+ EXPECT_EQ("Baz", value);
+
+ EXPECT_TRUE(modified_cors_exempt_request_headers.IsEmpty());
+
+ testing::Mock::VerifyAndClearExpectations(delegate);
+
+ // Phase 3: Complete the request.
+
+ EXPECT_CALL(*delegate, ProcessResponse(_, _))
+ .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin());
+ EXPECT_TRUE(adapter->IsMainFrame());
+
+ EXPECT_EQ(response_user_data_ptr,
+ adapter->GetUserData(kResponseUserDataKey));
+
+ const net::HttpResponseHeaders* headers = adapter->GetHeaders();
+ // This is a new response and so previous headers should not carry over.
+ EXPECT_FALSE(headers->HasHeader("X-Response-1"));
+ EXPECT_FALSE(headers->HasHeader("X-Response-2"));
+
+ EXPECT_TRUE(headers->HasHeader("X-Response-3"));
+ EXPECT_TRUE(headers->HasHeader("X-Response-4"));
+ adapter->RemoveHeader("X-Response-3");
+
+ EXPECT_EQ(GURL(), redirect_url);
+ }));
+
+ response_head = network::mojom::URLResponseHead::New();
+ response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
+ response_head->headers->SetHeader("X-Response-3", "Foo");
+ response_head->headers->SetHeader("X-Response-4", "Bar");
+
+ throttle->WillProcessResponse(kTestRedirectURL, response_head.get(), &defer);
+
+ EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3"));
+ EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4"));
+
+ EXPECT_FALSE(defer);
+
+ EXPECT_CALL(destruction_callback, Run()).Times(1);
+ EXPECT_CALL(ignored_destruction_callback, Run()).Times(0);
+ throttle.reset();
+}
+
+TEST(ChromeSigninURLLoaderThrottleTest, InterceptSubFrame) {
+ auto* delegate = new MockDelegate();
+ EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(true));
+ auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
+ NullWebContentsGetter());
+ ASSERT_TRUE(throttle);
+
+ EXPECT_CALL(*delegate, ProcessRequest(_, _))
+ .Times(2)
+ .WillRepeatedly(
+ [](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(network::mojom::RequestDestination::kIframe,
+ adapter->GetRequestDestination());
+ });
+
+ network::ResourceRequest request;
+ request.url = GURL("https://google.com");
+ request.destination = network::mojom::RequestDestination::kIframe;
+
+ bool defer = false;
+ throttle->WillStartRequest(&request, &defer);
+ EXPECT_FALSE(defer);
+
+ EXPECT_CALL(*delegate, ProcessResponse(_, _))
+ .Times(2)
+ .WillRepeatedly(([](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_FALSE(adapter->IsMainFrame());
+ }));
+
+ net::RedirectInfo redirect_info;
+ redirect_info.new_url = GURL("https://youtube.com");
+ auto response_head = network::mojom::URLResponseHead::New();
+
+ std::vector<std::string> request_headers_to_remove;
+ net::HttpRequestHeaders modified_request_headers;
+ net::HttpRequestHeaders modified_cors_exempt_request_headers;
+ throttle->WillRedirectRequest(
+ &redirect_info, *response_head, &defer, &request_headers_to_remove,
+ &modified_request_headers, &modified_cors_exempt_request_headers);
+ EXPECT_FALSE(defer);
+ EXPECT_TRUE(request_headers_to_remove.empty());
+ EXPECT_TRUE(modified_request_headers.IsEmpty());
+ EXPECT_TRUE(modified_cors_exempt_request_headers.IsEmpty());
+
+ throttle->WillProcessResponse(GURL("https://youtube.com"),
+ response_head.get(), &defer);
+ EXPECT_FALSE(defer);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc b/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc
new file mode 100644
index 00000000000..b5b9b6a5379
--- /dev/null
+++ b/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc
@@ -0,0 +1,174 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ash/login/login_manager_test.h"
+#include "chrome/browser/ash/login/test/login_manager_mixin.h"
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#include "chrome/browser/prefs/incognito_mode_prefs.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_key.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/supervised_user/supervised_user_constants.h"
+#include "chrome/browser/supervised_user/supervised_user_settings_service.h"
+#include "chrome/browser/supervised_user/supervised_user_settings_service_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/account_id/account_id.h"
+#include "components/google/core/common/google_switches.h"
+#include "components/network_session_configurator/common/network_switches.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/user_manager/user.h"
+#include "components/user_manager/user_manager.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "net/test/embedded_test_server/default_handlers.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace {
+
+constexpr char kGaiaDomain[] = "accounts.google.com";
+
+// Checks whether the "X-Chrome-Connected" header of a new request to Google
+// contains |expected_header_value|.
+void TestMirrorRequestForProfile(net::EmbeddedTestServer* test_server,
+ Profile* profile,
+ const std::string& expected_header_value) {
+ GURL gaia_url(test_server->GetURL("/echoheader?X-Chrome-Connected"));
+ GURL::Replacements replace_host;
+ replace_host.SetHostStr(kGaiaDomain);
+ gaia_url = gaia_url.ReplaceComponents(replace_host);
+
+ Browser* browser = Browser::Create(Browser::CreateParams(profile, true));
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser, gaia_url, WindowOpenDisposition::SINGLETON_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+
+ std::string inner_text;
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ browser->tab_strip_model()->GetActiveWebContents(),
+ "domAutomationController.send(document.body.innerText);", &inner_text));
+ // /echoheader returns "None" if the header isn't set.
+ inner_text = (inner_text == "None") ? "" : inner_text;
+ EXPECT_EQ(expected_header_value, inner_text);
+}
+
+} // namespace
+
+// This is a Chrome OS-only test ensuring that mirror account consistency is
+// enabled for child accounts, but not enabled for other account types.
+class ChromeOsMirrorAccountConsistencyTest : public ash::LoginManagerTest {
+ public:
+ ChromeOsMirrorAccountConsistencyTest(
+ const ChromeOsMirrorAccountConsistencyTest&) = delete;
+ ChromeOsMirrorAccountConsistencyTest& operator=(
+ const ChromeOsMirrorAccountConsistencyTest&) = delete;
+
+ protected:
+ ~ChromeOsMirrorAccountConsistencyTest() override {}
+
+ ChromeOsMirrorAccountConsistencyTest() : LoginManagerTest() {
+ login_mixin_.AppendRegularUsers(1);
+ account_id_ = login_mixin_.users()[0].account_id;
+ }
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ ash::LoginManagerTest::SetUpCommandLine(command_line);
+
+ // HTTPS server only serves a valid cert for localhost, so this is needed to
+ // load pages from "www.google.com" without an interstitial.
+ command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
+
+ // The production code only allows known ports (80 for http and 443 for
+ // https), but the test server runs on a random port.
+ command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers);
+ }
+
+ void SetUpOnMainThread() override {
+ // We can't use BrowserTestBase's EmbeddedTestServer because google.com
+ // URL's have to be https.
+ test_server_ = std::make_unique<net::EmbeddedTestServer>(
+ net::EmbeddedTestServer::TYPE_HTTPS);
+ net::test_server::RegisterDefaultHandlers(test_server_.get());
+ ASSERT_TRUE(test_server_->Start());
+
+ ash::LoginManagerTest::SetUpOnMainThread();
+ }
+
+ AccountId account_id_;
+ ash::LoginManagerMixin login_mixin_{&mixin_host_};
+
+ protected:
+ std::unique_ptr<net::EmbeddedTestServer> test_server_;
+};
+
+// Mirror is enabled for child accounts.
+IN_PROC_BROWSER_TEST_F(ChromeOsMirrorAccountConsistencyTest,
+ TestMirrorRequestChromeOsChildAccount) {
+ // Child user.
+ LoginUser(account_id_);
+
+ user_manager::User* user = user_manager::UserManager::Get()->GetActiveUser();
+ ASSERT_EQ(user, user_manager::UserManager::Get()->GetPrimaryUser());
+ ASSERT_EQ(user, user_manager::UserManager::Get()->FindUser(account_id_));
+ Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(user);
+
+ // Supervised flag uses `FindExtendedAccountInfoForAccountWithRefreshToken`,
+ // so wait for tokens to be loaded.
+ signin::WaitForRefreshTokensLoaded(
+ IdentityManagerFactory::GetForProfile(profile));
+
+ SupervisedUserSettingsService* supervised_user_settings_service =
+ SupervisedUserSettingsServiceFactory::GetForKey(profile->GetProfileKey());
+ supervised_user_settings_service->SetActive(true);
+
+ // Incognito is always disabled for child accounts.
+ PrefService* prefs = profile->GetPrefs();
+ prefs->SetInteger(
+ prefs::kIncognitoModeAvailability,
+ static_cast<int>(IncognitoModePrefs::Availability::kDisabled));
+ ASSERT_EQ(1, signin::PROFILE_MODE_INCOGNITO_DISABLED);
+
+ // TODO(http://crbug.com/1134144): This test seems to test supervised profiles
+ // instead of child accounts. With the current implementation,
+ // X-Chrome-Connected header gets a supervised=true argument only for child
+ // profiles. Verify if these tests needs to be updated to use child accounts
+ // or whether supervised profiles need to be supported as well.
+ TestMirrorRequestForProfile(
+ test_server_.get(), profile,
+ "source=Chrome,mode=1,enable_account_consistency=true,supervised=false,"
+ "consistency_enabled_by_default=false");
+}
+
+// Mirror is enabled for non-child accounts.
+IN_PROC_BROWSER_TEST_F(ChromeOsMirrorAccountConsistencyTest,
+ TestMirrorRequestChromeOsNotChildAccount) {
+ // Not a child user.
+ LoginUser(account_id_);
+
+ user_manager::User* user = user_manager::UserManager::Get()->GetActiveUser();
+ ASSERT_EQ(user, user_manager::UserManager::Get()->GetPrimaryUser());
+ ASSERT_EQ(user, user_manager::UserManager::Get()->FindUser(account_id_));
+ Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(user);
+
+ // Supervised flag uses `FindExtendedAccountInfoForAccountWithRefreshToken`,
+ // so wait for tokens to be loaded.
+ signin::WaitForRefreshTokensLoaded(
+ IdentityManagerFactory::GetForProfile(profile));
+
+ // With Chrome OS Account Manager enabled, this should be true.
+ EXPECT_TRUE(
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile));
+ TestMirrorRequestForProfile(
+ test_server_.get(), profile,
+ "source=Chrome,mode=0,enable_account_consistency=true,supervised=false,"
+ "consistency_enabled_by_default=false");
+}
diff --git a/chromium/chrome/browser/signin/cookie_reminter_factory.cc b/chromium/chrome/browser/signin/cookie_reminter_factory.cc
new file mode 100644
index 00000000000..f53de57d249
--- /dev/null
+++ b/chromium/chrome/browser/signin/cookie_reminter_factory.cc
@@ -0,0 +1,37 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/cookie_reminter_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/signin/core/browser/cookie_reminter.h"
+
+CookieReminterFactory::CookieReminterFactory()
+ : BrowserContextKeyedServiceFactory(
+ "CookieReminter",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+CookieReminterFactory::~CookieReminterFactory() {}
+
+// static
+CookieReminter* CookieReminterFactory::GetForProfile(Profile* profile) {
+ return static_cast<CookieReminter*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+CookieReminterFactory* CookieReminterFactory::GetInstance() {
+ return base::Singleton<CookieReminterFactory>::get();
+}
+
+KeyedService* CookieReminterFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ return new CookieReminter(identity_manager);
+}
diff --git a/chromium/chrome/browser/signin/cookie_reminter_factory.h b/chromium/chrome/browser/signin/cookie_reminter_factory.h
new file mode 100644
index 00000000000..8ce4768bebd
--- /dev/null
+++ b/chromium/chrome/browser/signin/cookie_reminter_factory.h
@@ -0,0 +1,30 @@
+// Copyright 2019 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class CookieReminter;
+class Profile;
+
+class CookieReminterFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static CookieReminter* GetForProfile(Profile* profile);
+ static CookieReminterFactory* GetInstance();
+
+ private:
+ friend struct base::DefaultSingletonTraits<CookieReminterFactory>;
+
+ CookieReminterFactory();
+ ~CookieReminterFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/dice_browsertest.cc b/chromium/chrome/browser/signin/dice_browsertest.cc
new file mode 100644
index 00000000000..150647b8b0b
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_browsertest.cc
@@ -0,0 +1,1236 @@
+// Copyright 2017 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 <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/auto_reset.h"
+#include "base/base_switches.h"
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/check.h"
+#include "base/command_line.h"
+#include "base/location.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/test_mock_time_task_runner.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "chrome/browser/apps/platform_apps/shortcut_manager.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/extensions/api/identity/web_auth_flow.h"
+#include "chrome/browser/policy/cloud/user_policy_signin_service.h"
+#include "chrome/browser/policy/cloud/user_policy_signin_service_internal.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/chrome_device_id_helper.h"
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/dice_response_handler.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/sync/user_event_service_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/profile_chooser_constants.h"
+#include "chrome/browser/ui/simple_message_box_internal.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
+#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/common/webui_url_constants.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/prefs/pref_service.h"
+#include "components/search/ntp_features.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/dice_header_helper.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_client.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "components/sync/base/sync_prefs.h"
+#include "components/sync_user_events/user_event_service.h"
+#include "components/variations/variations_switches.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/load_notification_details.h"
+#include "content/public/browser/notification_service.h"
+#include "content/public/browser/notification_types.h"
+#include "content/public/common/content_switches.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/gaia_switches.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/test/embedded_test_server/request_handler_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+using net::test_server::BasicHttpResponse;
+using net::test_server::HttpRequest;
+using net::test_server::HttpResponse;
+using signin::AccountConsistencyMethod;
+
+namespace {
+
+constexpr int kAccountReconcilorDelayMs = 10;
+
+enum SignoutType {
+ kSignoutTypeFirst = 0,
+
+ kAllAccounts = 0, // Sign out from all accounts.
+ kMainAccount = 1, // Sign out from main account only.
+ kSecondaryAccount = 2, // Sign out from secondary account only.
+
+ kSignoutTypeLast
+};
+
+const char kAuthorizationCode[] = "authorization_code";
+const char kDiceResponseHeader[] = "X-Chrome-ID-Consistency-Response";
+const char kChromeSyncEndpointURL[] = "/signin/chrome/sync";
+const char kEnableSyncURL[] = "/enable_sync";
+const char kGoogleSignoutResponseHeader[] = "Google-Accounts-SignOut";
+const char kMainGmailEmail[] = "main_email@gmail.com";
+const char kMainManagedEmail[] = "main_email@managed.com";
+const char kNoDiceRequestHeader[] = "NoDiceHeader";
+const char kOAuth2TokenExchangeURL[] = "/oauth2/v4/token";
+const char kOAuth2TokenRevokeURL[] = "/o/oauth2/revoke";
+const char kSecondaryEmail[] = "secondary_email@example.com";
+const char kSigninURL[] = "/signin";
+const char kSigninWithOutageInDiceURL[] = "/signin/outage";
+const char kSignoutURL[] = "/signout";
+
+// Test response that does not complete synchronously. It must be unblocked by
+// calling the completion closure.
+class BlockedHttpResponse : public net::test_server::BasicHttpResponse {
+ public:
+ explicit BlockedHttpResponse(
+ base::OnceCallback<void(base::OnceClosure)> callback)
+ : callback_(std::move(callback)) {}
+
+ void SendResponse(
+ base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override {
+ // Called on the IO thread to unblock the response.
+ base::OnceClosure unblock_io_thread =
+ base::BindOnce(&BlockedHttpResponse::SendResponseInternal,
+ weak_factory_.GetWeakPtr(), delegate);
+ // Unblock the response from any thread by posting a task to the IO thread.
+ base::OnceClosure unblock_any_thread =
+ base::BindOnce(base::IgnoreResult(&base::TaskRunner::PostTask),
+ base::ThreadTaskRunnerHandle::Get(), FROM_HERE,
+ std::move(unblock_io_thread));
+ // Pass |unblock_any_thread| to the caller on the UI thread.
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE,
+ base::BindOnce(std::move(callback_), std::move(unblock_any_thread)));
+ }
+
+ private:
+ void SendResponseInternal(
+ base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) {
+ if (delegate)
+ BasicHttpResponse::SendResponse(delegate);
+ }
+ base::OnceCallback<void(base::OnceClosure)> callback_;
+
+ base::WeakPtrFactory<BlockedHttpResponse> weak_factory_{this};
+};
+
+} // namespace
+
+namespace FakeGaia {
+
+// Handler for the signin page on the embedded test server.
+// The response has the content of the Dice request header in its body, and has
+// the Dice response header.
+// Handles both the "Chrome Sync" endpoint and the old endpoint.
+std::unique_ptr<HttpResponse> HandleSigninURL(
+ const std::string& main_email,
+ const base::RepeatingCallback<void(const std::string&)>& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kSigninURL) &&
+ !net::test_server::ShouldHandle(request, kChromeSyncEndpointURL) &&
+ !net::test_server::ShouldHandle(request, kSigninWithOutageInDiceURL))
+ return nullptr;
+
+ // Extract Dice request header.
+ std::string header_value = kNoDiceRequestHeader;
+ auto it = request.headers.find(signin::kDiceRequestHeader);
+ if (it != request.headers.end())
+ header_value = it->second;
+
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(callback, header_value));
+
+ // Add the SIGNIN dice header.
+ std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
+ if (header_value != kNoDiceRequestHeader) {
+ if (net::test_server::ShouldHandle(request, kSigninWithOutageInDiceURL)) {
+ http_response->AddCustomHeader(
+ kDiceResponseHeader,
+ base::StringPrintf("action=SIGNIN,authuser=1,id=%s,email=%s,"
+ "no_authorization_code=true",
+ signin::GetTestGaiaIdForEmail(main_email).c_str(),
+ main_email.c_str()));
+ } else {
+ http_response->AddCustomHeader(
+ kDiceResponseHeader,
+ base::StringPrintf(
+ "action=SIGNIN,authuser=1,id=%s,email=%s,authorization_code=%s",
+ signin::GetTestGaiaIdForEmail(main_email).c_str(),
+ main_email.c_str(), kAuthorizationCode));
+ }
+ }
+
+ // When hitting the Chrome Sync endpoint, redirect to kEnableSyncURL, which
+ // adds the ENABLE_SYNC dice header.
+ if (net::test_server::ShouldHandle(request, kChromeSyncEndpointURL)) {
+ http_response->set_code(net::HTTP_FOUND); // 302 redirect.
+ http_response->AddCustomHeader("location", kEnableSyncURL);
+ }
+
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for the Gaia endpoint adding the ENABLE_SYNC dice header.
+std::unique_ptr<HttpResponse> HandleEnableSyncURL(
+ const std::string& main_email,
+ const base::RepeatingCallback<void(base::OnceClosure)>& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kEnableSyncURL))
+ return nullptr;
+
+ std::unique_ptr<BlockedHttpResponse> http_response =
+ std::make_unique<BlockedHttpResponse>(callback);
+ http_response->AddCustomHeader(
+ kDiceResponseHeader,
+ base::StringPrintf("action=ENABLE_SYNC,authuser=1,id=%s,email=%s",
+ signin::GetTestGaiaIdForEmail(main_email).c_str(),
+ main_email.c_str()));
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for the signout page on the embedded test server.
+// Responds with a Google-Accounts-SignOut header for the main account, the
+// secondary account, or both (depending on the SignoutType, which is encoded in
+// the query string).
+std::unique_ptr<HttpResponse> HandleSignoutURL(const std::string& main_email,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kSignoutURL))
+ return nullptr;
+
+ // Build signout header.
+ int query_value;
+ EXPECT_TRUE(base::StringToInt(request.GetURL().query(), &query_value));
+ SignoutType signout_type = static_cast<SignoutType>(query_value);
+ EXPECT_GE(signout_type, kSignoutTypeFirst);
+ EXPECT_LT(signout_type, kSignoutTypeLast);
+ std::string signout_header_value;
+ if (signout_type == kAllAccounts || signout_type == kMainAccount) {
+ std::string main_gaia_id = signin::GetTestGaiaIdForEmail(main_email);
+ signout_header_value =
+ base::StringPrintf("email=\"%s\", obfuscatedid=\"%s\", sessionindex=1",
+ main_email.c_str(), main_gaia_id.c_str());
+ }
+ if (signout_type == kAllAccounts || signout_type == kSecondaryAccount) {
+ if (!signout_header_value.empty())
+ signout_header_value += ", ";
+ std::string secondary_gaia_id =
+ signin::GetTestGaiaIdForEmail(kSecondaryEmail);
+ signout_header_value +=
+ base::StringPrintf("email=\"%s\", obfuscatedid=\"%s\", sessionindex=2",
+ kSecondaryEmail, secondary_gaia_id.c_str());
+ }
+
+ std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
+ http_response->AddCustomHeader(kGoogleSignoutResponseHeader,
+ signout_header_value);
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for OAuth2 token exchange.
+// Checks that the request is well formatted and returns a refresh token in a
+// JSON dictionary.
+std::unique_ptr<HttpResponse> HandleOAuth2TokenExchangeURL(
+ const base::RepeatingCallback<void(base::OnceClosure)>& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kOAuth2TokenExchangeURL))
+ return nullptr;
+
+ // Check that the authorization code is somewhere in the request body.
+ if (!request.has_content)
+ return nullptr;
+ if (request.content.find(kAuthorizationCode) == std::string::npos)
+ return nullptr;
+
+ std::unique_ptr<BlockedHttpResponse> http_response =
+ std::make_unique<BlockedHttpResponse>(callback);
+
+ std::string content =
+ "{"
+ " \"access_token\":\"access_token\","
+ " \"refresh_token\":\"new_refresh_token\","
+ " \"expires_in\":9999"
+ "}";
+
+ http_response->set_content(content);
+ http_response->set_content_type("text/plain");
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for OAuth2 token revocation.
+std::unique_ptr<HttpResponse> HandleOAuth2TokenRevokeURL(
+ const base::RepeatingClosure& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kOAuth2TokenRevokeURL))
+ return nullptr;
+
+ content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, callback);
+
+ std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for ServiceLogin on the embedded test server.
+// Calls the callback with the dice request header, or kNoDiceRequestHeader if
+// there is no Dice header.
+std::unique_ptr<HttpResponse> HandleChromeSigninEmbeddedURL(
+ const base::RepeatingCallback<void(const std::string&)>& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request,
+ "/embedded/setup/chrome/usermenu"))
+ return nullptr;
+
+ std::string dice_request_header(kNoDiceRequestHeader);
+ auto it = request.headers.find(signin::kDiceRequestHeader);
+ if (it != request.headers.end())
+ dice_request_header = it->second;
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(callback, dice_request_header));
+
+ std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+} // namespace FakeGaia
+
+class DiceBrowserTest : public InProcessBrowserTest,
+ public AccountReconcilor::Observer,
+ public signin::IdentityManager::Observer {
+ public:
+ DiceBrowserTest(const DiceBrowserTest&) = delete;
+ DiceBrowserTest& operator=(const DiceBrowserTest&) = delete;
+
+ protected:
+ ~DiceBrowserTest() override {}
+
+ explicit DiceBrowserTest(const std::string& main_email = kMainGmailEmail)
+ : main_email_(main_email),
+ https_server_(net::EmbeddedTestServer::TYPE_HTTPS),
+ enable_sync_requested_(false),
+ token_requested_(false),
+ refresh_token_available_(false),
+ token_revoked_notification_count_(0),
+ token_revoked_count_(0),
+ reconcilor_blocked_count_(0),
+ reconcilor_unblocked_count_(0),
+ reconcilor_started_count_(0) {
+ feature_list_.InitAndEnableFeature(kSupportOAuthOutageInDice);
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleSigninURL, main_email_,
+ base::BindRepeating(&DiceBrowserTest::OnSigninRequest,
+ base::Unretained(this))));
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleEnableSyncURL, main_email_,
+ base::BindRepeating(&DiceBrowserTest::OnEnableSyncRequest,
+ base::Unretained(this))));
+ https_server_.RegisterDefaultHandler(
+ base::BindRepeating(&FakeGaia::HandleSignoutURL, main_email_));
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleOAuth2TokenExchangeURL,
+ base::BindRepeating(&DiceBrowserTest::OnTokenExchangeRequest,
+ base::Unretained(this))));
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleOAuth2TokenRevokeURL,
+ base::BindRepeating(&DiceBrowserTest::OnTokenRevocationRequest,
+ base::Unretained(this))));
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleChromeSigninEmbeddedURL,
+ base::BindRepeating(&DiceBrowserTest::OnChromeSigninEmbeddedRequest,
+ base::Unretained(this))));
+ signin::SetDiceAccountReconcilorBlockDelayForTesting(
+ kAccountReconcilorDelayMs);
+ }
+
+ // Navigates to the given path on the test server.
+ void NavigateToURL(const std::string& path) {
+ ASSERT_TRUE(
+ ui_test_utils::NavigateToURL(browser(), https_server_.GetURL(path)));
+ }
+
+ // Returns the identity manager.
+ signin::IdentityManager* GetIdentityManager() {
+ return IdentityManagerFactory::GetForProfile(browser()->profile());
+ }
+
+ // Returns the account ID associated with |main_email_| and its associated
+ // gaia ID.
+ CoreAccountId GetMainAccountID() {
+ return GetIdentityManager()->PickAccountIdForAccount(
+ signin::GetTestGaiaIdForEmail(main_email_), main_email_);
+ }
+
+ // Returns the account ID associated with kSecondaryEmail and its associated
+ // gaia ID.
+ CoreAccountId GetSecondaryAccountID() {
+ return GetIdentityManager()->PickAccountIdForAccount(
+ signin::GetTestGaiaIdForEmail(kSecondaryEmail), kSecondaryEmail);
+ }
+
+ std::string GetDeviceId() {
+ return GetSigninScopedDeviceIdForProfile(browser()->profile());
+ }
+
+ // Signin with a main account and add token for a secondary account.
+ void SetupSignedInAccounts(
+ signin::ConsentLevel primary_account_consent_level) {
+ // Signin main account.
+ AccountInfo primary_account_info = signin::MakePrimaryAccountAvailable(
+ GetIdentityManager(), main_email_, primary_account_consent_level);
+ ASSERT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ ASSERT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ GetMainAccountID()));
+ ASSERT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ primary_account_consent_level));
+
+ // Add a token for a secondary account.
+ AccountInfo secondary_account_info =
+ signin::MakeAccountAvailable(GetIdentityManager(), kSecondaryEmail);
+ ASSERT_TRUE(GetIdentityManager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ ASSERT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ secondary_account_info.account_id));
+ }
+
+ // Navigate to a Gaia URL setting the Google-Accounts-SignOut header.
+ void SignOutWithDice(SignoutType signout_type) {
+ NavigateToURL(base::StringPrintf("%s?%i", kSignoutURL, signout_type));
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+
+ base::RunLoop().RunUntilIdle();
+ }
+
+ // InProcessBrowserTest:
+ void SetUp() override {
+ ASSERT_TRUE(https_server_.InitializeAndListen());
+ InProcessBrowserTest::SetUp();
+ }
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ const GURL& base_url = https_server_.base_url();
+ command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec());
+ command_line->AppendSwitchASCII(switches::kGoogleApisUrl, base_url.spec());
+ command_line->AppendSwitchASCII(switches::kLsoUrl, base_url.spec());
+ }
+
+ void SetUpOnMainThread() override {
+ InProcessBrowserTest::SetUpOnMainThread();
+ https_server_.StartAcceptingConnections();
+
+ GetIdentityManager()->AddObserver(this);
+ // Wait for the token service to be ready.
+ if (!GetIdentityManager()->AreRefreshTokensLoaded()) {
+ WaitForClosure(&tokens_loaded_quit_closure_);
+ }
+ ASSERT_TRUE(GetIdentityManager()->AreRefreshTokensLoaded());
+
+ AccountReconcilor* reconcilor =
+ AccountReconcilorFactory::GetForProfile(browser()->profile());
+
+ // Reconcilor starts as soon as the token service finishes loading its
+ // credentials. Abort the reconcilor here to make sure tests start in a
+ // stable state.
+ reconcilor->AbortReconcile();
+ reconcilor->SetState(
+ signin_metrics::AccountReconcilorState::ACCOUNT_RECONCILOR_OK);
+ reconcilor->AddObserver(this);
+ }
+
+ void TearDownOnMainThread() override {
+ GetIdentityManager()->RemoveObserver(this);
+ AccountReconcilorFactory::GetForProfile(browser()->profile())
+ ->RemoveObserver(this);
+ }
+
+ // Calls |closure| if it is not null and resets it after.
+ void RunClosureIfValid(base::OnceClosure closure) {
+ if (closure)
+ std::move(closure).Run();
+ }
+
+ // Creates and runs a RunLoop until |closure| is called.
+ void WaitForClosure(base::OnceClosure* closure) {
+ base::RunLoop run_loop;
+ *closure = run_loop.QuitClosure();
+ run_loop.Run();
+ }
+
+ // FakeGaia callbacks:
+ void OnSigninRequest(const std::string& dice_request_header) {
+ EXPECT_EQ(dice_request_header != kNoDiceRequestHeader,
+ IsReconcilorBlocked());
+ dice_request_header_ = dice_request_header;
+ RunClosureIfValid(std::move(signin_requested_quit_closure_));
+ }
+
+ void OnChromeSigninEmbeddedRequest(const std::string& dice_request_header) {
+ dice_request_header_ = dice_request_header;
+ RunClosureIfValid(std::move(chrome_signin_embedded_quit_closure_));
+ }
+
+ void OnEnableSyncRequest(base::OnceClosure unblock_response_closure) {
+ EXPECT_TRUE(IsReconcilorBlocked());
+ enable_sync_requested_ = true;
+ RunClosureIfValid(std::move(enable_sync_requested_quit_closure_));
+ unblock_enable_sync_response_closure_ = std::move(unblock_response_closure);
+ }
+
+ void OnTokenExchangeRequest(base::OnceClosure unblock_response_closure) {
+ // The token must be exchanged only once.
+ EXPECT_FALSE(token_requested_);
+ EXPECT_TRUE(IsReconcilorBlocked());
+ token_requested_ = true;
+ RunClosureIfValid(std::move(token_requested_quit_closure_));
+ unblock_token_exchange_response_closure_ =
+ std::move(unblock_response_closure);
+ }
+
+ void OnTokenRevocationRequest() {
+ ++token_revoked_count_;
+ RunClosureIfValid(std::move(token_revoked_quit_closure_));
+ }
+
+ // AccountReconcilor::Observer:
+ void OnBlockReconcile() override { ++reconcilor_blocked_count_; }
+ void OnUnblockReconcile() override {
+ ++reconcilor_unblocked_count_;
+ RunClosureIfValid(std::move(unblock_count_quit_closure_));
+ }
+ void OnStateChanged(signin_metrics::AccountReconcilorState state) override {
+ if (state ==
+ signin_metrics::AccountReconcilorState::ACCOUNT_RECONCILOR_RUNNING) {
+ ++reconcilor_started_count_;
+ }
+ }
+
+ // signin::IdentityManager::Observer
+ void OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) override {
+ if (event.GetEventTypeFor(signin::ConsentLevel::kSync) ==
+ signin::PrimaryAccountChangeEvent::Type::kSet) {
+ RunClosureIfValid(std::move(on_primary_account_set_quit_closure_));
+ }
+ }
+
+ void OnRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info) override {
+ if (account_info.account_id == GetMainAccountID()) {
+ refresh_token_available_ = true;
+ RunClosureIfValid(std::move(refresh_token_available_quit_closure_));
+ }
+ }
+
+ void OnRefreshTokenRemovedForAccount(
+ const CoreAccountId& account_id) override {
+ ++token_revoked_notification_count_;
+ }
+
+ void OnRefreshTokensLoaded() override {
+ RunClosureIfValid(std::move(tokens_loaded_quit_closure_));
+ }
+
+ // Returns true if the account reconcilor is currently blocked.
+ bool IsReconcilorBlocked() {
+ EXPECT_GE(reconcilor_blocked_count_, reconcilor_unblocked_count_);
+ EXPECT_LE(reconcilor_blocked_count_, reconcilor_unblocked_count_ + 1);
+ return (reconcilor_unblocked_count_ + 1) == reconcilor_blocked_count_;
+ }
+
+ // Waits until |reconcilor_unblocked_count_| reaches |count|.
+ void WaitForReconcilorUnblockedCount(int count) {
+ if (reconcilor_unblocked_count_ == count)
+ return;
+
+ ASSERT_EQ(count - 1, reconcilor_unblocked_count_);
+ // Wait for the timeout after the request is complete.
+ WaitForClosure(&unblock_count_quit_closure_);
+ EXPECT_EQ(count, reconcilor_unblocked_count_);
+ }
+
+ // Waits until the user consented for sync.
+ void WaitForSigninSucceeded() {
+ if (GetIdentityManager()
+ ->GetPrimaryAccountId(signin::ConsentLevel::kSync)
+ .empty()) {
+ WaitForClosure(&on_primary_account_set_quit_closure_);
+ }
+ }
+
+ // Waits for the ENABLE_SYNC request to hit the server, and unblocks the
+ // response. If this is not called, ENABLE_SYNC will not be sent by the
+ // server.
+ // Note: this does not wait for the response to reach Chrome.
+ void SendEnableSyncResponse() {
+ if (!enable_sync_requested_)
+ WaitForClosure(&enable_sync_requested_quit_closure_);
+ DCHECK(unblock_enable_sync_response_closure_);
+ std::move(unblock_enable_sync_response_closure_).Run();
+ }
+
+ // Waits until the token request is sent to the server, the response is
+ // received and the refresh token is available. If this is not called, the
+ // refresh token will not be sent by the server.
+ void SendRefreshTokenResponse() {
+ // Wait for the request hitting the server.
+ if (!token_requested_)
+ WaitForClosure(&token_requested_quit_closure_);
+ EXPECT_TRUE(token_requested_);
+ // Unblock the server response.
+ DCHECK(unblock_token_exchange_response_closure_);
+ std::move(unblock_token_exchange_response_closure_).Run();
+ // Wait for the response coming back.
+ if (!refresh_token_available_)
+ WaitForClosure(&refresh_token_available_quit_closure_);
+ EXPECT_TRUE(refresh_token_available_);
+ }
+
+ void WaitForTokenRevokedCount(int count) {
+ EXPECT_LE(token_revoked_count_, count);
+ while (token_revoked_count_ < count)
+ WaitForClosure(&token_revoked_quit_closure_);
+ EXPECT_EQ(count, token_revoked_count_);
+ }
+
+ DiceResponseHandler* GetDiceResponseHandler() {
+ return DiceResponseHandler::GetForProfile(browser()->profile());
+ }
+
+ const std::string main_email_;
+ net::EmbeddedTestServer https_server_;
+ bool enable_sync_requested_;
+ bool token_requested_;
+ bool refresh_token_available_;
+ int token_revoked_notification_count_;
+ int token_revoked_count_;
+ int reconcilor_blocked_count_;
+ int reconcilor_unblocked_count_;
+ int reconcilor_started_count_;
+ std::string dice_request_header_;
+ base::test::ScopedFeatureList feature_list_;
+
+ // Unblocks the server responses.
+ base::OnceClosure unblock_token_exchange_response_closure_;
+ base::OnceClosure unblock_enable_sync_response_closure_;
+
+ // Used for waiting asynchronous events.
+ base::OnceClosure enable_sync_requested_quit_closure_;
+ base::OnceClosure token_requested_quit_closure_;
+ base::OnceClosure token_revoked_quit_closure_;
+ base::OnceClosure refresh_token_available_quit_closure_;
+ base::OnceClosure chrome_signin_embedded_quit_closure_;
+ base::OnceClosure unblock_count_quit_closure_;
+ base::OnceClosure tokens_loaded_quit_closure_;
+ base::OnceClosure on_primary_account_set_quit_closure_;
+ base::OnceClosure signin_requested_quit_closure_;
+};
+
+// Checks that signin on Gaia triggers the fetch for a refresh token.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Signin) {
+ EXPECT_EQ(0, reconcilor_started_count_);
+
+ // Navigate to Gaia and sign in.
+ NavigateToURL(kSigninURL);
+
+ // Check that the Dice request header was sent.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ // Check that the token was requested and added to the token service.
+ SendRefreshTokenResponse();
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ // Sync should not be enabled.
+ EXPECT_TRUE(GetIdentityManager()
+ ->GetPrimaryAccountId(signin::ConsentLevel::kSync)
+ .empty());
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(1, reconcilor_started_count_);
+}
+
+// Checks that the account reconcilor is blocked when where was OAuth
+// outage in Dice, and unblocked after the timeout.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SupportOAuthOutageInDice) {
+ DiceResponseHandler* dice_response_handler = GetDiceResponseHandler();
+ scoped_refptr<base::TestMockTimeTaskRunner> task_runner =
+ new base::TestMockTimeTaskRunner();
+ dice_response_handler->SetTaskRunner(task_runner);
+ NavigateToURL(kSigninWithOutageInDiceURL);
+ // Check that the Dice request header was sent.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+ // Check that the reconcilor was blocked and not unblocked before timeout.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ task_runner->FastForwardBy(
+ base::Hours(kLockAccountReconcilorTimeoutHours / 2));
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ task_runner->FastForwardBy(
+ base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2));
+ // Wait until reconcilor is unblocked.
+ WaitForReconcilorUnblockedCount(1);
+}
+
+// Checks that re-auth on Gaia triggers the fetch for a refresh token.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Reauth) {
+ EXPECT_EQ(0, reconcilor_started_count_);
+
+ // Start from a signed-in state.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+ EXPECT_EQ(1, reconcilor_started_count_);
+
+ // Navigate to Gaia and sign in again with the main account.
+ NavigateToURL(kSigninURL);
+
+ // Check that the Dice request header was sent.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ // Check that the token was requested and added to the token service.
+ SendRefreshTokenResponse();
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+
+ // Old token must not be revoked (see http://crbug.com/865189).
+ EXPECT_EQ(0, token_revoked_notification_count_);
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(2, reconcilor_started_count_);
+}
+
+// Checks that the Dice signout flow works and deletes all tokens.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutMainAccount) {
+ // Start from a signed-in state.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+
+ // Signout from main account.
+ SignOutWithDice(kMainAccount);
+
+ // Check that the user is in error state.
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ GetMainAccountID()));
+ EXPECT_TRUE(GetIdentityManager()->HasAccountWithRefreshToken(
+ GetSecondaryAccountID()));
+
+ // Token for main account is revoked on server but not notified in the client.
+ EXPECT_EQ(0, token_revoked_notification_count_);
+ WaitForTokenRevokedCount(1);
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+}
+
+// Checks that signing out from a secondary account does not delete the main
+// token.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutSecondaryAccount) {
+ // Start from a signed-in state.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+
+ // Signout from secondary account.
+ SignOutWithDice(kSecondaryAccount);
+
+ // Check that the user is still signed in from main account, but secondary
+ // token is deleted.
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_FALSE(GetIdentityManager()->HasAccountWithRefreshToken(
+ GetSecondaryAccountID()));
+ EXPECT_EQ(1, token_revoked_notification_count_);
+ WaitForTokenRevokedCount(1);
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+}
+
+// Checks that the Dice signout flow works and deletes all tokens.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutAllAccounts) {
+ // Start from a signed-in state.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+
+ // Signout from all accounts.
+ SignOutWithDice(kAllAccounts);
+
+ // Check that the user is in error state.
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ GetMainAccountID()));
+ EXPECT_FALSE(GetIdentityManager()->HasAccountWithRefreshToken(
+ GetSecondaryAccountID()));
+
+ // Token for main account is revoked on server but not notified in the client.
+ EXPECT_EQ(1, token_revoked_notification_count_);
+ WaitForTokenRevokedCount(2);
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+}
+
+// Checks that Dice request header is not set from request from WebUI.
+// See https://crbug.com/428396
+#if defined(OS_WIN)
+#define MAYBE_NoDiceFromWebUI DISABLED_NoDiceFromWebUI
+#else
+#define MAYBE_NoDiceFromWebUI NoDiceFromWebUI
+#endif
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, MAYBE_NoDiceFromWebUI) {
+ // Navigate to Gaia and from the native tab, which uses an extension.
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), GURL("chrome:chrome-signin?reason=5")));
+
+ // Check that the request had no Dice request header.
+ if (dice_request_header_.empty())
+ WaitForClosure(&chrome_signin_embedded_quit_closure_);
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest,
+ NoDiceExtensionConsent_LaunchWebAuthFlow) {
+ auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>(
+ nullptr, browser()->profile(), https_server_.GetURL(kSigninURL),
+ extensions::WebAuthFlow::INTERACTIVE,
+ extensions::WebAuthFlow::LAUNCH_WEB_AUTH_FLOW);
+ web_auth_flow->Start();
+
+ if (dice_request_header_.empty())
+ WaitForClosure(&signin_requested_quit_closure_);
+
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+
+ // Delete the web auth flow (uses DeleteSoon).
+ web_auth_flow.release()->DetachDelegateAndDelete();
+ base::RunLoop().RunUntilIdle();
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, DiceExtensionConsent_GetAuthToken) {
+ // Signin from extension consent flow.
+ class DummyDelegate : public extensions::WebAuthFlow::Delegate {
+ public:
+ void OnAuthFlowFailure(extensions::WebAuthFlow::Failure failure) override {}
+ ~DummyDelegate() override = default;
+ };
+
+ DummyDelegate delegate;
+ auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>(
+ &delegate, browser()->profile(), https_server_.GetURL(kSigninURL),
+ extensions::WebAuthFlow::INTERACTIVE,
+ extensions::WebAuthFlow::GET_AUTH_TOKEN);
+ web_auth_flow->Start();
+
+ // Check that the token was requested and added to the token service.
+ SendRefreshTokenResponse();
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+
+ // Check that the Dice request header was sent.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ // Sync should not be enabled.
+ EXPECT_TRUE(GetIdentityManager()
+ ->GetPrimaryAccountId(signin::ConsentLevel::kSync)
+ .empty());
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(1, reconcilor_started_count_);
+
+ // Delete the web auth flow (uses DeleteSoon).
+ web_auth_flow.release()->DetachDelegateAndDelete();
+ base::RunLoop().RunUntilIdle();
+}
+
+// Tests that Sync is enabled if the ENABLE_SYNC response is received after the
+// refresh token.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, EnableSyncAfterToken) {
+ EXPECT_EQ(0, reconcilor_started_count_);
+
+ // Signin using the Chrome Sync endpoint.
+ browser()->signin_view_controller()->ShowSignin(
+ profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN,
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS);
+
+ // Receive token.
+ EXPECT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ SendRefreshTokenResponse();
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+
+ // Receive ENABLE_SYNC.
+ SendEnableSyncResponse();
+
+ // Check that the Dice request header was sent, with signout confirmation.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ content::WindowedNotificationObserver ntp_url_observer(
+ content::NOTIFICATION_LOAD_STOP,
+ base::BindRepeating([](const content::NotificationSource&,
+ const content::NotificationDetails& details) {
+ auto url =
+ content::Details<content::LoadNotificationDetails>(details)->url;
+ // Some test flags (e.g. ForceWebRequestProxyForTest) can change whether
+ // the reported NTP URL is chrome://newtab or chrome://new-tab-page.
+ return url == GURL(chrome::kChromeUINewTabPageURL) ||
+ url == GURL(chrome::kChromeUINewTabURL);
+ }));
+
+ WaitForSigninSucceeded();
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(1, reconcilor_started_count_);
+
+ // Check that the tab was navigated to the NTP.
+ ntp_url_observer.Wait();
+
+ // Dismiss the Sync confirmation UI.
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(browser()));
+}
+
+// Tests that Sync is enabled if the ENABLE_SYNC response is received before the
+// refresh token.
+
+// https://crbug.com/1082858
+#if (defined(OS_LINUX) || defined(OS_CHROMEOS)) && !defined(NDEBUG)
+#define MAYBE_EnableSyncBeforeToken DISABLED_EnableSyncBeforeToken
+#else
+#define MAYBE_EnableSyncBeforeToken EnableSyncBeforeToken
+#endif
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, MAYBE_EnableSyncBeforeToken) {
+ EXPECT_EQ(0, reconcilor_started_count_);
+
+ ui_test_utils::UrlLoadObserver enable_sync_url_observer(
+ https_server_.GetURL(kEnableSyncURL),
+ content::NotificationService::AllSources());
+
+ // Signin using the Chrome Sync endpoint.
+ browser()->signin_view_controller()->ShowSignin(
+ profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN,
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS);
+
+ // Receive ENABLE_SYNC.
+ SendEnableSyncResponse();
+ // Wait for the page to be fully loaded.
+ enable_sync_url_observer.Wait();
+
+ // Receive token.
+ EXPECT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ SendRefreshTokenResponse();
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+
+ // Check that the Dice request header was sent, with signout confirmation.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ ui_test_utils::UrlLoadObserver ntp_url_observer(
+ GURL(chrome::kChromeUINewTabURL),
+ content::NotificationService::AllSources());
+
+ WaitForSigninSucceeded();
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(1, reconcilor_started_count_);
+
+ // Check that the tab was navigated to the NTP.
+ ntp_url_observer.Wait();
+
+ // Dismiss the Sync confirmation UI.
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(browser()));
+}
+
+// Tests that turning off Dice via preferences works when singed out.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_SignedOut) {
+ ASSERT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+ ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ // Turn off Dice for this profile.
+ browser()->profile()->GetPrefs()->SetBoolean(
+ prefs::kSigninAllowedOnNextStartup, false);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_SignedOut) {
+ // Check that Dice is disabled.
+ EXPECT_FALSE(
+ browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean(
+ prefs::kSigninAllowedOnNextStartup));
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ EXPECT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+
+ // Navigate to Gaia and sign in.
+ NavigateToURL(kSigninURL);
+ // Check that the Dice request header was not sent.
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+}
+
+// Tests that turning off Dice via preferences works when signed in without sync
+// consent.
+//
+// Regression test for crbug/1254325
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_NotOptedIntoSync) {
+ SetupSignedInAccounts(signin::ConsentLevel::kSignin);
+
+ ASSERT_TRUE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+ ASSERT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ // Turn off Dice for this profile.
+ browser()->profile()->GetPrefs()->SetBoolean(
+ prefs::kSigninAllowedOnNextStartup, false);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_NotOptedIntoSync) {
+ // Check that Dice is disabled.
+ EXPECT_FALSE(
+ browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean(
+ prefs::kSigninAllowedOnNextStartup));
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ EXPECT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+ EXPECT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_TRUE(GetIdentityManager()->GetAccountsWithRefreshTokens().empty());
+
+ // Navigate to Gaia and sign in.
+ NavigateToURL(kSigninURL);
+ // Check that the Dice request header was not sent.
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+}
+
+// Tests that turning off Dice via preferences works when signed in with sync
+// consent
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_OptedIntoSync) {
+ // Sign the profile in and turn sync on.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+ syncer::SyncPrefs(browser()->profile()->GetPrefs()).SetFirstSetupComplete();
+
+ ASSERT_TRUE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ // Turn off Dice for this profile.
+ browser()->profile()->GetPrefs()->SetBoolean(
+ prefs::kSigninAllowedOnNextStartup, false);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_OptedIntoSync) {
+ EXPECT_FALSE(
+ browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean(
+ prefs::kSigninAllowedOnNextStartup));
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ EXPECT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ EXPECT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+ EXPECT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_TRUE(GetIdentityManager()->GetAccountsWithRefreshTokens().empty());
+
+ // Navigate to Gaia and sign in.
+ NavigateToURL(kSigninURL);
+ // Check that the Dice request header was not sent.
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+}
+
+// Checks that Dice is disabled in incognito mode.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Incognito) {
+ Browser* incognito_browser = Browser::Create(Browser::CreateParams(
+ browser()->profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true),
+ true));
+
+ // Check that Dice is disabled.
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ incognito_browser->profile()));
+}
+
+// This test is not specifically related to DICE, but it extends
+// |DiceBrowserTest| for convenience.
+class DiceManageAccountBrowserTest : public DiceBrowserTest {
+ public:
+ DiceManageAccountBrowserTest()
+ : DiceBrowserTest(kMainManagedEmail),
+ // Skip showing the error message box to avoid freezing the main thread.
+ skip_message_box_auto_reset_(
+ &chrome::internal::g_should_skip_message_box_for_test,
+ true),
+ // Force the policy component to prohibit clearing the primary account
+ // even when the policy core component is not initialized.
+ prohibit_sigout_auto_reset_(
+ &policy::internal::g_force_prohibit_signout_for_tests,
+ true) {}
+
+ void SetUp() override {
+#if defined(OS_WIN)
+ // Shortcut deletion delays tests shutdown on Win-7 and results in time out.
+ // See crbug.com/1073451.
+ AppShortcutManager::SuppressShortcutsForTesting();
+#endif
+ DiceBrowserTest::SetUp();
+ }
+
+ protected:
+ base::AutoReset<bool> skip_message_box_auto_reset_;
+ base::AutoReset<bool> prohibit_sigout_auto_reset_;
+ unsigned int number_of_profiles_added_ = 0;
+};
+
+// Tests that prohiting sign-in on startup for a managed profile clears the
+// profile directory on next start-up.
+IN_PROC_BROWSER_TEST_F(DiceManageAccountBrowserTest,
+ PRE_ClearManagedProfileOnStartup) {
+ // Ensure that there are not deleted profiles before running this test.
+ PrefService* local_state = g_browser_process->local_state();
+ DCHECK(local_state);
+ const base::ListValue* deleted_profiles =
+ local_state->GetList(prefs::kProfilesDeleted);
+ ASSERT_TRUE(deleted_profiles);
+ ASSERT_TRUE(deleted_profiles->GetList().empty());
+
+ // Sign the profile in.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+
+ // Prohibit sign-in on next start-up.
+ browser()->profile()->GetPrefs()->SetBoolean(
+ prefs::kSigninAllowedOnNextStartup, false);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceManageAccountBrowserTest,
+ ClearManagedProfileOnStartup) {
+ // Initial profile should have been deleted as sign-in and sign out were no
+ // longer allowed.
+ PrefService* local_state = g_browser_process->local_state();
+ DCHECK(local_state);
+ const base::ListValue* deleted_profiles =
+ local_state->GetList(prefs::kProfilesDeleted);
+ EXPECT_TRUE(deleted_profiles);
+ EXPECT_EQ(1U, deleted_profiles->GetList().size());
+
+ content::RunAllTasksUntilIdle();
+
+ // Verify that there is an active profile.
+ Profile* initial_profile = browser()->profile();
+ EXPECT_EQ(1U, g_browser_process->profile_manager()->GetNumberOfProfiles());
+ EXPECT_EQ(g_browser_process->profile_manager()->GetLastUsedProfile(),
+ initial_profile);
+}
diff --git a/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc
new file mode 100644
index 00000000000..ecd8893f92b
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc
@@ -0,0 +1,179 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
+
+#include <algorithm>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/browser_navigator.h"
+#include "chrome/browser/ui/browser_navigator_params.h"
+#include "chrome/common/webui_url_constants.h"
+#include "components/signin/public/base/multilogin_parameters.h"
+#include "components/signin/public/identity_manager/accounts_cookie_mutator.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/set_accounts_in_cookie_result.h"
+#include "content/public/browser/web_contents.h"
+#include "google_apis/gaia/gaia_auth_fetcher.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "url/gurl.h"
+
+namespace {
+
+// Returns true if |account_id| is signed in the cookies.
+bool CookieInfoContains(const signin::AccountsInCookieJarInfo& cookie_info,
+ const CoreAccountId& account_id) {
+ const std::vector<gaia::ListedAccount>& accounts =
+ cookie_info.signed_in_accounts;
+ return std::find_if(accounts.begin(), accounts.end(),
+ [&account_id](const gaia::ListedAccount& account) {
+ return account.id == account_id;
+ }) != accounts.end();
+}
+
+} // namespace
+
+DiceInterceptedSessionStartupHelper::DiceInterceptedSessionStartupHelper(
+ Profile* profile,
+ bool is_new_profile,
+ CoreAccountId account_id,
+ content::WebContents* tab_to_move)
+ : profile_(profile),
+ use_multilogin_(is_new_profile),
+ account_id_(account_id) {
+ if (tab_to_move)
+ web_contents_ = tab_to_move->GetWeakPtr();
+}
+
+DiceInterceptedSessionStartupHelper::~DiceInterceptedSessionStartupHelper() =
+ default;
+
+void DiceInterceptedSessionStartupHelper::Startup(base::OnceClosure callback) {
+ callback_ = std::move(callback);
+
+ // Wait until the account is set in cookies of the newly created profile
+ // before opening the URL, so that the user is signed-in in content area. If
+ // the account is still not in the cookie after some timeout, proceed without
+ // cookies, so that the user can at least take some action in the new profile.
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile_);
+ signin::AccountsInCookieJarInfo cookie_info =
+ identity_manager->GetAccountsInCookieJar();
+ if (cookie_info.accounts_are_fresh &&
+ CookieInfoContains(cookie_info, account_id_)) {
+ MoveTab();
+ } else {
+ // Set the timeout.
+ on_cookie_update_timeout_.Reset(base::BindOnce(
+ &DiceInterceptedSessionStartupHelper::MoveTab, base::Unretained(this)));
+ // Adding accounts to the cookies can be an expensive operation. In
+ // particular the ExternalCCResult fetch may time out after multiple seconds
+ // (see kExternalCCResultTimeoutSeconds and https://crbug.com/750316#c37).
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE, on_cookie_update_timeout_.callback(), base::Seconds(12));
+
+ accounts_in_cookie_observer_.Observe(identity_manager);
+ if (use_multilogin_)
+ StartupMultilogin(identity_manager);
+ else
+ StartupReconcilor(identity_manager);
+ }
+}
+
+void DiceInterceptedSessionStartupHelper::OnAccountsInCookieUpdated(
+ const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+ const GoogleServiceAuthError& error) {
+ if (error != GoogleServiceAuthError::AuthErrorNone())
+ return;
+ if (!accounts_in_cookie_jar_info.accounts_are_fresh)
+ return;
+ if (!CookieInfoContains(accounts_in_cookie_jar_info, account_id_))
+ return;
+
+ MoveTab();
+}
+
+void DiceInterceptedSessionStartupHelper::OnStateChanged(
+ signin_metrics::AccountReconcilorState state) {
+ DCHECK(!use_multilogin_);
+ if (state == signin_metrics::ACCOUNT_RECONCILOR_ERROR) {
+ reconcile_error_encountered_ = true;
+ return;
+ }
+
+ // TODO(https://crbug.com/1051864): remove this when the cookie updates are
+ // correctly sent after reconciliation.
+ if (state == signin_metrics::ACCOUNT_RECONCILOR_OK) {
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile_);
+ // GetAccountsInCookieJar() automatically re-schedules a /ListAccounts call
+ // if the cookie is not fresh.
+ signin::AccountsInCookieJarInfo cookie_info =
+ identity_manager->GetAccountsInCookieJar();
+ OnAccountsInCookieUpdated(cookie_info,
+ GoogleServiceAuthError::AuthErrorNone());
+ }
+}
+
+void DiceInterceptedSessionStartupHelper::StartupMultilogin(
+ signin::IdentityManager* identity_manager) {
+ // Lock the reconcilor to avoid making multiple multilogin calls.
+ reconcilor_lock_ = std::make_unique<AccountReconcilor::Lock>(
+ AccountReconcilorFactory::GetForProfile(profile_));
+
+ // Start the multilogin call.
+ signin::MultiloginParameters params = {
+ /*mode=*/gaia::MultiloginMode::MULTILOGIN_UPDATE_COOKIE_ACCOUNTS_ORDER,
+ /*accounts_to_send=*/{account_id_}};
+ identity_manager->GetAccountsCookieMutator()->SetAccountsInCookie(
+ params, gaia::GaiaSource::kChrome,
+ base::BindOnce(
+ &DiceInterceptedSessionStartupHelper::OnSetAccountInCookieCompleted,
+ weak_factory_.GetWeakPtr()));
+}
+
+void DiceInterceptedSessionStartupHelper::StartupReconcilor(
+ signin::IdentityManager* identity_manager) {
+ // TODO(https://crbug.com/1051864): cookie notifications are not triggered
+ // when the account is added by the reconcilor. Observe the reconcilor and
+ // re-trigger the cookie update when it completes.
+ reconcilor_observer_.Observe(
+ AccountReconcilorFactory::GetForProfile(profile_));
+ identity_manager->GetAccountsCookieMutator()->TriggerCookieJarUpdate();
+}
+
+void DiceInterceptedSessionStartupHelper::OnSetAccountInCookieCompleted(
+ signin::SetAccountsInCookieResult result) {
+ DCHECK(use_multilogin_);
+ MoveTab();
+}
+
+void DiceInterceptedSessionStartupHelper::MoveTab() {
+ accounts_in_cookie_observer_.Reset();
+ reconcilor_observer_.Reset();
+ on_cookie_update_timeout_.Cancel();
+ reconcilor_lock_.reset();
+
+ GURL url_to_open = GURL(chrome::kChromeUINewTabURL);
+ // If the intercepted web contents is still alive, close it now.
+ if (web_contents_) {
+ url_to_open = web_contents_->GetURL();
+ web_contents_->Close();
+ }
+
+ // Open a new browser.
+ NavigateParams params(profile_, url_to_open,
+ ui::PAGE_TRANSITION_AUTO_BOOKMARK);
+ Navigate(&params);
+
+ if (callback_)
+ std::move(callback_).Run();
+}
diff --git a/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h
new file mode 100644
index 00000000000..2895a90d66b
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h
@@ -0,0 +1,102 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_
+
+#include "base/callback_forward.h"
+#include "base/cancelable_callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "google_apis/gaia/core_account_id.h"
+
+namespace content {
+class WebContents;
+}
+
+namespace signin {
+struct AccountsInCookieJarInfo;
+class IdentityManager;
+enum class SetAccountsInCookieResult;
+}
+
+class GoogleServiceAuthError;
+class Profile;
+
+// Called when the user accepted the dice signin interception and the new
+// profile has been created. Creates a new browser and moves the intercepted tab
+// to the new browser.
+// It is assumed that the account is already in the profile, but not necessarily
+// in the content area (cookies).
+class DiceInterceptedSessionStartupHelper
+ : public signin::IdentityManager::Observer,
+ public AccountReconcilor::Observer {
+ public:
+ // |profile| is the new profile that was created after signin interception.
+ // |account_id| is the main account for the profile, it's already in the
+ // profile.
+ // |tab_to_move| is the tab where the interception happened, in the source
+ // profile.
+ DiceInterceptedSessionStartupHelper(Profile* profile,
+ bool is_new_profile,
+ CoreAccountId account_id,
+ content::WebContents* tab_to_move);
+
+ ~DiceInterceptedSessionStartupHelper() override;
+
+ DiceInterceptedSessionStartupHelper(
+ const DiceInterceptedSessionStartupHelper&) = delete;
+ DiceInterceptedSessionStartupHelper& operator=(
+ const DiceInterceptedSessionStartupHelper&) = delete;
+
+ // Start up the session. Can only be called once.
+ void Startup(base::OnceClosure callback);
+
+ // signin::IdentityManager::Observer:
+ void OnAccountsInCookieUpdated(
+ const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+ const GoogleServiceAuthError& error) override;
+
+ // AccountReconcilor::Observer:
+ void OnStateChanged(signin_metrics::AccountReconcilorState state) override;
+
+ private:
+ // For new profiles, the account is added directly using multilogin.
+ void StartupMultilogin(signin::IdentityManager* identity_manager);
+
+ // For existing profiles, simply wait for the reconcilor to update the
+ // accounts.
+ void StartupReconcilor(signin::IdentityManager* identity_manager);
+
+ // Called when multilogin completes.
+ void OnSetAccountInCookieCompleted(signin::SetAccountsInCookieResult result);
+
+ // Creates a browser with a new tab, and closes the intercepted tab if it's
+ // still open.
+ void MoveTab();
+
+ const raw_ptr<Profile> profile_;
+ base::WeakPtr<content::WebContents> web_contents_;
+ bool use_multilogin_;
+ CoreAccountId account_id_;
+ base::OnceClosure callback_;
+ bool reconcile_error_encountered_ = false;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ accounts_in_cookie_observer_{this};
+ base::ScopedObservation<AccountReconcilor, AccountReconcilor::Observer>
+ reconcilor_observer_{this};
+ std::unique_ptr<AccountReconcilor::Lock> reconcilor_lock_;
+ // Timeout while waiting for the account to be added to the cookies in the new
+ // profile.
+ base::CancelableOnceCallback<void()> on_cookie_update_timeout_;
+
+ base::WeakPtrFactory<DiceInterceptedSessionStartupHelper> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_
diff --git a/chromium/chrome/browser/signin/dice_response_handler.cc b/chromium/chrome/browser/signin/dice_response_handler.cc
new file mode 100644
index 00000000000..88d7563846f
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_response_handler.cc
@@ -0,0 +1,426 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_response_handler.h"
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/memory/singleton.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/stringprintf.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chrome/browser/signin/about_signin_internals_factory.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/webui/profile_helper.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+#include "components/signin/core/browser/about_signin_internals.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/signin_client.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "google_apis/gaia/gaia_auth_fetcher.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+
+const int kDiceTokenFetchTimeoutSeconds = 10;
+// Timeout for locking the account reconcilor when
+// there was OAuth outage in Dice.
+const int kLockAccountReconcilorTimeoutHours = 12;
+
+const base::Feature kSupportOAuthOutageInDice{"SupportOAuthOutageInDice",
+ base::FEATURE_ENABLED_BY_DEFAULT};
+
+namespace {
+
+// The UMA histograms that logs events related to Dice responses.
+const char kDiceResponseHeaderHistogram[] = "Signin.DiceResponseHeader";
+const char kDiceTokenFetchResultHistogram[] = "Signin.DiceTokenFetchResult";
+
+// Used for UMA. Do not reorder, append new values at the end.
+enum DiceResponseHeader {
+ // Received a signin header.
+ kSignin = 0,
+ // Received a signout header including the Chrome primary account.
+ kSignoutPrimary = 1,
+ // Received a signout header for other account(s).
+ kSignoutSecondary = 2,
+ // Received a "EnableSync" header.
+ kEnableSync = 3,
+
+ kDiceResponseHeaderCount
+};
+
+// Used for UMA. Do not reorder, append new values at the end.
+enum DiceTokenFetchResult {
+ // The token fetch succeeded.
+ kFetchSuccess = 0,
+ // The token fetch was aborted. For example, if another request for the same
+ // account is already in flight.
+ kFetchAbort = 1,
+ // The token fetch failed because Gaia responsed with an error.
+ kFetchFailure = 2,
+ // The token fetch failed because no response was received from Gaia.
+ kFetchTimeout = 3,
+
+ kDiceTokenFetchResultCount
+};
+
+class DiceResponseHandlerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns an instance of the factory singleton.
+ static DiceResponseHandlerFactory* GetInstance() {
+ return base::Singleton<DiceResponseHandlerFactory>::get();
+ }
+
+ static DiceResponseHandler* GetForProfile(Profile* profile) {
+ return static_cast<DiceResponseHandler*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+ }
+
+ private:
+ friend struct base::DefaultSingletonTraits<DiceResponseHandlerFactory>;
+
+ DiceResponseHandlerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "DiceResponseHandler",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(AboutSigninInternalsFactory::GetInstance());
+ DependsOn(AccountReconcilorFactory::GetInstance());
+ DependsOn(ChromeSigninClientFactory::GetInstance());
+ DependsOn(IdentityManagerFactory::GetInstance());
+ }
+
+ ~DiceResponseHandlerFactory() override {}
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override {
+ if (context->IsOffTheRecord())
+ return nullptr;
+
+ Profile* profile = static_cast<Profile*>(context);
+ return new DiceResponseHandler(
+ ChromeSigninClientFactory::GetForProfile(profile),
+ IdentityManagerFactory::GetForProfile(profile),
+ AccountReconcilorFactory::GetForProfile(profile),
+ AboutSigninInternalsFactory::GetForProfile(profile),
+ profile->GetPath());
+ }
+};
+
+// Histogram macros expand to a lot of code, so it is better to wrap them in
+// functions.
+
+void RecordDiceResponseHeader(DiceResponseHeader header) {
+ UMA_HISTOGRAM_ENUMERATION(kDiceResponseHeaderHistogram, header,
+ kDiceResponseHeaderCount);
+}
+
+void RecordDiceFetchTokenResult(DiceTokenFetchResult result) {
+ UMA_HISTOGRAM_ENUMERATION(kDiceTokenFetchResultHistogram, result,
+ kDiceTokenFetchResultCount);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// DiceTokenFetcher
+////////////////////////////////////////////////////////////////////////////////
+
+DiceResponseHandler::DiceTokenFetcher::DiceTokenFetcher(
+ const std::string& gaia_id,
+ const std::string& email,
+ const std::string& authorization_code,
+ SigninClient* signin_client,
+ AccountReconcilor* account_reconcilor,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate,
+ DiceResponseHandler* dice_response_handler)
+ : gaia_id_(gaia_id),
+ email_(email),
+ authorization_code_(authorization_code),
+ delegate_(std::move(delegate)),
+ dice_response_handler_(dice_response_handler),
+ timeout_closure_(
+ base::BindOnce(&DiceResponseHandler::DiceTokenFetcher::OnTimeout,
+ base::Unretained(this))),
+ should_enable_sync_(false) {
+ DCHECK(dice_response_handler_);
+ account_reconcilor_lock_ =
+ std::make_unique<AccountReconcilor::Lock>(account_reconcilor);
+ gaia_auth_fetcher_ =
+ signin_client->CreateGaiaAuthFetcher(this, gaia::GaiaSource::kChrome);
+ VLOG(1) << "Start fetching token for account: " << email;
+ gaia_auth_fetcher_->StartAuthCodeForOAuth2TokenExchange(authorization_code_);
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE, timeout_closure_.callback(),
+ base::Seconds(kDiceTokenFetchTimeoutSeconds));
+}
+
+DiceResponseHandler::DiceTokenFetcher::~DiceTokenFetcher() {}
+
+void DiceResponseHandler::DiceTokenFetcher::OnTimeout() {
+ RecordDiceFetchTokenResult(kFetchTimeout);
+ gaia_auth_fetcher_.reset();
+ timeout_closure_.Cancel();
+ dice_response_handler_->OnTokenExchangeFailure(
+ this, GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED));
+ // |this| may be deleted at this point.
+}
+
+void DiceResponseHandler::DiceTokenFetcher::OnClientOAuthSuccess(
+ const GaiaAuthConsumer::ClientOAuthResult& result) {
+ RecordDiceFetchTokenResult(kFetchSuccess);
+ gaia_auth_fetcher_.reset();
+ timeout_closure_.Cancel();
+ dice_response_handler_->OnTokenExchangeSuccess(
+ this, result.refresh_token, result.is_under_advanced_protection);
+ // |this| may be deleted at this point.
+}
+
+void DiceResponseHandler::DiceTokenFetcher::OnClientOAuthFailure(
+ const GoogleServiceAuthError& error) {
+ RecordDiceFetchTokenResult(kFetchFailure);
+ gaia_auth_fetcher_.reset();
+ timeout_closure_.Cancel();
+ dice_response_handler_->OnTokenExchangeFailure(this, error);
+ // |this| may be deleted at this point.
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DiceResponseHandler
+////////////////////////////////////////////////////////////////////////////////
+
+// static
+DiceResponseHandler* DiceResponseHandler::GetForProfile(Profile* profile) {
+ return DiceResponseHandlerFactory::GetForProfile(profile);
+}
+
+DiceResponseHandler::DiceResponseHandler(
+ SigninClient* signin_client,
+ signin::IdentityManager* identity_manager,
+ AccountReconcilor* account_reconcilor,
+ AboutSigninInternals* about_signin_internals,
+ const base::FilePath& profile_path)
+ : signin_client_(signin_client),
+ identity_manager_(identity_manager),
+ account_reconcilor_(account_reconcilor),
+ about_signin_internals_(about_signin_internals),
+ profile_path_(profile_path) {
+ DCHECK(signin_client_);
+ DCHECK(identity_manager_);
+ DCHECK(account_reconcilor_);
+ DCHECK(about_signin_internals_);
+}
+
+DiceResponseHandler::~DiceResponseHandler() {}
+
+void DiceResponseHandler::ProcessDiceHeader(
+ const signin::DiceResponseParams& dice_params,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate) {
+ DCHECK(delegate);
+ switch (dice_params.user_intention) {
+ case signin::DiceAction::SIGNIN: {
+ const signin::DiceResponseParams::AccountInfo& info =
+ dice_params.signin_info->account_info;
+ ProcessDiceSigninHeader(
+ info.gaia_id, info.email, dice_params.signin_info->authorization_code,
+ dice_params.signin_info->no_authorization_code, std::move(delegate));
+ return;
+ }
+ case signin::DiceAction::ENABLE_SYNC: {
+ const signin::DiceResponseParams::AccountInfo& info =
+ dice_params.enable_sync_info->account_info;
+ ProcessEnableSyncHeader(info.gaia_id, info.email, std::move(delegate));
+ return;
+ }
+ case signin::DiceAction::SIGNOUT:
+ DCHECK_GT(dice_params.signout_info->account_infos.size(), 0u);
+ ProcessDiceSignoutHeader(dice_params.signout_info->account_infos);
+ return;
+ case signin::DiceAction::NONE:
+ NOTREACHED() << "Invalid Dice response parameters.";
+ return;
+ }
+ NOTREACHED();
+}
+
+size_t DiceResponseHandler::GetPendingDiceTokenFetchersCountForTesting() const {
+ return token_fetchers_.size();
+}
+
+void DiceResponseHandler::OnTimeoutUnlockReconcilor() {
+ lock_.reset();
+}
+
+void DiceResponseHandler::SetTaskRunner(
+ scoped_refptr<base::SequencedTaskRunner> task_runner) {
+ task_runner_ = std::move(task_runner);
+}
+
+void DiceResponseHandler::ProcessDiceSigninHeader(
+ const std::string& gaia_id,
+ const std::string& email,
+ const std::string& authorization_code,
+ bool no_authorization_code,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate) {
+ if (no_authorization_code) {
+ if (base::FeatureList::IsEnabled(kSupportOAuthOutageInDice)) {
+ lock_ = std::make_unique<AccountReconcilor::Lock>(account_reconcilor_);
+ about_signin_internals_->OnRefreshTokenReceived(
+ "Missing authorization code due to OAuth outage in Dice.");
+ if (!timer_) {
+ timer_ = std::make_unique<base::OneShotTimer>();
+ if (task_runner_)
+ timer_->SetTaskRunner(task_runner_);
+ }
+ // If there is already another lock, the timer will be reset and
+ // we'll wait another full timeout.
+ timer_->Start(
+ FROM_HERE, base::Hours(kLockAccountReconcilorTimeoutHours),
+ base::BindOnce(&DiceResponseHandler::OnTimeoutUnlockReconcilor,
+ base::Unretained(this)));
+ }
+ return;
+ }
+
+ DCHECK(!gaia_id.empty());
+ DCHECK(!email.empty());
+ DCHECK(!authorization_code.empty());
+ VLOG(1) << "Start processing Dice signin response";
+ RecordDiceResponseHeader(kSignin);
+
+ for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) {
+ if ((it->get()->gaia_id() == gaia_id) && (it->get()->email() == email) &&
+ (it->get()->authorization_code() == authorization_code)) {
+ RecordDiceFetchTokenResult(kFetchAbort);
+ return; // There is already a request in flight with the same parameters.
+ }
+ }
+ token_fetchers_.push_back(std::make_unique<DiceTokenFetcher>(
+ gaia_id, email, authorization_code, signin_client_, account_reconcilor_,
+ std::move(delegate), this));
+}
+
+void DiceResponseHandler::ProcessEnableSyncHeader(
+ const std::string& gaia_id,
+ const std::string& email,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate) {
+ VLOG(1) << "Start processing Dice enable sync response";
+ RecordDiceResponseHeader(kEnableSync);
+ for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) {
+ DiceTokenFetcher* fetcher = it->get();
+ if (fetcher->gaia_id() == gaia_id) {
+ DCHECK(gaia::AreEmailsSame(fetcher->email(), email));
+ // If there is a fetch in progress for a resfresh token for the given
+ // account, then simply mark it to enable sync after the refresh token is
+ // available.
+ fetcher->set_should_enable_sync(true);
+ return; // There is already a request in flight with the same parameters.
+ }
+ }
+ CoreAccountId account_id =
+ identity_manager_->PickAccountIdForAccount(gaia_id, email);
+ delegate->EnableSync(account_id);
+}
+
+void DiceResponseHandler::ProcessDiceSignoutHeader(
+ const std::vector<signin::DiceResponseParams::AccountInfo>& account_infos) {
+ VLOG(1) << "Start processing Dice signout response";
+
+ CoreAccountId primary_account =
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync);
+ bool primary_account_signed_out = false;
+ auto* accounts_mutator = identity_manager_->GetAccountsMutator();
+ for (const auto& account_info : account_infos) {
+ CoreAccountId signed_out_account =
+ identity_manager_->PickAccountIdForAccount(account_info.gaia_id,
+ account_info.email);
+ if (signed_out_account == primary_account) {
+ primary_account_signed_out = true;
+ RecordDiceResponseHeader(kSignoutPrimary);
+
+ // Put the account in error state.
+ accounts_mutator->InvalidateRefreshTokenForPrimaryAccount(
+ signin_metrics::SourceForRefreshTokenOperation::
+ kDiceResponseHandler_Signout);
+ } else {
+ accounts_mutator->RemoveAccount(
+ signed_out_account, signin_metrics::SourceForRefreshTokenOperation::
+ kDiceResponseHandler_Signout);
+ }
+
+ // If a token fetch is in flight for the same account, cancel it.
+ for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) {
+ CoreAccountId token_fetcher_account_id =
+ identity_manager_->PickAccountIdForAccount(it->get()->gaia_id(),
+ it->get()->email());
+ if (token_fetcher_account_id == signed_out_account) {
+ token_fetchers_.erase(it);
+ break;
+ }
+ }
+ }
+
+ if (!primary_account_signed_out)
+ RecordDiceResponseHeader(kSignoutSecondary);
+}
+
+void DiceResponseHandler::DeleteTokenFetcher(DiceTokenFetcher* token_fetcher) {
+ for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) {
+ if (it->get() == token_fetcher) {
+ token_fetchers_.erase(it);
+ return;
+ }
+ }
+ NOTREACHED();
+}
+
+void DiceResponseHandler::OnTokenExchangeSuccess(
+ DiceTokenFetcher* token_fetcher,
+ const std::string& refresh_token,
+ bool is_under_advanced_protection) {
+ const std::string& email = token_fetcher->email();
+ const std::string& gaia_id = token_fetcher->gaia_id();
+ VLOG(1) << "[Dice] OAuth success for email " << email;
+ bool should_enable_sync = token_fetcher->should_enable_sync();
+ CoreAccountId account_id =
+ identity_manager_->PickAccountIdForAccount(gaia_id, email);
+ bool is_new_account =
+ !identity_manager_->HasAccountWithRefreshToken(account_id);
+ identity_manager_->GetAccountsMutator()->AddOrUpdateAccount(
+ gaia_id, email, refresh_token, is_under_advanced_protection,
+ signin_metrics::SourceForRefreshTokenOperation::
+ kDiceResponseHandler_Signin);
+ about_signin_internals_->OnRefreshTokenReceived(
+ base::StringPrintf("Successful (%s)", account_id.ToString().c_str()));
+ token_fetcher->delegate()->HandleTokenExchangeSuccess(account_id,
+ is_new_account);
+ if (should_enable_sync)
+ token_fetcher->delegate()->EnableSync(account_id);
+
+ DeleteTokenFetcher(token_fetcher);
+}
+
+void DiceResponseHandler::OnTokenExchangeFailure(
+ DiceTokenFetcher* token_fetcher,
+ const GoogleServiceAuthError& error) {
+ const std::string& email = token_fetcher->email();
+ const std::string& gaia_id = token_fetcher->gaia_id();
+ CoreAccountId account_id =
+ identity_manager_->PickAccountIdForAccount(gaia_id, email);
+ about_signin_internals_->OnRefreshTokenReceived(
+ base::StringPrintf("Failure (%s)", account_id.ToString().c_str()));
+ token_fetcher->delegate()->HandleTokenExchangeFailure(email, error);
+
+ DeleteTokenFetcher(token_fetcher);
+}
diff --git a/chromium/chrome/browser/signin/dice_response_handler.h b/chromium/chrome/browser/signin/dice_response_handler.h
new file mode 100644
index 00000000000..2e90c9ddc11
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_response_handler.h
@@ -0,0 +1,187 @@
+// Copyright 2017 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_
+#define CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/cancelable_callback.h"
+#include "base/files/file_path.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/timer/timer.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "google_apis/gaia/gaia_auth_consumer.h"
+
+class AboutSigninInternals;
+class GaiaAuthFetcher;
+class GoogleServiceAuthError;
+class SigninClient;
+class Profile;
+
+namespace signin {
+class IdentityManager;
+}
+
+// Exposed for testing.
+extern const int kDiceTokenFetchTimeoutSeconds;
+// Exposed for testing.
+extern const int kLockAccountReconcilorTimeoutHours;
+extern const base::Feature kSupportOAuthOutageInDice;
+
+// Delegate interface for processing a dice request.
+class ProcessDiceHeaderDelegate {
+ public:
+ virtual ~ProcessDiceHeaderDelegate() = default;
+
+ // Called when a token was successfully exchanged.
+ // Called after the account was seeded in the account tracker service and
+ // after the refresh token was fetched and updated in the token service.
+ // |is_new_account| is true if the account was added to Chrome (it is not a
+ // re-auth).
+ virtual void HandleTokenExchangeSuccess(CoreAccountId account_id,
+ bool is_new_account) = 0;
+
+ // Asks the delegate to enable sync for the |account_id|.
+ // Called after the account was seeded in the account tracker service and
+ // after the refresh token was fetched and updated in the token service.
+ virtual void EnableSync(const CoreAccountId& account_id) = 0;
+
+ // Handles a failure in the token exchange (i.e. shows the error to the user).
+ virtual void HandleTokenExchangeFailure(
+ const std::string& email,
+ const GoogleServiceAuthError& error) = 0;
+};
+
+// Processes the Dice responses from Gaia.
+class DiceResponseHandler : public KeyedService {
+ public:
+ // Returns the DiceResponseHandler associated with this profile.
+ // May return nullptr if there is none (e.g. in incognito).
+ static DiceResponseHandler* GetForProfile(Profile* profile);
+
+ DiceResponseHandler(SigninClient* signin_client,
+ signin::IdentityManager* identity_manager,
+ AccountReconcilor* account_reconcilor,
+ AboutSigninInternals* about_signin_internals,
+ const base::FilePath& profile_path_);
+
+ DiceResponseHandler(const DiceResponseHandler&) = delete;
+ DiceResponseHandler& operator=(const DiceResponseHandler&) = delete;
+
+ ~DiceResponseHandler() override;
+
+ // Must be called when receiving a Dice response header.
+ void ProcessDiceHeader(const signin::DiceResponseParams& dice_params,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate);
+
+ // Returns the number of pending DiceTokenFetchers. Exposed for testing.
+ size_t GetPendingDiceTokenFetchersCountForTesting() const;
+
+ // Sets |task_runner_| for testing.
+ void SetTaskRunner(scoped_refptr<base::SequencedTaskRunner> task_runner);
+
+ private:
+ // Helper class to fetch a refresh token from an authorization code.
+ class DiceTokenFetcher : public GaiaAuthConsumer {
+ public:
+ DiceTokenFetcher(const std::string& gaia_id,
+ const std::string& email,
+ const std::string& authorization_code,
+ SigninClient* signin_client,
+ AccountReconcilor* account_reconcilor,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate,
+ DiceResponseHandler* dice_response_handler);
+
+ DiceTokenFetcher(const DiceTokenFetcher&) = delete;
+ DiceTokenFetcher& operator=(const DiceTokenFetcher&) = delete;
+
+ ~DiceTokenFetcher() override;
+
+ const std::string& gaia_id() const { return gaia_id_; }
+ const std::string& email() const { return email_; }
+ const std::string& authorization_code() const {
+ return authorization_code_;
+ }
+ bool should_enable_sync() const { return should_enable_sync_; }
+ void set_should_enable_sync(bool should_enable_sync) {
+ should_enable_sync_ = should_enable_sync;
+ }
+ ProcessDiceHeaderDelegate* delegate() { return delegate_.get(); }
+
+ private:
+ // Called by |timeout_closure_| when the request times out.
+ void OnTimeout();
+
+ // GaiaAuthConsumer implementation:
+ void OnClientOAuthSuccess(
+ const GaiaAuthConsumer::ClientOAuthResult& result) override;
+ void OnClientOAuthFailure(const GoogleServiceAuthError& error) override;
+
+ // Lock the account reconcilor while tokens are being fetched.
+ std::unique_ptr<AccountReconcilor::Lock> account_reconcilor_lock_;
+
+ std::string gaia_id_;
+ std::string email_;
+ std::string authorization_code_;
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate_;
+ raw_ptr<DiceResponseHandler> dice_response_handler_;
+ base::CancelableOnceClosure timeout_closure_;
+ bool should_enable_sync_;
+ std::unique_ptr<GaiaAuthFetcher> gaia_auth_fetcher_;
+ };
+
+ // Deletes the token fetcher.
+ void DeleteTokenFetcher(DiceTokenFetcher* token_fetcher);
+
+ // Process the Dice signin action.
+ void ProcessDiceSigninHeader(
+ const std::string& gaia_id,
+ const std::string& email,
+ const std::string& authorization_code,
+ bool no_authorization_code,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate);
+
+ // Process the Dice enable sync action.
+ void ProcessEnableSyncHeader(
+ const std::string& gaia_id,
+ const std::string& email,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate);
+
+ // Process the Dice signout action.
+ void ProcessDiceSignoutHeader(
+ const std::vector<signin::DiceResponseParams::AccountInfo>&
+ account_infos);
+
+ // Called after exchanging an OAuth 2.0 authorization code for a refresh token
+ // after DiceAction::SIGNIN.
+ void OnTokenExchangeSuccess(DiceTokenFetcher* token_fetcher,
+ const std::string& refresh_token,
+ bool is_under_advanced_protection);
+ void OnTokenExchangeFailure(DiceTokenFetcher* token_fetcher,
+ const GoogleServiceAuthError& error);
+ // Called to unlock the reconcilor after a SLO outage.
+ void OnTimeoutUnlockReconcilor();
+
+ raw_ptr<SigninClient> signin_client_;
+ raw_ptr<signin::IdentityManager> identity_manager_;
+ raw_ptr<AccountReconcilor> account_reconcilor_;
+ raw_ptr<AboutSigninInternals> about_signin_internals_;
+ base::FilePath profile_path_;
+ std::vector<std::unique_ptr<DiceTokenFetcher>> token_fetchers_;
+ // Lock the account reconcilor for kLockAccountReconcilorTimeoutHours
+ // when there was OAuth outage in Dice.
+ std::unique_ptr<AccountReconcilor::Lock> lock_;
+ std::unique_ptr<base::OneShotTimer> timer_;
+ scoped_refptr<base::SequencedTaskRunner> task_runner_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_
diff --git a/chromium/chrome/browser/signin/dice_response_handler_unittest.cc b/chromium/chrome/browser/signin/dice_response_handler_unittest.cc
new file mode 100644
index 00000000000..437f4a4fb20
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_response_handler_unittest.cc
@@ -0,0 +1,820 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_response_handler.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/check.h"
+#include "base/command_line.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "base/notreached.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "base/time/time.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/signin/core/browser/about_signin_internals.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/dice_account_reconcilor_delegate.h"
+#include "components/signin/core/browser/signin_error_controller.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/test_signin_client.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using signin::DiceAction;
+using signin::DiceResponseParams;
+
+namespace {
+
+const char kAuthorizationCode[] = "authorization_code";
+const char kEmail[] = "test@email.com";
+const int kSessionIndex = 42;
+
+// TestSigninClient implementation that intercepts the GaiaAuthConsumer and
+// replaces it by a dummy one.
+class DiceTestSigninClient : public TestSigninClient, public GaiaAuthConsumer {
+ public:
+ explicit DiceTestSigninClient(PrefService* pref_service)
+ : TestSigninClient(pref_service), consumer_(nullptr) {}
+
+ DiceTestSigninClient(const DiceTestSigninClient&) = delete;
+ DiceTestSigninClient& operator=(const DiceTestSigninClient&) = delete;
+
+ ~DiceTestSigninClient() override {}
+
+ std::unique_ptr<GaiaAuthFetcher> CreateGaiaAuthFetcher(
+ GaiaAuthConsumer* consumer,
+ gaia::GaiaSource source) override {
+ DCHECK(!consumer_ || (consumer_ == consumer));
+ consumer_ = consumer;
+
+ // Pass |this| as a dummy consumer to CreateGaiaAuthFetcher().
+ // Since DiceTestSigninClient does not overrides any consumer method,
+ // everything will be dropped on the floor.
+ return TestSigninClient::CreateGaiaAuthFetcher(this, source);
+ }
+
+ // We want to reset |consumer_| here before the test interacts with the last
+ // consumer. Interacting with the last consumer (simulating success of the
+ // fetcher) namely sometimes immediately triggers another fetch with another
+ // consumer. If |consumer_| is non-null, we would hit the DCHECK.
+ GaiaAuthConsumer* GetAndClearConsumer() {
+ GaiaAuthConsumer* last_consumer = consumer_;
+ consumer_ = nullptr;
+ return last_consumer;
+ }
+
+ private:
+ raw_ptr<GaiaAuthConsumer> consumer_;
+};
+
+class DiceResponseHandlerTest : public testing::Test,
+ public AccountReconcilor::Observer {
+ public:
+ // Called after the refresh token was fetched and added in the token service.
+ void HandleTokenExchangeSuccess(CoreAccountId account_id,
+ bool is_new_account) {
+ token_exchange_account_id_ = account_id;
+ token_exchange_is_new_account_ = is_new_account;
+ }
+
+ // Called after the refresh token was fetched and added in the token service.
+ void EnableSync(const CoreAccountId& account_id) {
+ enable_sync_account_id_ = account_id;
+ }
+
+ void HandleTokenExchangeFailure(const std::string& email,
+ const GoogleServiceAuthError& error) {
+ auth_error_email_ = email;
+ auth_error_ = error;
+ }
+
+ protected:
+ DiceResponseHandlerTest()
+ : task_environment_(
+ base::test::SingleThreadTaskEnvironment::MainThreadType::IO,
+ base::test::SingleThreadTaskEnvironment::TimeSource::
+ MOCK_TIME), // URLRequestContext requires IO.
+ signin_client_(&pref_service_),
+ identity_test_env_(/*test_url_loader_factory=*/nullptr,
+ &pref_service_,
+ signin::AccountConsistencyMethod::kDice,
+ &signin_client_),
+ signin_error_controller_(
+ SigninErrorController::AccountMode::PRIMARY_ACCOUNT,
+ identity_test_env_.identity_manager()) {
+ EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
+ AboutSigninInternals::RegisterPrefs(pref_service_.registry());
+ auto account_reconcilor_delegate =
+ std::make_unique<signin::DiceAccountReconcilorDelegate>();
+ account_reconcilor_ = std::make_unique<AccountReconcilor>(
+ identity_test_env_.identity_manager(), &signin_client_,
+ std::move(account_reconcilor_delegate));
+ account_reconcilor_->AddObserver(this);
+
+ about_signin_internals_ = std::make_unique<AboutSigninInternals>(
+ identity_test_env_.identity_manager(), &signin_error_controller_,
+ signin::AccountConsistencyMethod::kDice, &signin_client_,
+ account_reconcilor_.get());
+
+ dice_response_handler_ = std::make_unique<DiceResponseHandler>(
+ &signin_client_, identity_test_env_.identity_manager(),
+ account_reconcilor_.get(), about_signin_internals_.get(),
+ temp_dir_.GetPath());
+ }
+
+ ~DiceResponseHandlerTest() override {
+ account_reconcilor_->RemoveObserver(this);
+ account_reconcilor_->Shutdown();
+ about_signin_internals_->Shutdown();
+ signin_error_controller_.Shutdown();
+ }
+
+ DiceResponseParams MakeDiceParams(DiceAction action) {
+ DiceResponseParams dice_params;
+ dice_params.user_intention = action;
+ DiceResponseParams::AccountInfo account_info;
+ account_info.gaia_id = signin::GetTestGaiaIdForEmail(kEmail);
+ account_info.email = kEmail;
+ account_info.session_index = kSessionIndex;
+ switch (action) {
+ case DiceAction::SIGNIN:
+ dice_params.signin_info =
+ std::make_unique<DiceResponseParams::SigninInfo>();
+ dice_params.signin_info->account_info = account_info;
+ dice_params.signin_info->authorization_code = kAuthorizationCode;
+ break;
+ case DiceAction::ENABLE_SYNC:
+ dice_params.enable_sync_info =
+ std::make_unique<DiceResponseParams::EnableSyncInfo>();
+ dice_params.enable_sync_info->account_info = account_info;
+ break;
+ case DiceAction::SIGNOUT:
+ dice_params.signout_info =
+ std::make_unique<DiceResponseParams::SignoutInfo>();
+ dice_params.signout_info->account_infos.push_back(account_info);
+ break;
+ case DiceAction::NONE:
+ NOTREACHED();
+ break;
+ }
+ return dice_params;
+ }
+
+ // AccountReconcilor::Observer:
+ void OnBlockReconcile() override { ++reconcilor_blocked_count_; }
+ void OnUnblockReconcile() override { ++reconcilor_unblocked_count_; }
+
+ signin::IdentityManager* identity_manager() {
+ return identity_test_env_.identity_manager();
+ }
+
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ base::ScopedTempDir temp_dir_;
+ sync_preferences::TestingPrefServiceSyncable pref_service_;
+ DiceTestSigninClient signin_client_;
+ signin::IdentityTestEnvironment identity_test_env_;
+ SigninErrorController signin_error_controller_;
+ std::unique_ptr<AboutSigninInternals> about_signin_internals_;
+ std::unique_ptr<AccountReconcilor> account_reconcilor_;
+ std::unique_ptr<DiceResponseHandler> dice_response_handler_;
+ int reconcilor_blocked_count_ = 0;
+ int reconcilor_unblocked_count_ = 0;
+ CoreAccountId token_exchange_account_id_;
+ bool token_exchange_is_new_account_ = false;
+ CoreAccountId enable_sync_account_id_;
+ GoogleServiceAuthError auth_error_;
+ std::string auth_error_email_;
+};
+
+class TestProcessDiceHeaderDelegate : public ProcessDiceHeaderDelegate {
+ public:
+ explicit TestProcessDiceHeaderDelegate(DiceResponseHandlerTest* owner)
+ : owner_(owner) {}
+
+ ~TestProcessDiceHeaderDelegate() override = default;
+
+ // Called after the refresh token was fetched and added in the token service.
+ void HandleTokenExchangeSuccess(CoreAccountId account_id,
+ bool is_new_account) override {
+ owner_->HandleTokenExchangeSuccess(account_id, is_new_account);
+ }
+
+ // Called after the refresh token was fetched and added in the token service.
+ void EnableSync(const CoreAccountId& account_id) override {
+ owner_->EnableSync(account_id);
+ }
+
+ void HandleTokenExchangeFailure(
+ const std::string& email,
+ const GoogleServiceAuthError& error) override {
+ owner_->HandleTokenExchangeFailure(email, error);
+ }
+
+ private:
+ raw_ptr<DiceResponseHandlerTest> owner_;
+};
+
+// Checks that a SIGNIN action triggers a token exchange request.
+TEST_F(DiceResponseHandlerTest, Signin) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ true /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_TRUE(auth_error_email_.empty());
+ EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id);
+ EXPECT_TRUE(token_exchange_is_new_account_);
+ // Check that the reconcilor was blocked and unblocked exactly once.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+ // Check that the AccountInfo::is_under_advanced_protection is set.
+ EXPECT_TRUE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id)
+ .is_under_advanced_protection);
+}
+
+// Checks that the account reconcilor is blocked when where was OAuth
+// outage in Dice, and unblocked after the timeout.
+TEST_F(DiceResponseHandlerTest, SupportOAuthOutageInDice) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice);
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params.signin_info->authorization_code.clear();
+ dice_params.signin_info->no_authorization_code = true;
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that the reconcilor was blocked and not unblocked before timeout.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ task_environment_.FastForwardBy(
+ base::Hours(kLockAccountReconcilorTimeoutHours + 1));
+ // Check that the reconcilor was unblocked.
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+}
+
+// Check that after receiving two headers with no authorization code,
+// timeout still restarts.
+TEST_F(DiceResponseHandlerTest, CheckTimersDuringOutageinDice) {
+ ASSERT_GT(kLockAccountReconcilorTimeoutHours, 3);
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice);
+ // Create params for the first header with no authorization code.
+ DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params_1.signin_info->authorization_code.clear();
+ dice_params_1.signin_info->no_authorization_code = true;
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that the reconcilor was blocked and not unblocked before timeout.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Wait half of the timeout.
+ task_environment_.FastForwardBy(
+ base::Hours(kLockAccountReconcilorTimeoutHours / 2));
+ // Create params for the second header with no authorization code.
+ DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params_2.signin_info->authorization_code.clear();
+ dice_params_2.signin_info->no_authorization_code = true;
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ task_environment_.FastForwardBy(
+ base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2 + 1));
+ // Check that the reconcilor was not unblocked after the first timeout
+ // passed, timer should be restarted after getting the second header.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ task_environment_.FastForwardBy(
+ base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2));
+ // Check that the reconcilor was unblocked.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+}
+
+// Check that signin works normally (the token is fetched and added to chrome)
+// on valid headers after getting a no_authorization_code header.
+TEST_F(DiceResponseHandlerTest, CheckSigninAfterOutageInDice) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice);
+ // Create params for the header with no authorization code.
+ DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params_1.signin_info->authorization_code.clear();
+ dice_params_1.signin_info->no_authorization_code = true;
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Create params for the valid header with an authorization code.
+ DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info_2 = dice_params_2.signin_info->account_info;
+ CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
+ account_info_2.gaia_id, account_info_2.email);
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that the reconcilor was blocked and not unblocked before timeout.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ true /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ EXPECT_TRUE(auth_error_email_.empty());
+ EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id_2);
+ EXPECT_TRUE(token_exchange_is_new_account_);
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Check that the AccountInfo::is_under_advanced_protection is set.
+ EXPECT_TRUE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id_2)
+ .is_under_advanced_protection);
+ task_environment_.FastForwardBy(
+ base::Hours(kLockAccountReconcilorTimeoutHours + 1));
+ // Check that the reconcilor was unblocked.
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+}
+
+// Checks that a SIGNIN action triggers a token exchange request when the
+// account is in authentication error.
+TEST_F(DiceResponseHandlerTest, Reauth) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ dice_params.signin_info->account_info.email, signin::ConsentLevel::kSync);
+ dice_params.signin_info->account_info.gaia_id = account_info.gaia;
+ CoreAccountId account_id = account_info.account_id;
+ identity_test_env_.UpdatePersistentErrorOfRefreshTokenForAccount(
+ account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ true /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id));
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id);
+ EXPECT_FALSE(token_exchange_is_new_account_);
+}
+
+// Checks that a GaiaAuthFetcher failure is handled correctly.
+TEST_F(DiceResponseHandlerTest, SigninFailure) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ EXPECT_EQ(
+ 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Simulate GaiaAuthFetcher failure.
+ GoogleServiceAuthError::State error_state =
+ GoogleServiceAuthError::SERVICE_UNAVAILABLE;
+ consumer->OnClientOAuthFailure(GoogleServiceAuthError(error_state));
+ EXPECT_EQ(
+ 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Check that the token has not been inserted in the token service.
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_EQ(account_info.email, auth_error_email_);
+ EXPECT_EQ(error_state, auth_error_.state());
+}
+
+// Checks that a second token for the same account is not requested when a
+// request is already in flight.
+TEST_F(DiceResponseHandlerTest, SigninRepeatedWithSameAccount) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_1, testing::NotNull());
+ // Start a second request for the same account.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that there is no new request.
+ GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_2, testing::IsNull());
+ // Simulate GaiaAuthFetcher success for the first request.
+ consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_FALSE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id)
+ .is_under_advanced_protection);
+}
+
+// Checks that two SIGNIN requests can happen concurrently.
+TEST_F(DiceResponseHandlerTest, SigninWithTwoAccounts) {
+ DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info_1 = dice_params_1.signin_info->account_info;
+ DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params_2.signin_info->account_info.email = "other_email";
+ dice_params_2.signin_info->account_info.gaia_id = "other_gaia_id";
+ const auto& account_info_2 = dice_params_2.signin_info->account_info;
+ CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount(
+ account_info_1.gaia_id, account_info_1.email);
+ CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
+ account_info_2.gaia_id, account_info_2.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ // Start first request.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_1, testing::NotNull());
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Start second request.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_2, testing::NotNull());
+ // Simulate GaiaAuthFetcher success for the first request.
+ consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ true /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
+ EXPECT_TRUE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id_1)
+ .is_under_advanced_protection);
+ // Simulate GaiaAuthFetcher success for the second request.
+ consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ EXPECT_FALSE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id_2)
+ .is_under_advanced_protection);
+ // Check that the reconcilor was blocked and unblocked exactly once.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+}
+
+// Checks that a ENABLE_SYNC action received after the refresh token is added
+// to the token service, triggers a call to enable sync on the delegate.
+TEST_F(DiceResponseHandlerTest, SigninEnableSyncAfterRefreshTokenFetched) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id);
+ EXPECT_TRUE(token_exchange_is_new_account_);
+ // Check that delegate was not called to enable sync.
+ EXPECT_TRUE(enable_sync_account_id_.empty());
+
+ // Enable sync.
+ dice_response_handler_->ProcessDiceHeader(
+ MakeDiceParams(DiceAction::ENABLE_SYNC),
+ std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that delegate was called to enable sync.
+ EXPECT_EQ(account_id, enable_sync_account_id_);
+}
+
+// Checks that a ENABLE_SYNC action received before the refresh token is added
+// to the token service, is schedules a call to enable sync on the delegate
+// once the refresh token is received.
+TEST_F(DiceResponseHandlerTest, SigninEnableSyncBeforeRefreshTokenFetched) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+
+ // Enable sync.
+ dice_response_handler_->ProcessDiceHeader(
+ MakeDiceParams(DiceAction::ENABLE_SYNC),
+ std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that delegate was not called to enable sync.
+ EXPECT_TRUE(enable_sync_account_id_.empty());
+
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id);
+ EXPECT_TRUE(token_exchange_is_new_account_);
+ // Check that delegate was called to enable sync.
+ EXPECT_EQ(account_id, enable_sync_account_id_);
+}
+
+TEST_F(DiceResponseHandlerTest, Timeout) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ EXPECT_EQ(
+ 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Force a timeout.
+ task_environment_.FastForwardBy(
+ base::Seconds(kDiceTokenFetchTimeoutSeconds + 1));
+ EXPECT_EQ(
+ 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Check that the token has not been inserted in the token service.
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ // Check that the reconcilor was blocked and unblocked exactly once.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+}
+
+TEST_F(DiceResponseHandlerTest, SignoutMainAccount) {
+ const char kSecondaryEmail[] = "other@gmail.com";
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
+ const auto& dice_account_info = dice_params.signout_info->account_infos[0];
+ // User is signed in to Chrome, and has some refresh token for a secondary
+ // account.
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ dice_account_info.email, signin::ConsentLevel::kSync);
+ AccountInfo secondary_account_info =
+ identity_test_env_.MakeAccountAvailable(kSecondaryEmail);
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ // Receive signout response for the main account.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+
+ // User is not signed out, token for the main account is now invalid,
+ // secondary account is untouched.
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id));
+ auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount(
+ account_info.account_id);
+ EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state());
+ EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason::
+ CREDENTIALS_REJECTED_BY_CLIENT,
+ error.GetInvalidGaiaCredentialsReason());
+
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ secondary_account_info.account_id));
+
+ EXPECT_TRUE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ // Check that the reconcilor was not blocked.
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+}
+
+TEST_F(DiceResponseHandlerTest, SignoutSecondaryAccount) {
+ const char kMainEmail[] = "main@gmail.com";
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
+ const auto& secondary_dice_account_info =
+ dice_params.signout_info->account_infos[0];
+ // User is signed in to Chrome, and has some refresh token for a secondary
+ // account.
+ AccountInfo main_account_info =
+ identity_test_env_.MakePrimaryAccountAvailable(
+ kMainEmail, signin::ConsentLevel::kSync);
+ AccountInfo secondary_account_info = identity_test_env_.MakeAccountAvailable(
+ secondary_dice_account_info.email);
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ main_account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ // Receive signout response for the secondary account.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+
+ // Only the token corresponding the the Dice parameter has been removed, and
+ // the user is still signed in.
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ main_account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+TEST_F(DiceResponseHandlerTest, SignoutWebOnly) {
+ const char kSecondaryEmail[] = "other@gmail.com";
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
+ const auto& dice_account_info = dice_params.signout_info->account_infos[0];
+ // User is NOT signed in to Chrome, and has some refresh tokens for two
+ // accounts.
+ AccountInfo account_info =
+ identity_test_env_.MakeAccountAvailable(dice_account_info.email);
+ AccountInfo secondary_account_info =
+ identity_test_env_.MakeAccountAvailable(kSecondaryEmail);
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ // Receive signout response.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Only the token corresponding the the Dice parameter has been removed.
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// Checks that signin in progress is canceled by a signout.
+TEST_F(DiceResponseHandlerTest, SigninSignoutSameAccount) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
+ const auto& dice_account_info = dice_params.signout_info->account_infos[0];
+
+ // User is signed in to Chrome.
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ dice_account_info.email, signin::ConsentLevel::kSync);
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id));
+ // Start Dice signin (reauth).
+ DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created and is pending.
+ ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull());
+ EXPECT_EQ(
+ 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Signout while signin is in flight.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that the token fetcher has been canceled and the token is invalid.
+ EXPECT_EQ(
+ 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id));
+ auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount(
+ account_info.account_id);
+ EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state());
+ EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason::
+ CREDENTIALS_REJECTED_BY_CLIENT,
+ error.GetInvalidGaiaCredentialsReason());
+}
+
+// Checks that signin in progress is not canceled by a signout for a different
+// account.
+TEST_F(DiceResponseHandlerTest, SigninSignoutDifferentAccount) {
+ // User starts signin in the web with two accounts.
+ DiceResponseParams signout_params_1 = MakeDiceParams(DiceAction::SIGNOUT);
+ DiceResponseParams signin_params_1 = MakeDiceParams(DiceAction::SIGNIN);
+ DiceResponseParams signin_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ signin_params_2.signin_info->account_info.email = "other_email";
+ signin_params_2.signin_info->account_info.gaia_id = "other_gaia_id";
+ const auto& signin_account_info_1 = signin_params_1.signin_info->account_info;
+ const auto& signin_account_info_2 = signin_params_2.signin_info->account_info;
+ CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount(
+ signin_account_info_1.gaia_id, signin_account_info_1.email);
+ CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
+ signin_account_info_2.gaia_id, signin_account_info_2.email);
+ dice_response_handler_->ProcessDiceHeader(
+ signin_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+
+ GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_1, testing::NotNull());
+ dice_response_handler_->ProcessDiceHeader(
+ signin_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_2, testing::NotNull());
+ EXPECT_EQ(
+ 2u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ ASSERT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id_1));
+ ASSERT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id_2));
+ // Signout from one of the accounts while signin is in flight.
+ dice_response_handler_->ProcessDiceHeader(
+ signout_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that one of the fetchers is cancelled.
+ EXPECT_EQ(
+ 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Allow the remaining fetcher to complete.
+ consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ EXPECT_EQ(
+ 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Check that the right token is available.
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id_2));
+}
+
+// Tests that the DiceResponseHandler is created for a normal profile but not
+// for off-the-record profiles.
+TEST(DiceResponseHandlerFactoryTest, NotInOffTheRecord) {
+ content::BrowserTaskEnvironment task_environment;
+ TestingProfile profile;
+ EXPECT_THAT(DiceResponseHandler::GetForProfile(&profile), testing::NotNull());
+ EXPECT_THAT(DiceResponseHandler::GetForProfile(
+ profile.GetPrimaryOTRProfile(/*create_if_needed=*/true)),
+ testing::IsNull());
+ EXPECT_THAT(DiceResponseHandler::GetForProfile(profile.GetOffTheRecordProfile(
+ Profile::OTRProfileID::CreateUniqueForTesting(),
+ /*create_if_needed=*/true)),
+ testing::IsNull());
+}
+
+} // namespace
diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc
new file mode 100644
index 00000000000..494a17b4695
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc
@@ -0,0 +1,211 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_signed_in_profile_creator.h"
+
+#include <string>
+
+#include "base/check.h"
+#include "base/location.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_avatar_icon_util.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+// Waits until the tokens are loaded and calls the callback. The callback is
+// called immediately if the tokens are already loaded, and called with nullptr
+// if the profile is destroyed before the tokens are loaded.
+class TokensLoadedCallbackRunner : public signin::IdentityManager::Observer {
+ public:
+ ~TokensLoadedCallbackRunner() override = default;
+ TokensLoadedCallbackRunner(const TokensLoadedCallbackRunner&) = delete;
+ TokensLoadedCallbackRunner& operator=(const TokensLoadedCallbackRunner&) =
+ delete;
+
+ // Runs the callback when the tokens are loaded. If tokens are already loaded
+ // the callback is called synchronously and this returns nullptr.
+ static std::unique_ptr<TokensLoadedCallbackRunner> RunWhenLoaded(
+ Profile* profile,
+ base::OnceCallback<void(Profile*)> callback);
+
+ private:
+ TokensLoadedCallbackRunner(Profile* profile,
+ base::OnceCallback<void(Profile*)> callback);
+
+ // signin::IdentityManager::Observer implementation:
+ void OnRefreshTokensLoaded() override {
+ scoped_identity_manager_observer_.Reset();
+ std::move(callback_).Run(profile_.get());
+ }
+
+ void OnIdentityManagerShutdown(signin::IdentityManager* manager) override {
+ scoped_identity_manager_observer_.Reset();
+ std::move(callback_).Run(nullptr);
+ }
+
+ raw_ptr<Profile> profile_;
+ raw_ptr<signin::IdentityManager> identity_manager_;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ scoped_identity_manager_observer_{this};
+ base::OnceCallback<void(Profile*)> callback_;
+};
+
+// static
+std::unique_ptr<TokensLoadedCallbackRunner>
+TokensLoadedCallbackRunner::RunWhenLoaded(
+ Profile* profile,
+ base::OnceCallback<void(Profile*)> callback) {
+ if (IdentityManagerFactory::GetForProfile(profile)
+ ->AreRefreshTokensLoaded()) {
+ std::move(callback).Run(profile);
+ return nullptr;
+ }
+
+ return base::WrapUnique(
+ new TokensLoadedCallbackRunner(profile, std::move(callback)));
+}
+
+TokensLoadedCallbackRunner::TokensLoadedCallbackRunner(
+ Profile* profile,
+ base::OnceCallback<void(Profile*)> callback)
+ : profile_(profile),
+ identity_manager_(IdentityManagerFactory::GetForProfile(profile)),
+ callback_(std::move(callback)) {
+ DCHECK(profile_);
+ DCHECK(identity_manager_);
+ DCHECK(callback_);
+ DCHECK(!identity_manager_->AreRefreshTokensLoaded());
+ scoped_identity_manager_observer_.Observe(identity_manager_.get());
+}
+
+DiceSignedInProfileCreator::DiceSignedInProfileCreator(
+ Profile* source_profile,
+ CoreAccountId account_id,
+ const std::u16string& local_profile_name,
+ absl::optional<size_t> icon_index,
+ bool use_guest_profile,
+ base::OnceCallback<void(Profile*)> callback)
+ : source_profile_(source_profile),
+ account_id_(account_id),
+ callback_(std::move(callback)) {
+ // Passing the sign-in token to an ephemeral Guest profile is part of the
+ // experiment to surface a Guest mode link in the DiceWebSigninIntercept
+ // and is only used to sign in to the web through account consistency and
+ // does NOT enable sync or any other browser level functionality.
+ // TODO(https://crbug.com/1225171): Revise the comment after Guest mode plans
+ // are finalized.
+ if (use_guest_profile) {
+ // TODO(https://crbug.com/1225171): Re-enabled if ephemeral based Guest mode
+ // is added. Remove the code otherwise.
+ NOTREACHED();
+
+ // Make sure the callback is not called synchronously.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&ProfileManager::CreateProfileAsync,
+ base::Unretained(g_browser_process->profile_manager()),
+ ProfileManager::GetGuestProfilePath(),
+ base::BindRepeating(
+ &DiceSignedInProfileCreator::OnNewProfileCreated,
+ weak_pointer_factory_.GetWeakPtr())));
+ } else {
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ if (!icon_index.has_value())
+ icon_index = storage.ChooseAvatarIconIndexForNewProfile();
+ std::u16string name = local_profile_name.empty()
+ ? storage.ChooseNameForNewProfile(*icon_index)
+ : local_profile_name;
+ ProfileManager::CreateMultiProfileAsync(
+ name, *icon_index, /*is_hidden=*/false,
+ base::BindRepeating(&DiceSignedInProfileCreator::OnNewProfileCreated,
+ weak_pointer_factory_.GetWeakPtr()));
+ }
+}
+
+DiceSignedInProfileCreator::DiceSignedInProfileCreator(
+ Profile* source_profile,
+ CoreAccountId account_id,
+ const base::FilePath& target_profile_path,
+ base::OnceCallback<void(Profile*)> callback)
+ : source_profile_(source_profile),
+ account_id_(account_id),
+ callback_(std::move(callback)) {
+ // Make sure the callback is not called synchronously.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ base::IgnoreResult(&ProfileManager::LoadProfileByPath),
+ base::Unretained(g_browser_process->profile_manager()),
+ target_profile_path, /*incognito=*/false,
+ base::BindOnce(&DiceSignedInProfileCreator::OnNewProfileInitialized,
+ weak_pointer_factory_.GetWeakPtr())));
+}
+
+DiceSignedInProfileCreator::~DiceSignedInProfileCreator() = default;
+
+void DiceSignedInProfileCreator::OnNewProfileCreated(
+ Profile* new_profile,
+ Profile::CreateStatus status) {
+ switch (status) {
+ case Profile::CREATE_STATUS_CREATED:
+ // Ignore this, wait for profile to be initialized.
+ return;
+ case Profile::CREATE_STATUS_INITIALIZED:
+ OnNewProfileInitialized(new_profile);
+ return;
+ case Profile::CREATE_STATUS_LOCAL_FAIL:
+ NOTREACHED() << "Error creating new profile";
+ if (callback_)
+ std::move(callback_).Run(nullptr);
+ return;
+ }
+}
+
+void DiceSignedInProfileCreator::OnNewProfileInitialized(Profile* new_profile) {
+ if (!new_profile) {
+ if (callback_)
+ std::move(callback_).Run(nullptr);
+ return;
+ }
+
+ DCHECK(!tokens_loaded_callback_runner_);
+ // base::Unretained is fine because the runner is owned by this.
+ auto tokens_loaded_callback_runner =
+ TokensLoadedCallbackRunner::RunWhenLoaded(
+ new_profile,
+ base::BindOnce(&DiceSignedInProfileCreator::OnNewProfileTokensLoaded,
+ base::Unretained(this)));
+ // If the callback was called synchronously, |this| may have been deleted.
+ if (tokens_loaded_callback_runner) {
+ tokens_loaded_callback_runner_ = std::move(tokens_loaded_callback_runner);
+ }
+}
+
+void DiceSignedInProfileCreator::OnNewProfileTokensLoaded(
+ Profile* new_profile) {
+ tokens_loaded_callback_runner_.reset();
+ if (!new_profile) {
+ if (callback_)
+ std::move(callback_).Run(nullptr);
+ return;
+ }
+
+ auto* accounts_mutator =
+ IdentityManagerFactory::GetForProfile(source_profile_)
+ ->GetAccountsMutator();
+ auto* new_profile_accounts_mutator =
+ IdentityManagerFactory::GetForProfile(new_profile)->GetAccountsMutator();
+ accounts_mutator->MoveAccount(new_profile_accounts_mutator, account_id_);
+ if (callback_)
+ std::move(callback_).Run(new_profile);
+}
diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h
new file mode 100644
index 00000000000..97bb462da96
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h
@@ -0,0 +1,70 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_
+#define CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_
+
+#include <string>
+
+#include "base/callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/profiles/profile.h"
+#include "google_apis/gaia/core_account_id.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+class TokensLoadedCallbackRunner;
+
+// Extracts an account from an existing profile and moves it to a new profile.
+class DiceSignedInProfileCreator {
+ public:
+ // Creates a new profile or uses Guest profile if |use_guest_profile|, and
+ // moves the account from source_profile to it.
+ // The callback is called with the new profile or nullptr in case of failure.
+ // The callback is never called synchronously.
+ // If |local_profile_name| is not empty, it will be set as local name for the
+ // new profile.
+ // If |icon_index| is nullopt, a random icon will be selected.
+ DiceSignedInProfileCreator(Profile* source_profile,
+ CoreAccountId account_id,
+ const std::u16string& local_profile_name,
+ absl::optional<size_t> icon_index,
+ bool use_guest_profile,
+ base::OnceCallback<void(Profile*)> callback);
+
+ // Uses this version when the profile already exists at `target_profile_path`
+ // but may not be loaded in memory. The profile is loaded if necessary, and
+ // the account is moved.
+ DiceSignedInProfileCreator(Profile* source_profile,
+ CoreAccountId account_id,
+ const base::FilePath& target_profile_path,
+ base::OnceCallback<void(Profile*)> callback);
+
+ ~DiceSignedInProfileCreator();
+
+ DiceSignedInProfileCreator(const DiceSignedInProfileCreator&) = delete;
+ DiceSignedInProfileCreator& operator=(const DiceSignedInProfileCreator&) =
+ delete;
+
+ private:
+ // Callback invoked once a profile is created, so we can transfer the
+ // credentials.
+ void OnNewProfileCreated(Profile* new_profile, Profile::CreateStatus status);
+
+ // Called when the profile is initialized.
+ void OnNewProfileInitialized(Profile* new_profile);
+
+ // Callback invoked once the token service is ready for the new profile.
+ void OnNewProfileTokensLoaded(Profile* new_profile);
+
+ const raw_ptr<Profile> source_profile_;
+ const CoreAccountId account_id_;
+
+ base::OnceCallback<void(Profile*)> callback_;
+ std::unique_ptr<TokensLoadedCallbackRunner> tokens_loaded_callback_runner_;
+
+ base::WeakPtrFactory<DiceSignedInProfileCreator> weak_pointer_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_
diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc b/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc
new file mode 100644
index 00000000000..b0d813a17ec
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc
@@ -0,0 +1,285 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_signed_in_profile_creator.h"
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_avatar_icon_util.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_manager_observer.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/test/base/fake_profile_manager.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const char16_t kProfileTestName[] = u"profile_test_name";
+
+std::unique_ptr<TestingProfile> BuildTestingProfile(const base::FilePath& path,
+ Profile::Delegate* delegate,
+ bool tokens_loaded) {
+ TestingProfile::Builder profile_builder;
+ profile_builder.SetDelegate(delegate);
+ profile_builder.SetPath(path);
+ std::unique_ptr<TestingProfile> profile =
+ IdentityTestEnvironmentProfileAdaptor::
+ CreateProfileForIdentityTestEnvironment(profile_builder);
+ if (!tokens_loaded) {
+ IdentityTestEnvironmentProfileAdaptor adaptor(profile.get());
+ adaptor.identity_test_env()->ResetToAccountsNotYetLoadedFromDiskState();
+ }
+ if (profile->GetPath() == ProfileManager::GetGuestProfilePath())
+ profile->SetGuestSession(true);
+ return profile;
+}
+
+class UnittestProfileManager : public FakeProfileManager {
+ public:
+ explicit UnittestProfileManager(const base::FilePath& user_data_dir)
+ : FakeProfileManager(user_data_dir) {}
+
+ void set_tokens_loaded_at_creation(bool loaded) {
+ tokens_loaded_at_creation_ = loaded;
+ }
+
+ std::unique_ptr<TestingProfile> BuildTestingProfile(
+ const base::FilePath& path,
+ Profile::Delegate* delegate) override {
+ return ::BuildTestingProfile(path, delegate, tokens_loaded_at_creation_);
+ }
+
+ bool tokens_loaded_at_creation_ = true;
+};
+
+} // namespace
+
+class DiceSignedInProfileCreatorTest : public testing::Test,
+ public ProfileManagerObserver {
+ public:
+ DiceSignedInProfileCreatorTest()
+ : local_state_(TestingBrowserProcess::GetGlobal()) {
+ EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
+ auto profile_manager_unique =
+ std::make_unique<UnittestProfileManager>(temp_dir_.GetPath());
+ profile_manager_ = profile_manager_unique.get();
+ TestingBrowserProcess::GetGlobal()->SetProfileManager(
+ std::move(profile_manager_unique));
+ profile_ = BuildTestingProfile(base::FilePath(), /*delegate=*/nullptr,
+ /*tokens_loaded=*/true);
+ identity_test_env_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+ profile_manager()->AddObserver(this);
+ }
+
+ ~DiceSignedInProfileCreatorTest() override { DeleteProfiles(); }
+
+ UnittestProfileManager* profile_manager() { return profile_manager_; }
+
+ // Test environment attached to profile().
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return identity_test_env_profile_adaptor_->identity_test_env();
+ }
+
+ // Source profile (the one which we are extracting credentials from).
+ Profile* profile() { return profile_.get(); }
+
+ // Profile created by the DiceSignedInProfileCreator.
+ Profile* signed_in_profile() { return signed_in_profile_; }
+
+ // Profile added to the ProfileManager. In general this should be the same as
+ // signed_in_profile() except in error cases.
+ Profile* added_profile() { return added_profile_; }
+
+ bool creator_callback_called() { return creator_callback_called_; }
+
+ void set_profile_added_closure(base::OnceClosure closure) {
+ profile_added_closure_ = std::move(closure);
+ }
+
+ bool use_guest_profile() const { return use_guest_profile_; }
+
+ void DeleteProfiles() {
+ identity_test_env_profile_adaptor_.reset();
+ if (profile_manager_) {
+ profile_manager()->RemoveObserver(this);
+ TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr);
+ profile_manager_ = nullptr;
+ }
+ }
+
+ // Callback for the DiceSignedInProfileCreator.
+ void OnProfileCreated(base::OnceClosure quit_closure, Profile* profile) {
+ creator_callback_called_ = true;
+ signed_in_profile_ = profile;
+ if (quit_closure)
+ std::move(quit_closure).Run();
+ }
+
+ // ProfileManagerObserver:
+ void OnProfileAdded(Profile* profile) override {
+ added_profile_ = profile;
+ if (profile_added_closure_)
+ std::move(profile_added_closure_).Run();
+ }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ base::ScopedTempDir temp_dir_;
+ ScopedTestingLocalState local_state_;
+ raw_ptr<UnittestProfileManager> profile_manager_ = nullptr;
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_env_profile_adaptor_;
+ std::unique_ptr<TestingProfile> profile_;
+ raw_ptr<Profile> signed_in_profile_ = nullptr;
+ raw_ptr<Profile> added_profile_ = nullptr;
+ base::OnceClosure profile_added_closure_;
+ bool creator_callback_called_ = false;
+ base::test::ScopedFeatureList scoped_feature_list_;
+ bool use_guest_profile_ = false;
+};
+
+TEST_F(DiceSignedInProfileCreatorTest, CreateWithTokensLoaded) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ size_t kTestIcon = profiles::GetModernAvatarIconStartIndex();
+
+ base::RunLoop loop;
+ std::unique_ptr<DiceSignedInProfileCreator> creator =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile(), account_info.account_id, kProfileTestName, kTestIcon,
+ use_guest_profile(),
+ base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated,
+ base::Unretained(this), loop.QuitClosure()));
+ loop.Run();
+
+ // Check that the account was moved.
+ EXPECT_TRUE(creator_callback_called());
+ EXPECT_TRUE(signed_in_profile());
+ EXPECT_NE(profile(), signed_in_profile());
+ EXPECT_EQ(signed_in_profile(), added_profile());
+ EXPECT_FALSE(IdentityManagerFactory::GetForProfile(profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_EQ(1u, IdentityManagerFactory::GetForProfile(signed_in_profile())
+ ->GetAccountsWithRefreshTokens()
+ .size());
+ EXPECT_TRUE(IdentityManagerFactory::GetForProfile(signed_in_profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+
+ // Check profile type
+ ASSERT_EQ(use_guest_profile(), signed_in_profile()->IsGuestSession());
+
+ // Check the profile name and icon.
+ ProfileAttributesStorage& storage =
+ profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(signed_in_profile()->GetPath());
+ ASSERT_TRUE(entry);
+ if (!use_guest_profile()) {
+ EXPECT_EQ(kProfileTestName, entry->GetLocalProfileName());
+ EXPECT_EQ(kTestIcon, entry->GetAvatarIconIndex());
+ }
+}
+
+TEST_F(DiceSignedInProfileCreatorTest, CreateWithTokensNotLoaded) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ profile_manager()->set_tokens_loaded_at_creation(false);
+
+ base::RunLoop creator_loop;
+ base::RunLoop profile_added_loop;
+ set_profile_added_closure(profile_added_loop.QuitClosure());
+ std::unique_ptr<DiceSignedInProfileCreator> creator =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile(), account_info.account_id, std::u16string(), absl::nullopt,
+ use_guest_profile(),
+ base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated,
+ base::Unretained(this), creator_loop.QuitClosure()));
+ profile_added_loop.Run();
+ base::RunLoop().RunUntilIdle();
+
+ // The profile was created, but tokens not loaded. The callback has not been
+ // called yet.
+ EXPECT_FALSE(creator_callback_called());
+ EXPECT_TRUE(added_profile());
+ EXPECT_NE(profile(), added_profile());
+
+ // Load the tokens.
+ IdentityTestEnvironmentProfileAdaptor adaptor(added_profile());
+ adaptor.identity_test_env()->ReloadAccountsFromDisk();
+ creator_loop.Run();
+
+ // Check that the account was moved.
+ EXPECT_EQ(signed_in_profile(), added_profile());
+ EXPECT_TRUE(creator_callback_called());
+ EXPECT_FALSE(IdentityManagerFactory::GetForProfile(profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_EQ(1u, IdentityManagerFactory::GetForProfile(signed_in_profile())
+ ->GetAccountsWithRefreshTokens()
+ .size());
+ EXPECT_TRUE(IdentityManagerFactory::GetForProfile(signed_in_profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+}
+
+// Deleting the creator while it is running does not crash.
+TEST_F(DiceSignedInProfileCreatorTest, DeleteWhileCreating) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ std::unique_ptr<DiceSignedInProfileCreator> creator =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile(), account_info.account_id, std::u16string(), absl::nullopt,
+ use_guest_profile(),
+ base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated,
+ base::Unretained(this), base::OnceClosure()));
+ EXPECT_FALSE(creator_callback_called());
+ creator.reset();
+ base::RunLoop().RunUntilIdle();
+}
+
+// Deleting the profile while waiting for the tokens.
+TEST_F(DiceSignedInProfileCreatorTest, DeleteProfile) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ profile_manager()->set_tokens_loaded_at_creation(false);
+
+ base::RunLoop creator_loop;
+ base::RunLoop profile_added_loop;
+ set_profile_added_closure(profile_added_loop.QuitClosure());
+ std::unique_ptr<DiceSignedInProfileCreator> creator =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile(), account_info.account_id, std::u16string(), absl::nullopt,
+ use_guest_profile(),
+ base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated,
+ base::Unretained(this), creator_loop.QuitClosure()));
+ profile_added_loop.Run();
+ base::RunLoop().RunUntilIdle();
+
+ // The profile was created, but tokens not loaded. The callback has not been
+ // called yet.
+ EXPECT_FALSE(creator_callback_called());
+ EXPECT_TRUE(added_profile());
+ EXPECT_NE(profile(), added_profile());
+
+ DeleteProfiles();
+ creator_loop.Run();
+
+ // The callback is called with nullptr profile.
+ EXPECT_TRUE(creator_callback_called());
+ EXPECT_FALSE(signed_in_profile());
+}
diff --git a/chromium/chrome/browser/signin/dice_tab_helper.cc b/chromium/chrome/browser/signin/dice_tab_helper.cc
new file mode 100644
index 00000000000..4e538d31a98
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_tab_helper.cc
@@ -0,0 +1,117 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_tab_helper.h"
+
+#include "base/check_op.h"
+#include "base/metrics/user_metrics.h"
+#include "chrome/browser/signin/dice_tab_helper.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/navigation_entry.h"
+#include "content/public/browser/navigation_handle.h"
+#include "google_apis/gaia/gaia_urls.h"
+
+DiceTabHelper::DiceTabHelper(content::WebContents* web_contents)
+ : content::WebContentsUserData<DiceTabHelper>(*web_contents),
+ content::WebContentsObserver(web_contents) {}
+
+DiceTabHelper::~DiceTabHelper() = default;
+
+void DiceTabHelper::InitializeSigninFlow(
+ const GURL& signin_url,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ signin_metrics::PromoAction promo_action,
+ const GURL& redirect_url) {
+ DCHECK(signin_url.is_valid());
+ DCHECK(signin_url_.is_empty() || signin_url_ == signin_url);
+
+ signin_url_ = signin_url;
+ signin_access_point_ = access_point;
+ signin_reason_ = reason;
+ signin_promo_action_ = promo_action;
+ is_chrome_signin_page_ = true;
+ signin_page_load_recorded_ = false;
+ redirect_url_ = redirect_url;
+ sync_signin_flow_status_ = SyncSigninFlowStatus::kNotStarted;
+
+ if (reason == signin_metrics::Reason::kSigninPrimaryAccount) {
+ sync_signin_flow_status_ = SyncSigninFlowStatus::kStarted;
+ signin_metrics::LogSigninAccessPointStarted(access_point, promo_action);
+ signin_metrics::RecordSigninUserActionForAccessPoint(access_point,
+ promo_action);
+ base::RecordAction(base::UserMetricsAction("Signin_SigninPage_Loading"));
+ }
+}
+
+bool DiceTabHelper::IsChromeSigninPage() const {
+ return is_chrome_signin_page_;
+}
+
+bool DiceTabHelper::IsSyncSigninInProgress() const {
+ return sync_signin_flow_status_ == SyncSigninFlowStatus::kStarted;
+}
+
+void DiceTabHelper::OnSyncSigninFlowComplete() {
+ // The flow is complete, reset to initial state.
+ sync_signin_flow_status_ = SyncSigninFlowStatus::kNotStarted;
+}
+
+void DiceTabHelper::DidStartNavigation(
+ content::NavigationHandle* navigation_handle) {
+ if (!is_chrome_signin_page_)
+ return;
+
+ // Ignore internal navigations.
+ if (!navigation_handle->IsInPrimaryMainFrame() ||
+ navigation_handle->IsSameDocument()) {
+ return;
+ }
+
+ if (!IsSigninPageNavigation(navigation_handle)) {
+ // Navigating away from the signin page.
+ // Note that currently any indication of a navigation is enough to consider
+ // this tab unsuitable for re-use, even if the navigation does not end up
+ // committing.
+ is_chrome_signin_page_ = false;
+ }
+}
+
+void DiceTabHelper::DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) {
+ if (!is_chrome_signin_page_)
+ return;
+
+ // Ignore internal navigations.
+ if (!navigation_handle->IsInPrimaryMainFrame() ||
+ navigation_handle->IsSameDocument()) {
+ return;
+ }
+
+ if (!IsSigninPageNavigation(navigation_handle)) {
+ // Navigating away from the signin page.
+ // Note that currently any indication of a navigation is enough to consider
+ // this tab unsuitable for re-use, even if the navigation does not end up
+ // committing.
+ is_chrome_signin_page_ = false;
+ return;
+ }
+
+ if (!signin_page_load_recorded_) {
+ signin_page_load_recorded_ = true;
+ base::RecordAction(base::UserMetricsAction("Signin_SigninPage_Shown"));
+ }
+}
+
+bool DiceTabHelper::IsSigninPageNavigation(
+ content::NavigationHandle* navigation_handle) const {
+ return !navigation_handle->IsErrorPage() &&
+ navigation_handle->GetRedirectChain()[0] == signin_url_ &&
+ navigation_handle->GetURL().DeprecatedGetOriginAsURL() ==
+ GaiaUrls::GetInstance()->gaia_url();
+}
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(DiceTabHelper);
diff --git a/chromium/chrome/browser/signin/dice_tab_helper.h b/chromium/chrome/browser/signin/dice_tab_helper.h
new file mode 100644
index 00000000000..2f6190a4f98
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_tab_helper.h
@@ -0,0 +1,97 @@
+// Copyright 2017 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_
+
+#include "components/signin/public/base/signin_metrics.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+
+namespace content {
+class NavigationHandle;
+}
+
+// Tab helper used for DICE to tag signin tabs. Signin tabs can be reused.
+class DiceTabHelper : public content::WebContentsUserData<DiceTabHelper>,
+ public content::WebContentsObserver {
+ public:
+ DiceTabHelper(const DiceTabHelper&) = delete;
+ DiceTabHelper& operator=(const DiceTabHelper&) = delete;
+
+ ~DiceTabHelper() override;
+
+ signin_metrics::AccessPoint signin_access_point() const {
+ return signin_access_point_;
+ }
+
+ signin_metrics::PromoAction signin_promo_action() const {
+ return signin_promo_action_;
+ }
+
+ signin_metrics::Reason signin_reason() const { return signin_reason_; }
+
+ const GURL& redirect_url() const { return redirect_url_; }
+
+ const GURL& signin_url() const { return signin_url_; }
+
+ // Initializes the DiceTabHelper for a new signin flow. Must be called once
+ // per signin flow happening in the tab, when the signin URL is being loaded.
+ void InitializeSigninFlow(const GURL& signin_url,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ signin_metrics::PromoAction promo_action,
+ const GURL& redirect_url);
+
+ // Returns true if this the tab is a re-usable chrome sign-in page (the signin
+ // page is loading or loaded in the tab).
+ // Returns false if the user or the page has navigated away from |signin_url|.
+ bool IsChromeSigninPage() const;
+
+ // Returns true if a signin flow was initialized with the reason
+ // kSigninPrimaryAccount and is not yet complete.
+ // Note that there is not guarantee that the flow would ever finish, and in
+ // some rare cases it is possible that a "non-sync" signin happens while this
+ // is true (if the user aborts the flow and then re-uses the same tab for a
+ // normal web signin).
+ bool IsSyncSigninInProgress() const;
+
+ // Called to notify that the sync signin is complete.
+ void OnSyncSigninFlowComplete();
+
+ private:
+ friend class content::WebContentsUserData<DiceTabHelper>;
+ explicit DiceTabHelper(content::WebContents* web_contents);
+
+ // kStarted: a Sync signin flow was started and not completed.
+ // kNotStarted: there is no sync signin flow in progress.
+ enum class SyncSigninFlowStatus { kNotStarted, kStarted };
+
+ // content::WebContentsObserver:
+ void DidStartNavigation(
+ content::NavigationHandle* navigation_handle) override;
+ void DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) override;
+
+ // Returns true if this is a navigation to the signin URL.
+ bool IsSigninPageNavigation(
+ content::NavigationHandle* navigation_handle) const;
+
+ GURL redirect_url_;
+ GURL signin_url_;
+ signin_metrics::AccessPoint signin_access_point_ =
+ signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
+ signin_metrics::PromoAction signin_promo_action_ =
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
+ signin_metrics::Reason signin_reason_ =
+ signin_metrics::Reason::kUnknownReason;
+ bool is_chrome_signin_page_ = false;
+ bool signin_page_load_recorded_ = false;
+ SyncSigninFlowStatus sync_signin_flow_status_ =
+ SyncSigninFlowStatus::kNotStarted;
+
+ WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_
diff --git a/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc b/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc
new file mode 100644
index 00000000000..96eab075f2a
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc
@@ -0,0 +1,253 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_tab_helper.h"
+
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/metrics/user_action_tester.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "content/public/common/content_features.h"
+#include "content/public/test/back_forward_cache_util.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/web_contents_tester.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
+
+class DiceTabHelperTest : public ChromeRenderViewHostTestHarness {
+ public:
+ DiceTabHelperTest() {
+ signin_url_ = GaiaUrls::GetInstance()->signin_chrome_sync_dice();
+ feature_list_.InitWithFeaturesAndParameters(
+ {{features::kBackForwardCache, {}},
+ {features::kBackForwardCacheMemoryControls, {}}},
+ {});
+ }
+
+ // Does a navigation to Gaia and initializes the tab helper.
+ void InitializeDiceTabHelper(DiceTabHelper* helper,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason) {
+ // Load the signin page.
+ std::unique_ptr<content::NavigationSimulator> simulator =
+ content::NavigationSimulator::CreateRendererInitiated(signin_url_,
+ main_rfh());
+ simulator->Start();
+ helper->InitializeSigninFlow(
+ signin_url_, access_point, reason,
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO,
+ GURL::EmptyGURL());
+ EXPECT_TRUE(helper->IsChromeSigninPage());
+ simulator->Commit();
+ }
+
+ GURL signin_url_;
+ base::test::ScopedFeatureList feature_list_;
+};
+
+// Tests DiceTabHelper intialization.
+TEST_F(DiceTabHelperTest, Initialization) {
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+
+ // Check default state.
+ EXPECT_EQ(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN,
+ dice_tab_helper->signin_access_point());
+ EXPECT_EQ(signin_metrics::Reason::kUnknownReason,
+ dice_tab_helper->signin_reason());
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+
+ // Initialize the signin flow.
+ signin_metrics::AccessPoint access_point =
+ signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE;
+ signin_metrics::Reason reason = signin_metrics::Reason::kSigninPrimaryAccount;
+ InitializeDiceTabHelper(dice_tab_helper, access_point, reason);
+ EXPECT_EQ(access_point, dice_tab_helper->signin_access_point());
+ EXPECT_EQ(reason, dice_tab_helper->signin_reason());
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+}
+
+TEST_F(DiceTabHelperTest, SigninPageStatus) {
+ // The test assumes the previous page gets deleted after navigation and will
+ // be recreated after navigation (which resets the signin page state). Disable
+ // back/forward cache to ensure that it doesn't get preserved in the cache.
+ content::DisableBackForwardCacheForTesting(
+ web_contents(), content::BackForwardCache::TEST_ASSUMES_NO_CACHING);
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+
+ // Load the signin page.
+ signin_metrics::AccessPoint access_point =
+ signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE;
+ signin_metrics::Reason reason = signin_metrics::Reason::kSigninPrimaryAccount;
+ InitializeDiceTabHelper(dice_tab_helper, access_point, reason);
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+
+ // Reloading the signin page does not interrupt the signin flow.
+ content::NavigationSimulator::Reload(web_contents());
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+
+ // Subframe navigation are ignored.
+ std::unique_ptr<content::NavigationSimulator> simulator =
+ content::NavigationSimulator::CreateRendererInitiated(
+ signin_url_.Resolve("#baz"), main_rfh());
+ simulator->CommitSameDocument();
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+
+ // Navigation in subframe does not interrupt the signin flow.
+ content::RenderFrameHostTester* render_frame_host_tester =
+ content::RenderFrameHostTester::For(main_rfh());
+ content::RenderFrameHost* sub_frame =
+ render_frame_host_tester->AppendChild("subframe");
+ content::NavigationSimulator::NavigateAndCommitFromDocument(signin_url_,
+ sub_frame);
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+
+ // Navigating to a different page resets the page status.
+ simulator = content::NavigationSimulator::CreateRendererInitiated(
+ signin_url_.Resolve("/foo"), main_rfh());
+ simulator->Start();
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+ simulator->Commit();
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+
+ // Go Back to the signin page
+ content::NavigationSimulator::GoBack(web_contents());
+ // IsChromeSigninPage() returns false after navigating away from the
+ // signin page.
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+
+ // Navigate away from the signin page
+ content::NavigationSimulator::GoForward(web_contents());
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+}
+
+// Tests DiceTabHelper metrics.
+TEST_F(DiceTabHelperTest, Metrics) {
+ base::UserActionTester ua_tester;
+ base::HistogramTester h_tester;
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+
+ // No metrics are logged when the Dice tab helper is created.
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_Signin_FromStartPage"));
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Check metrics logged when the Dice tab helper is initialized.
+ std::unique_ptr<content::NavigationSimulator> simulator =
+ content::NavigationSimulator::CreateRendererInitiated(signin_url_,
+ main_rfh());
+ simulator->Start();
+ dice_tab_helper->InitializeSigninFlow(
+ signin_url_, signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS,
+ signin_metrics::Reason::kSigninPrimaryAccount,
+ signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT,
+ GURL::EmptyGURL());
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_Signin_FromSettings"));
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+ h_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint",
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1);
+ h_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount",
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1);
+
+ // First call to did finish load does logs any Signin_SigninPage_Shown user
+ // action.
+ simulator->Commit();
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Second call to did finish load does not log any metrics.
+ dice_tab_helper->DidFinishLoad(main_rfh(), signin_url_);
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Check metrics are logged again when the Dice tab helper is re-initialized.
+ simulator = content::NavigationSimulator::CreateRendererInitiated(signin_url_,
+ main_rfh());
+ simulator->Start();
+ dice_tab_helper->InitializeSigninFlow(
+ signin_url_, signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS,
+ signin_metrics::Reason::kSigninPrimaryAccount,
+ signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT,
+ GURL::EmptyGURL());
+ EXPECT_EQ(2, ua_tester.GetActionCount("Signin_Signin_FromSettings"));
+ EXPECT_EQ(2, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ h_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint",
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 2);
+ h_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.WithDefault",
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1);
+}
+
+TEST_F(DiceTabHelperTest, IsSyncSigninInProgress) {
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+
+ // Non-sync signin.
+ InitializeDiceTabHelper(dice_tab_helper,
+ signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS,
+ signin_metrics::Reason::kAddSecondaryAccount);
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+
+ // Sync signin
+ InitializeDiceTabHelper(dice_tab_helper,
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS,
+ signin_metrics::Reason::kSigninPrimaryAccount);
+ EXPECT_TRUE(dice_tab_helper->IsSyncSigninInProgress());
+ dice_tab_helper->OnSyncSigninFlowComplete();
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+}
+
+class DiceTabHelperPrerenderTest : public DiceTabHelperTest {
+ public:
+ DiceTabHelperPrerenderTest() {
+ feature_list_.InitWithFeatures(
+ {blink::features::kPrerender2},
+ // Disable the memory requirement of Prerender2 so the test can run on
+ // any bot.
+ {blink::features::kPrerender2MemoryControls});
+ }
+
+ ~DiceTabHelperPrerenderTest() override = default;
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(DiceTabHelperPrerenderTest, SigninStatusAfterPrerendering) {
+ base::UserActionTester ua_tester;
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Sync signin
+ InitializeDiceTabHelper(dice_tab_helper,
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS,
+ signin_metrics::Reason::kSigninPrimaryAccount);
+ dice_tab_helper->OnSyncSigninFlowComplete();
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Starting prerendering a page doesn't navigate away from the signin page.
+ content::WebContentsTester::For(web_contents())
+ ->AddPrerenderAndCommitNavigation(signin_url_.Resolve("/foo/test.html"));
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+}
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc
new file mode 100644
index 00000000000..751c3a58fad
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc
@@ -0,0 +1,851 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+
+#include <string>
+
+#include "base/check.h"
+#include "base/hash/hash.h"
+#include "base/i18n/case_conversion.h"
+#include "base/metrics/field_trial_params.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/net/system_network_context_manager.h"
+#include "chrome/browser/new_tab_page/chrome_colors/generated_colors_info.h"
+#include "chrome/browser/password_manager/chrome_password_manager_client.h"
+#include "chrome/browser/policy/chrome_browser_policy_connector.h"
+#include "chrome/browser/policy/profile_policy_connector.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_avatar_icon_util.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_metrics.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
+#include "chrome/browser/signin/dice_signed_in_profile_creator.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/themes/theme_service.h"
+#include "chrome/browser/themes/theme_service_factory.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
+#include "chrome/browser/ui/signin/profile_colors_util.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper_delegate_impl.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/themes/autogenerated_theme_util.h"
+#include "components/password_manager/core/browser/password_manager.h"
+#include "components/password_manager/core/common/password_manager_ui.h"
+#include "components/policy/core/browser/browser_policy_connector.h"
+#include "components/policy/core/browser/signin/user_cloud_signin_restriction_policy_fetcher.h"
+#include "components/policy/core/common/features.h"
+#include "components/policy/core/common/policy_map.h"
+#include "components/policy/core/common/policy_namespace.h"
+#include "components/policy/core/common/policy_service.h"
+#include "components/policy/policy_constants.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "ui/base/l10n/l10n_util.h"
+
+namespace {
+
+constexpr char kProfileCreationInterceptionDeclinedPref[] =
+ "signin.ProfileCreationInterceptionDeclinedPref";
+
+void RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome outcome) {
+ base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome);
+}
+
+// Helper function to return the primary account info. The returned info is
+// empty if there is no primary account, and non-empty otherwise. Extended
+// fields may be missing if they are not available.
+AccountInfo GetPrimaryAccountInfo(signin::IdentityManager* manager) {
+ CoreAccountInfo primary_core_account_info =
+ manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ if (primary_core_account_info.IsEmpty())
+ return AccountInfo();
+
+ AccountInfo primary_account_info =
+ manager->FindExtendedAccountInfo(primary_core_account_info);
+
+ if (!primary_account_info.IsEmpty())
+ return primary_account_info;
+
+ // Return an AccountInfo without extended fields, based on the core info.
+ AccountInfo account_info;
+ account_info.gaia = primary_core_account_info.gaia;
+ account_info.email = primary_core_account_info.email;
+ account_info.account_id = primary_core_account_info.account_id;
+ return account_info;
+}
+
+bool HasNoBrowser(content::WebContents* web_contents) {
+ return chrome::FindBrowserWithWebContents(web_contents) == nullptr;
+}
+
+// Returns true if enterprise separation is required.
+// Returns false is enterprise separation is not required.
+// Returns no value if info is required to determine if enterprise separation is
+// required.
+// If `managed_account_profile_level_signin_restriction` is `absl::nullopt` then
+// the user cloud policy value of ManagedAccountsSigninRestriction has not yet
+// been fetched. If it is an empty string, then the value has been fetched but
+// no policy was set.
+absl::optional<bool> EnterpriseSeparationMaybeRequired(
+ Profile* profile,
+ const std::string& email,
+ signin::IdentityManager* identity_manager,
+ bool is_new_account_interception,
+ absl::optional<std::string>
+ managed_account_profile_level_signin_restriction) {
+ // No enterprise separation required if the feature is disabled.
+ if (!base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync))
+ return false;
+ // No enterprise separation required for consumer accounts.
+ if (policy::BrowserPolicyConnector::IsNonEnterpriseUser(email))
+ return false;
+
+ auto intercepted_account_info =
+ identity_manager->FindExtendedAccountInfoByEmailAddress(email);
+ // If the account info is not found, we need to wait for the info to be
+ // available.
+ if (!intercepted_account_info.IsValid())
+ return absl::nullopt;
+ // If the intercepted account is not managed, no interception required.
+ if (!intercepted_account_info.IsManaged())
+ return false;
+ // If `profile` requires enterprise profile separation, return true.
+ if (signin_util::ProfileSeparationEnforcedByPolicy(
+ profile, managed_account_profile_level_signin_restriction.value_or(
+ std::string()))) {
+ return true;
+ }
+ // If we still do not know if profile separation is required, the account
+ // level policies for the intercepted account must be fetched if possible.
+ if (is_new_account_interception &&
+ base::FeatureList::IsEnabled(
+ policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher) &&
+ !managed_account_profile_level_signin_restriction.has_value() &&
+ g_browser_process->system_network_context_manager()) {
+ return absl::nullopt;
+ }
+
+ return false;
+}
+
+} // namespace
+
+ScopedDiceWebSigninInterceptionBubbleHandle::
+ ~ScopedDiceWebSigninInterceptionBubbleHandle() = default;
+
+bool SigninInterceptionHeuristicOutcomeIsSuccess(
+ SigninInterceptionHeuristicOutcome outcome) {
+ return outcome == SigninInterceptionHeuristicOutcome::kInterceptEnterprise ||
+ outcome == SigninInterceptionHeuristicOutcome::kInterceptMultiUser ||
+ outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
+}
+
+DiceWebSigninInterceptor::DiceWebSigninInterceptor(
+ Profile* profile,
+ std::unique_ptr<Delegate> delegate)
+ : profile_(profile),
+ identity_manager_(IdentityManagerFactory::GetForProfile(profile)),
+ delegate_(std::move(delegate)) {
+ DCHECK(profile_);
+ DCHECK(identity_manager_);
+ DCHECK(delegate_);
+}
+
+DiceWebSigninInterceptor::~DiceWebSigninInterceptor() = default;
+
+// static
+void DiceWebSigninInterceptor::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ registry->RegisterDictionaryPref(kProfileCreationInterceptionDeclinedPref);
+ registry->RegisterBooleanPref(prefs::kSigninInterceptionEnabled, true);
+ registry->RegisterStringPref(prefs::kManagedAccountsSigninRestriction,
+ std::string());
+ registry->RegisterBooleanPref(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, false);
+}
+
+absl::optional<SigninInterceptionHeuristicOutcome>
+DiceWebSigninInterceptor::GetHeuristicOutcome(
+ bool is_new_account,
+ bool is_sync_signin,
+ const std::string& email,
+ const ProfileAttributesEntry** entry) const {
+ if (!profile_->GetPrefs()->GetBoolean(prefs::kSigninInterceptionEnabled))
+ return SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled;
+
+ if (is_sync_signin) {
+ // Do not intercept signins from the Sync startup flow.
+ // Note: |is_sync_signin| is an approximation, and in rare cases it may be
+ // true when in fact the signin was not a sync signin. In this case the
+ // interception is missed.
+ return SigninInterceptionHeuristicOutcome::kAbortSyncSignin;
+ }
+ // Wait for more account info is enterprise separation is required or if more
+ // info is needed.
+ if (EnterpriseSeparationMaybeRequired(
+ profile_, email, identity_manager_, is_new_account,
+ /*managed_account_profile_level_signin_restriction=*/absl::nullopt)
+ .value_or(true)) {
+ return absl::nullopt;
+ }
+
+ if (!is_new_account) {
+ // Do not intercept reauth.
+ return SigninInterceptionHeuristicOutcome::kAbortAccountNotNew;
+ }
+
+ const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble(
+ email,
+ &g_browser_process->profile_manager()->GetProfileAttributesStorage());
+ if (switch_to_entry) {
+ if (entry)
+ *entry = switch_to_entry;
+ return SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
+ }
+
+ // From this point the remaining possible interceptions involve creating a new
+ // profile.
+ if (!profiles::IsProfileCreationAllowed()) {
+ return SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed;
+ }
+
+ std::vector<CoreAccountInfo> accounts_in_chrome =
+ identity_manager_->GetAccountsWithRefreshTokens();
+ if (accounts_in_chrome.size() == 0 ||
+ (accounts_in_chrome.size() == 1 &&
+ gaia::AreEmailsSame(email, accounts_in_chrome[0].email))) {
+ // Enterprise and multi-user bubbles are only shown if there are multiple
+ // accounts. The intercepted account may not be added to chrome yet.
+ return SigninInterceptionHeuristicOutcome::kAbortSingleAccount;
+ }
+
+ if (HasUserDeclinedProfileCreation(email)) {
+ return SigninInterceptionHeuristicOutcome::
+ kAbortUserDeclinedProfileForAccount;
+ }
+
+ return absl::nullopt;
+}
+
+void DiceWebSigninInterceptor::MaybeInterceptWebSignin(
+ content::WebContents* web_contents,
+ CoreAccountId account_id,
+ bool is_new_account,
+ bool is_sync_signin) {
+ if (is_interception_in_progress_) {
+ // Multiple concurrent interceptions are not supported.
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress);
+ return;
+ }
+
+ if (!web_contents) {
+ // The tab has been closed (typically during the token exchange, which may
+ // take some time).
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortTabClosed);
+ return;
+ }
+
+ if (HasNoBrowser(web_contents)) {
+ // Do not intercept from the profile creation flow.
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortNoBrowser);
+ return;
+ }
+
+ // Do not show the interception UI if a password update is required: both
+ // bubbles cannot be shown at the same time and the password update is more
+ // important.
+ ChromePasswordManagerClient* password_manager_client =
+ ChromePasswordManagerClient::FromWebContents(web_contents);
+ if (password_manager_client && password_manager_client->GetPasswordManager()
+ ->IsFormManagerPendingPasswordUpdate()) {
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortPasswordUpdatePending);
+ return;
+ }
+
+ ManagePasswordsUIController* password_controller =
+ ManagePasswordsUIController::FromWebContents(web_contents);
+ if (password_controller &&
+ password_controller->GetState() ==
+ password_manager::ui::State::PENDING_PASSWORD_UPDATE_STATE) {
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortPasswordUpdate);
+ return;
+ }
+
+ AccountInfo account_info =
+ identity_manager_->FindExtendedAccountInfoByAccountId(account_id);
+ DCHECK(!account_info.IsEmpty()) << "Intercepting unknown account.";
+ const ProfileAttributesEntry* entry = nullptr;
+ absl::optional<SigninInterceptionHeuristicOutcome> heuristic_outcome =
+ GetHeuristicOutcome(is_new_account, is_sync_signin, account_info.email,
+ &entry);
+ account_id_ = account_id;
+ is_interception_in_progress_ = true;
+ new_account_interception_ = is_new_account;
+ web_contents_ = web_contents->GetWeakPtr();
+
+ if (heuristic_outcome) {
+ RecordSigninInterceptionHeuristicOutcome(*heuristic_outcome);
+ if (*heuristic_outcome ==
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch) {
+ DCHECK(entry);
+ Delegate::BubbleParameters bubble_parameters{
+ SigninInterceptionType::kProfileSwitch, account_info,
+ GetPrimaryAccountInfo(identity_manager_),
+ entry->GetProfileThemeColors().profile_highlight_color,
+ /*show_guest_option=*/false};
+ interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble(
+ web_contents, bubble_parameters,
+ base::BindOnce(&DiceWebSigninInterceptor::OnProfileSwitchChoice,
+ base::Unretained(this), account_info.email,
+ entry->GetPath()));
+ was_interception_ui_displayed_ = true;
+ } else {
+ // Interception is aborted.
+ DCHECK(!SigninInterceptionHeuristicOutcomeIsSuccess(*heuristic_outcome));
+ Reset();
+ }
+ return;
+ }
+
+ account_info_fetch_start_time_ = base::TimeTicks::Now();
+ if (account_info.IsValid()) {
+ OnExtendedAccountInfoUpdated(account_info);
+ } else {
+ on_account_info_update_timeout_.Reset(base::BindOnce(
+ &DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout,
+ base::Unretained(this)));
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE, on_account_info_update_timeout_.callback(),
+ base::Seconds(5));
+ account_info_update_observation_.Observe(identity_manager_.get());
+ }
+}
+
+void DiceWebSigninInterceptor::CreateBrowserAfterSigninInterception(
+ CoreAccountId account_id,
+ content::WebContents* intercepted_contents,
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> bubble_handle,
+ bool is_new_profile) {
+ DCHECK(!session_startup_helper_);
+ DCHECK(bubble_handle);
+ interception_bubble_handle_ = std::move(bubble_handle);
+ session_startup_helper_ =
+ std::make_unique<DiceInterceptedSessionStartupHelper>(
+ profile_, is_new_profile, account_id, intercepted_contents);
+ session_startup_helper_->Startup(
+ base::BindOnce(&DiceWebSigninInterceptor::OnNewBrowserCreated,
+ base::Unretained(this), is_new_profile));
+}
+
+void DiceWebSigninInterceptor::Shutdown() {
+ if (is_interception_in_progress_ && !was_interception_ui_displayed_) {
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortShutdown);
+ }
+ Reset();
+}
+
+void DiceWebSigninInterceptor::Reset() {
+ web_contents_ = nullptr;
+ account_info_update_observation_.Reset();
+ on_account_info_update_timeout_.Cancel();
+ is_interception_in_progress_ = false;
+ account_id_ = CoreAccountId();
+ new_account_interception_ = false;
+ intercepted_account_management_accepted_ = false;
+ dice_signed_in_profile_creator_.reset();
+ was_interception_ui_displayed_ = false;
+ account_info_fetch_start_time_ = base::TimeTicks();
+ profile_creation_start_time_ = base::TimeTicks();
+ interception_bubble_handle_.reset();
+ on_intercepted_account_level_policy_value_timeout_.Cancel();
+ account_level_signin_restriction_policy_fetcher_.reset();
+ intercepted_account_level_policy_value_.reset();
+}
+
+const ProfileAttributesEntry*
+DiceWebSigninInterceptor::ShouldShowProfileSwitchBubble(
+ const std::string& intercepted_email,
+ ProfileAttributesStorage* profile_attribute_storage) const {
+ // Check if there is already an existing profile with this account.
+ base::FilePath profile_path = profile_->GetPath();
+ for (const auto* entry :
+ profile_attribute_storage->GetAllProfilesAttributes()) {
+ if (entry->GetPath() == profile_path)
+ continue;
+ if (gaia::AreEmailsSame(intercepted_email,
+ base::UTF16ToUTF8(entry->GetUserName()))) {
+ return entry;
+ }
+ }
+ return nullptr;
+}
+
+bool DiceWebSigninInterceptor::ShouldEnforceEnterpriseProfileSeparation(
+ const AccountInfo& intercepted_account_info) const {
+ DCHECK(intercepted_account_info.IsValid());
+
+ if (!signin_util::ProfileSeparationEnforcedByPolicy(
+ profile_,
+ intercepted_account_level_policy_value_.value_or(std::string()))) {
+ return false;
+ }
+ if (new_account_interception_)
+ return intercepted_account_info.IsManaged();
+
+ CoreAccountInfo primary_core_account_info =
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ // In case of re-auth, do not show the enterprise separation dialog if the
+ // user already consented to enterprise management.
+ if (!new_account_interception_ && primary_core_account_info.account_id ==
+ intercepted_account_info.account_id) {
+ return !chrome::enterprise_util::UserAcceptedAccountManagement(profile_);
+ }
+
+ return false;
+}
+
+bool DiceWebSigninInterceptor::ShouldShowEnterpriseBubble(
+ const AccountInfo& intercepted_account_info) const {
+ DCHECK(intercepted_account_info.IsValid());
+ // Check if the intercepted account or the primary account is managed.
+ CoreAccountInfo primary_core_account_info =
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+
+ if (primary_core_account_info.IsEmpty() ||
+ primary_core_account_info.account_id ==
+ intercepted_account_info.account_id) {
+ return false;
+ }
+
+ if (intercepted_account_info.IsManaged())
+ return true;
+
+ return identity_manager_->FindExtendedAccountInfo(primary_core_account_info)
+ .IsManaged();
+}
+
+bool DiceWebSigninInterceptor::ShouldShowMultiUserBubble(
+ const AccountInfo& intercepted_account_info) const {
+ DCHECK(intercepted_account_info.IsValid());
+ if (identity_manager_->GetAccountsWithRefreshTokens().size() <= 1u)
+ return false;
+ // Check if the account has the same name as another account in the profile.
+ for (const auto& account_info :
+ identity_manager_->GetExtendedAccountInfoForAccountsWithRefreshToken()) {
+ if (account_info.account_id == intercepted_account_info.account_id)
+ continue;
+ // Case-insensitve comparison supporting non-ASCII characters.
+ if (base::i18n::FoldCase(base::UTF8ToUTF16(account_info.given_name)) ==
+ base::i18n::FoldCase(
+ base::UTF8ToUTF16(intercepted_account_info.given_name))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void DiceWebSigninInterceptor::OnInterceptionReadyToBeProcessed(
+ const AccountInfo& info) {
+ DCHECK_EQ(info.account_id, account_id_);
+ DCHECK(info.IsValid());
+
+ absl::optional<SigninInterceptionType> interception_type;
+
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile_->GetPath());
+ SkColor profile_color = GenerateNewProfileColor(entry).color;
+
+ const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble(
+ info.email,
+ &g_browser_process->profile_manager()->GetProfileAttributesStorage());
+
+ bool force_profile_separation =
+ ShouldEnforceEnterpriseProfileSeparation(info);
+
+#if DCHECK_IS_ON()
+ if (force_profile_separation) {
+ DCHECK(base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync) ||
+ !profile_->GetPrefs()
+ ->GetString(prefs::kManagedAccountsSigninRestriction)
+ .empty());
+ }
+#endif
+
+ if (switch_to_entry) {
+ // Propose account switching if we skipped in GetHeuristicOutcome because we
+ // returned a nullptr to get more information about forced enterprise
+ // profile separation.
+ interception_type = force_profile_separation
+ ? SigninInterceptionType::kProfileSwitchForced
+ : SigninInterceptionType::kProfileSwitch;
+ RecordSigninInterceptionHeuristicOutcome(
+ force_profile_separation
+ ? SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch
+ : SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+ } else if (force_profile_separation) {
+ // In case of a reauth of an account that already had sync enabled,
+ // the user already accepted to use a managed profile. Simply update that
+ // fact.
+ if (!new_account_interception_ &&
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync) ==
+ info.account_id) {
+ chrome::enterprise_util::SetUserAcceptedAccountManagement(profile_, true);
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortAccountNotNew);
+ Reset();
+ return;
+ }
+ interception_type = SigninInterceptionType::kEnterpriseForced;
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+ } else if (ShouldShowEnterpriseBubble(info)) {
+ interception_type = SigninInterceptionType::kEnterprise;
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise);
+ } else if (ShouldShowMultiUserBubble(info)) {
+ interception_type = SigninInterceptionType::kMultiUser;
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kInterceptMultiUser);
+ }
+
+ if (!interception_type) {
+ // Signin should not be intercepted.
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible);
+ Reset();
+ return;
+ }
+
+ Delegate::BubbleParameters bubble_parameters{
+ *interception_type, info, GetPrimaryAccountInfo(identity_manager_),
+ GetAutogeneratedThemeColors(profile_color).frame_color,
+ /*show_guest_option=*/false};
+
+ base::OnceCallback<void(SigninInterceptionResult)> callback;
+ switch (*interception_type) {
+ case SigninInterceptionType::kProfileSwitchForced:
+ callback = base::BindOnce(
+ &DiceWebSigninInterceptor::OnProfileSwitchChoice,
+ base::Unretained(this), info.email, switch_to_entry->GetPath());
+ break;
+ case SigninInterceptionType::kEnterpriseForced:
+ callback = base::BindOnce(
+ &DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult,
+ base::Unretained(this), info, profile_color);
+ break;
+ case SigninInterceptionType::kProfileSwitch:
+ case SigninInterceptionType::kEnterprise:
+ case SigninInterceptionType::kMultiUser:
+ callback =
+ base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice,
+ base::Unretained(this), info, profile_color);
+ break;
+ }
+ interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble(
+ web_contents_.get(), bubble_parameters, std::move(callback));
+
+ was_interception_ui_displayed_ = true;
+}
+
+void DiceWebSigninInterceptor::OnExtendedAccountInfoUpdated(
+ const AccountInfo& info) {
+ if (info.account_id != account_id_)
+ return;
+ if (!info.IsValid())
+ return;
+
+ account_info_update_observation_.Reset();
+ on_account_info_update_timeout_.Cancel();
+ base::UmaHistogramTimes(
+ "Signin.Intercept.AccountInfoFetchDuration",
+ base::TimeTicks::Now() - account_info_fetch_start_time_);
+
+ // Fetch the ManagedAccountsSigninRestriction policy value for the intercepted
+ // account with a timeout.
+ if (!EnterpriseSeparationMaybeRequired(
+ profile_, info.email, identity_manager_, new_account_interception_,
+ intercepted_account_level_policy_value_)
+ .has_value()) {
+ FetchAccountLevelSigninRestrictionForInterceptedAccount(
+ info, base::BindOnce(
+ &DiceWebSigninInterceptor::
+ OnAccountLevelManagedAccountsSigninRestrictionReceived,
+ base::Unretained(this), /*timed_out=*/false, info));
+ return;
+ }
+
+ OnInterceptionReadyToBeProcessed(info);
+}
+
+void DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout() {
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortAccountInfoTimeout);
+ Reset();
+}
+
+void DiceWebSigninInterceptor::OnProfileCreationChoice(
+ const AccountInfo& account_info,
+ SkColor profile_color,
+ SigninInterceptionResult create) {
+ if (create != SigninInterceptionResult::kAccepted &&
+ create != SigninInterceptionResult::kAcceptedWithGuest) {
+ if (create == SigninInterceptionResult::kDeclined)
+ RecordProfileCreationDeclined(account_info.email);
+ Reset();
+ return;
+ }
+
+ DCHECK(interception_bubble_handle_);
+ profile_creation_start_time_ = base::TimeTicks::Now();
+ std::u16string profile_name;
+ profile_name = profiles::GetDefaultNameForNewSignedInProfile(account_info);
+
+ DCHECK(!dice_signed_in_profile_creator_);
+ // Unretained is fine because the profile creator is owned by this.
+ dice_signed_in_profile_creator_ =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile_, account_id_, profile_name,
+ profiles::GetPlaceholderAvatarIndex(),
+ create == SigninInterceptionResult::kAcceptedWithGuest,
+ base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
+ base::Unretained(this), profile_color));
+}
+
+void DiceWebSigninInterceptor::OnProfileSwitchChoice(
+ const std::string& email,
+ const base::FilePath& profile_path,
+ SigninInterceptionResult switch_profile) {
+ if (switch_profile != SigninInterceptionResult::kAccepted) {
+ Reset();
+ return;
+ }
+
+ DCHECK(interception_bubble_handle_);
+ DCHECK(!dice_signed_in_profile_creator_);
+ profile_creation_start_time_ = base::TimeTicks::Now();
+ // Unretained is fine because the profile creator is owned by this.
+ dice_signed_in_profile_creator_ =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile_, account_id_, profile_path,
+ base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
+ base::Unretained(this), absl::nullopt));
+}
+
+void DiceWebSigninInterceptor::OnNewSignedInProfileCreated(
+ absl::optional<SkColor> profile_color,
+ Profile* new_profile) {
+ DCHECK(dice_signed_in_profile_creator_);
+ dice_signed_in_profile_creator_.reset();
+
+ if (!new_profile) {
+ Reset();
+ return;
+ }
+
+ // The profile color is defined only when the profile has just been created
+ // (with interception type kMultiUser or kEnterprise). If the profile is not
+ // new (kProfileSwitch) or if it is a guest profile, then the color is not
+ // updated.
+ bool is_new_profile = profile_color.has_value();
+ if (is_new_profile) {
+ base::UmaHistogramTimes(
+ "Signin.Intercept.ProfileCreationDuration",
+ base::TimeTicks::Now() - profile_creation_start_time_);
+ ProfileMetrics::LogProfileAddNewUser(
+ ProfileMetrics::ADD_NEW_USER_SIGNIN_INTERCEPTION);
+ // TODO(https://crbug.com/1225171): Remove the condition if Guest mode
+ // option is removed.
+ if (!new_profile->IsGuestSession()) {
+ // Apply the new color to the profile.
+ ThemeServiceFactory::GetForProfile(new_profile)
+ ->BuildAutogeneratedThemeFromColor(*profile_color);
+ }
+ } else {
+ base::UmaHistogramTimes(
+ "Signin.Intercept.ProfileSwitchDuration",
+ base::TimeTicks::Now() - profile_creation_start_time_);
+ }
+
+ if (base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)) {
+ chrome::enterprise_util::SetUserAcceptedAccountManagement(
+ new_profile, intercepted_account_management_accepted_);
+ }
+
+ // Work is done in this profile, the flow continues in the
+ // DiceWebSigninInterceptor that is attached to the new profile.
+ DiceWebSigninInterceptorFactory::GetForProfile(new_profile)
+ ->CreateBrowserAfterSigninInterception(
+ account_id_, web_contents_.get(),
+ std::move(interception_bubble_handle_), is_new_profile);
+ Reset();
+}
+
+void DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult(
+ const AccountInfo& account_info,
+ SkColor profile_color,
+ SigninInterceptionResult create) {
+ DCHECK(base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync));
+ if (create == SigninInterceptionResult::kAccepted) {
+ intercepted_account_management_accepted_ = true;
+ // In case of a reauth if there was no consent for management, do not create
+ // a new profile.
+ if (!new_account_interception_ &&
+ GetPrimaryAccountInfo(identity_manager_).account_id ==
+ account_info.account_id) {
+ chrome::enterprise_util::SetUserAcceptedAccountManagement(
+ profile_, intercepted_account_management_accepted_);
+ Reset();
+ } else {
+ OnProfileCreationChoice(account_info, profile_color,
+ SigninInterceptionResult::kAccepted);
+ }
+ } else {
+ DCHECK_EQ(SigninInterceptionResult::kDeclined, create)
+ << "The user can only accept or decline";
+ OnProfileCreationChoice(account_info, profile_color,
+ SigninInterceptionResult::kDeclined);
+ auto* accounts_mutator = identity_manager_->GetAccountsMutator();
+ accounts_mutator->RemoveAccount(
+ account_info.account_id,
+ signin_metrics::SourceForRefreshTokenOperation::
+ kDiceTurnOnSyncHelper_Abort);
+ }
+ signin_util::RecordEnterpriseProfileCreationUserChoice(
+ /*enforced_by_policy=*/signin_util::ProfileSeparationEnforcedByPolicy(
+ profile_,
+ intercepted_account_level_policy_value_.value_or(std::string())),
+ /*created=*/create == SigninInterceptionResult::kAccepted);
+}
+
+void DiceWebSigninInterceptor::OnNewBrowserCreated(bool is_new_profile) {
+ DCHECK(interception_bubble_handle_);
+ interception_bubble_handle_.reset(); // Close the bubble now.
+ session_startup_helper_.reset();
+
+ // TODO(https://crbug.com/1225171): Remove |IsGuestSession| if Guest option is
+ // no more supported.
+ if (!is_new_profile || profile_->IsGuestSession())
+ return;
+
+ // Don't show the customization bubble if a valid policy theme is set.
+ Browser* browser = chrome::FindBrowserWithProfile(profile_);
+ if (ThemeServiceFactory::GetForProfile(profile_)->UsingPolicyTheme()) {
+ // Show the profile switch IPH that is normally shown after the
+ // customization bubble.
+ browser->window()->MaybeShowProfileSwitchIPH();
+ return;
+ }
+
+ DCHECK(browser);
+ delegate_->ShowProfileCustomizationBubble(browser);
+}
+
+// static
+std::string DiceWebSigninInterceptor::GetPersistentEmailHash(
+ const std::string& email) {
+ int hash = base::PersistentHash(
+ gaia::CanonicalizeEmail(gaia::SanitizeEmail(email))) &
+ 0xFF;
+ return base::StringPrintf("email_%i", hash);
+}
+
+void DiceWebSigninInterceptor::RecordProfileCreationDeclined(
+ const std::string& email) {
+ DictionaryPrefUpdate update(profile_->GetPrefs(),
+ kProfileCreationInterceptionDeclinedPref);
+ std::string key = GetPersistentEmailHash(email);
+ absl::optional<int> declined_count = update->FindIntKey(key);
+ update->SetIntKey(key, declined_count.value_or(0) + 1);
+}
+
+bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation(
+ const std::string& email) const {
+ const base::DictionaryValue* pref_data = profile_->GetPrefs()->GetDictionary(
+ kProfileCreationInterceptionDeclinedPref);
+ absl::optional<int> declined_count =
+ pref_data->FindIntKey(GetPersistentEmailHash(email));
+ // Check if the user declined 2 times.
+ constexpr int kMaxProfileCreationDeclinedCount = 2;
+ return declined_count &&
+ declined_count.value() >= kMaxProfileCreationDeclinedCount;
+}
+
+void DiceWebSigninInterceptor::
+ FetchAccountLevelSigninRestrictionForInterceptedAccount(
+ const AccountInfo& account_info,
+ base::OnceCallback<void(const std::string&)> callback) {
+ DCHECK(base::FeatureList::IsEnabled(
+ policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher));
+ if (intercepted_account_level_policy_value_fetch_result_for_testing_
+ .has_value()) {
+ std::move(callback).Run(
+ intercepted_account_level_policy_value_fetch_result_for_testing_
+ .value());
+ return;
+ }
+
+ account_level_signin_restriction_policy_fetcher_ =
+ std::make_unique<policy::UserCloudSigninRestrictionPolicyFetcher>(
+ g_browser_process->browser_policy_connector(),
+ g_browser_process->system_network_context_manager()
+ ->GetSharedURLLoaderFactory());
+ account_level_signin_restriction_policy_fetcher_
+ ->GetManagedAccountsSigninRestriction(
+ identity_manager_, account_info.account_id, std::move(callback));
+
+ on_intercepted_account_level_policy_value_timeout_.Reset(base::BindOnce(
+ &DiceWebSigninInterceptor::
+ OnAccountLevelManagedAccountsSigninRestrictionReceived,
+ base::Unretained(this), /*timed_out=*/true, account_info, std::string()));
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE, on_intercepted_account_level_policy_value_timeout_.callback(),
+ base::Seconds(5));
+}
+
+void DiceWebSigninInterceptor::
+ OnAccountLevelManagedAccountsSigninRestrictionReceived(
+ bool timed_out,
+ const AccountInfo& account_info,
+ const std::string& signin_restriction) {
+#if DCHECK_IS_ON()
+ if (timed_out) {
+ DCHECK(signin_restriction.empty())
+ << "There should be no signin restriction at the account level in case "
+ "of a timeout";
+ }
+#endif
+ intercepted_account_level_policy_value_ = signin_restriction;
+ OnInterceptionReadyToBeProcessed(account_info);
+}
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h
new file mode 100644
index 00000000000..0e3a03f41c3
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h
@@ -0,0 +1,411 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
+#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
+
+#include <memory>
+
+#include "base/callback_forward.h"
+#include "base/cancelable_callback.h"
+#include "base/feature_list.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
+#include "base/time/time.h"
+#include "chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "google_apis/gaia/core_account_id.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/skia/include/core/SkColor.h"
+
+namespace base {
+class FilePath;
+}
+
+namespace content {
+class WebContents;
+}
+
+namespace policy {
+class UserCloudSigninRestrictionPolicyFetcher;
+}
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+struct AccountInfo;
+class Browser;
+class DiceSignedInProfileCreator;
+class DiceInterceptedSessionStartupHelper;
+class Profile;
+class ProfileAttributesEntry;
+class ProfileAttributesStorage;
+
+// Outcome of the interception heuristic (decision whether the interception
+// bubble is shown or not).
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class SigninInterceptionHeuristicOutcome {
+ // Interception succeeded:
+ kInterceptProfileSwitch = 0,
+ kInterceptMultiUser = 1,
+ kInterceptEnterprise = 2,
+
+ // Interception aborted:
+ // This is a "Sync" sign in and not a "web" sign in.
+ kAbortSyncSignin = 3,
+ // Another interception is already in progress.
+ kAbortInterceptInProgress = 4,
+ // This is not a new account (reauth).
+ kAbortAccountNotNew = 5,
+ // New profile is not offered when there is only one account.
+ kAbortSingleAccount = 6,
+ // Extended account info could not be downloaded.
+ kAbortAccountInfoTimeout = 7,
+ // Account info not compatible with interception (e.g. same Gaia name).
+ kAbortAccountInfoNotCompatible = 8,
+ // Profile creation disallowed.
+ kAbortProfileCreationDisallowed = 9,
+ // The interceptor was shut down before the heuristic completed.
+ kAbortShutdown = 10,
+ // The interceptor is not offered when WebContents has no browser associated.
+ kAbortNoBrowser = 11,
+ // A password update is required for the account, and this takes priority over
+ // signin interception.
+ kAbortPasswordUpdate = 12,
+ // A password update will be required for the account: the password used on
+ // the form does not match the stored password.
+ kAbortPasswordUpdatePending = 13,
+ // The user already declined a new profile for this account, the UI is not
+ // shown again.
+ kAbortUserDeclinedProfileForAccount = 14,
+ // Signin interception is disabled by the SigninInterceptionEnabled policy.
+ kAbortInterceptionDisabled = 15,
+
+ // Interception succeeded when enteprise account separation is mandatory.
+ kInterceptEnterpriseForced = 16,
+ kInterceptEnterpriseForcedProfileSwitch = 17,
+
+ // The interceptor is not triggered if the tab has already been closed.
+ kAbortTabClosed = 18,
+
+ kMaxValue = kAbortTabClosed,
+};
+
+// User selection in the interception bubble.
+enum class SigninInterceptionUserChoice { kAccept, kDecline, kGuest };
+
+// User action resulting from the interception bubble.
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class SigninInterceptionResult {
+ kAccepted = 0,
+ kDeclined = 1,
+ kIgnored = 2,
+
+ // Used when the bubble was not shown because it's not implemented.
+ kNotDisplayed = 3,
+
+ // Accepted to be opened in Guest profile.
+ kAcceptedWithGuest = 4,
+
+ kMaxValue = kAcceptedWithGuest,
+};
+
+// The ScopedDiceWebSigninInterceptionBubbleHandle closes the signin intercept
+// bubble when it is destroyed, if the bubble is still opened. Note that this
+// handle does not prevent the bubble from being closed for other reasons.
+class ScopedDiceWebSigninInterceptionBubbleHandle {
+ public:
+ virtual ~ScopedDiceWebSigninInterceptionBubbleHandle() = 0;
+};
+
+// Returns whether the heuristic outcome is a success (the signin should be
+// intercepted).
+bool SigninInterceptionHeuristicOutcomeIsSuccess(
+ SigninInterceptionHeuristicOutcome outcome);
+
+// Called after web signed in, after a successful token exchange through Dice.
+// The DiceWebSigninInterceptor may offer the user to create a new profile or
+// switch to another existing profile.
+//
+// Implementation notes: here is how an entire interception flow work for the
+// enterprise or multi-user case:
+// * MaybeInterceptWebSignin() is called when the new signin happens.
+// * Wait until the account info is downloaded.
+// * Interception UI is shown by the delegate. Keep a handle on the bubble.
+// * If the user approved, a new profile is created and the token is moved from
+// this profile to the new profile, using DiceSignedInProfileCreator.
+// * At this point, the flow ends in this profile, and continues in the new
+// profile using DiceInterceptedSessionStartupHelper to add the account.
+// * When the account is available on the web in the new profile:
+// - A new browser window is created for the new profile,
+// - The tab is moved to the new profile,
+// - The interception bubble is closed by deleting the handle,
+// - The profile customization bubble is shown.
+class DiceWebSigninInterceptor : public KeyedService,
+ public signin::IdentityManager::Observer {
+ public:
+ enum class SigninInterceptionType {
+ kProfileSwitch,
+ kEnterprise,
+ kMultiUser,
+ kEnterpriseForced,
+ kProfileSwitchForced
+ };
+
+ // Delegate class responsible for showing the various interception UIs.
+ class Delegate {
+ public:
+ // Parameters for interception bubble UIs.
+ struct BubbleParameters {
+ SigninInterceptionType interception_type;
+ AccountInfo intercepted_account;
+ AccountInfo primary_account;
+ SkColor profile_highlight_color;
+ bool show_guest_option;
+ };
+
+ virtual ~Delegate() = default;
+
+ // Shows the signin interception bubble and calls |callback| to indicate
+ // whether the user should continue in a new profile.
+ // The callback is never called if the delegate is deleted before it
+ // completes.
+ // May return a nullptr handle if the bubble cannot be shown.
+ // Warning: the handle closes the bubble when it is destroyed ; it is the
+ // responsibility of the caller to keep the handle alive until the bubble
+ // should be closed.
+ // The callback must not be called synchronously if this function returns a
+ // valid handle (because the caller needs to be able to close the bubble
+ // from the callback).
+ virtual std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ ShowSigninInterceptionBubble(
+ content::WebContents* web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback) = 0;
+
+ // Shows the profile customization bubble.
+ virtual void ShowProfileCustomizationBubble(Browser* browser) = 0;
+ };
+
+ DiceWebSigninInterceptor(Profile* profile,
+ std::unique_ptr<Delegate> delegate);
+ ~DiceWebSigninInterceptor() override;
+
+ DiceWebSigninInterceptor(const DiceWebSigninInterceptor&) = delete;
+ DiceWebSigninInterceptor& operator=(const DiceWebSigninInterceptor&) = delete;
+
+ static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+ // Called when an account has been added in Chrome from the web (using the
+ // DICE protocol).
+ // |web_contents| is the tab where the signin event happened. It must belong
+ // to the profile associated with this service. It may be nullptr if the tab
+ // was closed.
+ // |is_new_account| is true if the account was not already in Chrome (i.e.
+ // this is not a reauth).
+ // |is_sync_signin| is true if the user is signing in with the intent of
+ // enabling sync for that account.
+ // Virtual for testing.
+ virtual void MaybeInterceptWebSignin(content::WebContents* web_contents,
+ CoreAccountId account_id,
+ bool is_new_account,
+ bool is_sync_signin);
+
+ // Called after the new profile was created during a signin interception.
+ // The token has been moved to the new profile, but the account is not yet in
+ // the cookies.
+ // `intercepted_contents` may be null if the tab was already closed.
+ // The intercepted web contents belong to the source profile (which is not the
+ // profile attached to this service).
+ void CreateBrowserAfterSigninInterception(
+ CoreAccountId account_id,
+ content::WebContents* intercepted_contents,
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ bubble_handle,
+ bool is_new_profile);
+
+ // Returns the outcome of the interception heuristic.
+ // If the outcome is kInterceptProfileSwitch, the target profile is returned
+ // in |entry|.
+ // In some cases the outcome cannot be fully computed synchronously, when this
+ // happens, the signin interception is highly likely (but not guaranteed).
+ absl::optional<SigninInterceptionHeuristicOutcome> GetHeuristicOutcome(
+ bool is_new_account,
+ bool is_sync_signin,
+ const std::string& email,
+ const ProfileAttributesEntry** entry = nullptr) const;
+
+ // Returns true if the interception is in progress (running the heuristic or
+ // showing on screen).
+ bool is_interception_in_progress() const {
+ return is_interception_in_progress_;
+ }
+
+ void SetAccountLevelSigninRestrictionFetchResultForTesting(
+ absl::optional<std::string> value) {
+ intercepted_account_level_policy_value_fetch_result_for_testing_ =
+ std::move(value);
+ }
+
+ // KeyedService:
+ void Shutdown() override;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ ShouldShowProfileSwitchBubble);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ NoBubbleWithSingleAccount);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ ShouldShowEnterpriseBubble);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ ShouldShowEnterpriseBubbleWithoutUPA);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ ShouldShowMultiUserBubble);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, PersistentHash);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparation);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationWithoutUPA);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationReauth);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimary);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationReauth);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestAccountLevelPolicy);
+ FRIEND_TEST_ALL_PREFIXES(
+ DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestNoForcedInterception);
+
+ // Cancels any current signin interception and resets the interceptor to its
+ // initial state.
+ void Reset();
+
+ // Helper functions to determine which interception UI should be shown.
+ const ProfileAttributesEntry* ShouldShowProfileSwitchBubble(
+ const std::string& intercepted_email,
+ ProfileAttributesStorage* profile_attribute_storage) const;
+ bool ShouldEnforceEnterpriseProfileSeparation(
+ const AccountInfo& intercepted_account_info) const;
+ bool ShouldShowEnterpriseBubble(
+ const AccountInfo& intercepted_account_info) const;
+ bool ShouldShowMultiUserBubble(
+ const AccountInfo& intercepted_account_info) const;
+
+ void OnInterceptionReadyToBeProcessed(const AccountInfo& info);
+
+ // signin::IdentityManager::Observer:
+ void OnExtendedAccountInfoUpdated(const AccountInfo& info) override;
+
+ // Called when the extended account info was not updated after a timeout.
+ void OnExtendedAccountInfoFetchTimeout();
+
+ // Called after the user chose whether a new profile would be created.
+ void OnProfileCreationChoice(const AccountInfo& account_info,
+ SkColor profile_color,
+ SigninInterceptionResult create);
+ // Called after the user chose whether the session should continue in a new
+ // profile.
+ void OnProfileSwitchChoice(const std::string& email,
+ const base::FilePath& profile_path,
+ SigninInterceptionResult switch_profile);
+
+ // Called when the new profile is created or loaded from disk.
+ // `profile_color` is set as theme color for the profile ; it should be
+ // nullopt if the profile is not new (loaded from disk).
+ void OnNewSignedInProfileCreated(absl::optional<SkColor> profile_color,
+ Profile* new_profile);
+
+ // Called after the user choses whether the session should continue in a new
+ // work profile or not. If the user choses not to continue in a work profile,
+ // the account is signed out.
+ void OnEnterpriseProfileCreationResult(const AccountInfo& account_info,
+ SkColor profile_color,
+ SigninInterceptionResult create);
+
+ // Called when the new browser is created after interception. Passed as
+ // callback to `session_startup_helper_`.
+ void OnNewBrowserCreated(bool is_new_profile);
+
+ // Returns a 8-bit hash of the email that can be persisted.
+ static std::string GetPersistentEmailHash(const std::string& email);
+
+ // Should be called when the user declines profile creation, in order to
+ // remember their decision. This information is stored in prefs. Only a hash
+ // of the email is saved, as Chrome does not need to store the actual email,
+ // but only need to compare emails. The hash has low entropy to ensure it
+ // cannot be reversed.
+ void RecordProfileCreationDeclined(const std::string& email);
+
+ // Checks if the user previously declined 2 times creating a new profile for
+ // this account.
+ bool HasUserDeclinedProfileCreation(const std::string& email) const;
+
+ // Fetches the value of the cloud user level value of the
+ // ManagedAccountsSigninRestriction policy for 'account_info' and runs
+ // `callback` with the result. This is a network call that has a 5 seconds
+ // timeout.
+ void FetchAccountLevelSigninRestrictionForInterceptedAccount(
+ const AccountInfo& account_info,
+ base::OnceCallback<void(const std::string&)> callback);
+
+ // Called when the the value of the cloud user level value of the
+ // ManagedAccountsSigninRestriction is received.
+ void OnAccountLevelManagedAccountsSigninRestrictionReceived(
+ bool timed_out,
+ const AccountInfo& account_info,
+ const std::string& signin_restriction);
+
+ const raw_ptr<Profile> profile_;
+ const raw_ptr<signin::IdentityManager> identity_manager_;
+ std::unique_ptr<Delegate> delegate_;
+
+ // Used in the profile that was created after the interception succeeded.
+ std::unique_ptr<DiceInterceptedSessionStartupHelper> session_startup_helper_;
+
+ // Members below are related to the interception in progress.
+ base::WeakPtr<content::WebContents> web_contents_;
+ bool is_interception_in_progress_ = false;
+ CoreAccountId account_id_;
+ bool new_account_interception_ = false;
+ bool intercepted_account_management_accepted_ = false;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ account_info_update_observation_{this};
+ // Timeout for the fetch of the extended account info. The signin interception
+ // is cancelled if the account info cannot be fetched quickly.
+ base::CancelableOnceCallback<void()> on_account_info_update_timeout_;
+ std::unique_ptr<DiceSignedInProfileCreator> dice_signed_in_profile_creator_;
+ // Used to retain the interception UI bubble until profile creation completes.
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ interception_bubble_handle_;
+ // Used for metrics:
+ bool was_interception_ui_displayed_ = false;
+ base::TimeTicks account_info_fetch_start_time_;
+ base::TimeTicks profile_creation_start_time_;
+
+ // Timeout for the fetch of cloud user level policy value of
+ // ManagedAccountsSigninRestriction. The signin interception continue with an
+ // empty value for the policy if we cannot get the value.
+ base::CancelableOnceCallback<void()>
+ on_intercepted_account_level_policy_value_timeout_;
+
+ // Used to fetch the cloud user level policy value of
+ // ManagedAccountsSigninRestriction. This can only fetch one policy value for
+ // one account at the time.
+ std::unique_ptr<policy::UserCloudSigninRestrictionPolicyFetcher>
+ account_level_signin_restriction_policy_fetcher_;
+ // Value of the ManagedAccountsSigninRestriction for the intercepted account.
+ // If no value is set, then we have not yet received the policy value.
+ absl::optional<std::string> intercepted_account_level_policy_value_;
+ absl::optional<std::string>
+ intercepted_account_level_policy_value_fetch_result_for_testing_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc
new file mode 100644
index 00000000000..5a53ea95baa
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc
@@ -0,0 +1,1200 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+
+#include <map>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_init_params.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_manager_observer.h"
+#include "chrome/browser/profiles/profile_window.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_test_util.h"
+#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/themes/theme_service.h"
+#include "chrome/browser/themes/theme_service_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/profile_waiter.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/account_id/account_id.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/policy/core/common/features.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace {
+
+// Fake response for OAuth multilogin.
+const char kMultiloginSuccessResponse[] =
+ R"()]}'
+ {
+ "status": "OK",
+ "cookies":[
+ {
+ "name":"SID",
+ "value":"SID_value",
+ "domain":".google.fr",
+ "path":"/",
+ "isSecure":true,
+ "isHttpOnly":false,
+ "priority":"HIGH",
+ "maxAge":63070000
+ }
+ ]
+ }
+ )";
+
+class FakeDiceWebSigninInterceptorDelegate;
+
+class FakeBubbleHandle : public ScopedDiceWebSigninInterceptionBubbleHandle,
+ public base::SupportsWeakPtr<FakeBubbleHandle> {
+ public:
+ ~FakeBubbleHandle() override = default;
+};
+
+// Dummy interception delegate that automatically accepts multi user
+// interception.
+class FakeDiceWebSigninInterceptorDelegate
+ : public DiceWebSigninInterceptor::Delegate {
+ public:
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ ShowSigninInterceptionBubble(
+ content::WebContents* web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback) override {
+ EXPECT_EQ(bubble_parameters.interception_type, expected_interception_type_);
+ auto bubble_handle = std::make_unique<FakeBubbleHandle>();
+ weak_bubble_handle_ = bubble_handle->AsWeakPtr();
+ // The callback must not be called synchronously (see the documentation for
+ // ShowSigninInterceptionBubble).
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(std::move(callback), expected_interception_result_));
+ return bubble_handle;
+ }
+
+ void ShowProfileCustomizationBubble(Browser* browser) override {
+ EXPECT_FALSE(customized_browser_)
+ << "Customization must be shown only once.";
+ customized_browser_ = browser;
+ }
+
+ Browser* customized_browser() { return customized_browser_; }
+
+ void set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType type) {
+ expected_interception_type_ = type;
+ }
+
+ void set_expected_interception_result(SigninInterceptionResult result) {
+ expected_interception_result_ = result;
+ }
+
+ bool intercept_bubble_shown() const { return weak_bubble_handle_.get(); }
+
+ bool intercept_bubble_destroyed() const {
+ return weak_bubble_handle_.WasInvalidated();
+ }
+
+ private:
+ raw_ptr<Browser> customized_browser_ = nullptr;
+ DiceWebSigninInterceptor::SigninInterceptionType expected_interception_type_ =
+ DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser;
+ SigninInterceptionResult expected_interception_result_ =
+ SigninInterceptionResult::kAccepted;
+ base::WeakPtr<FakeBubbleHandle> weak_bubble_handle_;
+};
+
+class BrowserCloseObserver : public BrowserListObserver {
+ public:
+ explicit BrowserCloseObserver(Browser* browser) : browser_(browser) {
+ BrowserList::AddObserver(this);
+ }
+
+ BrowserCloseObserver(const BrowserCloseObserver&) = delete;
+ BrowserCloseObserver& operator=(const BrowserCloseObserver&) = delete;
+
+ ~BrowserCloseObserver() override { BrowserList::RemoveObserver(this); }
+
+ void Wait() { run_loop_.Run(); }
+
+ // BrowserListObserver implementation.
+ void OnBrowserRemoved(Browser* browser) override {
+ if (browser == browser_)
+ run_loop_.Quit();
+ }
+
+ private:
+ raw_ptr<Browser> browser_;
+ base::RunLoop run_loop_;
+};
+
+// Runs the interception and returns the new profile that was created.
+Profile* InterceptAndWaitProfileCreation(content::WebContents* contents,
+ const CoreAccountId& account_id) {
+ ProfileWaiter profile_waiter;
+ // Start the interception.
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(
+ Profile::FromBrowserContext(contents->GetBrowserContext()));
+ interceptor->MaybeInterceptWebSignin(contents, account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+ // Wait for the interception to be complete.
+ return profile_waiter.WaitForProfileAdded();
+}
+
+// Checks that the interception histograms were correctly recorded.
+void CheckHistograms(const base::HistogramTester& histogram_tester,
+ SigninInterceptionHeuristicOutcome outcome,
+ bool reauth = false) {
+ int profile_switch_count =
+ outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch ||
+ outcome == SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch
+ ? 1
+ : 0;
+ int profile_creation_count = reauth ? 0 : 1 - profile_switch_count;
+ int fetched_account_count =
+ reauth || outcome == SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch
+ ? 1
+ : profile_creation_count;
+
+ histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome",
+ outcome, 1);
+ histogram_tester.ExpectTotalCount(
+ "Signin.Intercept.AccountInfoFetchDuration",
+ base::FeatureList::IsEnabled(
+ policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher)
+ ? 1
+ : fetched_account_count);
+ histogram_tester.ExpectTotalCount("Signin.Intercept.ProfileCreationDuration",
+ profile_creation_count);
+ histogram_tester.ExpectTotalCount("Signin.Intercept.ProfileSwitchDuration",
+ profile_switch_count);
+}
+
+} // namespace
+
+class DiceWebSigninInterceptorBrowserTest : public InProcessBrowserTest {
+ public:
+ DiceWebSigninInterceptorBrowserTest() = default;
+
+ Profile* profile() { return browser()->profile(); }
+
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return identity_test_env_profile_adaptor_->identity_test_env();
+ }
+
+ network::TestURLLoaderFactory* test_url_loader_factory() {
+ return &test_url_loader_factory_;
+ }
+
+ content::WebContents* AddTab(const GURL& url) {
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+ return browser()->tab_strip_model()->GetActiveWebContents();
+ }
+
+ FakeDiceWebSigninInterceptorDelegate* GetInterceptorDelegate(
+ Profile* profile) {
+ // Make sure the interceptor has been created.
+ DiceWebSigninInterceptorFactory::GetForProfile(profile);
+ FakeDiceWebSigninInterceptorDelegate* interceptor_delegate =
+ interceptor_delegates_[profile];
+ return interceptor_delegate;
+ }
+
+ void SetupGaiaResponses() {
+ // Instantly return from Gaia calls, to avoid timing out when injecting the
+ // account in the new profile.
+ network::TestURLLoaderFactory* loader_factory = test_url_loader_factory();
+ loader_factory->SetInterceptor(base::BindLambdaForTesting(
+ [loader_factory](const network::ResourceRequest& request) {
+ std::string path = request.url.path();
+ if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") {
+ loader_factory->AddResponse(request.url.spec(), std::string());
+ return;
+ }
+ if (path == "/oauth/multilogin") {
+ loader_factory->AddResponse(request.url.spec(),
+ kMultiloginSuccessResponse);
+ return;
+ }
+ }));
+ }
+
+ private:
+ // InProcessBrowserTest:
+ void SetUpOnMainThread() override {
+ ASSERT_TRUE(embedded_test_server()->Start());
+ identity_test_env_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+ DiceWebSigninInterceptorFactory::GetForProfile(profile())
+ ->SetAccountLevelSigninRestrictionFetchResultForTesting("");
+ }
+
+ void TearDownOnMainThread() override {
+ // Must be destroyed before the Profile.
+ identity_test_env_profile_adaptor_.reset();
+ }
+
+ void SetUpInProcessBrowserTestFixture() override {
+ InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
+ create_services_subscription_ =
+ BrowserContextDependencyManager::GetInstance()
+ ->RegisterCreateServicesCallbackForTesting(
+ base::BindRepeating(&DiceWebSigninInterceptorBrowserTest::
+ OnWillCreateBrowserContextServices,
+ base::Unretained(this)));
+ }
+
+ void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
+ IdentityTestEnvironmentProfileAdaptor::
+ SetIdentityTestEnvironmentFactoriesOnBrowserContext(context);
+ ChromeSigninClientFactory::GetInstance()->SetTestingFactory(
+ context, base::BindRepeating(&BuildChromeSigninClientWithURLLoader,
+ &test_url_loader_factory_));
+ DiceWebSigninInterceptorFactory::GetInstance()->SetTestingFactory(
+ context,
+ base::BindRepeating(&DiceWebSigninInterceptorBrowserTest::
+ BuildDiceWebSigninInterceptorWithFakeDelegate,
+ base::Unretained(this)));
+ }
+
+ // Builds a DiceWebSigninInterceptor with a fake delegate. To be used as a
+ // testing factory.
+ std::unique_ptr<KeyedService> BuildDiceWebSigninInterceptorWithFakeDelegate(
+ content::BrowserContext* context) {
+ std::unique_ptr<FakeDiceWebSigninInterceptorDelegate> fake_delegate =
+ std::make_unique<FakeDiceWebSigninInterceptorDelegate>();
+ interceptor_delegates_[context] = fake_delegate.get();
+ return std::make_unique<DiceWebSigninInterceptor>(
+ Profile::FromBrowserContext(context), std::move(fake_delegate));
+ }
+
+ network::TestURLLoaderFactory test_url_loader_factory_;
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_env_profile_adaptor_;
+ base::CallbackListSubscription create_services_subscription_;
+ std::map<content::BrowserContext*, FakeDiceWebSigninInterceptorDelegate*>
+ interceptor_delegates_;
+};
+
+// Tests the complete interception flow including profile and browser creation.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, InterceptionTest) {
+ base::HistogramTester histogram_tester;
+ // Setup profile for interception.
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = kNoHostedDomainFound;
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ SetupGaiaResponses();
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ ASSERT_TRUE(new_profile);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ IdentityTestEnvironmentProfileAdaptor adaptor(new_profile);
+ adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
+
+ // Check the profile name.
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(new_profile->GetPath());
+ ASSERT_TRUE(entry);
+ EXPECT_EQ("givenname", base::UTF16ToUTF8(entry->GetLocalProfileName()));
+ // Check the profile color.
+ EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile)
+ ->UsingAutogeneratedTheme());
+
+ // A browser has been created for the new profile and the tab was moved there.
+ Browser* added_browser = ui_test_utils::WaitForBrowserToOpen();
+ ASSERT_TRUE(added_browser);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptMultiUser);
+ // Interception bubble is destroyed in the source profile, and was not shown
+ // in the new profile.
+ FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate =
+ GetInterceptorDelegate(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed());
+ EXPECT_FALSE(new_interceptor_delegate->intercept_bubble_shown());
+ EXPECT_FALSE(new_interceptor_delegate->intercept_bubble_destroyed());
+ // Profile customization UI was shown exactly once in the new profile.
+ EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete profile switch flow when the profile is not loaded.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, SwitchAndLoad) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ // Add a profile in the cache (simulate the profile on disk).
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ProfileAttributesStorage* profile_storage =
+ &profile_manager->GetProfileAttributesStorage();
+ const base::FilePath profile_path =
+ profile_manager->GenerateNextProfileDirectoryPath();
+ ProfileAttributesInitParams params;
+ params.profile_path = profile_path;
+ params.profile_name = u"TestProfileName";
+ params.gaia_id = account_info.gaia;
+ params.user_name = base::UTF8ToUTF16(account_info.email);
+ profile_storage->AddProfile(std::move(params));
+ ProfileAttributesEntry* entry =
+ profile_storage->GetProfileAttributesWithPath(profile_path);
+ ASSERT_TRUE(entry);
+ ASSERT_EQ(entry->GetGAIAId(), account_info.gaia);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ // Check that the right profile was opened.
+ EXPECT_EQ(new_profile->GetPath(), profile_path);
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // A browser has been created for the new profile and the tab was moved there.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* added_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(added_browser);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+ // Interception bubble was closed.
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed());
+ // Profile customization was not shown.
+ EXPECT_EQ(GetInterceptorDelegate(new_profile)->customized_browser(), nullptr);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete profile switch flow when the profile is already loaded.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, SwitchAlreadyOpen) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ // Create another profile with a browser window.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ const base::FilePath profile_path =
+ profile_manager->GenerateNextProfileDirectoryPath();
+ base::RunLoop loop;
+ Profile* other_profile = nullptr;
+ ProfileManager::CreateCallback callback = base::BindLambdaForTesting(
+ [&other_profile, &loop](Profile* profile, Profile::CreateStatus status) {
+ DCHECK_EQ(status, Profile::CREATE_STATUS_INITIALIZED);
+ other_profile = profile;
+ loop.Quit();
+ });
+ profiles::SwitchToProfile(profile_path, /*always_create=*/true,
+ std::move(callback));
+ loop.Run();
+ ASSERT_TRUE(other_profile);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* other_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(other_browser);
+ ASSERT_EQ(other_browser->profile(), other_profile);
+ // Add the account to the other profile.
+ signin::IdentityManager* other_identity_manager =
+ IdentityManagerFactory::GetForProfile(other_profile);
+ other_identity_manager->GetAccountsMutator()->AddOrUpdateAccount(
+ account_info.gaia, account_info.email, "dummy_refresh_token",
+ /*is_under_advanced_protection=*/false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ other_identity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount(
+ account_info.account_id, signin::ConsentLevel::kSync);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+ int other_original_tab_count = other_browser->tab_strip_model()->count();
+
+ // Start the interception.
+ GetInterceptorDelegate(profile())->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch);
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(profile());
+ interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ signin::SetCookieAccounts(other_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // The tab was moved to the new browser.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(other_browser->tab_strip_model()->count(),
+ other_original_tab_count + 1);
+ EXPECT_EQ(other_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+ // Profile customization was not shown.
+ EXPECT_EQ(GetInterceptorDelegate(other_profile)->customized_browser(),
+ nullptr);
+ EXPECT_EQ(GetInterceptorDelegate(profile())->customized_browser(), nullptr);
+}
+
+// Close the source tab during the interception and check that the NTP is opened
+// in the new profile (regression test for https://crbug.com/1153321).
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, CloseSourceTab) {
+ // Setup profile for interception.
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = kNoHostedDomainFound;
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ ProfileWaiter profile_waiter;
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(
+ Profile::FromBrowserContext(contents->GetBrowserContext()));
+ interceptor->MaybeInterceptWebSignin(contents, account_info.account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+ // Close the source tab during the profile creation.
+ contents->Close();
+ // Wait for the interception to be complete.
+ Profile* new_profile = profile_waiter.WaitForProfileAdded();
+ ASSERT_TRUE(new_profile);
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // A browser has been created for the new profile on the new tab page.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* added_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(added_browser);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ GURL("chrome://newtab/"));
+}
+
+class DiceWebSigninInterceptorEnterpriseBrowserTest
+ : public DiceWebSigninInterceptorBrowserTest {
+ public:
+ DiceWebSigninInterceptorEnterpriseBrowserTest() {
+ enterprise_feature_list_.InitWithFeatures(
+ {kAccountPoliciesLoadedWithoutSync,
+ policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher},
+ {});
+ }
+
+ private:
+ base::test::ScopedFeatureList enterprise_feature_list_;
+};
+
+// Tests the complete interception flow including profile and browser creation.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestNoForcedInterception) {
+ base::HistogramTester histogram_tester;
+
+ AccountInfo primary_account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ primary_account_info.full_name = "fullname";
+ primary_account_info.given_name = "givenname";
+ primary_account_info.hosted_domain = "example.com";
+ primary_account_info.locale = "en";
+ primary_account_info.picture_url = "https://example.com";
+ DCHECK(primary_account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);
+ IdentityManagerFactory::GetForProfile(profile())
+ ->GetPrimaryAccountMutator()
+ ->SetPrimaryAccount(primary_account_info.account_id,
+ signin::ConsentLevel::kSync);
+
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Enforce enterprise profile sepatation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "none");
+ DiceWebSigninInterceptorFactory::GetForProfile(profile())
+ ->SetAccountLevelSigninRestrictionFetchResultForTesting("");
+
+ // Instantly return from Gaia calls, to avoid timing out when injecting the
+ // account in the new profile.
+ network::TestURLLoaderFactory* loader_factory = test_url_loader_factory();
+ loader_factory->SetInterceptor(base::BindLambdaForTesting(
+ [loader_factory](const network::ResourceRequest& request) {
+ std::string path = request.url.path();
+ if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") {
+ loader_factory->AddResponse(request.url.spec(), std::string());
+ return;
+ }
+ if (path == "/oauth/multilogin") {
+ loader_factory->AddResponse(request.url.spec(),
+ kMultiloginSuccessResponse);
+ return;
+ }
+ }));
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ EXPECT_FALSE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(new_profile));
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ IdentityTestEnvironmentProfileAdaptor adaptor(new_profile);
+ adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
+
+ // Check the profile name.
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(new_profile->GetPath());
+ ASSERT_TRUE(entry);
+ EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName()));
+ // Check the profile color.
+ EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile)
+ ->UsingAutogeneratedTheme());
+
+ // A browser has been created for the new profile and the tab was moved there.
+ Browser* added_browser = ui_test_utils::WaitForBrowserToOpen();
+ ASSERT_TRUE(added_browser);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise);
+ // Interception bubble is destroyed in the source profile, and was not shown
+ // in the new profile.
+ FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate =
+ GetInterceptorDelegate(new_profile);
+
+ // Profile customization UI was shown exactly once in the new profile.
+ EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete interception flow including profile and browser creation.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestAccountLevelPolicy) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Enforce enterprise profile sepatation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "none");
+ DiceWebSigninInterceptorFactory::GetForProfile(profile())
+ ->SetAccountLevelSigninRestrictionFetchResultForTesting(
+ "primary_account");
+
+ // Instantly return from Gaia calls, to avoid timing out when injecting the
+ // account in the new profile.
+ network::TestURLLoaderFactory* loader_factory = test_url_loader_factory();
+ loader_factory->SetInterceptor(base::BindLambdaForTesting(
+ [loader_factory](const network::ResourceRequest& request) {
+ std::string path = request.url.path();
+ if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") {
+ loader_factory->AddResponse(request.url.spec(), std::string());
+ return;
+ }
+ if (path == "/oauth/multilogin") {
+ loader_factory->AddResponse(request.url.spec(),
+ kMultiloginSuccessResponse);
+ return;
+ }
+ }));
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ EXPECT_TRUE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(new_profile));
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ IdentityTestEnvironmentProfileAdaptor adaptor(new_profile);
+ adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
+
+ // Check the profile name.
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(new_profile->GetPath());
+ ASSERT_TRUE(entry);
+ EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName()));
+ // Check the profile color.
+ EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile)
+ ->UsingAutogeneratedTheme());
+
+ // A browser has been created for the new profile and the tab was moved there.
+ Browser* added_browser = ui_test_utils::WaitForBrowserToOpen();
+ ASSERT_TRUE(added_browser);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(
+ histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+ // Interception bubble is destroyed in the source profile, and was not shown
+ // in the new profile.
+ FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate =
+ GetInterceptorDelegate(new_profile);
+
+ // Profile customization UI was shown exactly once in the new profile.
+ EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete interception flow including profile and browser creation.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTest) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ SetupGaiaResponses();
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ EXPECT_TRUE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(new_profile));
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ IdentityTestEnvironmentProfileAdaptor adaptor(new_profile);
+ adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
+
+ // Check the profile name.
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(new_profile->GetPath());
+ ASSERT_TRUE(entry);
+ EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName()));
+ // Check the profile color.
+ EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile)
+ ->UsingAutogeneratedTheme());
+
+ // A browser has been created for the new profile and the tab was moved there.
+ Browser* added_browser = ui_test_utils::WaitForBrowserToOpen();
+ ASSERT_TRUE(added_browser);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(
+ histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+ // Interception bubble is destroyed in the source profile, and was not shown
+ // in the new profile.
+ FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate =
+ GetInterceptorDelegate(new_profile);
+
+ // Profile customization UI was shown exactly once in the new profile.
+ EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete interception flow for a reauth of the primary account of a
+// non-syncing profile.
+IN_PROC_BROWSER_TEST_F(
+ DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionPrimaryACcountReauthSyncDisabledTest) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ IdentityManagerFactory::GetForProfile(profile())
+ ->GetPrimaryAccountMutator()
+ ->SetPrimaryAccount(account_info.account_id,
+ signin::ConsentLevel::kSignin);
+
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ SetupGaiaResponses();
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced);
+
+ EXPECT_FALSE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile()));
+ // Start the interception.
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(profile());
+ interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id,
+ /*is_new_account=*/false,
+ /*is_sync_signin=*/false);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile()));
+ // Interception bubble was closed.
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed());
+ EXPECT_TRUE(IdentityManagerFactory::GetForProfile(profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 1u);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count);
+ EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(
+ histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced,
+ /*reauth=*/true);
+}
+
+// Tests the complete interception flow for a reauth of the primary account of a
+// syncing profile.
+IN_PROC_BROWSER_TEST_F(
+ DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionPrimaryACcountReauthSyncEnabledTest) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ IdentityManagerFactory::GetForProfile(profile())
+ ->GetPrimaryAccountMutator()
+ ->SetPrimaryAccount(account_info.account_id, signin::ConsentLevel::kSync);
+
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ SetupGaiaResponses();
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced);
+
+ EXPECT_FALSE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile()));
+ // Start the interception.
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(profile());
+ interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id,
+ /*is_new_account=*/false,
+ /*is_sync_signin=*/false);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile()));
+ // Interception bubble was closed.
+ EXPECT_FALSE(source_interceptor_delegate->intercept_bubble_shown());
+ EXPECT_FALSE(source_interceptor_delegate->intercept_bubble_destroyed());
+ EXPECT_TRUE(IdentityManagerFactory::GetForProfile(profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 1u);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count);
+ EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kAbortAccountNotNew,
+ /*reauth=*/true);
+}
+
+// Tests the complete profile switch flow when the profile is not loaded.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ EnterpriseSwitchAndLoad) {
+ base::HistogramTester histogram_tester;
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Add a profile in the cache (simulate the profile on disk).
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ProfileAttributesStorage* profile_storage =
+ &profile_manager->GetProfileAttributesStorage();
+ const base::FilePath profile_path =
+ profile_manager->GenerateNextProfileDirectoryPath();
+ ProfileAttributesInitParams params;
+ params.profile_path = profile_path;
+ params.profile_name = u"TestProfileName";
+ params.gaia_id = account_info.gaia;
+ params.user_name = base::UTF8ToUTF16(account_info.email);
+ profile_storage->AddProfile(std::move(params));
+ ProfileAttributesEntry* entry =
+ profile_storage->GetProfileAttributesWithPath(profile_path);
+ ASSERT_TRUE(entry);
+ ASSERT_EQ(entry->GetGAIAId(), account_info.gaia);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ // Check that the right profile was opened.
+ EXPECT_EQ(new_profile->GetPath(), profile_path);
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // A browser has been created for the new profile and the tab was moved there.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* added_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(added_browser);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch);
+
+ // Interception bubble was closed.
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed());
+
+ // Profile customization was not shown.
+ EXPECT_EQ(GetInterceptorDelegate(new_profile)->customized_browser(), nullptr);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Failed run on MAC CI builder. https://crbug.com/1245200
+#if defined(OS_MAC)
+#define MAYBE_EnterpriseSwitchAlreadyOpen DISABLED_EnterpriseSwitchAlreadyOpen
+#else
+#define MAYBE_EnterpriseSwitchAlreadyOpen EnterpriseSwitchAlreadyOpen
+#endif
+// Tests the complete profile switch flow when the profile is already loaded.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ MAYBE_EnterpriseSwitchAlreadyOpen) {
+ base::HistogramTester histogram_tester;
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ // Create another profile with a browser window.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ const base::FilePath profile_path =
+ profile_manager->GenerateNextProfileDirectoryPath();
+ base::RunLoop loop;
+ Profile* other_profile = nullptr;
+ ProfileManager::CreateCallback callback = base::BindLambdaForTesting(
+ [&other_profile, &loop](Profile* profile, Profile::CreateStatus status) {
+ DCHECK_EQ(status, Profile::CREATE_STATUS_INITIALIZED);
+ other_profile = profile;
+ loop.Quit();
+ });
+ profiles::SwitchToProfile(profile_path, /*always_create=*/true,
+ std::move(callback));
+ loop.Run();
+ ASSERT_TRUE(other_profile);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* other_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(other_browser);
+ ASSERT_EQ(other_browser->profile(), other_profile);
+ // Add the account to the other profile.
+ signin::IdentityManager* other_identity_manager =
+ IdentityManagerFactory::GetForProfile(other_profile);
+ other_identity_manager->GetAccountsMutator()->AddOrUpdateAccount(
+ account_info.gaia, account_info.email, "dummy_refresh_token",
+ /*is_under_advanced_protection=*/false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ other_identity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount(
+ account_info.account_id, signin::ConsentLevel::kSync);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+ int other_original_tab_count = other_browser->tab_strip_model()->count();
+
+ // Start the interception.
+ GetInterceptorDelegate(profile())->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced);
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(profile());
+ interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ signin::SetCookieAccounts(other_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // The tab was moved to the new browser.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(other_browser->tab_strip_model()->count(),
+ other_original_tab_count + 1);
+ EXPECT_EQ(other_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch);
+ // Profile customization was not shown.
+ EXPECT_EQ(GetInterceptorDelegate(other_profile)->customized_browser(),
+ nullptr);
+ EXPECT_EQ(GetInterceptorDelegate(profile())->customized_browser(), nullptr);
+}
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc
new file mode 100644
index 00000000000..9377d94d2a8
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc
@@ -0,0 +1,45 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+// static
+DiceWebSigninInterceptor* DiceWebSigninInterceptorFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<DiceWebSigninInterceptor*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+DiceWebSigninInterceptorFactory*
+DiceWebSigninInterceptorFactory::GetInstance() {
+ return base::Singleton<DiceWebSigninInterceptorFactory>::get();
+}
+
+DiceWebSigninInterceptorFactory::DiceWebSigninInterceptorFactory()
+ : BrowserContextKeyedServiceFactory(
+ "DiceWebSigninInterceptor",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+DiceWebSigninInterceptorFactory::~DiceWebSigninInterceptorFactory() = default;
+
+void DiceWebSigninInterceptorFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ DiceWebSigninInterceptor::RegisterProfilePrefs(registry);
+}
+
+KeyedService* DiceWebSigninInterceptorFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ return new DiceWebSigninInterceptor(
+ Profile::FromBrowserContext(context),
+ std::make_unique<DiceWebSigninInterceptorDelegate>());
+}
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h
new file mode 100644
index 00000000000..ad6aac1ca3e
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h
@@ -0,0 +1,37 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class DiceWebSigninInterceptor;
+class Profile;
+
+class DiceWebSigninInterceptorFactory
+ : public BrowserContextKeyedServiceFactory {
+ public:
+ static DiceWebSigninInterceptor* GetForProfile(Profile* profile);
+ static DiceWebSigninInterceptorFactory* GetInstance();
+
+ DiceWebSigninInterceptorFactory(const DiceWebSigninInterceptorFactory&) =
+ delete;
+ DiceWebSigninInterceptorFactory& operator=(
+ const DiceWebSigninInterceptorFactory&) = delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<DiceWebSigninInterceptorFactory>;
+ DiceWebSigninInterceptorFactory();
+ ~DiceWebSigninInterceptorFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc
new file mode 100644
index 00000000000..1f66d5d96ed
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc
@@ -0,0 +1,953 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+
+#include <memory>
+
+#include "base/callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_test_util.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/chrome_constants.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/browser_with_test_window_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "url/gurl.h"
+
+namespace {
+
+class MockDiceWebSigninInterceptorDelegate
+ : public DiceWebSigninInterceptor::Delegate {
+ public:
+ MOCK_METHOD(std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>,
+ ShowSigninInterceptionBubble,
+ (content::WebContents * web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback),
+ (override));
+ void ShowProfileCustomizationBubble(Browser* browser) override {}
+};
+
+// Matches BubbleParameters fields excepting the color. This is useful in the
+// test because the color is randomly generated.
+testing::Matcher<const DiceWebSigninInterceptor::Delegate::BubbleParameters&>
+MatchBubbleParameters(
+ const DiceWebSigninInterceptor::Delegate::BubbleParameters& parameters) {
+ return testing::AllOf(
+ testing::Field("interception_type",
+ &DiceWebSigninInterceptor::Delegate::BubbleParameters::
+ interception_type,
+ parameters.interception_type),
+ testing::Field("intercepted_account",
+ &DiceWebSigninInterceptor::Delegate::BubbleParameters::
+ intercepted_account,
+ parameters.intercepted_account),
+ testing::Field("primary_account",
+ &DiceWebSigninInterceptor::Delegate::BubbleParameters::
+ primary_account,
+ parameters.primary_account));
+}
+
+// If the account info is valid, does nothing. Otherwise fills the extended
+// fields with default values.
+void MakeValidAccountInfo(AccountInfo* info) {
+ if (info->IsValid())
+ return;
+ info->full_name = "fullname";
+ info->given_name = "givenname";
+ info->hosted_domain = kNoHostedDomainFound;
+ info->locale = "en";
+ info->picture_url = "https://example.com";
+ DCHECK(info->IsValid());
+}
+
+} // namespace
+
+class DiceWebSigninInterceptorTest : public BrowserWithTestWindowTest {
+ public:
+ DiceWebSigninInterceptorTest() = default;
+ ~DiceWebSigninInterceptorTest() override = default;
+
+ DiceWebSigninInterceptor* interceptor() {
+ return dice_web_signin_interceptor_.get();
+ }
+
+ MockDiceWebSigninInterceptorDelegate* mock_delegate() {
+ return mock_delegate_;
+ }
+
+ content::WebContents* web_contents() {
+ return browser()->tab_strip_model()->GetActiveWebContents();
+ }
+
+ ProfileAttributesStorage* profile_attributes_storage() {
+ return profile_manager()->profile_attributes_storage();
+ }
+
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return identity_test_env_profile_adaptor_->identity_test_env();
+ }
+
+ Profile* CreateTestingProfile(const std::string& name) {
+ return profile_manager()->CreateTestingProfile(name);
+ }
+
+ // Helper function that calls MaybeInterceptWebSignin with parameters
+ // compatible with interception.
+ void MaybeIntercept(CoreAccountId account_id) {
+ interceptor()->MaybeInterceptWebSignin(web_contents(), account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+ }
+
+ // Calls MaybeInterceptWebSignin and verifies the heuristic outcome, the
+ // histograms and whether the interception is in progress.
+ // This function only works if the interception decision can be made
+ // synchronously (GetHeuristicOutcome() returns a value).
+ void TestSynchronousInterception(
+ AccountInfo account_info,
+ bool is_new_account,
+ bool is_sync_signin,
+ SigninInterceptionHeuristicOutcome expected_outcome) {
+ ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin,
+ account_info.email,
+ /*entry=*/nullptr),
+ expected_outcome);
+ base::HistogramTester histogram_tester;
+ interceptor()->MaybeInterceptWebSignin(web_contents(),
+ account_info.account_id,
+ is_new_account, is_sync_signin);
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+ histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome",
+ expected_outcome, 1);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(),
+ SigninInterceptionHeuristicOutcomeIsSuccess(expected_outcome));
+ }
+
+ // Calls MaybeInterceptWebSignin and verifies the heuristic outcome and the
+ // histograms.
+ // This function only works if the interception decision cannot be made
+ // synchronously (GetHeuristicOutcome() returns no value).
+ void TestAsynchronousInterception(
+ AccountInfo account_info,
+ bool is_new_account,
+ bool is_sync_signin,
+ SigninInterceptionHeuristicOutcome expected_outcome) {
+ ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin,
+ account_info.email,
+ /*entry=*/nullptr),
+ absl::nullopt);
+ base::HistogramTester histogram_tester;
+ interceptor()->MaybeInterceptWebSignin(web_contents(),
+ account_info.account_id,
+ is_new_account, is_sync_signin);
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+ histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome",
+ expected_outcome, 1);
+ EXPECT_TRUE(interceptor()->is_interception_in_progress());
+ }
+
+ private:
+ // testing::Test:
+ void SetUp() override {
+ BrowserWithTestWindowTest::SetUp();
+
+ identity_test_env_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+ identity_test_env_profile_adaptor_->identity_test_env()
+ ->SetTestURLLoaderFactory(&test_url_loader_factory_);
+
+ auto delegate = std::make_unique<
+ testing::StrictMock<MockDiceWebSigninInterceptorDelegate>>();
+ mock_delegate_ = delegate.get();
+ dice_web_signin_interceptor_ = std::make_unique<DiceWebSigninInterceptor>(
+ profile(), std::move(delegate));
+
+ // Create the first tab so that web_contents() exists.
+ AddTab(browser(), GURL("http://foo/1"));
+ }
+
+ void TearDown() override {
+ dice_web_signin_interceptor_->Shutdown();
+ identity_test_env_profile_adaptor_.reset();
+ BrowserWithTestWindowTest::TearDown();
+ }
+
+ TestingProfile::TestingFactories GetTestingFactories() override {
+ TestingProfile::TestingFactories factories =
+ IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories();
+ factories.push_back(
+ {ChromeSigninClientFactory::GetInstance(),
+ base::BindRepeating(&BuildChromeSigninClientWithURLLoader,
+ &test_url_loader_factory_)});
+ return factories;
+ }
+
+ network::TestURLLoaderFactory test_url_loader_factory_;
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_env_profile_adaptor_;
+ std::unique_ptr<DiceWebSigninInterceptor> dice_web_signin_interceptor_;
+ raw_ptr<MockDiceWebSigninInterceptorDelegate> mock_delegate_ = nullptr;
+};
+
+TEST_F(DiceWebSigninInterceptorTest, ShouldShowProfileSwitchBubble) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ const std::string& email = account_info.email;
+ EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble(
+ email, profile_attributes_storage()));
+
+ // Add another profile with no account.
+ CreateTestingProfile("Profile 1");
+ EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble(
+ email, profile_attributes_storage()));
+
+ // Add another profile with a different account.
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ std::string kOtherGaiaID = "SomeOtherGaiaID";
+ ASSERT_NE(kOtherGaiaID, account_info.gaia);
+ entry->SetAuthInfo(kOtherGaiaID, u"alice@gmail.com",
+ /*is_consented_primary_account=*/true);
+ EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble(
+ email, profile_attributes_storage()));
+
+ // Change the account to match.
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+ const ProfileAttributesEntry* switch_to_entry =
+ interceptor()->ShouldShowProfileSwitchBubble(
+ email, profile_attributes_storage());
+ EXPECT_EQ(entry, switch_to_entry);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, NoBubbleWithSingleAccount) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Without UPA.
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+ EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info));
+
+ // With UPA.
+ identity_test_env()->SetPrimaryAccount("bob@example.com",
+ signin::ConsentLevel::kSignin);
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+}
+
+TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubble) {
+ // Setup 3 accounts in the profile:
+ // - primary account
+ // - other enterprise account that is not primary (should be ignored)
+ // - intercepted account.
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "alice@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo other_account_info =
+ identity_test_env()->MakeAccountAvailable("dummy@example.com");
+ MakeValidAccountInfo(&other_account_info);
+ other_account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(other_account_info);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info);
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSignin),
+ primary_account_info.account_id);
+
+ // The primary account does not have full account info (empty domain).
+ ASSERT_TRUE(identity_test_env()
+ ->identity_manager()
+ ->FindExtendedAccountInfo(primary_account_info)
+ .hosted_domain.empty());
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+
+ // The primary account has full info.
+ MakeValidAccountInfo(&primary_account_info);
+ identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);
+ // The intercepted account is enterprise.
+ EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+ // Two consummer accounts.
+ account_info.hosted_domain = kNoHostedDomainFound;
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+ // The primary account is enterprise.
+ primary_account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);
+ EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+}
+
+class DiceWebSigninInterceptorForcedSeparationTest
+ : public DiceWebSigninInterceptorTest {
+ public:
+ DiceWebSigninInterceptorForcedSeparationTest()
+ : feature_list_(kAccountPoliciesLoadedWithoutSync) {}
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparation) {
+ profile()->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ // Setup 3 accounts in the profile:
+ // - primary account
+ // - other enterprise account that is not primary (should be ignored)
+ // - intercepted account.
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "alice@gmail.com", signin::ConsentLevel::kSignin);
+
+ AccountInfo other_account_info =
+ identity_test_env()->MakeAccountAvailable("dummy@example.com");
+ MakeValidAccountInfo(&other_account_info);
+ other_account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(other_account_info);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info);
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSignin),
+ primary_account_info.account_id);
+ interceptor()->new_account_interception_ = true;
+ // Consumer account not intercepted.
+ EXPECT_FALSE(
+ interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info));
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ // Managed account intercepted.
+ EXPECT_TRUE(
+ interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info));
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationWithoutUPA) {
+ profile()->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ AccountInfo account_info_1 =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info_1);
+ account_info_1.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+
+ interceptor()->new_account_interception_ = true;
+ // Primary account is not set.
+ ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount(
+ signin::ConsentLevel::kSignin));
+ EXPECT_TRUE(
+ interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info_1));
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationReauth) {
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "alice@example.com", signin::ConsentLevel::kSignin);
+ MakeValidAccountInfo(&primary_account_info);
+ primary_account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);
+
+ // Primary account is set.
+ ASSERT_TRUE(identity_test_env()->identity_manager()->HasPrimaryAccount(
+ signin::ConsentLevel::kSignin));
+ EXPECT_TRUE(interceptor()->ShouldEnforceEnterpriseProfileSeparation(
+ primary_account_info));
+
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile()->GetPath());
+ entry->SetUserAcceptedAccountManagement(true);
+
+ EXPECT_FALSE(interceptor()->ShouldEnforceEnterpriseProfileSeparation(
+ primary_account_info));
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimaryReauth) {
+ profile()->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+
+ // Reauth intercepted if enterprise confirmation not shown yet for forced
+ // managed separation.
+ AccountInfo account_info = identity_test_env()->MakePrimaryAccountAvailable(
+ "alice@example.com", signin::ConsentLevel::kSignin);
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+
+ // Check that interception works otherwise, as a sanity check.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced,
+ account_info, account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+
+ TestAsynchronousInterception(
+ account_info, /*is_new_account=*/false, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimaryManaged) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ // Check that interception works otherwise, as a sanity check.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced,
+ account_info, AccountInfo(), SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ TestAsynchronousInterception(
+ account_info, /*is_new_account=*/true, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimaryProfileSwitch) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ profile()->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ // Setup for profile switch interception.
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(account_info.email),
+ /*is_consented_primary_account=*/false);
+ // Check that interception works otherwise, as a sanity check.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced,
+ account_info, AccountInfo(), SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ TestAsynchronousInterception(account_info, /*is_new_account=*/true,
+ /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubbleWithoutUPA) {
+ AccountInfo account_info_1 =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info_1);
+ account_info_1.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ AccountInfo account_info_2 =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info_2);
+ account_info_2.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_2);
+
+ // Primary account is not set.
+ ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount(
+ signin::ConsentLevel::kSignin));
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info_1));
+}
+
+TEST_F(DiceWebSigninInterceptorTest, ShouldShowMultiUserBubble) {
+ // Setup two accounts in the profile.
+ AccountInfo account_info_1 =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info_1);
+ account_info_1.given_name = "Bob";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ AccountInfo account_info_2 =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+
+ // The other account does not have full account info (empty name).
+ ASSERT_TRUE(account_info_2.given_name.empty());
+ EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1));
+
+ // Accounts with different names.
+ account_info_1.given_name = "Bob";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ MakeValidAccountInfo(&account_info_2);
+ account_info_2.given_name = "Alice";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_2);
+ EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1));
+
+ // Accounts with same names.
+ account_info_1.given_name = "Alice";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1));
+
+ // Comparison is case insensitive.
+ account_info_1.given_name = "alice";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1));
+}
+
+TEST_F(DiceWebSigninInterceptorTest, NoInterception) {
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email);
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+
+ // Check that Sync signin is not intercepted.
+ TestSynchronousInterception(
+ account_info, /*is_new_account=*/true, /*is_sync_signin=*/true,
+ SigninInterceptionHeuristicOutcome::kAbortSyncSignin);
+
+ // Check that reauth is not intercepted.
+ TestSynchronousInterception(
+ account_info, /*is_new_account=*/false, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kAbortAccountNotNew);
+
+ // Check that interception works otherwise, as a sanity check.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch,
+ account_info, AccountInfo(), SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ TestSynchronousInterception(
+ account_info, /*is_new_account=*/true, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+}
+
+// Checks that the heuristic still works if the account was not added to Chrome
+// yet.
+TEST_F(DiceWebSigninInterceptorTest, HeuristicAccountNotAdded) {
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+ EXPECT_EQ(interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, email,
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+}
+
+// Checks that the heuristic defaults to gmail.com when no domain is specified.
+TEST_F(DiceWebSigninInterceptorTest, HeuristicDefaultsToGmail) {
+ // Setup for profile switch interception.
+ std::string email = "bob@gmail.com";
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+ // No domain defaults to gmail.com
+ EXPECT_EQ(interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, "bob",
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+ // Using wrong domain does not trigger the interception.
+ EXPECT_EQ(
+ interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com",
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kAbortSingleAccount);
+}
+
+// Checks that no heuristic is returned if signin interception is disabled.
+TEST_F(DiceWebSigninInterceptorTest, InterceptionDisabled) {
+ // Setup for profile switch interception.
+ std::string email = "bob@gmail.com";
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ profile()->GetPrefs()->SetBoolean(prefs::kSigninInterceptionEnabled, false);
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+ EXPECT_EQ(interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, "bob",
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled);
+ EXPECT_EQ(
+ interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com",
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, TabClosed) {
+ base::HistogramTester histogram_tester;
+ interceptor()->MaybeInterceptWebSignin(
+ /*web_contents=*/nullptr, CoreAccountId(),
+ /*is_new_account=*/true, /*is_sync_signin=*/false);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kAbortTabClosed, 1);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, InterceptionInProgress) {
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email);
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+
+ // Start an interception.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch,
+ account_info, AccountInfo(), SkColor()};
+ base::OnceCallback<void(SigninInterceptionResult)> delegate_callback;
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_))
+ .WillOnce(testing::WithArg<2>(testing::Invoke(
+ [&delegate_callback](
+ base::OnceCallback<void(SigninInterceptionResult)> callback) {
+ delegate_callback = std::move(callback);
+ return nullptr;
+ })));
+ MaybeIntercept(account_info.account_id);
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+ EXPECT_TRUE(interceptor()->is_interception_in_progress());
+
+ // Check that there is no interception while another one is in progress.
+ base::HistogramTester histogram_tester;
+ MaybeIntercept(account_info.account_id);
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress, 1);
+
+ // Complete the interception that was in progress.
+ std::move(delegate_callback).Run(SigninInterceptionResult::kDeclined);
+ EXPECT_FALSE(interceptor()->is_interception_in_progress());
+
+ // A new interception can now start.
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, DeclineCreationRepeatedly) {
+ base::HistogramTester histogram_tester;
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "bob@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ const int kMaxProfileCreationDeclinedCount = 2;
+ // Decline the interception kMaxProfileCreationDeclinedCount times.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise,
+ account_info, primary_account_info, SkColor()};
+ for (int i = 0; i < kMaxProfileCreationDeclinedCount; ++i) {
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_))
+ .WillOnce(testing::WithArg<2>(testing::Invoke(
+ [](base::OnceCallback<void(SigninInterceptionResult)> callback) {
+ std::move(callback).Run(SigninInterceptionResult::kDeclined);
+ return nullptr;
+ })));
+ MaybeIntercept(account_info.account_id);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(), false);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise, i + 1);
+ }
+
+ // Next time the interception is not shown again.
+ MaybeIntercept(account_info.account_id);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(), false);
+ histogram_tester.ExpectBucketCount(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kAbortUserDeclinedProfileForAccount,
+ 1);
+
+ // Another account can still be intercepted.
+ account_info.email = "oscar@example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise,
+ account_info, primary_account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+ histogram_tester.ExpectBucketCount(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise,
+ kMaxProfileCreationDeclinedCount + 1);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(), true);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, DeclineSwitchRepeatedly_NoLimit) {
+ base::HistogramTester histogram_tester;
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email);
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+
+ // Test that the profile switch can be declined multiple times.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch,
+ account_info, AccountInfo(), SkColor()};
+ for (int i = 0; i < 10; ++i) {
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_))
+ .WillOnce(testing::WithArg<2>(testing::Invoke(
+ [](base::OnceCallback<void(SigninInterceptionResult)> callback) {
+ std::move(callback).Run(SigninInterceptionResult::kDeclined);
+ return nullptr;
+ })));
+ MaybeIntercept(account_info.account_id);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(), false);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch, i + 1);
+ }
+}
+
+TEST_F(DiceWebSigninInterceptorTest, PersistentHash) {
+ // The hash is persistent (the value should never change).
+ EXPECT_EQ("email_174",
+ interceptor()->GetPersistentEmailHash("alice@example.com"));
+ // Different email get another hash.
+ EXPECT_NE(interceptor()->GetPersistentEmailHash("bob@gmail.com"),
+ interceptor()->GetPersistentEmailHash("alice@example.com"));
+ // Equivalent emails get the same hash.
+ EXPECT_EQ(interceptor()->GetPersistentEmailHash("bob"),
+ interceptor()->GetPersistentEmailHash("bob@gmail.com"));
+ EXPECT_EQ(interceptor()->GetPersistentEmailHash("bo.b@gmail.com"),
+ interceptor()->GetPersistentEmailHash("bob@gmail.com"));
+ // Dots are removed only for gmail accounts.
+ EXPECT_NE(interceptor()->GetPersistentEmailHash("alice@example.com"),
+ interceptor()->GetPersistentEmailHash("al.ice@example.com"));
+}
+
+// Interception other than the profile switch require at least 2 accounts.
+TEST_F(DiceWebSigninInterceptorTest, NoInterceptionWithOneAccount) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ // Interception aborts even if the account info is not available.
+ ASSERT_FALSE(identity_test_env()
+ ->identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_info.account_id)
+ .IsValid());
+ TestSynchronousInterception(
+ account_info, /*is_new_account=*/true, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kAbortSingleAccount);
+}
+
+// When profile creation is disallowed, profile switch interception is still
+// enabled, but others are disabled.
+TEST_F(DiceWebSigninInterceptorTest, ProfileCreationDisallowed) {
+ base::HistogramTester histogram_tester;
+ g_browser_process->local_state()->SetBoolean(prefs::kBrowserAddPersonEnabled,
+ false);
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email);
+ AccountInfo other_account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+
+ // Interception that would offer creating a new profile does not work.
+ TestSynchronousInterception(
+ other_account_info, /*is_new_account=*/true, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed);
+
+ // Profile switch interception still works.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch,
+ account_info, AccountInfo(), SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, WaitForAccountInfoAvailable) {
+ base::HistogramTester histogram_tester;
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "bob@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ EXPECT_FALSE(interceptor()
+ ->GetHeuristicOutcome(/*is_new_account=*/true,
+ /*is_sync_signin=*/false,
+ account_info.email,
+ /*entry=*/nullptr)
+ .has_value());
+ MaybeIntercept(account_info.account_id);
+ // Delegate was not called yet.
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+
+ // Account info becomes available, interception happens.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise,
+ account_info, primary_account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration",
+ 1);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, AccountInfoAlreadyAvailable) {
+ base::HistogramTester histogram_tester;
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "bob@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Account info is already available, interception happens immediately.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise,
+ account_info, primary_account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+ histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration",
+ 1);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise, 1);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, MultiUserInterception) {
+ base::HistogramTester histogram_tester;
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "bob@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Account info is already available, interception happens immediately.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser,
+ account_info, primary_account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptMultiUser, 1);
+}
diff --git a/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc b/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc
new file mode 100644
index 00000000000..3e042e76b87
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc
@@ -0,0 +1,776 @@
+// Copyright 2019 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 "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/scoped_observation.h"
+#include "base/strings/stringprintf.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/e2e_tests/live_test.h"
+#include "chrome/browser/signin/e2e_tests/test_accounts_util.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/sync/sync_service_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
+#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h"
+#include "chrome/browser/ui/webui/signin/signin_email_confirmation_dialog.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/identity_manager/account_capabilities.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/signin/public/identity_manager/tribool.h"
+#include "components/sync/driver/sync_service.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/core_account_id.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "ui/compositor/scoped_animation_duration_scale_mode.h"
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/sync/sync_ui_util.h"
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+namespace signin {
+namespace test {
+
+const base::TimeDelta kDialogTimeout = base::Seconds(10);
+
+// A wrapper importing the settings module when the chrome://settings serve the
+// Polymer 3 version.
+const char kSettingsScriptWrapperFormat[] =
+ "import('./settings.js').then(settings => {%s});";
+
+enum class PrimarySyncAccountWait { kWaitForAdded, kWaitForCleared, kNotWait };
+
+// Observes various sign-in events and allows to wait for a specific state of
+// signed-in accounts.
+class SignInTestObserver : public IdentityManager::Observer,
+ public AccountReconcilor::Observer {
+ public:
+ explicit SignInTestObserver(IdentityManager* identity_manager,
+ AccountReconcilor* reconcilor)
+ : identity_manager_(identity_manager), reconcilor_(reconcilor) {
+ identity_manager_observation_.Observe(identity_manager_.get());
+ account_reconcilor_observation_.Observe(reconcilor_.get());
+ }
+ ~SignInTestObserver() override = default;
+
+ // IdentityManager::Observer:
+ void OnPrimaryAccountChanged(
+ const PrimaryAccountChangeEvent& event) override {
+ if (event.GetEventTypeFor(ConsentLevel::kSync) ==
+ PrimaryAccountChangeEvent::Type::kNone) {
+ return;
+ }
+ QuitIfConditionIsSatisfied();
+ }
+ void OnRefreshTokenUpdatedForAccount(const CoreAccountInfo&) override {
+ QuitIfConditionIsSatisfied();
+ }
+ void OnRefreshTokenRemovedForAccount(const CoreAccountId&) override {
+ QuitIfConditionIsSatisfied();
+ }
+ void OnErrorStateOfRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo&,
+ const GoogleServiceAuthError&) override {
+ QuitIfConditionIsSatisfied();
+ }
+ void OnAccountsInCookieUpdated(const AccountsInCookieJarInfo&,
+ const GoogleServiceAuthError&) override {
+ QuitIfConditionIsSatisfied();
+ }
+
+ // AccountReconcilor::Observer:
+ // TODO(https://crbug.com/1051864): Remove this obsever method once the bug is
+ // fixed.
+ void OnStateChanged(signin_metrics::AccountReconcilorState state) override {
+ if (state == signin_metrics::ACCOUNT_RECONCILOR_OK) {
+ // This will trigger cookie update if accounts are stale.
+ identity_manager_->GetAccountsInCookieJar();
+ }
+ }
+
+ void WaitForAccountChanges(int signed_in_accounts,
+ PrimarySyncAccountWait primary_sync_account_wait) {
+ expected_signed_in_accounts_ = signed_in_accounts;
+ primary_sync_account_wait_ = primary_sync_account_wait;
+ are_expectations_set = true;
+ QuitIfConditionIsSatisfied();
+ run_loop_.Run();
+ }
+
+ private:
+ void QuitIfConditionIsSatisfied() {
+ if (!are_expectations_set)
+ return;
+
+ int accounts_with_valid_refresh_token =
+ CountAccountsWithValidRefreshToken();
+ int accounts_in_cookie = CountSignedInAccountsInCookie();
+
+ if (accounts_with_valid_refresh_token != accounts_in_cookie ||
+ accounts_with_valid_refresh_token != expected_signed_in_accounts_) {
+ return;
+ }
+
+ switch (primary_sync_account_wait_) {
+ case PrimarySyncAccountWait::kWaitForAdded:
+ if (!HasValidPrimarySyncAccount())
+ return;
+ break;
+ case PrimarySyncAccountWait::kWaitForCleared:
+ if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync))
+ return;
+ break;
+ case PrimarySyncAccountWait::kNotWait:
+ break;
+ }
+
+ run_loop_.Quit();
+ }
+
+ int CountAccountsWithValidRefreshToken() const {
+ std::vector<CoreAccountInfo> accounts_with_refresh_tokens =
+ identity_manager_->GetAccountsWithRefreshTokens();
+ int valid_accounts = 0;
+ for (const auto& account_info : accounts_with_refresh_tokens) {
+ if (!identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id)) {
+ ++valid_accounts;
+ }
+ }
+ return valid_accounts;
+ }
+
+ int CountSignedInAccountsInCookie() const {
+ signin::AccountsInCookieJarInfo accounts_in_cookie_jar =
+ identity_manager_->GetAccountsInCookieJar();
+ if (!accounts_in_cookie_jar.accounts_are_fresh)
+ return -1;
+
+ return accounts_in_cookie_jar.signed_in_accounts.size();
+ }
+
+ bool HasValidPrimarySyncAccount() const {
+ CoreAccountId primary_account_id =
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync);
+ if (primary_account_id.empty())
+ return false;
+
+ return !identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
+ primary_account_id);
+ }
+
+ const raw_ptr<signin::IdentityManager> identity_manager_;
+ const raw_ptr<AccountReconcilor> reconcilor_;
+ base::ScopedObservation<IdentityManager, IdentityManager::Observer>
+ identity_manager_observation_{this};
+ base::ScopedObservation<AccountReconcilor, AccountReconcilor::Observer>
+ account_reconcilor_observation_{this};
+ base::RunLoop run_loop_;
+
+ bool are_expectations_set = false;
+ int expected_signed_in_accounts_ = 0;
+ PrimarySyncAccountWait primary_sync_account_wait_ =
+ PrimarySyncAccountWait::kNotWait;
+};
+
+// Observer class allowing to wait for account capabilities to be known.
+class AccountCapabilitiesObserver : public IdentityManager::Observer {
+ public:
+ explicit AccountCapabilitiesObserver(IdentityManager* identity_manager)
+ : identity_manager_(identity_manager) {
+ identity_manager_observation_.Observe(identity_manager);
+ }
+
+ // IdentityManager::Observer:
+ void OnExtendedAccountInfoUpdated(const AccountInfo& info) override {
+ if (info.account_id != account_id_)
+ return;
+
+ if (info.capabilities.AreAllCapabilitiesKnown())
+ run_loop_.Quit();
+ }
+
+ // This should be called only once per AccountCapabilitiesObserver instance.
+ void WaitForAllCapabilitiesToBeKnown(CoreAccountId account_id) {
+ DCHECK(identity_manager_observation_.IsObservingSource(
+ identity_manager_.get()));
+ AccountInfo info =
+ identity_manager_->FindExtendedAccountInfoByAccountId(account_id);
+ if (info.capabilities.AreAllCapabilitiesKnown())
+ return;
+
+ account_id_ = account_id;
+ run_loop_.Run();
+ identity_manager_observation_.Reset();
+ }
+
+ private:
+ raw_ptr<IdentityManager> identity_manager_ = nullptr;
+ CoreAccountId account_id_;
+ base::RunLoop run_loop_;
+ base::ScopedObservation<IdentityManager, IdentityManager::Observer>
+ identity_manager_observation_{this};
+};
+
+// Live tests for SignIn.
+// These tests can be run with:
+// browser_tests --gtest_filter=LiveSignInTest.* --run-live-tests --run-manual
+class LiveSignInTest : public signin::test::LiveTest {
+ public:
+ LiveSignInTest() = default;
+ ~LiveSignInTest() override = default;
+
+ void SetUp() override {
+ LiveTest::SetUp();
+ // Always disable animation for stability.
+ ui::ScopedAnimationDurationScaleMode disable_animation(
+ ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
+ }
+
+ void SignInFromWeb(const TestAccount& test_account,
+ int previously_signed_in_accounts) {
+ AddTabAtIndex(0, GaiaUrls::GetInstance()->add_account_url(),
+ ui::PageTransition::PAGE_TRANSITION_TYPED);
+ SignInFromCurrentPage(test_account, previously_signed_in_accounts);
+ }
+
+ void SignInFromSettings(const TestAccount& test_account,
+ int previously_signed_in_accounts) {
+ GURL settings_url("chrome://settings");
+ AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED);
+ auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents();
+ EXPECT_TRUE(content::ExecuteScript(
+ settings_tab,
+ base::StringPrintf(
+ kSettingsScriptWrapperFormat,
+ "settings.SyncBrowserProxyImpl.getInstance().startSignIn();")));
+ SignInFromCurrentPage(test_account, previously_signed_in_accounts);
+ }
+
+ void SignInFromCurrentPage(const TestAccount& test_account,
+ int previously_signed_in_accounts) {
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ login_ui_test_utils::ExecuteJsToSigninInSigninFrame(
+ browser(), test_account.user, test_account.password);
+ observer.WaitForAccountChanges(previously_signed_in_accounts + 1,
+ PrimarySyncAccountWait::kNotWait);
+ }
+
+ void TurnOnSync(const TestAccount& test_account,
+ int previously_signed_in_accounts) {
+ SignInFromSettings(test_account, previously_signed_in_accounts);
+
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(
+ browser(), kDialogTimeout));
+ observer.WaitForAccountChanges(previously_signed_in_accounts + 1,
+ PrimarySyncAccountWait::kWaitForAdded);
+ }
+
+ void SignOutFromWeb() {
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ AddTabAtIndex(0, GaiaUrls::GetInstance()->service_logout_url(),
+ ui::PageTransition::PAGE_TRANSITION_TYPED);
+ observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kNotWait);
+ }
+
+ void TurnOffSync() {
+ GURL settings_url("chrome://settings");
+ AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED);
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents();
+ EXPECT_TRUE(content::ExecuteScript(
+ settings_tab,
+ base::StringPrintf(
+ kSettingsScriptWrapperFormat,
+ "settings.SyncBrowserProxyImpl.getInstance().signOut(false)")));
+ observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared);
+ }
+
+ signin::IdentityManager* identity_manager() {
+ return identity_manager(browser());
+ }
+
+ signin::IdentityManager* identity_manager(Browser* browser) {
+ return IdentityManagerFactory::GetForProfile(browser->profile());
+ }
+
+ syncer::SyncService* sync_service() { return sync_service(browser()); }
+
+ syncer::SyncService* sync_service(Browser* browser) {
+ return SyncServiceFactory::GetForProfile(browser->profile());
+ }
+
+ AccountReconcilor* account_reconcilor() {
+ return account_reconcilor(browser());
+ }
+
+ AccountReconcilor* account_reconcilor(Browser* browser) {
+ return AccountReconcilorFactory::GetForProfile(browser->profile());
+ }
+};
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Sings in an account through the settings page and checks that the account is
+// added to Chrome. Sync should be disabled because the test doesn't pass
+// through the Sync confirmation dialog.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_SimpleSignInFlow) {
+ TestAccount ta;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", ta));
+ SignInFromSettings(ta, 0);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size());
+ EXPECT_TRUE(accounts_in_cookie_jar.signed_out_accounts.empty());
+ const gaia::ListedAccount& account =
+ accounts_in_cookie_jar.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(ta.user, account.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account.id));
+ EXPECT_FALSE(sync_service()->IsSyncFeatureEnabled());
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Signs in an account through the settings page and enables Sync. Checks that
+// Sync is enabled.
+// Then, signs out on the web and checks that the account is removed from
+// cookies and Sync paused error is displayed.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_WebSignOut) {
+ TestAccount test_account;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account));
+ TurnOnSync(test_account, 0);
+
+ const CoreAccountInfo& primary_account =
+ identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account.user, primary_account.email));
+ EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled());
+
+ SignOutFromWeb();
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty());
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_out_accounts.size());
+ EXPECT_TRUE(gaia::AreEmailsSame(
+ test_account.user, accounts_in_cookie_jar.signed_out_accounts[0].email));
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ primary_account.account_id));
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ EXPECT_EQ(GetAvatarSyncErrorType(browser()->profile()),
+ AvatarSyncErrorType::kAuthError);
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Sings in two accounts on the web and checks that cookies and refresh tokens
+// are added to Chrome. Sync should be disabled.
+// Then, signs out on the web and checks that accounts are removed from Chrome.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_WebSignInAndSignOut) {
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ SignInFromWeb(test_account_1, 0);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_1 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_1.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar_1.signed_in_accounts.size());
+ EXPECT_TRUE(accounts_in_cookie_jar_1.signed_out_accounts.empty());
+ const gaia::ListedAccount& account_1 =
+ accounts_in_cookie_jar_1.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, account_1.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_1.id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromWeb(test_account_2, 1);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh);
+ ASSERT_EQ(2u, accounts_in_cookie_jar_2.signed_in_accounts.size());
+ EXPECT_TRUE(accounts_in_cookie_jar_2.signed_out_accounts.empty());
+ EXPECT_EQ(accounts_in_cookie_jar_2.signed_in_accounts[0].id, account_1.id);
+ const gaia::ListedAccount& account_2 =
+ accounts_in_cookie_jar_2.signed_in_accounts[1];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account_2.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_2.id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+
+ SignOutFromWeb();
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_3 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_3.accounts_are_fresh);
+ ASSERT_TRUE(accounts_in_cookie_jar_3.signed_in_accounts.empty());
+ EXPECT_EQ(2u, accounts_in_cookie_jar_3.signed_out_accounts.size());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Signs in an account through the settings page and enables Sync. Checks that
+// Sync is enabled. Signs in a second account on the web.
+// Then, turns Sync off from the settings page and checks that both accounts are
+// removed from Chrome and from cookies.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_TurnOffSync) {
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromWeb(test_account_2, 1);
+
+ const CoreAccountInfo& primary_account =
+ identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, primary_account.email));
+ EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled());
+
+ TurnOffSync();
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh);
+ ASSERT_TRUE(accounts_in_cookie_jar_2.signed_in_accounts.empty());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// In "Sync paused" state, when the primary account is invalid, turns off sync
+// from settings. Checks that the account is removed from Chrome.
+// Regression test for https://crbug.com/1114646
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_TurnOffSyncWhenPaused) {
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+
+ // Get in sync paused state.
+ SignOutFromWeb();
+
+ const CoreAccountInfo& primary_account =
+ identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, primary_account.email));
+ EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled());
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ primary_account.account_id));
+
+ TurnOffSync();
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Signs in an account on the web. Goes to the Chrome settings to enable Sync
+// but cancels the sync confirmation dialog. Checks that the account is still
+// signed in on the web but Sync is disabled.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_CancelSyncWithWebAccount) {
+ TestAccount test_account;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account));
+ SignInFromWeb(test_account, 0);
+
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ GURL settings_url("chrome://settings");
+ AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED);
+ auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents();
+ std::string start_syncing_script = base::StringPrintf(
+ "settings.SyncBrowserProxyImpl.getInstance()."
+ "startSyncingWithEmail(\"%s\", true);",
+ test_account.user.c_str());
+ EXPECT_TRUE(content::ExecuteScript(
+ settings_tab, base::StringPrintf(kSettingsScriptWrapperFormat,
+ start_syncing_script.c_str())));
+ EXPECT_TRUE(login_ui_test_utils::CancelSyncConfirmationDialog(
+ browser(), kDialogTimeout));
+ observer.WaitForAccountChanges(1, PrimarySyncAccountWait::kWaitForCleared);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size());
+ const gaia::ListedAccount& account =
+ accounts_in_cookie_jar.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account.user, account.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account.id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Starts the sign in flow from the settings page, enters credentials on the
+// login page but cancels the Sync confirmation dialog. Checks that Sync is
+// disabled and no account was added to Chrome.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_CancelSync) {
+ TestAccount test_account;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account));
+ SignInFromSettings(test_account, 0);
+
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ EXPECT_TRUE(login_ui_test_utils::CancelSyncConfirmationDialog(
+ browser(), kDialogTimeout));
+ observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ EXPECT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Enables and disables sync to account 1. Enables sync to account 2 and clicks
+// on "This wasn't me" in the email confirmation dialog. Checks that the new
+// profile is created. Checks that Sync to account 2 is enabled in the new
+// profile. Checks that account 2 was removed from the original profile.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest,
+ MANUAL_SyncSecondAccount_CreateNewProfile) {
+ // Enable and disable sync for the first account.
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+ TurnOffSync();
+
+ // Start enable sync for the second account.
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromSettings(test_account_2, 0);
+
+ // Set up an observer for removing the second account from the original
+ // profile.
+ SignInTestObserver original_browser_observer(identity_manager(),
+ account_reconcilor());
+
+ // Check there is only one profile.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Click "This wasn't me" on the email confirmation dialog and wait for a new
+ // browser and profile created.
+ EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog(
+ browser(), kDialogTimeout,
+ SigninEmailConfirmationDialog::CREATE_NEW_USER));
+ Browser* new_browser = ui_test_utils::WaitForBrowserToOpen();
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 2U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 2U);
+ EXPECT_NE(browser()->profile(), new_browser->profile());
+
+ // Confirm sync in the new browser window.
+ SignInTestObserver new_browser_observer(identity_manager(new_browser),
+ account_reconcilor(new_browser));
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(
+ new_browser, kDialogTimeout));
+ new_browser_observer.WaitForAccountChanges(
+ 1, PrimarySyncAccountWait::kWaitForAdded);
+
+ // Check accounts in cookies in the new profile.
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager(new_browser)->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size());
+ const gaia::ListedAccount& account =
+ accounts_in_cookie_jar.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account.email));
+
+ // Check the primary account in the new profile is set and syncing.
+ const CoreAccountInfo& primary_account =
+ identity_manager(new_browser)
+ ->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, primary_account.email));
+ EXPECT_TRUE(identity_manager(new_browser)
+ ->HasAccountWithRefreshToken(primary_account.account_id));
+ EXPECT_TRUE(sync_service(new_browser)->IsSyncFeatureEnabled());
+
+ // Check that the second account was removed from the original profile.
+ original_browser_observer.WaitForAccountChanges(
+ 0, PrimarySyncAccountWait::kWaitForCleared);
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh);
+ ASSERT_TRUE(accounts_in_cookie_jar_2.signed_in_accounts.empty());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Enables and disables sync to account 1. Enables sync to account 2 and clicks
+// on "This was me" in the email confirmation dialog. Checks that Sync to
+// account 2 is enabled in the current profile.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest,
+ MANUAL_SyncSecondAccount_InExistingProfile) {
+ // Enable and disable sync for the first account.
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+ TurnOffSync();
+
+ // Start enable sync for the second account.
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromSettings(test_account_2, 0);
+
+ // Check there is only one profile.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Click "This was me" on the email confirmation dialog, confirm sync and wait
+ // for a primary account to be set.
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog(
+ browser(), kDialogTimeout, SigninEmailConfirmationDialog::START_SYNC));
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(
+ browser(), kDialogTimeout));
+ observer.WaitForAccountChanges(1, PrimarySyncAccountWait::kWaitForAdded);
+
+ // Check no profile was created.
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Check accounts in cookies.
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size());
+ const gaia::ListedAccount& account =
+ accounts_in_cookie_jar.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account.email));
+
+ // Check the primary account is set and syncing.
+ const CoreAccountInfo& primary_account =
+ identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, primary_account.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ primary_account.account_id));
+ EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled());
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Enables and disables sync to account 1. Enables sync to account 2 and clicks
+// on "Cancel" in the email confirmation dialog. Checks that the signin flow is
+// canceled and no accounts are added to Chrome.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest,
+ MANUAL_SyncSecondAccount_CancelOnEmailConfirmation) {
+ // Enable and disable sync for the first account.
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+ TurnOffSync();
+
+ // Start enable sync for the second account.
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromSettings(test_account_2, 0);
+
+ // Check there is only one profile.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Click "Cancel" on the email confirmation dialog and wait for an account to
+ // removed from Chrome.
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog(
+ browser(), kDialogTimeout, SigninEmailConfirmationDialog::CLOSE));
+ observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared);
+
+ // Check no profile was created.
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Check Chrome has no accounts.
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ EXPECT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+IN_PROC_BROWSER_TEST_F(LiveSignInTest,
+ MANUAL_AccountCapabilities_FetchedOnSignIn) {
+ EnableAccountCapabilitiesFetches(identity_manager());
+
+ // Test primary adult account.
+ {
+ AccountCapabilitiesObserver capabilities_observer(identity_manager());
+
+ TestAccount ta;
+ ASSERT_TRUE(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", ta));
+ SignInFromSettings(ta, 0);
+
+ CoreAccountInfo core_account_info =
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin);
+ ASSERT_TRUE(gaia::AreEmailsSame(core_account_info.email, ta.user));
+
+ capabilities_observer.WaitForAllCapabilitiesToBeKnown(
+ core_account_info.account_id);
+ AccountInfo account_info =
+ identity_manager()->FindExtendedAccountInfoByAccountId(
+ core_account_info.account_id);
+ EXPECT_EQ(account_info.capabilities.can_offer_extended_chrome_sync_promos(),
+ Tribool::kTrue);
+ }
+
+ // Test secondary minor account.
+ {
+ AccountCapabilitiesObserver capabilities_observer(identity_manager());
+
+ TestAccount ta;
+ ASSERT_TRUE(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_MINOR", ta));
+ SignInFromWeb(ta, /*previously_signed_in_accounts=*/1);
+
+ CoreAccountInfo core_account_info =
+ identity_manager()->FindExtendedAccountInfoByEmailAddress(ta.user);
+ ASSERT_FALSE(core_account_info.IsEmpty());
+
+ capabilities_observer.WaitForAllCapabilitiesToBeKnown(
+ core_account_info.account_id);
+ AccountInfo account_info =
+ identity_manager()->FindExtendedAccountInfoByAccountId(
+ core_account_info.account_id);
+ EXPECT_EQ(account_info.capabilities.can_offer_extended_chrome_sync_promos(),
+ Tribool::kFalse);
+ }
+}
+
+} // namespace test
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/e2e_tests/live_test.cc b/chromium/chrome/browser/signin/e2e_tests/live_test.cc
new file mode 100644
index 00000000000..e8d22189e73
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/live_test.cc
@@ -0,0 +1,61 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+#include "chrome/browser/signin/e2e_tests/live_test.h"
+
+#include "base/files/file_util.h"
+#include "base/path_service.h"
+#include "net/dns/mock_host_resolver.h"
+
+base::FilePath::StringPieceType kTestAccountFilePath = FILE_PATH_LITERAL(
+ "chrome/browser/internal/resources/signin/test_accounts.json");
+
+const char* kRunLiveTestFlag = "run-live-tests";
+
+namespace signin {
+namespace test {
+
+void LiveTest::SetUpInProcessBrowserTestFixture() {
+ // Whitelists a bunch of hosts.
+ host_resolver()->AllowDirectLookup("*.google.com");
+ host_resolver()->AllowDirectLookup("*.geotrust.com");
+ host_resolver()->AllowDirectLookup("*.gstatic.com");
+ host_resolver()->AllowDirectLookup("*.googleapis.com");
+ // Allows country-specific TLDs.
+ host_resolver()->AllowDirectLookup("accounts.google.*");
+
+ InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
+}
+
+void LiveTest::SetUp() {
+ // Only run live tests when specified.
+ auto* cmd_line = base::CommandLine::ForCurrentProcess();
+ if (!cmd_line->HasSwitch(kRunLiveTestFlag)) {
+ LOG(INFO) << "This test should get skipped.";
+ skip_test_ = true;
+ GTEST_SKIP();
+ }
+ base::FilePath root_path;
+ base::PathService::Get(base::BasePathKey::DIR_SOURCE_ROOT, &root_path);
+ base::FilePath config_path =
+ base::MakeAbsoluteFilePath(root_path.Append(kTestAccountFilePath));
+ test_accounts_.Init(config_path);
+ InProcessBrowserTest::SetUp();
+}
+
+void LiveTest::TearDown() {
+ // This test was skipped, no need to tear down.
+ if (skip_test_)
+ return;
+ InProcessBrowserTest::TearDown();
+}
+
+void LiveTest::PostRunTestOnMainThread() {
+ // This test was skipped. Running PostRunTestOnMainThread can cause
+ // TIMED_OUT on Win7.
+ if (skip_test_)
+ return;
+ InProcessBrowserTest::PostRunTestOnMainThread();
+}
+} // namespace test
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/e2e_tests/live_test.h b/chromium/chrome/browser/signin/e2e_tests/live_test.h
new file mode 100644
index 00000000000..6de9f8f4ded
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/live_test.h
@@ -0,0 +1,33 @@
+// Copyright 2019 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_
+#define CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_
+
+#include "chrome/browser/signin/e2e_tests/test_accounts_util.h"
+#include "chrome/test/base/in_process_browser_test.h"
+
+namespace signin {
+namespace test {
+
+class LiveTest : public InProcessBrowserTest {
+ protected:
+ void SetUpInProcessBrowserTestFixture() override;
+ void SetUp() override;
+ void TearDown() override;
+ void PostRunTestOnMainThread() override;
+
+ const TestAccountsUtil* GetTestAccountsUtil() const {
+ return &test_accounts_;
+ }
+
+ private:
+ TestAccountsUtil test_accounts_;
+ bool skip_test_ = false;
+};
+
+} // namespace test
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_
diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc
new file mode 100644
index 00000000000..c533ea0fb3c
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc
@@ -0,0 +1,75 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/e2e_tests/test_accounts_util.h"
+
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/json/json_file_value_serializer.h"
+#include "base/json/json_reader.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+
+using base::Value;
+
+namespace signin {
+namespace test {
+
+#if defined(OS_WIN)
+std::string kPlatform = "win";
+#elif defined(OS_MAC)
+std::string kPlatform = "mac";
+#elif BUILDFLAG(IS_CHROMEOS_ASH)
+std::string kPlatform = "chromeos";
+#elif defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)
+std::string kPlatform = "linux";
+#elif defined(OS_ANDROID)
+std::string kPlatform = "android";
+#else
+std::string kPlatform = "all_platform";
+#endif
+
+TestAccountsUtil::TestAccountsUtil() = default;
+TestAccountsUtil::~TestAccountsUtil() = default;
+
+bool TestAccountsUtil::Init(const base::FilePath& config_path) {
+ int error_code = 0;
+ std::string error_str;
+ JSONFileValueDeserializer deserializer(config_path);
+ std::unique_ptr<Value> content_json =
+ deserializer.Deserialize(&error_code, &error_str);
+ CHECK(error_code == 0) << "Error reading json file. Error code: "
+ << error_code << " " << error_str;
+ CHECK(content_json);
+
+ // Only store platform specific users. If an account does not have
+ // platform specific user, try to use all_platform user.
+ for (auto account : content_json->DictItems()) {
+ const Value* platform_account = account.second.FindDictKey(kPlatform);
+ if (platform_account == nullptr) {
+ platform_account = account.second.FindDictKey("all_platform");
+ if (platform_account == nullptr) {
+ continue;
+ }
+ }
+ TestAccount ta(*(platform_account->FindStringKey("user")),
+ *(platform_account->FindStringKey("password")));
+ all_accounts_.insert(
+ std::pair<std::string, TestAccount>(account.first, ta));
+ }
+ return true;
+}
+
+bool TestAccountsUtil::GetAccount(const std::string& name,
+ TestAccount& out_account) const {
+ auto it = all_accounts_.find(name);
+ if (it == all_accounts_.end()) {
+ return false;
+ }
+ out_account = it->second;
+ return true;
+}
+
+} // namespace test
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h
new file mode 100644
index 00000000000..4f73dd76d32
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h
@@ -0,0 +1,46 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_
+
+#include <map>
+#include <string>
+
+namespace base {
+class FilePath;
+}
+
+namespace signin {
+namespace test {
+
+struct TestAccount {
+ std::string user;
+ std::string password;
+ TestAccount() = default;
+ TestAccount(const std::string& user, const std::string& password) {
+ this->user = user;
+ this->password = password;
+ }
+};
+
+class TestAccountsUtil {
+ public:
+ TestAccountsUtil();
+
+ TestAccountsUtil(const TestAccountsUtil&) = delete;
+ TestAccountsUtil& operator=(const TestAccountsUtil&) = delete;
+
+ virtual ~TestAccountsUtil();
+ bool Init(const base::FilePath& config_path);
+ bool GetAccount(const std::string& name, TestAccount& out_account) const;
+
+ private:
+ std::map<std::string, TestAccount> all_accounts_;
+};
+
+} // namespace test
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_
diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc
new file mode 100644
index 00000000000..7b63dd6128a
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc
@@ -0,0 +1,100 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/e2e_tests/test_accounts_util.h"
+#include "base/files/file_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using base::FilePath;
+
+namespace signin {
+namespace test {
+
+class TestAccountsUtilTest : public testing::Test {};
+
+FilePath WriteContentToTemporaryFile(const char* contents,
+ unsigned int length) {
+ FilePath tmp_file;
+ CHECK(base::CreateTemporaryFile(&tmp_file));
+ unsigned int bytes_written = base::WriteFile(tmp_file, contents, length);
+ CHECK_EQ(bytes_written, length);
+ return tmp_file;
+}
+
+TEST(TestAccountsUtilTest, ParsingJson) {
+ const char contents[] =
+ "{ \n"
+ " \"TEST_ACCOUNT_1\": {\n"
+ " \"win\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " }\n"
+ " }\n"
+ "}";
+ FilePath tmp_file =
+ WriteContentToTemporaryFile(contents, sizeof(contents) - 1);
+ TestAccountsUtil util;
+ util.Init(tmp_file);
+}
+
+TEST(TestAccountsUtilTest, GetAccountForPlatformSpecific) {
+ const char contents[] =
+ "{ \n"
+ " \"TEST_ACCOUNT_1\": {\n"
+ " \"win\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " },\n"
+ " \"mac\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " },\n"
+ " \"linux\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " },\n"
+ " \"chromeos\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " },\n"
+ " \"android\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " }\n"
+ " }\n"
+ "}";
+ FilePath tmp_file =
+ WriteContentToTemporaryFile(contents, sizeof(contents) - 1);
+ TestAccountsUtil util;
+ util.Init(tmp_file);
+ TestAccount ta;
+ bool ret = util.GetAccount("TEST_ACCOUNT_1", ta);
+ ASSERT_TRUE(ret);
+ ASSERT_EQ(ta.user, "user1");
+ ASSERT_EQ(ta.password, "pwd1");
+}
+
+TEST(TestAccountsUtilTest, GetAccountForAllPlatform) {
+ const char contents[] =
+ "{ \n"
+ " \"TEST_ACCOUNT_1\": {\n"
+ " \"all_platform\": {\n"
+ " \"user\": \"user_allplatform\",\n"
+ " \"password\": \"pwd_allplatform\"\n"
+ " }\n"
+ " }\n"
+ "}";
+ FilePath tmp_file =
+ WriteContentToTemporaryFile(contents, sizeof(contents) - 1);
+ TestAccountsUtil util;
+ util.Init(tmp_file);
+ TestAccount ta;
+ bool ret = util.GetAccount("TEST_ACCOUNT_1", ta);
+ ASSERT_TRUE(ret);
+ ASSERT_EQ(ta.user, "user_allplatform");
+ ASSERT_EQ(ta.password, "pwd_allplatform");
+}
+
+} // namespace test
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/force_signin_verifier.cc b/chromium/chrome/browser/signin/force_signin_verifier.cc
new file mode 100644
index 00000000000..951efcd003c
--- /dev/null
+++ b/chromium/chrome/browser/signin/force_signin_verifier.cc
@@ -0,0 +1,195 @@
+// Copyright 2017 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 <string>
+
+#include "chrome/browser/signin/force_signin_verifier.h"
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/files/file_path.h"
+#include "base/metrics/histogram_macros.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/profile_picker.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/access_token_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "components/signin/public/identity_manager/scope_set.h"
+#include "content/public/browser/network_service_instance.h"
+#include "google_apis/gaia/gaia_constants.h"
+
+namespace {
+const net::BackoffEntry::Policy kForceSigninVerifierBackoffPolicy = {
+ 0, // Number of initial errors to ignore before applying
+ // exponential back-off rules.
+ 2000, // Initial delay in ms.
+ 2, // Factor by which the waiting time will be multiplied.
+ 0.2, // Fuzzing percentage.
+ 4 * 60 * 1000, // Maximum amount of time to delay th request in ms.
+ -1, // Never discard the entry.
+ false // Do not always use initial delay.
+};
+
+} // namespace
+
+ForceSigninVerifier::ForceSigninVerifier(
+ Profile* profile,
+ signin::IdentityManager* identity_manager)
+ : has_token_verified_(false),
+ backoff_entry_(&kForceSigninVerifierBackoffPolicy),
+ creation_time_(base::TimeTicks::Now()),
+ profile_(profile),
+ identity_manager_(identity_manager) {
+ content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
+ // Most of time (~94%), sign-in token can be verified with server.
+ SendRequest();
+}
+
+ForceSigninVerifier::~ForceSigninVerifier() {
+ Cancel();
+}
+
+void ForceSigninVerifier::OnAccessTokenFetchComplete(
+ GoogleServiceAuthError error,
+ signin::AccessTokenInfo token_info) {
+ if (error.state() != GoogleServiceAuthError::NONE) {
+ if (error.IsPersistentError()) {
+ // Based on the obsolete UMA Signin.ForceSigninVerificationTime.Failure,
+ // about 7% verifications are failed. Most of them are finished within
+ // 113ms but some of them (<3%) could take longer than 3 minutes.
+ has_token_verified_ = true;
+ CloseAllBrowserWindows();
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(
+ this);
+ Cancel();
+ } else {
+ backoff_entry_.InformOfRequest(false);
+ backoff_request_timer_.Start(
+ FROM_HERE, backoff_entry_.GetTimeUntilRelease(),
+ base::BindOnce(&ForceSigninVerifier::SendRequest,
+ weak_factory_.GetWeakPtr()));
+ access_token_fetcher_.reset();
+ }
+ return;
+ }
+
+ // Based on the obsolete UMA Signin.ForceSigninVerificationTime.Success, about
+ // 93% verifications are succeeded. Most of them are finished ~1 second but
+ // some of them (<3%) could take longer than 3 minutes.
+ has_token_verified_ = true;
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);
+ Cancel();
+}
+
+void ForceSigninVerifier::OnConnectionChanged(
+ network::mojom::ConnectionType type) {
+ // Try again immediately once the network is back and cancel any pending
+ // request.
+ backoff_entry_.Reset();
+ if (backoff_request_timer_.IsRunning())
+ backoff_request_timer_.Stop();
+
+ SendRequestIfNetworkAvailable(type);
+}
+
+void ForceSigninVerifier::Cancel() {
+ backoff_entry_.Reset();
+ backoff_request_timer_.Stop();
+ access_token_fetcher_.reset();
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);
+}
+
+bool ForceSigninVerifier::HasTokenBeenVerified() {
+ return has_token_verified_;
+}
+
+void ForceSigninVerifier::SendRequest() {
+ auto type = network::mojom::ConnectionType::CONNECTION_NONE;
+ if (content::GetNetworkConnectionTracker()->GetConnectionType(
+ &type,
+ base::BindOnce(&ForceSigninVerifier::SendRequestIfNetworkAvailable,
+ weak_factory_.GetWeakPtr()))) {
+ SendRequestIfNetworkAvailable(type);
+ }
+}
+
+void ForceSigninVerifier::SendRequestIfNetworkAvailable(
+ network::mojom::ConnectionType network_type) {
+ if (network_type == network::mojom::ConnectionType::CONNECTION_NONE ||
+ !ShouldSendRequest()) {
+ return;
+ }
+
+ signin::ScopeSet oauth2_scopes;
+ oauth2_scopes.insert(GaiaConstants::kChromeSyncOAuth2Scope);
+ access_token_fetcher_ =
+ std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
+ "force_signin_verifier", identity_manager_, oauth2_scopes,
+ base::BindOnce(&ForceSigninVerifier::OnAccessTokenFetchComplete,
+ weak_factory_.GetWeakPtr()),
+ signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate);
+}
+
+bool ForceSigninVerifier::ShouldSendRequest() {
+ return !has_token_verified_ && access_token_fetcher_.get() == nullptr &&
+ identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync);
+}
+
+void ForceSigninVerifier::CloseAllBrowserWindows() {
+ if (base::FeatureList::IsEnabled(features::kForceSignInReauth)) {
+ // Do not sign the user out to allow them to reauthenticate from the profile
+ // picker.
+ BrowserList::CloseAllBrowsersWithProfile(
+ profile_,
+ base::BindRepeating(&ForceSigninVerifier::OnCloseBrowsersSuccess,
+ weak_factory_.GetWeakPtr()),
+ base::DoNothing(),
+ /*skip_beforeunload=*/true);
+ } else {
+ // Do not close window if there is ongoing reauth. If it fails later, the
+ // signin process should take care of the signout.
+ auto* primary_account_mutator =
+ identity_manager_->GetPrimaryAccountMutator();
+ if (!primary_account_mutator)
+ return;
+ primary_account_mutator->ClearPrimaryAccount(
+ signin_metrics::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN,
+ signin_metrics::SignoutDelete::kIgnoreMetric);
+ }
+}
+
+void ForceSigninVerifier::OnCloseBrowsersSuccess(
+ const base::FilePath& profile_path) {
+ Cancel();
+
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile_path);
+ if (!entry)
+ return;
+ entry->LockForceSigninProfile(true);
+ ProfilePicker::Show(ProfilePicker::EntryPoint::kProfileLocked);
+}
+
+signin::PrimaryAccountAccessTokenFetcher*
+ForceSigninVerifier::GetAccessTokenFetcherForTesting() {
+ return access_token_fetcher_.get();
+}
+
+net::BackoffEntry* ForceSigninVerifier::GetBackoffEntryForTesting() {
+ return &backoff_entry_;
+}
+
+base::OneShotTimer* ForceSigninVerifier::GetOneShotTimerForTesting() {
+ return &backoff_request_timer_;
+}
diff --git a/chromium/chrome/browser/signin/force_signin_verifier.h b/chromium/chrome/browser/signin/force_signin_verifier.h
new file mode 100644
index 00000000000..7260aa6f3fe
--- /dev/null
+++ b/chromium/chrome/browser/signin/force_signin_verifier.h
@@ -0,0 +1,95 @@
+// Copyright 2017 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_
+#define CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_
+
+#include <memory>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "net/base/backoff_entry.h"
+#include "services/network/public/cpp/network_connection_tracker.h"
+
+class Profile;
+
+namespace base {
+class FilePath;
+}
+
+namespace signin {
+class IdentityManager;
+class PrimaryAccountAccessTokenFetcher;
+struct AccessTokenInfo;
+} // namespace signin
+
+// ForceSigninVerifier will verify profile's auth token when profile is loaded
+// into memory by the first time via gaia server. It will retry on any transient
+// error.
+class ForceSigninVerifier
+ : public network::NetworkConnectionTracker::NetworkConnectionObserver {
+ public:
+ explicit ForceSigninVerifier(Profile* profile,
+ signin::IdentityManager* identity_manager);
+
+ ForceSigninVerifier(const ForceSigninVerifier&) = delete;
+ ForceSigninVerifier& operator=(const ForceSigninVerifier&) = delete;
+
+ ~ForceSigninVerifier() override;
+
+ void OnAccessTokenFetchComplete(GoogleServiceAuthError error,
+ signin::AccessTokenInfo token_info);
+
+ // override network::NetworkConnectionTracker::NetworkConnectionObserver
+ void OnConnectionChanged(network::mojom::ConnectionType type) override;
+
+ // Cancel any pending or ongoing verification.
+ void Cancel();
+
+ // Return the value of |has_token_verified_|.
+ bool HasTokenBeenVerified();
+
+ protected:
+ // Send the token verification request. The request will be sent only if
+ // - The token has never been verified before.
+ // - There is no on going verification.
+ // - There is network connection.
+ // - The profile has signed in.
+ void SendRequest();
+
+ // Send the request if |network_type| is not CONNECTION_NONE and
+ // ShouldSendRequest returns true.
+ void SendRequestIfNetworkAvailable(
+ network::mojom::ConnectionType network_type);
+
+ bool ShouldSendRequest();
+
+ virtual void CloseAllBrowserWindows();
+ void OnCloseBrowsersSuccess(const base::FilePath& profile_path);
+
+ signin::PrimaryAccountAccessTokenFetcher* GetAccessTokenFetcherForTesting();
+ net::BackoffEntry* GetBackoffEntryForTesting();
+ base::OneShotTimer* GetOneShotTimerForTesting();
+
+ private:
+ std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher>
+ access_token_fetcher_;
+
+ // Indicates whether the verification is finished successfully or with a
+ // persistent error.
+ bool has_token_verified_ = false;
+ net::BackoffEntry backoff_entry_;
+ base::OneShotTimer backoff_request_timer_;
+ base::TimeTicks creation_time_;
+
+ raw_ptr<Profile> profile_ = nullptr;
+ raw_ptr<signin::IdentityManager> identity_manager_ = nullptr;
+
+ base::WeakPtrFactory<ForceSigninVerifier> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_
diff --git a/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc b/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc
new file mode 100644
index 00000000000..dcd382d6b38
--- /dev/null
+++ b/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc
@@ -0,0 +1,416 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/force_signin_verifier.h"
+
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "content/public/browser/network_service_instance.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class ForceSigninVerifierWithAccessToInternalsForTesting
+ : public ForceSigninVerifier {
+ public:
+ explicit ForceSigninVerifierWithAccessToInternalsForTesting(
+ signin::IdentityManager* identity_manager)
+ : ForceSigninVerifier(nullptr, identity_manager) {}
+
+ bool IsDelayTaskPosted() { return GetOneShotTimerForTesting()->IsRunning(); }
+
+ int FailureCount() { return GetBackoffEntryForTesting()->failure_count(); }
+
+ signin::PrimaryAccountAccessTokenFetcher* access_token_fetcher() {
+ return GetAccessTokenFetcherForTesting();
+ }
+
+ MOCK_METHOD0(CloseAllBrowserWindows, void(void));
+};
+
+// A NetworkConnectionObserver that invokes a base::RepeatingClosure when
+// NetworkConnectionObserver::OnConnectionChanged() is invoked.
+class NetworkConnectionObserverHelper
+ : public network::NetworkConnectionTracker::NetworkConnectionObserver {
+ public:
+ explicit NetworkConnectionObserverHelper(base::RepeatingClosure closure)
+ : closure_(std::move(closure)) {
+ DCHECK(!closure_.is_null());
+ content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
+ }
+
+ NetworkConnectionObserverHelper(const NetworkConnectionObserverHelper&) =
+ delete;
+ NetworkConnectionObserverHelper& operator=(
+ const NetworkConnectionObserverHelper&) = delete;
+
+ ~NetworkConnectionObserverHelper() override {
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(
+ this);
+ }
+
+ void OnConnectionChanged(network::mojom::ConnectionType type) override {
+ closure_.Run();
+ }
+
+ private:
+ base::RepeatingClosure closure_;
+};
+
+// Used to select which type of network type NetworkConnectionTracker should
+// be configured to.
+enum class NetworkConnectionType {
+ Undecided,
+ ConnectionNone,
+ ConnectionWifi,
+ Connection4G,
+};
+
+// Used to select which type of response NetworkConnectionTracker should give.
+enum class NetworkResponseType {
+ Undecided,
+ Synchronous,
+ Asynchronous,
+};
+
+// Forces the network connection type to change to |connection_type| and wait
+// till the notification has been propagated to the observers. Also change the
+// response type to be synchronous/asynchronous based on |response_type|.
+void ConfigureNetworkConnectionTracker(NetworkConnectionType connection_type,
+ NetworkResponseType response_type) {
+ network::TestNetworkConnectionTracker* tracker =
+ network::TestNetworkConnectionTracker::GetInstance();
+
+ switch (response_type) {
+ case NetworkResponseType::Undecided:
+ // nothing to do
+ break;
+
+ case NetworkResponseType::Synchronous:
+ tracker->SetRespondSynchronously(true);
+ break;
+
+ case NetworkResponseType::Asynchronous:
+ tracker->SetRespondSynchronously(false);
+ break;
+ }
+
+ if (connection_type != NetworkConnectionType::Undecided) {
+ network::mojom::ConnectionType mojom_connection_type =
+ network::mojom::ConnectionType::CONNECTION_UNKNOWN;
+
+ switch (connection_type) {
+ case NetworkConnectionType::Undecided:
+ NOTREACHED();
+ break;
+
+ case NetworkConnectionType::ConnectionNone:
+ mojom_connection_type = network::mojom::ConnectionType::CONNECTION_NONE;
+ break;
+
+ case NetworkConnectionType::ConnectionWifi:
+ mojom_connection_type = network::mojom::ConnectionType::CONNECTION_WIFI;
+ break;
+
+ case NetworkConnectionType::Connection4G:
+ mojom_connection_type = network::mojom::ConnectionType::CONNECTION_4G;
+ break;
+ }
+
+ DCHECK_NE(mojom_connection_type,
+ network::mojom::ConnectionType::CONNECTION_UNKNOWN);
+
+ base::RunLoop wait_for_network_type_change;
+ NetworkConnectionObserverHelper scoped_observer(
+ wait_for_network_type_change.QuitWhenIdleClosure());
+
+ tracker->SetConnectionType(mojom_connection_type);
+
+ wait_for_network_type_change.Run();
+ }
+}
+
+// Forces the current sequence's task runner to spin. This is used because the
+// ForceSigninVerifier ends up posting task to the sequence's task runner when
+// MetworkConnectionTracker is returning results asynchronously.
+void SpinCurrentSequenceTaskRunner() {
+ base::RunLoop run_loop;
+ base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
+ run_loop.QuitClosure());
+ run_loop.Run();
+}
+
+} // namespace
+
+TEST(ForceSigninVerifierTest, OnGetTokenSuccess) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(0);
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
+ account_info.account_id, /*token=*/"", base::Time());
+
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_TRUE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ ASSERT_EQ(0, verifier.FailureCount());
+}
+
+TEST(ForceSigninVerifierTest, OnGetTokenPersistentFailure) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(1);
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ GoogleServiceAuthError(
+ GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS));
+
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_TRUE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ ASSERT_EQ(0, verifier.FailureCount());
+}
+
+TEST(ForceSigninVerifierTest, OnGetTokenTransientFailure) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(0);
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED));
+
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.HasTokenBeenVerified());
+ ASSERT_TRUE(verifier.IsDelayTaskPosted());
+ ASSERT_EQ(1, verifier.FailureCount());
+}
+
+TEST(ForceSigninVerifierTest, OnLostConnection) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED));
+
+ ASSERT_EQ(1, verifier.FailureCount());
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_TRUE(verifier.IsDelayTaskPosted());
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionNone,
+ NetworkResponseType::Undecided);
+
+ ASSERT_EQ(0, verifier.FailureCount());
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+}
+
+TEST(ForceSigninVerifierTest, OnReconnected) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED));
+
+ ASSERT_EQ(1, verifier.FailureCount());
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_TRUE(verifier.IsDelayTaskPosted());
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi,
+ NetworkResponseType::Undecided);
+
+ ASSERT_EQ(0, verifier.FailureCount());
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+}
+
+TEST(ForceSigninVerifierTest, GetNetworkStatusAsync) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::Undecided,
+ NetworkResponseType::Asynchronous);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ // There is no network type at first.
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Waiting for the network type returns.
+ SpinCurrentSequenceTaskRunner();
+
+ // Get the type and send the request.
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+}
+
+TEST(ForceSigninVerifierTest, LaunchVerifierWithoutNetwork) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionNone,
+ NetworkResponseType::Asynchronous);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ // There is no network type.
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Waiting for the network type returns.
+ SpinCurrentSequenceTaskRunner();
+
+ // Get the type, there is no network connection, don't send the request.
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Network is resumed.
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi,
+ NetworkResponseType::Undecided);
+
+ // Send the request.
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+}
+
+TEST(ForceSigninVerifierTest, ChangeNetworkFromWIFITo4GWithOnGoingRequest) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi,
+ NetworkResponseType::Asynchronous);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ EXPECT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Waiting for the network type returns.
+ SpinCurrentSequenceTaskRunner();
+
+ // The network type if wifi, send the request.
+ auto* first_request = verifier.access_token_fetcher();
+ EXPECT_NE(nullptr, first_request);
+
+ // Network is changed to 4G.
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::Connection4G,
+ NetworkResponseType::Undecided);
+
+ // There is still one on-going request.
+ EXPECT_EQ(first_request, verifier.access_token_fetcher());
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
+ account_info.account_id, /*token=*/"", base::Time());
+}
+
+TEST(ForceSigninVerifierTest, ChangeNetworkFromWIFITo4GWithFinishedRequest) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi,
+ NetworkResponseType::Asynchronous);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ EXPECT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Waiting for the network type returns.
+ SpinCurrentSequenceTaskRunner();
+
+ // The network type if wifi, send the request.
+ EXPECT_NE(nullptr, verifier.access_token_fetcher());
+
+ // Finishes the request.
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
+ account_info.account_id, /*token=*/"", base::Time());
+ EXPECT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Network is changed to 4G.
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::Connection4G,
+ NetworkResponseType::Undecided);
+
+ // No more request because it's verfied already.
+ EXPECT_EQ(nullptr, verifier.access_token_fetcher());
+}
+
+// Regression test for https://crbug.com/1259864
+TEST(ForceSigninVerifierTest, DeleteWithPendingRequestShouldNotCrash) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::Undecided,
+ NetworkResponseType::Asynchronous);
+
+ {
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ // There is no network type at first.
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Delete the verifier while the request is pending.
+ }
+
+ // Waiting for the network type returns, this should not crash.
+ SpinCurrentSequenceTaskRunner();
+}
diff --git a/chromium/chrome/browser/signin/header_modification_delegate.h b/chromium/chrome/browser/signin/header_modification_delegate.h
new file mode 100644
index 00000000000..8d3bce133ac
--- /dev/null
+++ b/chromium/chrome/browser/signin/header_modification_delegate.h
@@ -0,0 +1,38 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_
+#define CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_
+
+class GURL;
+
+namespace content {
+class WebContents;
+}
+
+namespace signin {
+
+class ChromeRequestAdapter;
+class ResponseAdapter;
+
+class HeaderModificationDelegate {
+ public:
+ HeaderModificationDelegate() = default;
+
+ HeaderModificationDelegate(const HeaderModificationDelegate&) = delete;
+ HeaderModificationDelegate& operator=(const HeaderModificationDelegate&) =
+ delete;
+
+ virtual ~HeaderModificationDelegate() = default;
+
+ virtual bool ShouldInterceptNavigation(content::WebContents* contents) = 0;
+ virtual void ProcessRequest(ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url) = 0;
+ virtual void ProcessResponse(ResponseAdapter* response_adapter,
+ const GURL& redirect_url) = 0;
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_
diff --git a/chromium/chrome/browser/signin/header_modification_delegate_impl.cc b/chromium/chrome/browser/signin/header_modification_delegate_impl.cc
new file mode 100644
index 00000000000..6aa2d10614f
--- /dev/null
+++ b/chromium/chrome/browser/signin/header_modification_delegate_impl.cc
@@ -0,0 +1,154 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/header_modification_delegate_impl.h"
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/content_settings/cookie_settings_factory.h"
+#include "chrome/browser/extensions/api/identity/web_auth_flow.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/sync/sync_service_factory.h"
+#include "chrome/common/pref_names.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/tribool.h"
+#include "components/sync/base/pref_names.h"
+#include "components/sync/driver/sync_service.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/site_instance.h"
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+#include "extensions/browser/guest_view/web_view/web_view_guest.h"
+#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "components/account_manager_core/pref_names.h"
+#endif
+
+namespace signin {
+
+#if defined(OS_ANDROID)
+HeaderModificationDelegateImpl::HeaderModificationDelegateImpl(
+ Profile* profile,
+ bool incognito_enabled)
+ : profile_(profile),
+ cookie_settings_(CookieSettingsFactory::GetForProfile(profile_)),
+ incognito_enabled_(incognito_enabled) {}
+#else
+HeaderModificationDelegateImpl::HeaderModificationDelegateImpl(Profile* profile)
+ : profile_(profile),
+ cookie_settings_(CookieSettingsFactory::GetForProfile(profile_)) {}
+#endif
+
+HeaderModificationDelegateImpl::~HeaderModificationDelegateImpl() = default;
+
+bool HeaderModificationDelegateImpl::ShouldInterceptNavigation(
+ content::WebContents* contents) {
+ if (profile_->IsOffTheRecord())
+ return false;
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ if (ShouldIgnoreGuestWebViewRequest(contents))
+ return false;
+#endif
+
+ return true;
+}
+
+void HeaderModificationDelegateImpl::ProcessRequest(
+ ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ const PrefService* prefs = profile_->GetPrefs();
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ syncer::SyncService* sync_service =
+ SyncServiceFactory::GetForProfile(profile_);
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ bool is_secondary_account_addition_allowed = true;
+ if (!prefs->GetBoolean(
+ ::account_manager::prefs::kSecondaryGoogleAccountSigninAllowed)) {
+ is_secondary_account_addition_allowed = false;
+ }
+#endif
+
+ ConsentLevel consent_level = ConsentLevel::kSync;
+#if defined(OS_ANDROID)
+ consent_level = ConsentLevel::kSignin;
+#endif
+
+ IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile_);
+ CoreAccountInfo account =
+ identity_manager->GetPrimaryAccountInfo(consent_level);
+ signin::Tribool is_child_account =
+ // Defaults to kUnknown if the account is not found.
+ identity_manager->FindExtendedAccountInfo(account).is_child_account;
+
+ int incognito_mode_availability =
+ prefs->GetInteger(prefs::kIncognitoModeAvailability);
+#if defined(OS_ANDROID)
+ incognito_mode_availability =
+ incognito_enabled_
+ ? incognito_mode_availability
+ : static_cast<int>(IncognitoModePrefs::Availability::kDisabled);
+#endif
+
+ FixAccountConsistencyRequestHeader(
+ request_adapter, redirect_url, profile_->IsOffTheRecord(),
+ incognito_mode_availability,
+ AccountConsistencyModeManager::GetMethodForProfile(profile_),
+ account.gaia, is_child_account,
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ is_secondary_account_addition_allowed,
+#endif
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ sync_service && sync_service->IsSyncFeatureEnabled(),
+ prefs->GetString(prefs::kGoogleServicesSigninScopedDeviceId),
+#endif
+ cookie_settings_.get());
+}
+
+void HeaderModificationDelegateImpl::ProcessResponse(
+ ResponseAdapter* response_adapter,
+ const GURL& redirect_url) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ ProcessAccountConsistencyResponseHeaders(response_adapter, redirect_url,
+ profile_->IsOffTheRecord());
+}
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+// static
+bool HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest(
+ content::WebContents* contents) {
+ if (!contents)
+ return true;
+
+ if (extensions::WebViewRendererState::GetInstance()->IsGuest(
+ contents->GetMainFrame()->GetProcess()->GetID())) {
+ auto identity_api_config =
+ extensions::WebAuthFlow::GetWebViewPartitionConfig(
+ extensions::WebAuthFlow::GET_AUTH_TOKEN,
+ contents->GetBrowserContext());
+ if (contents->GetSiteInstance()->GetStoragePartitionConfig() !=
+ identity_api_config)
+ return true;
+
+ // If the StoragePartitionConfig matches, but |contents| is not using a
+ // guest SiteInstance, then there is likely a serious bug.
+ CHECK(contents->GetSiteInstance()->IsGuest());
+ }
+ return false;
+}
+#endif
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/header_modification_delegate_impl.h b/chromium/chrome/browser/signin/header_modification_delegate_impl.h
new file mode 100644
index 00000000000..8bc1fd7fb2d
--- /dev/null
+++ b/chromium/chrome/browser/signin/header_modification_delegate_impl.h
@@ -0,0 +1,68 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_
+#define CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_
+
+#include "base/memory/raw_ptr.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "components/content_settings/core/browser/cookie_settings.h"
+#include "content/public/browser/browser_thread.h"
+#include "extensions/buildflags/buildflags.h"
+
+class Profile;
+
+namespace signin {
+
+// This class wraps the FixAccountConsistencyRequestHeader and
+// ProcessAccountConsistencyResponseHeaders in the HeaderModificationDelegate
+// interface.
+class HeaderModificationDelegateImpl : public HeaderModificationDelegate {
+ public:
+#if defined(OS_ANDROID)
+ explicit HeaderModificationDelegateImpl(Profile* profile,
+ bool incognito_enabled);
+#else
+ explicit HeaderModificationDelegateImpl(Profile* profile);
+#endif
+
+ HeaderModificationDelegateImpl(const HeaderModificationDelegateImpl&) =
+ delete;
+ HeaderModificationDelegateImpl& operator=(
+ const HeaderModificationDelegateImpl&) = delete;
+
+ ~HeaderModificationDelegateImpl() override;
+
+ // HeaderModificationDelegate
+ bool ShouldInterceptNavigation(content::WebContents* contents) override;
+ void ProcessRequest(ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url) override;
+ void ProcessResponse(ResponseAdapter* response_adapter,
+ const GURL& redirect_url) override;
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ // Returns true if the request comes from a web view and should be ignored
+ // (i.e. not intercepted).
+ // Returns false if the request does not come from a web view.
+ // Requests coming from most guest web views are ignored. In particular the
+ // requests coming from the InlineLoginUI are not intercepted (see
+ // http://crbug.com/428396). Requests coming from the chrome identity
+ // extension consent flow are not ignored.
+ static bool ShouldIgnoreGuestWebViewRequest(content::WebContents* contents);
+#endif
+
+ private:
+ raw_ptr<Profile> profile_;
+ scoped_refptr<content_settings::CookieSettings> cookie_settings_;
+
+#if defined(OS_ANDROID)
+ bool incognito_enabled_;
+#endif
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_
diff --git a/chromium/chrome/browser/signin/identity_manager_factory.cc b/chromium/chrome/browser/signin/identity_manager_factory.cc
new file mode 100644
index 00000000000..cac771bf1e2
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_manager_factory.cc
@@ -0,0 +1,171 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/identity_manager_factory.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/files/file_path.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/image_fetcher/image_decoder_impl.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_provider.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_manager_builder.h"
+#include "components/signin/public/webdata/token_web_data.h"
+#include "content/public/browser/network_service_instance.h"
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "chrome/browser/content_settings/cookie_settings_factory.h"
+#include "chrome/browser/web_data_service_factory.h"
+#include "components/content_settings/core/browser/cookie_settings.h"
+#include "components/keyed_service/core/service_access_type.h"
+#include "components/signin/core/browser/cookie_settings_util.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#include "chrome/browser/browser_process_platform_part.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "chrome/browser/lacros/account_manager/profile_account_manager.h"
+#include "chrome/browser/lacros/account_manager/profile_account_manager_factory.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif
+
+#if defined(OS_WIN)
+#include "base/bind.h"
+#include "chrome/browser/signin/signin_util_win.h"
+#endif
+
+void IdentityManagerFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ signin::IdentityManager::RegisterProfilePrefs(registry);
+}
+
+IdentityManagerFactory::IdentityManagerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "IdentityManager",
+ BrowserContextDependencyManager::GetInstance()) {
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ DependsOn(WebDataServiceFactory::GetInstance());
+#endif
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ DependsOn(ProfileAccountManagerFactory::GetInstance());
+#endif
+ DependsOn(ChromeSigninClientFactory::GetInstance());
+ signin::SetIdentityManagerProvider(
+ base::BindRepeating([](content::BrowserContext* context) {
+ return GetForProfile(Profile::FromBrowserContext(context));
+ }));
+}
+
+IdentityManagerFactory::~IdentityManagerFactory() {
+ signin::SetIdentityManagerProvider({});
+}
+
+// static
+signin::IdentityManager* IdentityManagerFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<signin::IdentityManager*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+signin::IdentityManager* IdentityManagerFactory::GetForProfileIfExists(
+ const Profile* profile) {
+ return static_cast<signin::IdentityManager*>(
+ GetInstance()->GetServiceForBrowserContext(const_cast<Profile*>(profile),
+ false));
+}
+
+// static
+IdentityManagerFactory* IdentityManagerFactory::GetInstance() {
+ return base::Singleton<IdentityManagerFactory>::get();
+}
+
+// static
+void IdentityManagerFactory::EnsureFactoryAndDependeeFactoriesBuilt() {
+ IdentityManagerFactory::GetInstance();
+ ChromeSigninClientFactory::GetInstance();
+}
+
+void IdentityManagerFactory::AddObserver(Observer* observer) {
+ observer_list_.AddObserver(observer);
+}
+
+void IdentityManagerFactory::RemoveObserver(Observer* observer) {
+ observer_list_.RemoveObserver(observer);
+}
+
+KeyedService* IdentityManagerFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+
+ signin::IdentityManagerBuildParams params;
+ params.account_consistency =
+ AccountConsistencyModeManager::GetMethodForProfile(profile),
+ params.image_decoder = std::make_unique<ImageDecoderImpl>();
+ params.local_state = g_browser_process->local_state();
+ params.network_connection_tracker = content::GetNetworkConnectionTracker();
+ params.pref_service = profile->GetPrefs();
+ params.profile_path = profile->GetPath();
+ params.signin_client = ChromeSigninClientFactory::GetForProfile(profile);
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ params.delete_signin_cookies_on_exit =
+ signin::SettingsDeleteSigninCookiesOnExit(
+ CookieSettingsFactory::GetForProfile(profile).get());
+ params.token_web_data = WebDataServiceFactory::GetTokenWebDataForProfile(
+ profile, ServiceAccessType::EXPLICIT_ACCESS);
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ params.account_manager_facade =
+ GetAccountManagerFacade(profile->GetPath().value());
+ params.is_regular_profile =
+ chromeos::ProfileHelper::IsRegularProfile(profile);
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ // The system and (original profile of the) guest profiles are not regular.
+ const bool is_regular_profile = profile->IsRegularProfile();
+ const bool use_profile_account_manager =
+ is_regular_profile &&
+ // `ProfileManager` may be null in tests, and is required for account
+ // consistency.
+ g_browser_process->profile_manager();
+
+ params.account_manager_facade =
+ use_profile_account_manager
+ ? ProfileAccountManagerFactory::GetForProfile(profile)
+ : GetAccountManagerFacade(profile->GetPath().value());
+ params.is_regular_profile = is_regular_profile;
+#endif
+
+#if defined(OS_WIN)
+ params.reauth_callback =
+ base::BindRepeating(&signin_util::ReauthWithCredentialProviderIfPossible,
+ base::Unretained(profile));
+#endif
+
+ std::unique_ptr<signin::IdentityManager> identity_manager =
+ signin::BuildIdentityManager(&params);
+
+ for (Observer& observer : observer_list_)
+ observer.IdentityManagerCreated(identity_manager.get());
+
+ return identity_manager.release();
+}
diff --git a/chromium/chrome/browser/signin/identity_manager_factory.h b/chromium/chrome/browser/signin/identity_manager_factory.h
new file mode 100644
index 00000000000..a2e0590da42
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_manager_factory.h
@@ -0,0 +1,67 @@
+// Copyright 2017 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "base/observer_list.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace signin {
+class IdentityManager;
+}
+
+class Profile;
+
+// Singleton that owns all IdentityManager instances and associates them with
+// Profiles.
+class IdentityManagerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ class Observer : public base::CheckedObserver {
+ public:
+ // Called when a IdentityManager instance is created.
+ virtual void IdentityManagerCreated(
+ signin::IdentityManager* identity_manager) {}
+
+ protected:
+ ~Observer() override {}
+ };
+
+ static signin::IdentityManager* GetForProfile(Profile* profile);
+ static signin::IdentityManager* GetForProfileIfExists(const Profile* profile);
+
+ // Returns an instance of the IdentityManagerFactory singleton.
+ static IdentityManagerFactory* GetInstance();
+
+ IdentityManagerFactory(const IdentityManagerFactory&) = delete;
+ IdentityManagerFactory& operator=(const IdentityManagerFactory&) = delete;
+
+ // Ensures that IdentityManagerFactory and the factories on which it depends
+ // are built.
+ static void EnsureFactoryAndDependeeFactoriesBuilt();
+
+ // Methods to register or remove observers of IdentityManager
+ // creation/shutdown.
+ void AddObserver(Observer* observer);
+ void RemoveObserver(Observer* observer);
+
+ private:
+ friend struct base::DefaultSingletonTraits<IdentityManagerFactory>;
+
+ IdentityManagerFactory();
+ ~IdentityManagerFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+
+ // List of observers. Checks that list is empty on destruction.
+ base::ObserverList<Observer, /*check_empty=*/true, /*allow_reentrancy=*/false>
+ observer_list_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/identity_manager_provider.cc b/chromium/chrome/browser/signin/identity_manager_provider.cc
new file mode 100644
index 00000000000..ca42e587adf
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_manager_provider.cc
@@ -0,0 +1,38 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/identity_manager_provider.h"
+
+#include "base/check.h"
+#include "base/no_destructor.h"
+
+namespace signin {
+
+namespace {
+
+IdentityManagerProvider& GetIdentityManagerProvider() {
+ static base::NoDestructor<IdentityManagerProvider> provider;
+ return *provider;
+}
+
+} // namespace
+
+void SetIdentityManagerProvider(const IdentityManagerProvider& provider) {
+ IdentityManagerProvider& instance = GetIdentityManagerProvider();
+
+ // Exactly one of `provider` or `instance` should be non-null.
+ if (provider)
+ DCHECK(!instance);
+ else
+ DCHECK(instance);
+
+ instance = provider;
+}
+
+IdentityManager* GetIdentityManagerForBrowserContext(
+ content::BrowserContext* context) {
+ return GetIdentityManagerProvider().Run(context);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/identity_manager_provider.h b/chromium/chrome/browser/signin/identity_manager_provider.h
new file mode 100644
index 00000000000..9b6291d643b
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_manager_provider.h
@@ -0,0 +1,32 @@
+// Copyright 2021 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_
+#define CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_
+
+#include "base/callback.h"
+
+namespace content {
+class BrowserContext;
+}
+
+namespace signin {
+
+class IdentityManager;
+
+using IdentityManagerProvider =
+ base::RepeatingCallback<IdentityManager*(content::BrowserContext*)>;
+
+// Called by IdentityManagerFactory to expose a way to retrieve the
+// IdentityManager for a specific BrowserContext/Profile. This exists so that
+// components which don't depend on //chrome/browser can still access the
+// IdentityManager.
+void SetIdentityManagerProvider(const IdentityManagerProvider& provider);
+
+IdentityManager* GetIdentityManagerForBrowserContext(
+ content::BrowserContext* context);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_
diff --git a/chromium/chrome/browser/signin/identity_services_provider_android.cc b/chromium/chrome/browser/signin/identity_services_provider_android.cc
new file mode 100644
index 00000000000..cb49ede8c7d
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_services_provider_android.cc
@@ -0,0 +1,39 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/android/jni_android.h"
+#include "chrome/browser/profiles/profile_android.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/services/android/jni_headers/IdentityServicesProvider_jni.h"
+#include "chrome/browser/signin/signin_manager_android_factory.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+using base::android::JavaParamRef;
+using base::android::ScopedJavaLocalRef;
+
+static ScopedJavaLocalRef<jobject>
+JNI_IdentityServicesProvider_GetIdentityManager(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& j_profile_android) {
+ Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android);
+ return IdentityManagerFactory::GetForProfile(profile)->GetJavaObject();
+}
+
+static ScopedJavaLocalRef<jobject>
+JNI_IdentityServicesProvider_GetAccountTrackerService(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& j_profile_android) {
+ Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ return identity_manager->LegacyGetAccountTrackerServiceJavaObject();
+}
+
+static ScopedJavaLocalRef<jobject>
+JNI_IdentityServicesProvider_GetSigninManager(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& j_profile_android) {
+ Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android);
+ return SigninManagerAndroidFactory::GetJavaObjectForProfile(profile);
+}
diff --git a/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc
new file mode 100644
index 00000000000..ff94a971878
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc
@@ -0,0 +1,103 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+
+#include "base/bind.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "ash/components/account_manager/account_manager_factory.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/browser_process_platform_part.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif
+
+// static
+std::unique_ptr<TestingProfile> IdentityTestEnvironmentProfileAdaptor::
+ CreateProfileForIdentityTestEnvironment() {
+ return CreateProfileForIdentityTestEnvironment(
+ TestingProfile::TestingFactories());
+}
+
+// static
+std::unique_ptr<TestingProfile>
+IdentityTestEnvironmentProfileAdaptor::CreateProfileForIdentityTestEnvironment(
+ const TestingProfile::TestingFactories& input_factories) {
+ TestingProfile::Builder builder;
+
+ for (auto& input_factory : input_factories) {
+ builder.AddTestingFactory(input_factory.first, input_factory.second);
+ }
+
+ return CreateProfileForIdentityTestEnvironment(builder);
+}
+
+// static
+std::unique_ptr<TestingProfile>
+IdentityTestEnvironmentProfileAdaptor::CreateProfileForIdentityTestEnvironment(
+ TestingProfile::Builder& builder,
+ signin::AccountConsistencyMethod account_consistency) {
+ for (auto& identity_factory :
+ GetIdentityTestEnvironmentFactories(account_consistency)) {
+ builder.AddTestingFactory(identity_factory.first, identity_factory.second);
+ }
+
+ return builder.Build();
+}
+
+// static
+void IdentityTestEnvironmentProfileAdaptor::
+ SetIdentityTestEnvironmentFactoriesOnBrowserContext(
+ content::BrowserContext* context) {
+ for (const auto& factory_pair : GetIdentityTestEnvironmentFactories()) {
+ factory_pair.first->SetTestingFactory(context, factory_pair.second);
+ }
+}
+
+// static
+void IdentityTestEnvironmentProfileAdaptor::
+ AppendIdentityTestEnvironmentFactories(
+ TestingProfile::TestingFactories* factories_to_append_to) {
+ TestingProfile::TestingFactories identity_factories =
+ GetIdentityTestEnvironmentFactories();
+ factories_to_append_to->insert(factories_to_append_to->end(),
+ identity_factories.begin(),
+ identity_factories.end());
+}
+
+// static
+TestingProfile::TestingFactories
+IdentityTestEnvironmentProfileAdaptor::GetIdentityTestEnvironmentFactories(
+ signin::AccountConsistencyMethod account_consistency) {
+ return {{IdentityManagerFactory::GetInstance(),
+ base::BindRepeating(&BuildIdentityManagerForTests,
+ account_consistency)}};
+}
+
+// static
+std::unique_ptr<KeyedService>
+IdentityTestEnvironmentProfileAdaptor::BuildIdentityManagerForTests(
+ signin::AccountConsistencyMethod account_consistency,
+ content::BrowserContext* context) {
+ Profile* profile = Profile::FromBrowserContext(context);
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ return signin::IdentityTestEnvironment::BuildIdentityManagerForTests(
+ ChromeSigninClientFactory::GetForProfile(profile), profile->GetPrefs(),
+ profile->GetPath(),
+ g_browser_process->platform_part()->GetAccountManagerFactory(),
+ GetAccountManagerFacade(profile->GetPath().value()));
+#else
+ return signin::IdentityTestEnvironment::BuildIdentityManagerForTests(
+ ChromeSigninClientFactory::GetForProfile(profile), profile->GetPrefs(),
+ profile->GetPath(), account_consistency);
+#endif
+}
+
+IdentityTestEnvironmentProfileAdaptor::IdentityTestEnvironmentProfileAdaptor(
+ Profile* profile)
+ : identity_test_env_(IdentityManagerFactory::GetForProfile(profile),
+ ChromeSigninClientFactory::GetForProfile(profile)) {}
diff --git a/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h
new file mode 100644
index 00000000000..ef477efd9d3
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h
@@ -0,0 +1,101 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_
+#define CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_
+
+#include "chrome/test/base/testing_profile.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+
+// Adaptor that supports signin::IdentityTestEnvironment's usage in testing
+// contexts where the relevant fake objects must be injected via the
+// BrowserContextKeyedServiceFactory infrastructure as the production code
+// accesses IdentityManager via that infrastructure. Before using this
+// class, please consider whether you can change the production code in question
+// to take in the relevant dependencies directly rather than obtaining them from
+// the Profile; this is both cleaner in general and allows for direct usage of
+// signin::IdentityTestEnvironment in the test.
+class IdentityTestEnvironmentProfileAdaptor {
+ public:
+ // Creates and returns a TestingProfile that has been configured with the set
+ // of testing factories that IdentityTestEnvironment requires.
+ static std::unique_ptr<TestingProfile>
+ CreateProfileForIdentityTestEnvironment();
+
+ // Like the above, but additionally configures the returned Profile with
+ // |input_factories|.
+ static std::unique_ptr<TestingProfile>
+ CreateProfileForIdentityTestEnvironment(
+ const TestingProfile::TestingFactories& input_factories);
+
+ // Creates and returns a TestingProfile that has been configured with the
+ // given |builder| and the set of testing factories that
+ // IdentityTestEnvironment requires.
+ // See the above variant for comments on common parameters.
+ static std::unique_ptr<TestingProfile>
+ CreateProfileForIdentityTestEnvironment(
+ TestingProfile::Builder& builder,
+ signin::AccountConsistencyMethod account_consistency =
+ signin::AccountConsistencyMethod::kDisabled);
+
+ // Sets the testing factories that signin::IdentityTestEnvironment
+ // requires explicitly on a Profile that is passed to it.
+ // See the above variant for comments on common parameters.
+ static void SetIdentityTestEnvironmentFactoriesOnBrowserContext(
+ content::BrowserContext* browser_context);
+
+ // Appends the set of testing factories that signin::IdentityTestEnvironment
+ // requires to |factories_to_append_to|, which should be the set of testing
+ // factories supplied to TestingProfile (via one of the various mechanisms for
+ // doing so). Prefer the above API if possible, as it is less fragile. This
+ // API is primarily for use in tests that do not create the TestingProfile
+ // internally but rather simply supply the set of TestingFactories to some
+ // external facility (e.g., a superclass).
+ // See CreateProfileForIdentityTestEnvironment() for comments on common
+ // parameters.
+ static void AppendIdentityTestEnvironmentFactories(
+ TestingProfile::TestingFactories* factories_to_append_to);
+
+ // Returns the set of testing factories that signin::IdentityTestEnvironment
+ // requires, which can be useful to configure profiles for services that do
+ // not require any other testing factory than the ones specified in here.
+ static TestingProfile::TestingFactories GetIdentityTestEnvironmentFactories(
+ signin::AccountConsistencyMethod account_consistency =
+ signin::AccountConsistencyMethod::kDisabled);
+
+ // Constructs an adaptor that associates an IdentityTestEnvironment instance
+ // with |profile| via the relevant backing objects. Note that
+ // |profile| must have been configured with the IdentityTestEnvironment
+ // testing factories, either because it was created via
+ // CreateProfileForIdentityTestEnvironment() or because
+ // AppendIdentityTestEnvironmentFactories() was invoked on the set of
+ // factories supplied to it.
+ // |profile| must outlive this object.
+ explicit IdentityTestEnvironmentProfileAdaptor(Profile* profile);
+
+ IdentityTestEnvironmentProfileAdaptor(
+ const IdentityTestEnvironmentProfileAdaptor&) = delete;
+ IdentityTestEnvironmentProfileAdaptor& operator=(
+ const IdentityTestEnvironmentProfileAdaptor&) = delete;
+
+ ~IdentityTestEnvironmentProfileAdaptor() {}
+
+ // Returns the IdentityTestEnvironment associated with this object (and
+ // implicitly with the Profile passed to this object's constructor).
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return &identity_test_env_;
+ }
+
+ private:
+ // Testing factory that creates an IdentityManager
+ // with a FakeProfileOAuth2TokenService.
+ static std::unique_ptr<KeyedService> BuildIdentityManagerForTests(
+ signin::AccountConsistencyMethod account_consistency,
+ content::BrowserContext* context);
+
+ signin::IdentityTestEnvironment identity_test_env_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_
diff --git a/chromium/chrome/browser/signin/investigator_dependency_provider.cc b/chromium/chrome/browser/signin/investigator_dependency_provider.cc
new file mode 100644
index 00000000000..601806d2669
--- /dev/null
+++ b/chromium/chrome/browser/signin/investigator_dependency_provider.cc
@@ -0,0 +1,14 @@
+// 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 "chrome/browser/signin/investigator_dependency_provider.h"
+
+InvestigatorDependencyProvider::InvestigatorDependencyProvider(Profile* profile)
+ : profile_(profile) {}
+
+InvestigatorDependencyProvider::~InvestigatorDependencyProvider() {}
+
+PrefService* InvestigatorDependencyProvider::GetPrefs() {
+ return profile_->GetPrefs();
+}
diff --git a/chromium/chrome/browser/signin/investigator_dependency_provider.h b/chromium/chrome/browser/signin/investigator_dependency_provider.h
new file mode 100644
index 00000000000..199f7bb07a5
--- /dev/null
+++ b/chromium/chrome/browser/signin/investigator_dependency_provider.h
@@ -0,0 +1,33 @@
+// 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_
+#define CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_
+
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/signin_investigator.h"
+
+// This version should work for anything with a profile object, like desktop and
+// Android.
+class InvestigatorDependencyProvider
+ : public SigninInvestigator::DependencyProvider {
+ public:
+ explicit InvestigatorDependencyProvider(Profile* profile);
+
+ InvestigatorDependencyProvider(const InvestigatorDependencyProvider&) =
+ delete;
+ InvestigatorDependencyProvider& operator=(
+ const InvestigatorDependencyProvider&) = delete;
+
+ ~InvestigatorDependencyProvider() override;
+ PrefService* GetPrefs() override;
+
+ private:
+ // Non-owning pointer.
+ raw_ptr<Profile> profile_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_
diff --git a/chromium/chrome/browser/signin/logout_tab_helper.cc b/chromium/chrome/browser/signin/logout_tab_helper.cc
new file mode 100644
index 00000000000..db560b21a69
--- /dev/null
+++ b/chromium/chrome/browser/signin/logout_tab_helper.cc
@@ -0,0 +1,34 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/logout_tab_helper.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(LogoutTabHelper);
+
+LogoutTabHelper::LogoutTabHelper(content::WebContents* web_contents)
+ : content::WebContentsUserData<LogoutTabHelper>(*web_contents),
+ content::WebContentsObserver(web_contents) {}
+
+LogoutTabHelper::~LogoutTabHelper() = default;
+
+void LogoutTabHelper::PrimaryPageChanged(content::Page& page) {
+ if (page.GetMainDocument().IsErrorDocument()) {
+ // Failed to load the logout page, fallback to local signout.
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents()->GetBrowserContext());
+ IdentityManagerFactory::GetForProfile(profile)
+ ->GetAccountsMutator()
+ ->RemoveAllAccounts(signin_metrics::SourceForRefreshTokenOperation::
+ kLogoutTabHelper_PrimaryPageChanged);
+ }
+
+ // Delete this.
+ web_contents()->RemoveUserData(UserDataKey());
+}
diff --git a/chromium/chrome/browser/signin/logout_tab_helper.h b/chromium/chrome/browser/signin/logout_tab_helper.h
new file mode 100644
index 00000000000..4f2974c4568
--- /dev/null
+++ b/chromium/chrome/browser/signin/logout_tab_helper.h
@@ -0,0 +1,36 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_
+
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+
+// Tab helper used for logout tabs. Monitors if the logout tab loaded correctly
+// and fallbacks to local signout in case of failure.
+// Only the first navigation is monitored. Even though the logout page sometimes
+// redirects to the SAML provider through javascript, that second navigation is
+// not monitored. The logout is considered successful if the first navigation
+// succeeds, because the signout headers which cause the tokens to be revoked
+// are there.
+class LogoutTabHelper : public content::WebContentsUserData<LogoutTabHelper>,
+ public content::WebContentsObserver {
+ public:
+ ~LogoutTabHelper() override;
+
+ LogoutTabHelper(const LogoutTabHelper&) = delete;
+ LogoutTabHelper& operator=(const LogoutTabHelper&) = delete;
+
+ private:
+ friend class content::WebContentsUserData<LogoutTabHelper>;
+ explicit LogoutTabHelper(content::WebContents* web_contents);
+
+ // content::WebContentsObserver:
+ void PrimaryPageChanged(content::Page& page) override;
+
+ WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+#endif // CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_
diff --git a/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc b/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc
new file mode 100644
index 00000000000..7717ade215c
--- /dev/null
+++ b/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc
@@ -0,0 +1,24 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/logout_tab_helper.h"
+
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "content/public/test/navigation_simulator.h"
+#include "google_apis/gaia/gaia_urls.h"
+
+class LogoutTabHelperTest : public ChromeRenderViewHostTestHarness {};
+
+TEST_F(LogoutTabHelperTest, SelfDeleteInPrimaryPageChanged) {
+ LogoutTabHelper::CreateForWebContents(web_contents());
+
+ EXPECT_NE(nullptr, LogoutTabHelper::FromWebContents(web_contents()));
+
+ // Load the logout page.
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ web_contents(), GaiaUrls::GetInstance()->service_logout_url());
+
+ // The helper was deleted in PrimaryPageChanged.
+ EXPECT_EQ(nullptr, LogoutTabHelper::FromWebContents(web_contents()));
+}
diff --git a/chromium/chrome/browser/signin/mirror_browsertest.cc b/chromium/chrome/browser/signin/mirror_browsertest.cc
new file mode 100644
index 00000000000..abc65d011e6
--- /dev/null
+++ b/chromium/chrome/browser/signin/mirror_browsertest.cc
@@ -0,0 +1,277 @@
+// Copyright 2019 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 <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/base_switches.h"
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/command_line.h"
+#include "base/path_service.h"
+#include "base/run_loop.h"
+#include "base/task/post_task.h"
+#include "base/test/bind.h"
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/chrome_content_browser_client.h"
+#include "chrome/browser/extensions/api/identity/web_auth_flow.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/google/core/common/google_switches.h"
+#include "components/network_session_configurator/common/network_switches.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/dice_header_helper.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "content/public/common/content_client.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/request_handler_util.h"
+#include "third_party/blink/public/common/loader/url_loader_throttle.h"
+
+namespace {
+
+// A delegate to insert a user generated X-Chrome-Connected header
+// to a specifict URL.
+class HeaderModifyingThrottle : public blink::URLLoaderThrottle {
+ public:
+ HeaderModifyingThrottle() = default;
+
+ HeaderModifyingThrottle(const HeaderModifyingThrottle&) = delete;
+ HeaderModifyingThrottle& operator=(const HeaderModifyingThrottle&) = delete;
+
+ ~HeaderModifyingThrottle() override = default;
+
+ void WillStartRequest(network::ResourceRequest* request,
+ bool* defer) override {
+ request->headers.SetHeader(signin::kChromeConnectedHeader, "User Data");
+ }
+};
+
+class ThrottleContentBrowserClient : public ChromeContentBrowserClient {
+ public:
+ explicit ThrottleContentBrowserClient(const GURL& watch_url)
+ : watch_url_(watch_url) {}
+
+ ThrottleContentBrowserClient(const ThrottleContentBrowserClient&) = delete;
+ ThrottleContentBrowserClient& operator=(const ThrottleContentBrowserClient&) =
+ delete;
+
+ ~ThrottleContentBrowserClient() override = default;
+
+ // ContentBrowserClient overrides:
+ std::vector<std::unique_ptr<blink::URLLoaderThrottle>>
+ CreateURLLoaderThrottles(
+ const network::ResourceRequest& request,
+ content::BrowserContext* browser_context,
+ const base::RepeatingCallback<content::WebContents*()>& wc_getter,
+ content::NavigationUIData* navigation_ui_data,
+ int frame_tree_node_id) override {
+ std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles;
+ if (request.url == watch_url_)
+ throttles.push_back(std::make_unique<HeaderModifyingThrottle>());
+ return throttles;
+ }
+
+ private:
+ const GURL watch_url_;
+};
+
+// Subclass of DiceManageAccountBrowserTest with Mirror enabled.
+class MirrorBrowserTest : public InProcessBrowserTest {
+ protected:
+ void RunExtensionConsentTest(extensions::WebAuthFlow::Partition partition,
+ bool expects_header) {
+ net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server.AddDefaultHandlers(GetChromeTestDataDir());
+ const std::string kAuthPath = "/auth";
+ net::test_server::HttpRequest::HeaderMap headers;
+ base::RunLoop run_loop;
+ https_server.RegisterRequestMonitor(base::BindLambdaForTesting(
+ [&](const net::test_server::HttpRequest& request) {
+ if (request.GetURL().path() != kAuthPath)
+ return;
+
+ headers = request.headers;
+ run_loop.Quit();
+ }));
+ ASSERT_TRUE(https_server.Start());
+
+ auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>(
+ nullptr, browser()->profile(),
+ https_server.GetURL("google.com", kAuthPath),
+ extensions::WebAuthFlow::INTERACTIVE, partition);
+
+ web_auth_flow->Start();
+ run_loop.Run();
+ EXPECT_EQ(!!headers.count(signin::kChromeConnectedHeader), expects_header);
+
+ web_auth_flow.release()->DetachDelegateAndDelete();
+ base::RunLoop().RunUntilIdle();
+ }
+
+ private:
+ void SetUpOnMainThread() override {
+ // The test makes requests to google.com and other domains which we want to
+ // redirect to the test server.
+ host_resolver()->AddRule("*", "127.0.0.1");
+ }
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ // HTTPS server only serves a valid cert for localhost, so this is needed to
+ // load pages from "www.google.com" without an interstitial.
+ command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
+
+ // The production code only allows known ports (80 for http and 443 for
+ // https), but the test server runs on a random port.
+ command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers);
+ }
+};
+
+// Verify the following items:
+// 1- X-Chrome-Connected is appended on Google domains if account
+// consistency is enabled and access is secure.
+// 2- The header is stripped in case a request is redirected from a Gooogle
+// domain to non-google domain.
+// 3- The header is NOT stripped in case it is added directly by the page
+// and not because it was on a secure Google domain.
+// This is a regression test for crbug.com/588492.
+IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, MirrorRequestHeader) {
+ browser()->profile()->GetPrefs()->SetString(prefs::kGoogleServicesAccountId,
+ "account_id");
+
+ base::Lock lock;
+ // Map from the path of the URLs that test server sees to the request header.
+ // This is the path, and not URL, because the requests use different domains
+ // which the mock HostResolver converts to 127.0.0.1.
+ std::map<std::string, net::test_server::HttpRequest::HeaderMap> header_map;
+ embedded_test_server()->RegisterRequestMonitor(base::BindLambdaForTesting(
+ [&](const net::test_server::HttpRequest& request) {
+ base::AutoLock auto_lock(lock);
+ header_map[request.GetURL().path()] = request.headers;
+ }));
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server.AddDefaultHandlers(GetChromeTestDataDir());
+ https_server.RegisterRequestMonitor(base::BindLambdaForTesting(
+ [&](const net::test_server::HttpRequest& request) {
+ base::AutoLock auto_lock(lock);
+ header_map[request.GetURL().path()] = request.headers;
+ }));
+ ASSERT_TRUE(https_server.Start());
+
+ base::FilePath root_http;
+ base::PathService::Get(chrome::DIR_TEST_DATA, &root_http);
+ root_http = root_http.AppendASCII("mirror_request_header");
+
+ struct TestCase {
+ GURL original_url; // The URL from which the request begins.
+ // The path to which navigation is redirected.
+ std::string redirected_to_path;
+ bool inject_header; // Should X-Chrome-Connected header be injected to the
+ // original request.
+ bool original_url_expects_header; // Expectation: The header should be
+ // visible in original URL.
+ bool redirected_to_url_expects_header; // Expectation: The header should be
+ // visible in redirected URL.
+ };
+
+ std::vector<TestCase> all_tests;
+
+ // Neither should have the header.
+ // Note we need to replace the port of the redirect's URL.
+ base::StringPairs replacement_text;
+ replacement_text.push_back(std::make_pair(
+ "{{PORT}}", base::NumberToString(embedded_test_server()->port())));
+ std::string replacement_path = net::test_server::GetFilePathWithReplacements(
+ "/mirror_request_header/http.www.google.com.html", replacement_text);
+ all_tests.push_back(
+ {embedded_test_server()->GetURL("www.google.com", replacement_path),
+ "/simple.html", false, false, false});
+
+ // First one adds the header and transfers it to the second.
+ replacement_path = net::test_server::GetFilePathWithReplacements(
+ "/mirror_request_header/http.www.header_adder.com.html",
+ replacement_text);
+ all_tests.push_back(
+ {embedded_test_server()->GetURL("www.header_adder.com", replacement_path),
+ "/simple.html", true, true, true});
+
+ // First one should have the header, but not transfered to second one.
+ replacement_text.clear();
+ replacement_text.push_back(
+ std::make_pair("{{PORT}}", base::NumberToString(https_server.port())));
+ replacement_path = net::test_server::GetFilePathWithReplacements(
+ "/mirror_request_header/https.www.google.com.html", replacement_text);
+ all_tests.push_back({https_server.GetURL("www.google.com", replacement_path),
+ "/simple.html", false, true, false});
+
+ for (const auto& test_case : all_tests) {
+ SCOPED_TRACE(test_case.original_url);
+
+ // If test case requires adding header for the first url add a throttle.
+ ThrottleContentBrowserClient browser_client(test_case.original_url);
+ content::ContentBrowserClient* old_browser_client = nullptr;
+ if (test_case.inject_header)
+ old_browser_client = content::SetBrowserClientForTesting(&browser_client);
+
+ // Navigate to first url.
+ ASSERT_TRUE(
+ ui_test_utils::NavigateToURL(browser(), test_case.original_url));
+
+ if (test_case.inject_header)
+ content::SetBrowserClientForTesting(old_browser_client);
+
+ base::AutoLock auto_lock(lock);
+
+ // Check if header exists and X-Chrome-Connected is correctly provided.
+ ASSERT_EQ(1u, header_map.count(test_case.original_url.path()));
+ if (test_case.original_url_expects_header) {
+ ASSERT_TRUE(header_map[test_case.original_url.path()].count(
+ signin::kChromeConnectedHeader));
+ } else {
+ ASSERT_FALSE(header_map[test_case.original_url.path()].count(
+ signin::kChromeConnectedHeader));
+ }
+
+ ASSERT_EQ(1u, header_map.count(test_case.redirected_to_path));
+ if (test_case.redirected_to_url_expects_header) {
+ ASSERT_TRUE(header_map[test_case.redirected_to_path].count(
+ signin::kChromeConnectedHeader));
+ } else {
+ ASSERT_FALSE(header_map[test_case.redirected_to_path].count(
+ signin::kChromeConnectedHeader));
+ }
+
+ header_map.clear();
+ }
+}
+
+// Verifies that requests originated from chrome.identity.launchWebAuthFlow()
+// API don't have Mirror headers attached.
+// This is a regression test for crbug.com/1077504.
+IN_PROC_BROWSER_TEST_F(MirrorBrowserTest,
+ NoMirrorExtensionConsent_LaunchWebAuthFlow) {
+ RunExtensionConsentTest(extensions::WebAuthFlow::LAUNCH_WEB_AUTH_FLOW, false);
+}
+
+// Verifies that requests originated from chrome.identity.getAuthToken()
+// API have Mirror headers attached.
+IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, MirrorExtensionConsent_GetAuthToken) {
+ RunExtensionConsentTest(extensions::WebAuthFlow::GET_AUTH_TOKEN, true);
+}
+
+} // namespace
diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc
new file mode 100644
index 00000000000..5669e179b48
--- /dev/null
+++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc
@@ -0,0 +1,146 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/process_dice_header_delegate_impl.h"
+
+#include <utility>
+
+#include "base/callback.h"
+#include "base/logging.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/dice_tab_helper.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
+#include "chrome/common/url_constants.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/web_contents.h"
+#include "url/gurl.h"
+
+namespace {
+
+void RedirectToNtp(content::WebContents* contents) {
+ VLOG(1) << "RedirectToNtp";
+ contents->GetController().LoadURL(
+ GURL(chrome::kChromeUINewTabURL), content::Referrer(),
+ ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string());
+}
+
+// Helper function similar to DiceTabHelper::FromWebContents(), but also handles
+// the case where |contents| is nullptr.
+DiceTabHelper* GetDiceTabHelperFromWebContents(content::WebContents* contents) {
+ if (!contents)
+ return nullptr;
+ return DiceTabHelper::FromWebContents(contents);
+}
+
+} // namespace
+
+ProcessDiceHeaderDelegateImpl::ProcessDiceHeaderDelegateImpl(
+ content::WebContents* web_contents,
+ EnableSyncCallback enable_sync_callback,
+ ShowSigninErrorCallback show_signin_error_callback)
+ : web_contents_(web_contents->GetWeakPtr()),
+ profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())),
+ enable_sync_callback_(std::move(enable_sync_callback)),
+ show_signin_error_callback_(std::move(show_signin_error_callback)) {
+ DCHECK(profile_);
+
+ DiceTabHelper* tab_helper = DiceTabHelper::FromWebContents(web_contents);
+ if (tab_helper) {
+ is_sync_signin_tab_ = tab_helper->IsSyncSigninInProgress();
+ redirect_url_ = tab_helper->redirect_url();
+ access_point_ = tab_helper->signin_access_point();
+ promo_action_ = tab_helper->signin_promo_action();
+ reason_ = tab_helper->signin_reason();
+ }
+}
+
+ProcessDiceHeaderDelegateImpl::~ProcessDiceHeaderDelegateImpl() = default;
+
+bool ProcessDiceHeaderDelegateImpl::ShouldEnableSync() {
+ if (IdentityManagerFactory::GetForProfile(profile_)->HasPrimaryAccount(
+ signin::ConsentLevel::kSync)) {
+ VLOG(1) << "Do not start sync after web sign-in [already authenticated].";
+ return false;
+ }
+
+ if (!is_sync_signin_tab_) {
+ VLOG(1)
+ << "Do not start sync after web sign-in [not a Chrome sign-in tab].";
+ return false;
+ }
+
+ return true;
+}
+
+void ProcessDiceHeaderDelegateImpl::HandleTokenExchangeSuccess(
+ CoreAccountId account_id,
+ bool is_new_account) {
+ // is_sync_signin_tab_ tells whether the current signin is happening in a tab
+ // that was opened from a "Enable Sync" Chrome UI. Usually this is indeed a
+ // sync signin, but it is not always the case: the user may abandon the sync
+ // signin and do a simple web signin in the same tab instead.
+ DiceWebSigninInterceptorFactory::GetForProfile(profile_)
+ ->MaybeInterceptWebSignin(web_contents_.get(), account_id, is_new_account,
+ is_sync_signin_tab_);
+}
+
+void ProcessDiceHeaderDelegateImpl::EnableSync(
+ const CoreAccountId& account_id) {
+ DiceTabHelper* tab_helper =
+ GetDiceTabHelperFromWebContents(web_contents_.get());
+ if (tab_helper)
+ tab_helper->OnSyncSigninFlowComplete();
+
+ if (!ShouldEnableSync()) {
+ // No special treatment is needed if the user is not enabling sync.
+ return;
+ }
+
+ content::WebContents* web_contents = web_contents_.get();
+ VLOG(1) << "Start sync after web sign-in.";
+ std::move(enable_sync_callback_)
+ .Run(profile_.get(), access_point_, promo_action_, reason_, web_contents,
+ account_id);
+
+ if (!web_contents)
+ return;
+
+ // After signing in to Chrome, the user should be redirected to the NTP,
+ // unless specified otherwise.
+ if (redirect_url_.is_empty()) {
+ RedirectToNtp(web_contents);
+ return;
+ }
+
+ DCHECK(redirect_url_.is_valid());
+ web_contents->GetController().LoadURL(redirect_url_, content::Referrer(),
+ ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
+ std::string());
+}
+
+void ProcessDiceHeaderDelegateImpl::HandleTokenExchangeFailure(
+ const std::string& email,
+ const GoogleServiceAuthError& error) {
+ DCHECK_NE(GoogleServiceAuthError::NONE, error.state());
+ DiceTabHelper* tab_helper =
+ GetDiceTabHelperFromWebContents(web_contents_.get());
+ if (tab_helper)
+ tab_helper->OnSyncSigninFlowComplete();
+
+ bool should_enable_sync = ShouldEnableSync();
+
+ content::WebContents* web_contents = web_contents_.get();
+ if (should_enable_sync && web_contents)
+ RedirectToNtp(web_contents);
+
+ // Show the error even if the WebContents was closed, because the user may be
+ // signed out of the web.
+ std::move(show_signin_error_callback_)
+ .Run(profile_.get(), web_contents,
+ SigninUIError::FromGoogleServiceAuthError(email, error));
+}
diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h
new file mode 100644
index 00000000000..cd7116700c6
--- /dev/null
+++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h
@@ -0,0 +1,77 @@
+// Copyright 2017 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_
+#define CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_
+
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/signin/dice_response_handler.h"
+
+#include <memory>
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/memory/weak_ptr.h"
+#include "components/signin/public/base/signin_metrics.h"
+
+namespace content {
+class WebContents;
+}
+
+class Profile;
+class SigninUIError;
+
+class ProcessDiceHeaderDelegateImpl : public ProcessDiceHeaderDelegate {
+ public:
+ // Callback starting Sync.
+ using EnableSyncCallback =
+ base::OnceCallback<void(Profile*,
+ signin_metrics::AccessPoint,
+ signin_metrics::PromoAction,
+ signin_metrics::Reason,
+ content::WebContents*,
+ const CoreAccountId&)>;
+
+ // Callback showing a signin error UI.
+ using ShowSigninErrorCallback = base::OnceCallback<
+ void(Profile*, content::WebContents*, const SigninUIError&)>;
+
+ // |is_sync_signin_tab| is true if a sync signin flow has been started in that
+ // tab.
+ ProcessDiceHeaderDelegateImpl(
+ content::WebContents* web_contents,
+ EnableSyncCallback enable_sync_callback,
+ ShowSigninErrorCallback show_signin_error_callback);
+
+ ProcessDiceHeaderDelegateImpl(const ProcessDiceHeaderDelegateImpl&) = delete;
+ ProcessDiceHeaderDelegateImpl& operator=(
+ const ProcessDiceHeaderDelegateImpl&) = delete;
+
+ ~ProcessDiceHeaderDelegateImpl() override;
+
+ // ProcessDiceHeaderDelegate:
+ void HandleTokenExchangeSuccess(CoreAccountId account_id,
+ bool is_new_account) override;
+ void EnableSync(const CoreAccountId& account_id) override;
+ void HandleTokenExchangeFailure(const std::string& email,
+ const GoogleServiceAuthError& error) override;
+
+ private:
+ // Returns true if sync should be enabled after the user signs in.
+ bool ShouldEnableSync();
+
+ const base::WeakPtr<content::WebContents> web_contents_;
+ raw_ptr<Profile> profile_;
+ EnableSyncCallback enable_sync_callback_;
+ ShowSigninErrorCallback show_signin_error_callback_;
+ bool is_sync_signin_tab_ = false;
+ signin_metrics::AccessPoint access_point_ =
+ signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
+ signin_metrics::PromoAction promo_action_ =
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
+ signin_metrics::Reason reason_ = signin_metrics::Reason::kUnknownReason;
+ GURL redirect_url_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_
diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc b/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc
new file mode 100644
index 00000000000..60c842301f9
--- /dev/null
+++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc
@@ -0,0 +1,375 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/process_dice_header_delegate_impl.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/signin/dice_tab_helper.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/navigation_simulator.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using signin_metrics::Reason;
+
+namespace {
+
+signin_metrics::AccessPoint kTestAccessPoint =
+ signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE;
+
+signin_metrics::PromoAction kTestPromoAction =
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
+
+// Dummy delegate that declines all interceptions.
+class TestDiceWebSigninInterceptorDelegate
+ : public DiceWebSigninInterceptor::Delegate {
+ public:
+ ~TestDiceWebSigninInterceptorDelegate() override = default;
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ ShowSigninInterceptionBubble(
+ content::WebContents* web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback) override {
+ std::move(callback).Run(SigninInterceptionResult::kDeclined);
+ return nullptr;
+ }
+
+ void ShowProfileCustomizationBubble(Browser* browser) override {}
+};
+
+class MockDiceWebSigninInterceptor : public DiceWebSigninInterceptor {
+ public:
+ explicit MockDiceWebSigninInterceptor(Profile* profile)
+ : DiceWebSigninInterceptor(
+ profile,
+ std::make_unique<TestDiceWebSigninInterceptorDelegate>()) {}
+ ~MockDiceWebSigninInterceptor() override = default;
+
+ MOCK_METHOD(void,
+ MaybeInterceptWebSignin,
+ (content::WebContents * web_contents,
+ CoreAccountId account_id,
+ bool is_new_account,
+ bool is_sync_signin),
+ (override));
+};
+
+std::unique_ptr<KeyedService> CreateMockDiceWebSigninInterceptor(
+ content::BrowserContext* context) {
+ return std::make_unique<MockDiceWebSigninInterceptor>(
+ Profile::FromBrowserContext(context));
+}
+
+class ProcessDiceHeaderDelegateImplTest
+ : public ChromeRenderViewHostTestHarness {
+ public:
+ ProcessDiceHeaderDelegateImplTest()
+ : enable_sync_called_(false),
+ show_error_called_(false),
+ account_id_("12345"),
+ email_("foo@bar.com"),
+ auth_error_(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) {}
+
+ ~ProcessDiceHeaderDelegateImplTest() override {}
+
+ void AddAccount(bool is_primary) {
+ if (!identity_test_environment_profile_adaptor_)
+ InitializeIdentityTestEnvironment();
+ if (is_primary) {
+ identity_test_environment_profile_adaptor_->identity_test_env()
+ ->SetPrimaryAccount(email_, signin::ConsentLevel::kSync);
+ } else {
+ identity_test_environment_profile_adaptor_->identity_test_env()
+ ->MakeAccountAvailable(email_);
+ }
+ }
+
+ void InitializeIdentityTestEnvironment() {
+ DCHECK(profile());
+ identity_test_environment_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+ }
+
+ // Creates a ProcessDiceHeaderDelegateImpl instance.
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl>
+ CreateDelegateAndNavigateToSignin(
+ bool is_sync_signin_tab,
+ Reason reason = Reason::kSigninPrimaryAccount) {
+ signin_reason_ = reason;
+ if (!identity_test_environment_profile_adaptor_)
+ InitializeIdentityTestEnvironment();
+ // Load the signin page.
+ std::unique_ptr<content::NavigationSimulator> simulator =
+ content::NavigationSimulator::CreateRendererInitiated(signin_url_,
+ main_rfh());
+ simulator->Start();
+ if (is_sync_signin_tab) {
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ dice_tab_helper->InitializeSigninFlow(signin_url_, kTestAccessPoint,
+ signin_reason_, kTestPromoAction,
+ GURL::EmptyGURL());
+ }
+ simulator->Commit();
+ DCHECK_EQ(signin_url_, web_contents()->GetVisibleURL());
+ return std::make_unique<ProcessDiceHeaderDelegateImpl>(
+ web_contents(),
+ base::BindOnce(&ProcessDiceHeaderDelegateImplTest::StartSyncCallback,
+ base::Unretained(this)),
+ base::BindOnce(
+ &ProcessDiceHeaderDelegateImplTest::ShowSigninErrorCallback,
+ base::Unretained(this)));
+ }
+
+ // ChromeRenderViewHostTestHarness:
+ TestingProfile::TestingFactories GetTestingFactories() const override {
+ TestingProfile::TestingFactories factories = {
+ {DiceWebSigninInterceptorFactory::GetInstance(),
+ base::BindRepeating(&CreateMockDiceWebSigninInterceptor)}};
+ IdentityTestEnvironmentProfileAdaptor::
+ AppendIdentityTestEnvironmentFactories(&factories);
+ return factories;
+ }
+
+ void TearDown() override {
+ identity_test_environment_profile_adaptor_.reset();
+ ChromeRenderViewHostTestHarness::TearDown();
+ }
+
+ // Callback for the ProcessDiceHeaderDelegateImpl.
+ void StartSyncCallback(Profile* profile,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::PromoAction promo_action,
+ signin_metrics::Reason reason,
+ content::WebContents* contents,
+ const CoreAccountId& account_id) {
+ EXPECT_EQ(profile, this->profile());
+ EXPECT_EQ(access_point, kTestAccessPoint);
+ EXPECT_EQ(promo_action, kTestPromoAction);
+ EXPECT_EQ(reason, signin_reason_);
+ EXPECT_EQ(web_contents(), contents);
+ EXPECT_EQ(account_id_, account_id);
+ enable_sync_called_ = true;
+ }
+
+ // Callback for the ProcessDiceHeaderDelegateImpl.
+ void ShowSigninErrorCallback(Profile* profile,
+ content::WebContents* contents,
+ const SigninUIError& error) {
+ EXPECT_EQ(profile, this->profile());
+ EXPECT_EQ(web_contents(), contents);
+ EXPECT_EQ(base::UTF8ToUTF16(auth_error_.ToString()), error.message());
+ EXPECT_EQ(base::UTF8ToUTF16(email_), error.email());
+ show_error_called_ = true;
+ }
+
+ MockDiceWebSigninInterceptor* mock_interceptor() {
+ return static_cast<MockDiceWebSigninInterceptor*>(
+ DiceWebSigninInterceptorFactory::GetForProfile(profile()));
+ }
+
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_environment_profile_adaptor_;
+
+ const GURL signin_url_ = GURL("https://accounts.google.com");
+ bool enable_sync_called_;
+ bool show_error_called_;
+ CoreAccountId account_id_;
+ std::string email_;
+ GoogleServiceAuthError auth_error_;
+ Reason signin_reason_ = Reason::kSigninPrimaryAccount;
+};
+
+// Check that sync is enabled if the tab is closed during signin.
+TEST_F(ProcessDiceHeaderDelegateImplTest, CloseTabWhileStartingSync) {
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(true);
+
+ // Close the tab.
+ DeleteContents();
+
+ // Check expectations.
+ delegate->EnableSync(account_id_);
+ EXPECT_TRUE(enable_sync_called_);
+ EXPECT_FALSE(show_error_called_);
+}
+
+// Check that the error is still shown if the tab is closed before the error is
+// received.
+TEST_F(ProcessDiceHeaderDelegateImplTest, CloseTabWhileFailingSignin) {
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(true);
+
+ // Close the tab.
+ DeleteContents();
+
+ // Check expectations.
+ delegate->HandleTokenExchangeFailure(email_, auth_error_);
+ EXPECT_FALSE(enable_sync_called_);
+ EXPECT_TRUE(show_error_called_);
+}
+
+struct TestConfiguration {
+ // Test setup.
+ bool signed_in; // User was already signed in at the start of the flow.
+ bool signin_tab; // The tab is marked as a Sync signin tab.
+
+ // Test expectations.
+ bool callback_called; // The relevant callback was called.
+ bool show_ntp; // The NTP was shown.
+};
+
+TestConfiguration kEnableSyncTestCases[] = {
+ // clang-format off
+ // signed_in | signin_tab | callback_called | show_ntp
+ { false, false, false, false},
+ { false, true, true, true},
+ { true, false, false, false},
+ { true, true, false, false},
+ // clang-format on
+};
+
+// Parameterized version of ProcessDiceHeaderDelegateImplTest.
+class ProcessDiceHeaderDelegateImplTestEnableSync
+ : public ProcessDiceHeaderDelegateImplTest,
+ public ::testing::WithParamInterface<TestConfiguration> {};
+
+// Test the EnableSync() method in all configurations.
+TEST_P(ProcessDiceHeaderDelegateImplTestEnableSync, EnableSync) {
+ if (GetParam().signed_in)
+ AddAccount(/*is_primary=*/true);
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(GetParam().signin_tab);
+ delegate->EnableSync(account_id_);
+ EXPECT_EQ(GetParam().callback_called, enable_sync_called_);
+ GURL expected_url =
+ GetParam().show_ntp ? GURL(chrome::kChromeUINewTabURL) : signin_url_;
+ EXPECT_EQ(expected_url, web_contents()->GetVisibleURL());
+ EXPECT_FALSE(show_error_called_);
+ // Check that the sync signin flow is complete.
+ if (GetParam().signin_tab) {
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ ASSERT_TRUE(dice_tab_helper);
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(All,
+ ProcessDiceHeaderDelegateImplTestEnableSync,
+ ::testing::ValuesIn(kEnableSyncTestCases));
+
+TestConfiguration kHandleTokenExchangeFailureTestCases[] = {
+ // clang-format off
+ // signed_in | signin_tab | callback_called | show_ntp
+ { false, false, true, false},
+ { false, true, true, true},
+ { true, false, true, false},
+ { true, true, true, false},
+ // clang-format on
+};
+
+// Parameterized version of ProcessDiceHeaderDelegateImplTest.
+class ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure
+ : public ProcessDiceHeaderDelegateImplTest,
+ public ::testing::WithParamInterface<TestConfiguration> {};
+
+// Test the HandleTokenExchangeFailure() method in all configurations.
+TEST_P(ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure,
+ HandleTokenExchangeFailure) {
+ if (GetParam().signed_in)
+ AddAccount(/*is_primary=*/true);
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(GetParam().signin_tab);
+ delegate->HandleTokenExchangeFailure(email_, auth_error_);
+ EXPECT_FALSE(enable_sync_called_);
+ EXPECT_EQ(GetParam().callback_called, show_error_called_);
+ GURL expected_url =
+ GetParam().show_ntp ? GURL(chrome::kChromeUINewTabURL) : signin_url_;
+ EXPECT_EQ(expected_url, web_contents()->GetVisibleURL());
+ // Check that the sync signin flow is complete.
+ if (GetParam().signin_tab) {
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ ASSERT_TRUE(dice_tab_helper);
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ All,
+ ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure,
+ ::testing::ValuesIn(kHandleTokenExchangeFailureTestCases));
+
+struct TokenExchangeSuccessConfiguration {
+ bool is_reauth; // User was already signed in with the account.
+ bool signin_tab; // A DiceTabHelper is attached to the tab.
+ Reason reason;
+ bool sync_signin; // Expected value for the MaybeInterceptWebSigin call.
+};
+
+TokenExchangeSuccessConfiguration kHandleTokenExchangeSuccessTestCases[] = {
+ // clang-format off
+ // is_reauth | signin_tab | reason    | sync_signin
+ { false, false, Reason::kSigninPrimaryAccount, false },
+ { false, true, Reason::kSigninPrimaryAccount, true },
+ { false, true, Reason::kAddSecondaryAccount, false },
+ { true, false, Reason::kSigninPrimaryAccount, false },
+ { true, true, Reason::kSigninPrimaryAccount, true },
+ // clang-format on
+};
+
+// Parameterized version of ProcessDiceHeaderDelegateImplTest.
+class ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess
+ : public ProcessDiceHeaderDelegateImplTest,
+ public ::testing::WithParamInterface<TokenExchangeSuccessConfiguration> {
+};
+
+// Test the HandleTokenExchangeSuccess() method in all configurations.
+TEST_P(ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess,
+ HandleTokenExchangeSuccess) {
+ if (GetParam().is_reauth)
+ AddAccount(/*is_primary=*/false);
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(GetParam().signin_tab,
+ GetParam().reason);
+ EXPECT_CALL(
+ *mock_interceptor(),
+ MaybeInterceptWebSignin(web_contents(), account_id_,
+ !GetParam().is_reauth, GetParam().sync_signin));
+ delegate->HandleTokenExchangeSuccess(account_id_, !GetParam().is_reauth);
+
+ // Check that the sync signin flow is complete.
+ if (GetParam().signin_tab) {
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ ASSERT_TRUE(dice_tab_helper);
+ EXPECT_EQ(GetParam().sync_signin,
+ dice_tab_helper->IsSyncSigninInProgress());
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ All,
+ ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess,
+ ::testing::ValuesIn(kHandleTokenExchangeSuccessTestCases));
+
+} // namespace
diff --git a/chromium/chrome/browser/signin/reauth_result.h b/chromium/chrome/browser/signin/reauth_result.h
new file mode 100644
index 00000000000..9aa85bed81c
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_result.h
@@ -0,0 +1,38 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_
+#define CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_
+
+namespace signin {
+
+// Indicates the result of the Gaia Reauth flow.
+// Needs to be kept in sync with "SigninReauthResult" in enums.xml.
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class ReauthResult {
+ // The user was successfully re-authenticated.
+ kSuccess = 0,
+
+ // The user account is not signed in.
+ kAccountNotSignedIn = 1,
+
+ // The user dismissed the reauth prompt.
+ kDismissedByUser = 2,
+
+ // The reauth page failed to load.
+ kLoadFailed = 3,
+
+ // A caller canceled the reauth flow.
+ kCancelled = 4,
+
+ // An unexpected response was received from Gaia.
+ kUnexpectedResponse = 5,
+
+ kMaxValue = kUnexpectedResponse,
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_
diff --git a/chromium/chrome/browser/signin/reauth_tab_helper.cc b/chromium/chrome/browser/signin/reauth_tab_helper.cc
new file mode 100644
index 00000000000..e8bd1d8b1c8
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_tab_helper.cc
@@ -0,0 +1,96 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/reauth_tab_helper.h"
+#include "base/memory/ptr_util.h"
+#include "chrome/browser/signin/reauth_result.h"
+#include "content/public/browser/navigation_handle.h"
+#include "net/http/http_status_code.h"
+#include "url/origin.h"
+
+namespace signin {
+
+namespace {
+
+bool IsExpectedResponseCode(int response_code) {
+ return response_code == net::HTTP_OK || response_code == net::HTTP_NO_CONTENT;
+}
+
+} // namespace
+
+// static
+void ReauthTabHelper::CreateForWebContents(content::WebContents* web_contents,
+ const GURL& reauth_url,
+ ReauthCallback callback) {
+ DCHECK(web_contents);
+ if (!FromWebContents(web_contents)) {
+ web_contents->SetUserData(
+ UserDataKey(), base::WrapUnique(new ReauthTabHelper(
+ web_contents, reauth_url, std::move(callback))));
+ } else {
+ std::move(callback).Run(signin::ReauthResult::kCancelled);
+ }
+}
+
+ReauthTabHelper::~ReauthTabHelper() = default;
+
+void ReauthTabHelper::CompleteReauth(signin::ReauthResult result) {
+ if (callback_)
+ std::move(callback_).Run(result);
+}
+
+void ReauthTabHelper::DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) {
+ if (!navigation_handle->IsInPrimaryMainFrame())
+ return;
+
+ is_within_reauth_origin_ &=
+ url::IsSameOriginWith(reauth_url_, navigation_handle->GetURL());
+
+ if (navigation_handle->IsErrorPage()) {
+ has_last_committed_error_page_ = true;
+ return;
+ }
+
+ has_last_committed_error_page_ = false;
+
+ GURL::Replacements replacements;
+ replacements.ClearQuery();
+ GURL url_without_query =
+ navigation_handle->GetURL().ReplaceComponents(replacements);
+ if (url_without_query != reauth_url_)
+ return;
+
+ if (!navigation_handle->GetResponseHeaders() ||
+ !IsExpectedResponseCode(
+ navigation_handle->GetResponseHeaders()->response_code())) {
+ CompleteReauth(signin::ReauthResult::kUnexpectedResponse);
+ }
+
+ CompleteReauth(signin::ReauthResult::kSuccess);
+}
+
+void ReauthTabHelper::WebContentsDestroyed() {
+ CompleteReauth(signin::ReauthResult::kDismissedByUser);
+}
+
+bool ReauthTabHelper::is_within_reauth_origin() {
+ return is_within_reauth_origin_;
+}
+
+bool ReauthTabHelper::has_last_committed_error_page() {
+ return has_last_committed_error_page_;
+}
+
+ReauthTabHelper::ReauthTabHelper(content::WebContents* web_contents,
+ const GURL& reauth_url,
+ ReauthCallback callback)
+ : content::WebContentsUserData<ReauthTabHelper>(*web_contents),
+ content::WebContentsObserver(web_contents),
+ reauth_url_(reauth_url),
+ callback_(std::move(callback)) {}
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(ReauthTabHelper);
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/reauth_tab_helper.h b/chromium/chrome/browser/signin/reauth_tab_helper.h
new file mode 100644
index 00000000000..f830c24caec
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_tab_helper.h
@@ -0,0 +1,66 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_
+
+#include "base/callback.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+#include "url/gurl.h"
+
+namespace signin {
+
+enum class ReauthResult;
+
+// Tab helper class observing navigations within the reauth flow and notifying
+// a caller about a flow result.
+class ReauthTabHelper : public content::WebContentsUserData<ReauthTabHelper>,
+ public content::WebContentsObserver {
+ public:
+ using ReauthCallback = base::OnceCallback<void(signin::ReauthResult)>;
+
+ // Creates a new ReauthTabHelper and attaches it to |web_contents|. If an
+ // instance is already attached, no replacement happens, just notifies the
+ // caller by invoking |callback| with signin::ReauthResult::kCancelled.
+ // Initializes a helper with:
+ // - |callback| to be called when the reauth flow is complete.
+ // - |reauth_url| that should be the final destination of the reauth flow.
+ static void CreateForWebContents(content::WebContents* web_contents,
+ const GURL& reauth_url,
+ ReauthCallback callback);
+
+ ReauthTabHelper(const ReauthTabHelper&) = delete;
+ ReauthTabHelper& operator=(const ReauthTabHelper&) = delete;
+
+ ~ReauthTabHelper() override;
+
+ // If |callback_| is not null, calls |callback_| with |result|.
+ void CompleteReauth(signin::ReauthResult result);
+
+ // content::WebContentsObserver
+ void DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) override;
+ void WebContentsDestroyed() override;
+
+ bool is_within_reauth_origin();
+ bool has_last_committed_error_page();
+
+ private:
+ friend class content::WebContentsUserData<ReauthTabHelper>;
+ explicit ReauthTabHelper(content::WebContents* web_contents,
+ const GURL& reauth_url,
+ ReauthCallback callback);
+
+ const GURL reauth_url_;
+ ReauthCallback callback_;
+ bool is_within_reauth_origin_ = true;
+ bool has_last_committed_error_page_ = false;
+
+ WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_
diff --git a/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc b/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc
new file mode 100644
index 00000000000..a17078cb067
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc
@@ -0,0 +1,184 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/reauth_tab_helper.h"
+
+#include "base/memory/raw_ptr.h"
+#include "base/test/mock_callback.h"
+#include "chrome/browser/signin/reauth_result.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/web_contents_tester.h"
+#include "net/base/net_errors.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
+
+namespace signin {
+
+class ReauthTabHelperTest : public ChromeRenderViewHostTestHarness {
+ public:
+ ReauthTabHelperTest()
+ : reauth_url_("https://my-identity_provider.com/reauth") {}
+
+ void SetUp() override {
+ ChromeRenderViewHostTestHarness::SetUp();
+
+ ReauthTabHelper::CreateForWebContents(web_contents(), reauth_url(),
+ mock_callback_.Get());
+ tab_helper_ = ReauthTabHelper::FromWebContents(web_contents());
+ }
+
+ ReauthTabHelper* tab_helper() { return tab_helper_; }
+
+ base::MockOnceCallback<void(signin::ReauthResult)>* mock_callback() {
+ return &mock_callback_;
+ }
+
+ const GURL& reauth_url() { return reauth_url_; }
+
+ private:
+ raw_ptr<ReauthTabHelper> tab_helper_ = nullptr;
+ base::MockOnceCallback<void(signin::ReauthResult)> mock_callback_;
+ const GURL reauth_url_;
+};
+
+// Tests a direct call to CompleteReauth().
+TEST_F(ReauthTabHelperTest, CompleteReauth) {
+ signin::ReauthResult result = signin::ReauthResult::kSuccess;
+ EXPECT_CALL(*mock_callback(), Run(result));
+ tab_helper()->CompleteReauth(result);
+}
+
+// Tests a successful navigation to the reauth URL.
+TEST_F(ReauthTabHelperTest, NavigateToReauthURL) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator->Commit();
+}
+
+// Tests the reauth flow when the reauth URL has query parameters.
+TEST_F(ReauthTabHelperTest, NavigateToReauthURLWithQuery) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url().Resolve("?rapt=35be36ae"), web_contents());
+ simulator->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator->Commit();
+}
+
+// Tests the reauth flow with multiple navigations within the same origin.
+TEST_F(ReauthTabHelperTest, MultipleNavigationReauth) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ simulator->Redirect(
+ reauth_url().DeprecatedGetOriginAsURL().Resolve("/login"));
+ simulator->Commit();
+
+ auto simulator2 = content::NavigationSimulator::CreateRendererInitiated(
+ reauth_url(), main_rfh());
+ simulator2->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator2->Commit();
+}
+
+// Tests the reauth flow with multiple navigations across two different origins.
+// TODO(https://crbug.com/1045515): update this test once navigations outside of
+// reauth_url() are blocked.
+TEST_F(ReauthTabHelperTest, MultipleNavigationReauthThroughExternalOrigin) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ simulator->Redirect(GURL("https://other-identity-provider.com/login"));
+ simulator->Commit();
+
+ auto simulator2 = content::NavigationSimulator::CreateRendererInitiated(
+ reauth_url(), main_rfh());
+ simulator2->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator2->Commit();
+}
+
+// Tests a failed navigation to the reauth URL, followed by a successful
+// navigation.
+TEST_F(ReauthTabHelperTest, NavigationToReauthURLFailed) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ simulator->Fail(net::ERR_TIMED_OUT);
+ simulator->CommitErrorPage();
+ EXPECT_TRUE(tab_helper()->has_last_committed_error_page());
+ // Check that the navigation still counts as within the same origin.
+ EXPECT_TRUE(tab_helper()->is_within_reauth_origin());
+
+ auto simulator2 = content::NavigationSimulator::CreateRendererInitiated(
+ reauth_url(), main_rfh());
+ simulator2->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator2->Commit();
+ EXPECT_FALSE(tab_helper()->has_last_committed_error_page());
+}
+
+// Tests a failed navigation redirecting to an external origin, followed by a
+// successful navigation.
+TEST_F(ReauthTabHelperTest, NavigationToExternalOriginFailed) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ simulator->Redirect(GURL("https://other-identity-provider.com/login"));
+ simulator->Fail(net::ERR_TIMED_OUT);
+ simulator->CommitErrorPage();
+ EXPECT_TRUE(tab_helper()->has_last_committed_error_page());
+ // Check that the navigation doesn't count as within the same origin.
+ EXPECT_FALSE(tab_helper()->is_within_reauth_origin());
+
+ auto simulator2 = content::NavigationSimulator::CreateRendererInitiated(
+ reauth_url(), main_rfh());
+ simulator2->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator2->Commit();
+ EXPECT_FALSE(tab_helper()->has_last_committed_error_page());
+ EXPECT_FALSE(tab_helper()->is_within_reauth_origin());
+}
+
+// Tests the WebContents deletion.
+TEST_F(ReauthTabHelperTest, WebContentsDestroyed) {
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kDismissedByUser));
+ DeleteContents();
+}
+
+class ReauthTabHelperPrerenderTest : public ReauthTabHelperTest {
+ public:
+ ReauthTabHelperPrerenderTest() {
+ feature_list_.InitWithFeatures(
+ {blink::features::kPrerender2},
+ // Disable the memory requirement of Prerender2 so the test can run on
+ // any bot.
+ {blink::features::kPrerender2MemoryControls});
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(ReauthTabHelperPrerenderTest,
+ PrerenderDoesNotAffectLastCommittedErrorPage) {
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+ reauth_url());
+ EXPECT_FALSE(tab_helper()->has_last_committed_error_page());
+
+ // Fail prerendering navigation.
+ const GURL prerender_url = reauth_url().Resolve("?prerendering");
+ auto simulator = content::WebContentsTester::For(web_contents())
+ ->AddPrerenderAndStartNavigation(prerender_url);
+ simulator->Fail(net::ERR_TIMED_OUT);
+ simulator->CommitErrorPage();
+
+ // has_last_committed_error_page_ is not updated by preredering.
+ EXPECT_FALSE(tab_helper()->has_last_committed_error_page());
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/reauth_util.cc b/chromium/chrome/browser/signin/reauth_util.cc
new file mode 100644
index 00000000000..b770da50fb0
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_util.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 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 <string>
+
+#include "base/strings/string_number_conversions.h"
+#include "chrome/browser/signin/reauth_util.h"
+#include "chrome/common/webui_url_constants.h"
+#include "net/base/url_util.h"
+
+namespace signin {
+
+GURL GetReauthConfirmationURL(signin_metrics::ReauthAccessPoint access_point) {
+ GURL url = GURL(chrome::kChromeUISigninReauthURL);
+ url = net::AppendQueryParameter(
+ url, "access_point",
+ base::NumberToString(static_cast<int>(access_point)));
+ return url;
+}
+
+signin_metrics::ReauthAccessPoint GetReauthAccessPointForReauthConfirmationURL(
+ const GURL& url) {
+ std::string value;
+ if (!net::GetValueForKeyInQuery(url, "access_point", &value))
+ return signin_metrics::ReauthAccessPoint::kUnknown;
+
+ int access_point = -1;
+ base::StringToInt(value, &access_point);
+ if (access_point <=
+ static_cast<int>(signin_metrics::ReauthAccessPoint::kUnknown) ||
+ access_point >
+ static_cast<int>(signin_metrics::ReauthAccessPoint::kMaxValue)) {
+ return signin_metrics::ReauthAccessPoint::kUnknown;
+ }
+
+ return static_cast<signin_metrics::ReauthAccessPoint>(access_point);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/reauth_util.h b/chromium/chrome/browser/signin/reauth_util.h
new file mode 100644
index 00000000000..52150ee53f1
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_util.h
@@ -0,0 +1,24 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_
+
+#include "components/signin/public/base/signin_metrics.h"
+#include "url/gurl.h"
+
+namespace signin {
+
+// Returns a URL to display in the reauth confirmation dialog. The dialog was
+// triggered by |access_point|.
+GURL GetReauthConfirmationURL(signin_metrics::ReauthAccessPoint access_point);
+
+// Returns ReauthAccessPoint encoded in the query of the reauth confirmation
+// URL.
+signin_metrics::ReauthAccessPoint GetReauthAccessPointForReauthConfirmationURL(
+ const GURL& url);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_
diff --git a/chromium/chrome/browser/signin/reauth_util_unittest.cc b/chromium/chrome/browser/signin/reauth_util_unittest.cc
new file mode 100644
index 00000000000..44fb952465d
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_util_unittest.cc
@@ -0,0 +1,33 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/reauth_util.h"
+
+#include "chrome/common/webui_url_constants.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace signin {
+
+class ReauthUtilURLTest : public ::testing::TestWithParam<int> {};
+
+TEST_P(ReauthUtilURLTest, GetAndParseReauthConfirmationURL) {
+ auto access_point =
+ static_cast<signin_metrics::ReauthAccessPoint>(GetParam());
+ GURL url = GetReauthConfirmationURL(access_point);
+ ASSERT_TRUE(url.is_valid());
+ EXPECT_EQ(url.host(), chrome::kChromeUISigninReauthHost);
+ signin_metrics::ReauthAccessPoint get_access_point =
+ GetReauthAccessPointForReauthConfirmationURL(url);
+ EXPECT_EQ(get_access_point, access_point);
+}
+
+INSTANTIATE_TEST_CASE_P(
+ AllAccessPoints,
+ ReauthUtilURLTest,
+ ::testing::Range(
+ static_cast<int>(signin_metrics::ReauthAccessPoint::kUnknown),
+ static_cast<int>(signin_metrics::ReauthAccessPoint::kMaxValue) + 1));
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/remove_local_account_browsertest.cc b/chromium/chrome/browser/signin/remove_local_account_browsertest.cc
new file mode 100644
index 00000000000..259f3d2857e
--- /dev/null
+++ b/chromium/chrome/browser/signin/remove_local_account_browsertest.cc
@@ -0,0 +1,143 @@
+// Copyright 2021 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 <memory>
+
+#include "base/run_loop.h"
+#include "base/test/bind.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_tabstrip.h"
+#include "chrome/test/base/mixin_based_in_process_browser_test.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/signin/public/identity_manager/test_identity_manager_observer.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/fake_gaia.h"
+#include "google_apis/gaia/gaia_switches.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/login/test/network_portal_detector_mixin.h"
+#endif
+
+namespace {
+
+using testing::Contains;
+using testing::Not;
+
+MATCHER_P(ListedAccountMatchesGaiaId, gaia_id, "") {
+ return arg.gaia_id == std::string(gaia_id);
+}
+
+const char kTestGaiaId[] = "123";
+
+class RemoveLocalAccountTest : public MixinBasedInProcessBrowserTest {
+ protected:
+ RemoveLocalAccountTest()
+ : embedded_test_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
+ embedded_test_server_.RegisterRequestHandler(base::BindRepeating(
+ &FakeGaia::HandleRequest, base::Unretained(&fake_gaia_)));
+ }
+
+ ~RemoveLocalAccountTest() override = default;
+
+ signin::IdentityManager* identity_manager() {
+ return IdentityManagerFactory::GetForProfile(browser()->profile());
+ }
+
+ signin::AccountsInCookieJarInfo WaitUntilAccountsInCookieUpdated() {
+ signin::TestIdentityManagerObserver observer(identity_manager());
+ base::RunLoop run_loop;
+ observer.SetOnAccountsInCookieUpdatedCallback(run_loop.QuitClosure());
+ run_loop.Run();
+ return observer.AccountsInfoFromAccountsInCookieUpdatedCallback();
+ }
+
+ // MixinBasedInProcessBrowserTest:
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ MixinBasedInProcessBrowserTest::SetUpCommandLine(command_line);
+ ASSERT_TRUE(embedded_test_server_.InitializeAndListen());
+ const GURL base_url = embedded_test_server_.base_url();
+ command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec());
+ }
+
+ void SetUpOnMainThread() override {
+ MixinBasedInProcessBrowserTest::SetUpOnMainThread();
+ fake_gaia_.Initialize();
+
+ FakeGaia::MergeSessionParams params;
+ params.signed_out_gaia_ids.push_back(kTestGaiaId);
+ fake_gaia_.UpdateMergeSessionParams(params);
+
+ embedded_test_server_.StartAcceptingConnections();
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // ChromeSigninClient uses chromeos::DelayNetworkCall() which requires
+ // simulating being online.
+ network_portal_detector_.SimulateDefaultNetworkState(
+ ash::NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_ONLINE);
+#endif
+ }
+
+ FakeGaia fake_gaia_;
+ net::EmbeddedTestServer embedded_test_server_;
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ ash::NetworkPortalDetectorMixin network_portal_detector_{&mixin_host_};
+#endif
+};
+
+IN_PROC_BROWSER_TEST_F(RemoveLocalAccountTest, ShouldNotifyObservers) {
+ // To enforce an initial ListAccounts fetch and the corresponding notification
+ // to observers, make the current list as stale. This is done for the purpose
+ // of documenting assertions on the AccountsInCookieJarInfo passed to
+ // observers during notification.
+ signin::SetFreshnessOfAccountsInGaiaCookie(identity_manager(),
+ /*accounts_are_fresh=*/false);
+
+ ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh);
+ const signin::AccountsInCookieJarInfo
+ cookie_jar_info_in_initial_notification =
+ WaitUntilAccountsInCookieUpdated();
+ ASSERT_TRUE(cookie_jar_info_in_initial_notification.accounts_are_fresh);
+ ASSERT_THAT(cookie_jar_info_in_initial_notification.signed_out_accounts,
+ Contains(ListedAccountMatchesGaiaId(kTestGaiaId)));
+
+ const signin::AccountsInCookieJarInfo initial_cookie_jar_info =
+ identity_manager()->GetAccountsInCookieJar();
+ ASSERT_TRUE(initial_cookie_jar_info.accounts_are_fresh);
+ ASSERT_THAT(initial_cookie_jar_info.signed_out_accounts,
+ Contains(ListedAccountMatchesGaiaId(kTestGaiaId)));
+
+ // Open a FakeGaia page that issues the desired HTTP response header with
+ // Google-Accounts-RemoveLocalAccount.
+ chrome::AddTabAt(browser(),
+ fake_gaia_.GetDummyRemoveLocalAccountURL(kTestGaiaId),
+ /*index=*/0,
+ /*foreground=*/true);
+
+ // Wait until observers are notified with OnAccountsInCookieUpdated().
+ const signin::AccountsInCookieJarInfo
+ cookie_jar_info_in_updated_notification =
+ WaitUntilAccountsInCookieUpdated();
+
+ EXPECT_TRUE(cookie_jar_info_in_updated_notification.accounts_are_fresh);
+ EXPECT_THAT(cookie_jar_info_in_updated_notification.signed_out_accounts,
+ Not(Contains(ListedAccountMatchesGaiaId(kTestGaiaId))));
+
+ const signin::AccountsInCookieJarInfo updated_cookie_jar_info =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(updated_cookie_jar_info.accounts_are_fresh);
+ EXPECT_THAT(updated_cookie_jar_info.signed_out_accounts,
+ Not(Contains(ListedAccountMatchesGaiaId(kTestGaiaId))));
+}
+
+} // namespace
diff --git a/chromium/chrome/browser/signin/services/DIR_METADATA b/chromium/chrome/browser/signin/services/DIR_METADATA
new file mode 100644
index 00000000000..7a2580a646c
--- /dev/null
+++ b/chromium/chrome/browser/signin/services/DIR_METADATA
@@ -0,0 +1 @@
+os: ANDROID
diff --git a/chromium/chrome/browser/signin/services/OWNERS b/chromium/chrome/browser/signin/services/OWNERS
new file mode 100644
index 00000000000..1c49383244f
--- /dev/null
+++ b/chromium/chrome/browser/signin/services/OWNERS
@@ -0,0 +1,2 @@
+bsazonov@chromium.org
+aliceywang@chromium.org
diff --git a/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java
new file mode 100644
index 00000000000..f64eb974942
--- /dev/null
+++ b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java
@@ -0,0 +1,120 @@
+// Copyright 2021 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.
+
+package org.chromium.chrome.browser.signin.services;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+import org.robolectric.RuntimeEnvironment;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.JniMocker;
+import org.chromium.chrome.R;
+import org.chromium.chrome.test.util.browser.signin.AccountManagerTestRule;
+import org.chromium.components.signin.base.AccountInfo;
+import org.chromium.components.signin.base.CoreAccountId;
+import org.chromium.components.signin.identitymanager.AccountInfoServiceProvider;
+import org.chromium.components.signin.identitymanager.AccountTrackerService;
+import org.chromium.components.signin.identitymanager.IdentityManager;
+import org.chromium.components.signin.identitymanager.IdentityManagerJni;
+
+/**
+ * Unit tests for {@link ProfileDataCache}
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+public class ProfileDataCacheUnitTest {
+ private static final long NATIVE_IDENTITY_MANAGER = 10001L;
+ private static final String ACCOUNT_EMAIL = "test@gmail.com";
+
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+ @Rule
+ public final JniMocker mocker = new JniMocker();
+
+ @Rule
+ public final AccountManagerTestRule mAccountManagerTestRule = new AccountManagerTestRule();
+
+ @Mock
+ private AccountTrackerService mAccountTrackerServiceMock;
+
+ @Mock
+ private IdentityManager.Natives mIdentityManagerNativeMock;
+
+ @Mock
+ private ProfileDataCache.Observer mObserverMock;
+
+ private final IdentityManager mIdentityManager =
+ IdentityManager.create(NATIVE_IDENTITY_MANAGER, null /* OAuth2TokenService */);
+
+ private ProfileDataCache mProfileDataCache;
+
+ @Before
+ public void setUp() {
+ mocker.mock(IdentityManagerJni.TEST_HOOKS, mIdentityManagerNativeMock);
+ mProfileDataCache = ProfileDataCache.createWithDefaultImageSizeAndNoBadge(
+ RuntimeEnvironment.application.getApplicationContext());
+
+ // Add an observer for IdentityManager::onExtendedAccountInfoUpdated.
+ mAccountManagerTestRule.observeIdentityManager(mIdentityManager);
+ }
+
+ @After
+ public void tearDown() {
+ AccountInfoServiceProvider.resetForTests();
+ }
+
+ @Test
+ public void accountInfoIsUpdatedWithOnlyFullName() {
+ final String fullName = "full name1";
+ final AccountInfo accountInfo = new AccountInfo(new CoreAccountId("gaia-id-test"),
+ ACCOUNT_EMAIL, "gaia-id-test", fullName, null, null);
+ mProfileDataCache.addObserver(mObserverMock);
+ Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ Assert.assertNull(mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getFullName());
+
+ mIdentityManager.onExtendedAccountInfoUpdated(accountInfo);
+
+ Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ Assert.assertEquals(
+ fullName, mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getFullName());
+ }
+
+ @Test
+ public void accountInfoIsUpdatedWithOnlyGivenName() {
+ final String givenName = "given name1";
+ final AccountInfo accountInfo = new AccountInfo(new CoreAccountId("gaia-id-test"),
+ ACCOUNT_EMAIL, "gaia-id-test", null, givenName, null);
+ mProfileDataCache.addObserver(mObserverMock);
+ Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ Assert.assertNull(mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getGivenName());
+
+ mIdentityManager.onExtendedAccountInfoUpdated(accountInfo);
+
+ Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ Assert.assertEquals(
+ givenName, mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getGivenName());
+ }
+
+ @Test
+ public void accountInfoIsUpdatedWithOnlyBadgeConfig() {
+ mProfileDataCache.setBadge(R.drawable.ic_sync_badge_error_20dp);
+ final AccountInfo accountInfo = new AccountInfo(
+ new CoreAccountId("gaia-id-test"), ACCOUNT_EMAIL, "gaia-id-test", null, null, null);
+ mProfileDataCache.addObserver(mObserverMock);
+ Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+
+ mIdentityManager.onExtendedAccountInfoUpdated(accountInfo);
+
+ Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ }
+}
diff --git a/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java
new file mode 100644
index 00000000000..8acec65e1e9
--- /dev/null
+++ b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java
@@ -0,0 +1,89 @@
+// Copyright 2020 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.
+
+package org.chromium.chrome.browser.signin.services;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.JniMocker;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.components.signin.base.CoreAccountInfo;
+import org.chromium.components.signin.base.GoogleServiceAuthError;
+import org.chromium.components.signin.base.GoogleServiceAuthError.State;
+
+/**
+ * Unit tests for {@link WebSigninBridge}.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+public class WebSigninBridgeTest {
+ private static final CoreAccountInfo CORE_ACCOUNT_INFO =
+ CoreAccountInfo.createFromEmailAndGaiaId("user@domain.com", "gaia-id-user");
+ private static final long NATIVE_WEB_SIGNIN_BRIDGE = 1000L;
+
+ @Rule
+ public final JniMocker mocker = new JniMocker();
+
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock
+ private WebSigninBridge.Natives mNativeMock;
+
+ @Mock
+ private Profile mProfileMock;
+
+ @Mock
+ private WebSigninBridge.Listener mListenerMock;
+
+ private final WebSigninBridge.Factory mFactory = new WebSigninBridge.Factory();
+
+ @Before
+ public void setUp() {
+ mocker.mock(WebSigninBridgeJni.TEST_HOOKS, mNativeMock);
+ when(mNativeMock.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock))
+ .thenReturn(NATIVE_WEB_SIGNIN_BRIDGE);
+ }
+
+ @Test
+ public void testFactoryCreate() {
+ WebSigninBridge webSigninBridge =
+ mFactory.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock);
+ Assert.assertNotNull("Factory#create should not return null!", webSigninBridge);
+ verify(mNativeMock).create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock);
+ }
+
+ @Test
+ public void testDestroy() {
+ mFactory.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock).destroy();
+ verify(mNativeMock).destroy(NATIVE_WEB_SIGNIN_BRIDGE);
+ }
+
+ @Test
+ public void testOnSigninSucceed() {
+ WebSigninBridge.onSigninSucceeded(mListenerMock);
+ verify(mListenerMock).onSigninSucceeded();
+ verify(mListenerMock, never()).onSigninFailed(any());
+ }
+
+ @Test
+ public void testOnSigninFailed() {
+ final GoogleServiceAuthError error = new GoogleServiceAuthError(State.CONNECTION_FAILED);
+ WebSigninBridge.onSigninFailed(mListenerMock, error);
+ verify(mListenerMock).onSigninFailed(error);
+ verify(mListenerMock, never()).onSigninSucceeded();
+ }
+}
diff --git a/chromium/chrome/browser/signin/signin_error_controller_factory.cc b/chromium/chrome/browser/signin/signin_error_controller_factory.cc
new file mode 100644
index 00000000000..ebbd0612ba9
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_error_controller_factory.cc
@@ -0,0 +1,48 @@
+// 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 "chrome/browser/signin/signin_error_controller_factory.h"
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+SigninErrorControllerFactory::SigninErrorControllerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninErrorController",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SigninErrorControllerFactory::~SigninErrorControllerFactory() {}
+
+// static
+SigninErrorController* SigninErrorControllerFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<SigninErrorController*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+SigninErrorControllerFactory* SigninErrorControllerFactory::GetInstance() {
+ return base::Singleton<SigninErrorControllerFactory>::get();
+}
+
+KeyedService* SigninErrorControllerFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ SigninErrorController::AccountMode account_mode =
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ SigninErrorController::AccountMode::ANY_ACCOUNT;
+#else
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile)
+ ? SigninErrorController::AccountMode::ANY_ACCOUNT
+ : SigninErrorController::AccountMode::PRIMARY_ACCOUNT;
+#endif
+ return new SigninErrorController(
+ account_mode, IdentityManagerFactory::GetForProfile(profile));
+}
diff --git a/chromium/chrome/browser/signin/signin_error_controller_factory.h b/chromium/chrome/browser/signin/signin_error_controller_factory.h
new file mode 100644
index 00000000000..0d87f49799e
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_error_controller_factory.h
@@ -0,0 +1,37 @@
+// 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+#include "components/signin/core/browser/signin_error_controller.h"
+
+class Profile;
+
+// Singleton that owns all SigninErrorControllers and associates them with
+// Profiles.
+class SigninErrorControllerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of SigninErrorController associated with this profile
+ // (creating one if none exists). Returns NULL if this profile cannot have an
+ // SigninClient (for example, if |profile| is incognito).
+ static SigninErrorController* GetForProfile(Profile* profile);
+
+ // Returns an instance of the factory singleton.
+ static SigninErrorControllerFactory* GetInstance();
+
+ private:
+ friend struct base::DefaultSingletonTraits<SigninErrorControllerFactory>;
+
+ SigninErrorControllerFactory();
+ ~SigninErrorControllerFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_features.cc b/chromium/chrome/browser/signin/signin_features.cc
new file mode 100644
index 00000000000..b1932809890
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_features.cc
@@ -0,0 +1,16 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_features.h"
+
+// Enables the client-side processing of the HTTP response header
+// Google-Accounts-RemoveLocalAccount.
+const base::Feature kProcessGaiaRemoveLocalAccountHeader{
+ "ProcessGaiaRemoveLocalAccountHeader", base::FEATURE_ENABLED_BY_DEFAULT};
+
+// Allows policies to be loaded on a managed account without activating sync.
+// Uses enterprise confirmation dialog for managed accounts signin outside of
+// the profile picker.
+const base::Feature kAccountPoliciesLoadedWithoutSync{
+ "AccountPoliciesLoadedWithoutSync", base::FEATURE_DISABLED_BY_DEFAULT};
diff --git a/chromium/chrome/browser/signin/signin_features.h b/chromium/chrome/browser/signin/signin_features.h
new file mode 100644
index 00000000000..47c61c21949
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_features.h
@@ -0,0 +1,15 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_
+
+#include "base/feature_list.h"
+#include "build/chromeos_buildflags.h"
+
+extern const base::Feature kProcessGaiaRemoveLocalAccountHeader;
+
+extern const base::Feature kAccountPoliciesLoadedWithoutSync;
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_
diff --git a/chromium/chrome/browser/signin/signin_global_error.cc b/chromium/chrome/browser/signin/signin_global_error.cc
new file mode 100644
index 00000000000..d02d7ad0158
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error.cc
@@ -0,0 +1,170 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_global_error.h"
+
+#include "base/logging.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/chrome_pages.h"
+#include "chrome/browser/ui/global_error/global_error_service.h"
+#include "chrome/browser/ui/global_error/global_error_service_factory.h"
+#include "chrome/browser/ui/singleton_tabs.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/grit/chromium_strings.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "net/base/url_util.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/signin/signin_promo.h"
+#endif
+
+SigninGlobalError::SigninGlobalError(
+ SigninErrorController* error_controller,
+ Profile* profile)
+ : profile_(profile),
+ error_controller_(error_controller) {
+ error_controller_->AddObserver(this);
+}
+
+SigninGlobalError::~SigninGlobalError() {
+ DCHECK(!error_controller_)
+ << "SigninGlobalError::Shutdown() was not called";
+}
+
+bool SigninGlobalError::HasError() {
+ return HasMenuItem();
+}
+
+void SigninGlobalError::Shutdown() {
+ error_controller_->RemoveObserver(this);
+ error_controller_ = nullptr;
+}
+
+bool SigninGlobalError::HasMenuItem() {
+ return error_controller_->HasError();
+}
+
+int SigninGlobalError::MenuItemCommandID() {
+ return IDC_SHOW_SIGNIN_ERROR;
+}
+
+std::u16string SigninGlobalError::MenuItemLabel() {
+ // Notify the user if there's an auth error the user should know about.
+ if (error_controller_->HasError())
+ return l10n_util::GetStringUTF16(IDS_SYNC_SIGN_IN_ERROR_WRENCH_MENU_ITEM);
+ return std::u16string();
+}
+
+void SigninGlobalError::ExecuteMenuItem(Browser* browser) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (error_controller_->auth_error().state() !=
+ GoogleServiceAuthError::NONE) {
+ DVLOG(1) << "Signing out the user to fix a sync error.";
+ // TODO(beng): seems like this could just call chrome::AttemptUserExit().
+ chrome::ExecuteCommand(browser, IDC_EXIT);
+ return;
+ }
+#endif
+
+ // Global errors don't show up in the wrench menu on mobile.
+#if !defined(OS_ANDROID)
+ LoginUIService* login_ui = LoginUIServiceFactory::GetForProfile(profile_);
+ if (login_ui->current_login_ui()) {
+ login_ui->current_login_ui()->FocusUI();
+ return;
+ }
+
+ browser->window()->ShowAvatarBubbleFromAvatarButton(
+ BrowserWindow::AVATAR_BUBBLE_MODE_REAUTH,
+ signin_metrics::AccessPoint::ACCESS_POINT_MENU, false);
+#endif
+}
+
+bool SigninGlobalError::HasBubbleView() {
+ return !GetBubbleViewMessages().empty();
+}
+
+std::u16string SigninGlobalError::GetBubbleViewTitle() {
+ return l10n_util::GetStringUTF16(IDS_SIGNIN_ERROR_BUBBLE_VIEW_TITLE);
+}
+
+std::vector<std::u16string> SigninGlobalError::GetBubbleViewMessages() {
+ std::vector<std::u16string> messages;
+
+ // If the user isn't signed in, no need to display an error bubble.
+ auto* identity_manager =
+ IdentityManagerFactory::GetForProfileIfExists(profile_);
+ if (identity_manager &&
+ !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ return messages;
+ }
+
+ if (!error_controller_->HasError())
+ return messages;
+
+ switch (error_controller_->auth_error().state()) {
+ // TODO(rogerta): use account id in error messages.
+
+ // User credentials are invalid (bad acct, etc).
+ case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS:
+ case GoogleServiceAuthError::SERVICE_ERROR:
+ messages.push_back(l10n_util::GetStringUTF16(
+ IDS_SYNC_SIGN_IN_ERROR_BUBBLE_VIEW_MESSAGE));
+ break;
+
+ // Sync service is not available for this account's domain.
+ case GoogleServiceAuthError::SERVICE_UNAVAILABLE:
+ messages.push_back(l10n_util::GetStringUTF16(
+ IDS_SYNC_UNAVAILABLE_ERROR_BUBBLE_VIEW_MESSAGE));
+ break;
+
+ // Generic message for "other" errors.
+ default:
+ messages.push_back(l10n_util::GetStringUTF16(
+ IDS_SYNC_OTHER_SIGN_IN_ERROR_BUBBLE_VIEW_MESSAGE));
+ }
+ return messages;
+}
+
+std::u16string SigninGlobalError::GetBubbleViewAcceptButtonLabel() {
+ // If the auth service is unavailable, don't give the user the option to try
+ // signing in again.
+ if (error_controller_->auth_error().state() ==
+ GoogleServiceAuthError::SERVICE_UNAVAILABLE) {
+ return l10n_util::GetStringUTF16(
+ IDS_SYNC_UNAVAILABLE_ERROR_BUBBLE_VIEW_ACCEPT);
+ } else {
+ return l10n_util::GetStringUTF16(IDS_SYNC_SIGN_IN_ERROR_BUBBLE_VIEW_ACCEPT);
+ }
+}
+
+std::u16string SigninGlobalError::GetBubbleViewCancelButtonLabel() {
+ return std::u16string();
+}
+
+void SigninGlobalError::OnBubbleViewDidClose(Browser* browser) {
+}
+
+void SigninGlobalError::BubbleViewAcceptButtonPressed(Browser* browser) {
+ ExecuteMenuItem(browser);
+}
+
+void SigninGlobalError::BubbleViewCancelButtonPressed(Browser* browser) {
+ NOTREACHED();
+}
+
+void SigninGlobalError::OnErrorChanged() {
+ GlobalErrorServiceFactory::GetForProfile(profile_)->NotifyErrorsChanged();
+}
diff --git a/chromium/chrome/browser/signin/signin_global_error.h b/chromium/chrome/browser/signin/signin_global_error.h
new file mode 100644
index 00000000000..50e110ea0bc
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error.h
@@ -0,0 +1,64 @@
+// Copyright (c) 2013 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_
+
+#include <set>
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/ui/global_error/global_error.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/signin/core/browser/signin_error_controller.h"
+
+class Profile;
+
+// Shows auth errors on the wrench menu using a bubble view and a menu item.
+class SigninGlobalError : public GlobalErrorWithStandardBubble,
+ public SigninErrorController::Observer,
+ public KeyedService {
+ public:
+ SigninGlobalError(SigninErrorController* error_controller,
+ Profile* profile);
+
+ SigninGlobalError(const SigninGlobalError&) = delete;
+ SigninGlobalError& operator=(const SigninGlobalError&) = delete;
+
+ ~SigninGlobalError() override;
+
+ // Returns true if there is an authentication error.
+ bool HasError();
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(SigninGlobalErrorTest, Basic);
+ FRIEND_TEST_ALL_PREFIXES(SigninGlobalErrorTest, AuthStatusEnumerateAllErrors);
+
+ // KeyedService:
+ void Shutdown() override;
+
+ // GlobalErrorWithStandardBubble:
+ bool HasMenuItem() override;
+ int MenuItemCommandID() override;
+ std::u16string MenuItemLabel() override;
+ void ExecuteMenuItem(Browser* browser) override;
+ bool HasBubbleView() override;
+ std::u16string GetBubbleViewTitle() override;
+ std::vector<std::u16string> GetBubbleViewMessages() override;
+ std::u16string GetBubbleViewAcceptButtonLabel() override;
+ std::u16string GetBubbleViewCancelButtonLabel() override;
+ void OnBubbleViewDidClose(Browser* browser) override;
+ void BubbleViewAcceptButtonPressed(Browser* browser) override;
+ void BubbleViewCancelButtonPressed(Browser* browser) override;
+
+ // SigninErrorController::Observer:
+ void OnErrorChanged() override;
+
+ // The Profile this service belongs to.
+ raw_ptr<Profile> profile_;
+
+ // The SigninErrorController that provides auth status.
+ raw_ptr<SigninErrorController> error_controller_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_
diff --git a/chromium/chrome/browser/signin/signin_global_error_factory.cc b/chromium/chrome/browser/signin/signin_global_error_factory.cc
new file mode 100644
index 00000000000..304d6a59cce
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error_factory.cc
@@ -0,0 +1,46 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_global_error_factory.h"
+
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/signin_error_controller_factory.h"
+#include "chrome/browser/signin/signin_global_error.h"
+#include "chrome/browser/ui/global_error/global_error_service_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+SigninGlobalErrorFactory::SigninGlobalErrorFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninGlobalError",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(SigninErrorControllerFactory::GetInstance());
+ DependsOn(GlobalErrorServiceFactory::GetInstance());
+}
+
+SigninGlobalErrorFactory::~SigninGlobalErrorFactory() {}
+
+// static
+SigninGlobalError* SigninGlobalErrorFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<SigninGlobalError*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+SigninGlobalErrorFactory* SigninGlobalErrorFactory::GetInstance() {
+ return base::Singleton<SigninGlobalErrorFactory>::get();
+}
+
+KeyedService* SigninGlobalErrorFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ return nullptr;
+#endif
+
+ Profile* profile = static_cast<Profile*>(context);
+ return new SigninGlobalError(
+ SigninErrorControllerFactory::GetForProfile(profile), profile);
+}
diff --git a/chromium/chrome/browser/signin/signin_global_error_factory.h b/chromium/chrome/browser/signin/signin_global_error_factory.h
new file mode 100644
index 00000000000..d13b4f6df61
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error_factory.h
@@ -0,0 +1,40 @@
+// Copyright 2014 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class SigninGlobalError;
+class Profile;
+
+// Singleton that owns all SigninGlobalErrors and associates them with
+// Profiles. Listens for the Profile's destruction notification and cleans up
+// the associated SigninGlobalError.
+class SigninGlobalErrorFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of SigninGlobalError associated with this
+ // profile, creating one if none exists. In Ash, this will return NULL.
+ static SigninGlobalError* GetForProfile(Profile* profile);
+
+ // Returns an instance of the SigninGlobalErrorFactory singleton.
+ static SigninGlobalErrorFactory* GetInstance();
+
+ SigninGlobalErrorFactory(const SigninGlobalErrorFactory&) = delete;
+ SigninGlobalErrorFactory& operator=(const SigninGlobalErrorFactory&) = delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<SigninGlobalErrorFactory>;
+
+ SigninGlobalErrorFactory();
+ ~SigninGlobalErrorFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_global_error_unittest.cc b/chromium/chrome/browser/signin/signin_global_error_unittest.cc
new file mode 100644
index 00000000000..8d4f5f66eb6
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error_unittest.cc
@@ -0,0 +1,166 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_global_error.h"
+
+#include <stddef.h>
+
+#include <memory>
+#include <string>
+
+#include "base/bind.h"
+#include "base/cxx17_backports.h"
+#include "base/memory/raw_ptr.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_metrics.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/signin/signin_error_controller_factory.h"
+#include "chrome/browser/signin/signin_global_error_factory.h"
+#include "chrome/browser/ui/global_error/global_error_service.h"
+#include "chrome/browser/ui/global_error/global_error_service_factory.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/signin_error_controller.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/sync_preferences/pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+static const char kTestEmail[] = "testuser@test.com";
+static const char16_t kTestEmail16[] = u"testuser@test.com";
+
+class SigninGlobalErrorTest : public testing::Test {
+ public:
+ SigninGlobalErrorTest() :
+ profile_manager_(TestingBrowserProcess::GetGlobal()) {}
+
+ void SetUp() override {
+ ASSERT_TRUE(profile_manager_.SetUp());
+
+ // Create a signed-in profile.
+ TestingProfile::TestingFactories testing_factories =
+ IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories();
+
+ profile_ = profile_manager_.CreateTestingProfile(
+ "Person 1", std::unique_ptr<sync_preferences::PrefServiceSyncable>(),
+ u"Person 1", 0, std::string(), std::move(testing_factories));
+
+ identity_test_env_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+
+ AccountInfo account_info =
+ identity_test_env_profile_adaptor_->identity_test_env()
+ ->MakePrimaryAccountAvailable(kTestEmail,
+ signin::ConsentLevel::kSync);
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile()->GetPath());
+ ASSERT_NE(entry, nullptr);
+
+ entry->SetAuthInfo(account_info.gaia, kTestEmail16,
+ /*is_consented_primary_account=*/true);
+
+ global_error_ = SigninGlobalErrorFactory::GetForProfile(profile());
+ error_controller_ = SigninErrorControllerFactory::GetForProfile(profile());
+ }
+
+ TestingProfile* profile() { return profile_; }
+ TestingProfileManager* testing_profile_manager() {
+ return &profile_manager_;
+ }
+
+ SigninGlobalError* global_error() { return global_error_; }
+ SigninErrorController* error_controller() { return error_controller_; }
+
+ void SetAuthError(GoogleServiceAuthError::State state) {
+ signin::IdentityTestEnvironment* identity_test_env =
+ identity_test_env_profile_adaptor_->identity_test_env();
+ CoreAccountId primary_account_id =
+ identity_test_env->identity_manager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync);
+
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ identity_test_env->identity_manager(), primary_account_id,
+ GoogleServiceAuthError(state));
+ }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfileManager profile_manager_;
+ raw_ptr<TestingProfile> profile_;
+
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_env_profile_adaptor_;
+
+ raw_ptr<SigninGlobalError> global_error_;
+ raw_ptr<SigninErrorController> error_controller_;
+};
+
+TEST_F(SigninGlobalErrorTest, Basic) {
+ ASSERT_FALSE(global_error()->HasMenuItem());
+
+ SetAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
+ EXPECT_TRUE(global_error()->HasMenuItem());
+
+ SetAuthError(GoogleServiceAuthError::NONE);
+ EXPECT_FALSE(global_error()->HasMenuItem());
+}
+
+// Verify that SigninGlobalError ignores certain errors.
+TEST_F(SigninGlobalErrorTest, AuthStatusEnumerateAllErrors) {
+ typedef struct {
+ GoogleServiceAuthError::State error_state;
+ bool is_error;
+ } ErrorTableEntry;
+
+ ErrorTableEntry table[] = {
+ {GoogleServiceAuthError::NONE, false},
+ {GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, true},
+ {GoogleServiceAuthError::USER_NOT_SIGNED_UP, true},
+ {GoogleServiceAuthError::CONNECTION_FAILED, false},
+ {GoogleServiceAuthError::SERVICE_UNAVAILABLE, false},
+ {GoogleServiceAuthError::REQUEST_CANCELED, false},
+ {GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE, true},
+ {GoogleServiceAuthError::SERVICE_ERROR, true},
+ };
+ static_assert(
+ base::size(table) == GoogleServiceAuthError::NUM_STATES -
+ GoogleServiceAuthError::kDeprecatedStateCount,
+ "table size should match number of auth error types");
+
+ // Mark the profile with an active timestamp so profile_metrics logs it.
+ testing_profile_manager()->UpdateLastUser(profile());
+
+ for (ErrorTableEntry entry : table) {
+ SetAuthError(GoogleServiceAuthError::NONE);
+
+ base::HistogramTester histogram_tester;
+ SetAuthError(entry.error_state);
+
+ EXPECT_EQ(global_error()->HasMenuItem(), entry.is_error);
+ EXPECT_EQ(global_error()->MenuItemLabel().empty(), !entry.is_error);
+ EXPECT_EQ(global_error()->GetBubbleViewMessages().empty(), !entry.is_error);
+ EXPECT_FALSE(global_error()->GetBubbleViewTitle().empty());
+ EXPECT_FALSE(global_error()->GetBubbleViewAcceptButtonLabel().empty());
+ EXPECT_TRUE(global_error()->GetBubbleViewCancelButtonLabel().empty());
+
+ ProfileMetrics::LogNumberOfProfiles(&testing_profile_manager()
+ ->profile_manager()
+ ->GetProfileAttributesStorage());
+
+ if (entry.is_error) {
+ histogram_tester.ExpectBucketCount("Signin.AuthError", entry.error_state,
+ 1);
+ }
+ }
+}
diff --git a/chromium/chrome/browser/signin/signin_manager.cc b/chromium/chrome/browser/signin/signin_manager.cc
new file mode 100644
index 00000000000..9cc73e0e49e
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager.cc
@@ -0,0 +1,200 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_manager.h"
+
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+
+SigninManager::SigninManager(PrefService* prefs,
+ signin::IdentityManager* identity_manager)
+ : prefs_(prefs), identity_manager_(identity_manager) {
+ signin_allowed_.Init(
+ prefs::kSigninAllowed, prefs_,
+ base::BindRepeating(&SigninManager::OnSigninAllowedPrefChanged,
+ base::Unretained(this)));
+
+ UpdateUnconsentedPrimaryAccount();
+ identity_manager_->AddObserver(this);
+}
+
+SigninManager::~SigninManager() {
+ identity_manager_->RemoveObserver(this);
+}
+
+void SigninManager::UpdateUnconsentedPrimaryAccount() {
+ // Only update the unconsented primary account only after accounts are loaded.
+ if (!identity_manager_->AreRefreshTokensLoaded()) {
+ return;
+ }
+
+ absl::optional<CoreAccountInfo> account =
+ ComputeUnconsentedPrimaryAccountInfo();
+
+ DCHECK(!account || !account->IsEmpty());
+ if (account) {
+ if (identity_manager_->GetPrimaryAccountInfo(
+ signin::ConsentLevel::kSignin) != account) {
+ DCHECK(
+ !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount(
+ account->account_id, signin::ConsentLevel::kSignin);
+ }
+ } else if (identity_manager_->HasPrimaryAccount(
+ signin::ConsentLevel::kSignin)) {
+ DCHECK(!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ identity_manager_->GetPrimaryAccountMutator()->ClearPrimaryAccount(
+ signin_metrics::USER_DELETED_ACCOUNT_COOKIES,
+ signin_metrics::SignoutDelete::kIgnoreMetric);
+ }
+}
+
+absl::optional<CoreAccountInfo>
+SigninManager::ComputeUnconsentedPrimaryAccountInfo() const {
+ DCHECK(identity_manager_->AreRefreshTokensLoaded());
+
+ // UPA is equal to the primary account with sync consent if it exists.
+ if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ return identity_manager_->GetPrimaryAccountInfo(
+ signin::ConsentLevel::kSync);
+ }
+
+ // Clearing the primary sync account when sign-in is not allowed is handled
+ // by PrimaryAccountPolicyManager. That flow is extremely hard to follow
+ // especially for the case when the user is syncing with a managed account
+ // as in that case the whole profile needs to be deleted.
+ //
+ // It was considered simpler to keep the logic to update the unconsented
+ // primary account in a single place.
+ if (!signin_allowed_.GetValue())
+ return absl::nullopt;
+
+ signin::AccountsInCookieJarInfo cookie_info =
+ identity_manager_->GetAccountsInCookieJar();
+
+ std::vector<gaia::ListedAccount> cookie_accounts =
+ cookie_info.signed_in_accounts;
+
+ // Fresh cookies and loaded tokens are needed to compute the UPA.
+ if (cookie_info.accounts_are_fresh) {
+ // Cookies are fresh and tokens are loaded, UPA is the first account
+ // in cookies if it exists and has a refresh token.
+ if (cookie_accounts.empty()) {
+ // Cookies are empty, the UPA is empty.
+ return absl::nullopt;
+ }
+
+ AccountInfo account_info =
+ identity_manager_->FindExtendedAccountInfoByAccountId(
+ cookie_accounts[0].id);
+
+ // Verify the first account in cookies has a refresh token that is valid.
+ bool error_state =
+ account_info.IsEmpty() ||
+ identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id);
+
+ return error_state ? absl::nullopt
+ : absl::make_optional<CoreAccountInfo>(account_info);
+ }
+
+ if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin))
+ return absl::nullopt;
+
+ // If cookies or tokens are not loaded, it is not possible to fully compute
+ // the unconsented primary account. However, if the current unconsented
+ // primary account is no longer valid, it has to be removed.
+ CoreAccountId current_account =
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSignin);
+
+ if (!identity_manager_->HasAccountWithRefreshToken(current_account)) {
+ // Tokens are loaded, but the current UPA doesn't have a refresh token.
+ // Clear the current UPA.
+ return absl::nullopt;
+ }
+
+ if (cookie_info.accounts_are_fresh) {
+ if (cookie_accounts.empty() || cookie_accounts[0].id != current_account) {
+ // The current UPA is not the first in fresh cookies. It needs to be
+ // cleared.
+ return absl::nullopt;
+ }
+ }
+
+ // No indication that the current UPA is invalid, return current UPA.
+ return identity_manager_->GetPrimaryAccountInfo(
+ signin::ConsentLevel::kSignin);
+}
+
+// signin::IdentityManager::Observer implementation.
+void SigninManager::OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event_details) {
+ // This is needed for the case where the user chooses to start syncing
+ // with an account that is different from the unconsented primary account
+ // (not the first in cookies) but then cancels. In that case, the tokens stay
+ // the same. In all the other cases, either the token will be revoked which
+ // will trigger an update for the unconsented primary account or the
+ // primary account stays the same but the sync consent is revoked.
+ if (event_details.GetEventTypeFor(signin::ConsentLevel::kSync) !=
+ signin::PrimaryAccountChangeEvent::Type::kCleared) {
+ return;
+ }
+
+ // It is important to update the primary account after all observers process
+ // the current OnPrimaryAccountChanged() as all observers should see the same
+ // value for the unconsented primary account. Schedule the potential update
+ // on the next run loop.
+ base::SequencedTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(&SigninManager::UpdateUnconsentedPrimaryAccount,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void SigninManager::OnRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info) {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnRefreshTokenRemovedForAccount(
+ const CoreAccountId& account_id) {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnRefreshTokensLoaded() {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnAccountsInCookieUpdated(
+ const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+ const GoogleServiceAuthError& error) {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnAccountsCookieDeletedByUserAction() {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnErrorStateOfRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info,
+ const GoogleServiceAuthError& error) {
+ CoreAccountInfo current_account =
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+
+ bool should_update = false;
+ if (error == GoogleServiceAuthError::AuthErrorNone()) {
+ should_update = current_account.IsEmpty();
+ } else {
+ // In error state, update if the account in error is the current UPA.
+ should_update = (account_info == current_account);
+ }
+
+ if (should_update)
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnSigninAllowedPrefChanged() {
+ UpdateUnconsentedPrimaryAccount();
+}
diff --git a/chromium/chrome/browser/signin/signin_manager.h b/chromium/chrome/browser/signin/signin_manager.h
new file mode 100644
index 00000000000..2feaf8dc737
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager.h
@@ -0,0 +1,71 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/prefs/pref_member.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+class PrefService;
+
+class SigninManager : public KeyedService,
+ public signin::IdentityManager::Observer {
+ public:
+ SigninManager(PrefService* prefs, signin::IdentityManager* identity_manger);
+ SigninManager(const SigninManager&) = delete;
+ SigninManager& operator=(const SigninManager&) = delete;
+
+ ~SigninManager() override;
+
+ private:
+ // Updates the cached version of unconsented primary account and notifies the
+ // observers if there is any change.
+ void UpdateUnconsentedPrimaryAccount();
+
+ // Computes and returns the unconsented primary account (UPA).
+ // - If a primary account with sync consent exists, the UPA is equal to it.
+ // - The UPA is the first account in cookies and must have a refresh token.
+ // For the UPA to be computed, it needs fresh cookies and tokens to be loaded.
+ // - If tokens are not loaded or cookies are not fresh, the UPA can't be
+ // computed but if one already exists it might be invalid. That can happen if
+ // cookies are fresh but are empty or the first account is different than the
+ // current UPA, the other cases are if tokens are not loaded but the current
+ // UPA's refresh token has been rekoved or tokens are loaded but the current
+ // UPA does not have a refresh token. If the UPA is invalid, it needs to be
+ // cleared, |absl::nullopt| is returned. If it is still valid, returns the
+ // valid UPA.
+ absl::optional<CoreAccountInfo> ComputeUnconsentedPrimaryAccountInfo() const;
+
+ // signin::IdentityManager::Observer implementation.
+ void OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event_details) override;
+ void OnRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info) override;
+ void OnRefreshTokenRemovedForAccount(
+ const CoreAccountId& account_id) override;
+ void OnRefreshTokensLoaded() override;
+ void OnAccountsInCookieUpdated(
+ const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+ const GoogleServiceAuthError& error) override;
+ void OnAccountsCookieDeletedByUserAction() override;
+ void OnErrorStateOfRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info,
+ const GoogleServiceAuthError& error) override;
+
+ void OnSigninAllowedPrefChanged();
+
+ raw_ptr<PrefService> prefs_;
+ raw_ptr<signin::IdentityManager> identity_manager_;
+
+ // Helper object to listen for changes to the signin allowed preference.
+ BooleanPrefMember signin_allowed_;
+
+ base::WeakPtrFactory<SigninManager> weak_ptr_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_
diff --git a/chromium/chrome/browser/signin/signin_manager_android_factory.cc b/chromium/chrome/browser/signin/signin_manager_android_factory.cc
new file mode 100644
index 00000000000..08d545c346a
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_android_factory.cc
@@ -0,0 +1,41 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_manager_android_factory.h"
+
+#include "chrome/browser/android/signin/signin_manager_android.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+SigninManagerAndroidFactory::SigninManagerAndroidFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninManagerAndroid",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SigninManagerAndroidFactory::~SigninManagerAndroidFactory() {}
+
+// static
+base::android::ScopedJavaLocalRef<jobject>
+SigninManagerAndroidFactory::GetJavaObjectForProfile(Profile* profile) {
+ return static_cast<SigninManagerAndroid*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true))
+ ->GetJavaObject();
+}
+
+// static
+SigninManagerAndroidFactory* SigninManagerAndroidFactory::GetInstance() {
+ static base::NoDestructor<SigninManagerAndroidFactory> instance;
+ return instance.get();
+}
+
+KeyedService* SigninManagerAndroidFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+
+ return new SigninManagerAndroid(profile, identity_manager);
+}
diff --git a/chromium/chrome/browser/signin/signin_manager_android_factory.h b/chromium/chrome/browser/signin/signin_manager_android_factory.h
new file mode 100644
index 00000000000..5eabc40186a
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_android_factory.h
@@ -0,0 +1,32 @@
+// Copyright 2019 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_
+
+#include "base/android/scoped_java_ref.h"
+#include "base/no_destructor.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class Profile;
+
+class SigninManagerAndroidFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static base::android::ScopedJavaLocalRef<jobject> GetJavaObjectForProfile(
+ Profile* profile);
+
+ // Returns an instance of the SigninManagerAndroidFactory singleton.
+ static SigninManagerAndroidFactory* GetInstance();
+
+ private:
+ friend class base::NoDestructor<SigninManagerAndroidFactory>;
+ SigninManagerAndroidFactory();
+
+ ~SigninManagerAndroidFactory() override;
+
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_manager_factory.cc b/chromium/chrome/browser/signin/signin_manager_factory.cc
new file mode 100644
index 00000000000..788dbee7f54
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_factory.cc
@@ -0,0 +1,48 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_manager_factory.h"
+
+#include "base/logging.h"
+#include "build/build_config.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+// static
+SigninManagerFactory* SigninManagerFactory::GetInstance() {
+ return base::Singleton<SigninManagerFactory>::get();
+}
+
+// static
+SigninManager* SigninManagerFactory::GetForProfile(Profile* profile) {
+ DCHECK(profile);
+ return static_cast<SigninManager*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+SigninManagerFactory::SigninManagerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninManager",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SigninManagerFactory::~SigninManagerFactory() = default;
+
+KeyedService* SigninManagerFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ return new SigninManager(profile->GetPrefs(),
+ IdentityManagerFactory::GetForProfile(profile));
+}
+
+bool SigninManagerFactory::ServiceIsCreatedWithBrowserContext() const {
+ return true;
+}
+
+bool SigninManagerFactory::ServiceIsNULLWhileTesting() const {
+ return true;
+}
diff --git a/chromium/chrome/browser/signin/signin_manager_factory.h b/chromium/chrome/browser/signin/signin_manager_factory.h
new file mode 100644
index 00000000000..d0ca640e22d
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_factory.h
@@ -0,0 +1,33 @@
+// Copyright 2020 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/signin_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class SigninManagerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns an instance of the factory singleton.
+ static SigninManagerFactory* GetInstance();
+
+ static SigninManager* GetForProfile(Profile* profile);
+
+ private:
+ friend struct base::DefaultSingletonTraits<SigninManagerFactory>;
+
+ SigninManagerFactory();
+ ~SigninManagerFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+ bool ServiceIsCreatedWithBrowserContext() const override;
+ bool ServiceIsNULLWhileTesting() const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_manager_unittest.cc b/chromium/chrome/browser/signin/signin_manager_unittest.cc
new file mode 100644
index 00000000000..e2bb33af9f0
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_unittest.cc
@@ -0,0 +1,421 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+#include "chrome/browser/signin/signin_manager.h"
+
+#include "base/memory/raw_ptr.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::_;
+using ::testing::Mock;
+
+namespace signin {
+namespace {
+const char kTestEmail[] = "me@gmail.com";
+const char kTestEmail2[] = "me2@gmail.com";
+
+class FakeIdentityManagerObserver : public IdentityManager::Observer {
+ public:
+ explicit FakeIdentityManagerObserver(IdentityManager* identity_manager)
+ : identity_manager_(identity_manager) {}
+ ~FakeIdentityManagerObserver() override = default;
+
+ void OnPrimaryAccountChanged(
+ const PrimaryAccountChangeEvent& event) override {
+ auto current_state = event.GetCurrentState();
+ EXPECT_EQ(
+ current_state.primary_account,
+ identity_manager_->GetPrimaryAccountInfo(current_state.consent_level));
+ events_.push_back(event);
+ }
+
+ const std::vector<PrimaryAccountChangeEvent>& events() const {
+ return events_;
+ }
+
+ void Reset() { events_.clear(); }
+
+ private:
+ raw_ptr<IdentityManager> identity_manager_;
+ std::vector<PrimaryAccountChangeEvent> events_;
+};
+} // namespace
+
+class SigninManagerTest : public testing::Test {
+ public:
+ SigninManagerTest()
+ : identity_test_env_(/*test_url_loader_factory=*/nullptr,
+ /*pref_service=*/&prefs_,
+ signin::AccountConsistencyMethod::kDice,
+ /*test_signin_client=*/nullptr),
+ observer_(identity_test_env_.identity_manager()) {}
+
+ SigninManagerTest(const SigninManagerTest&) = delete;
+ SigninManagerTest& operator=(const SigninManagerTest&) = delete;
+
+ void SetUp() override {
+ testing::Test::SetUp();
+ RecreateSigninManager();
+ identity_manager()->AddObserver(&observer_);
+ }
+
+ void TearDown() override { identity_manager()->RemoveObserver(&observer_); }
+
+ void RecreateSigninManager() {
+ signin_manger_ =
+ std::make_unique<SigninManager>(&prefs_, identity_manager());
+ }
+
+ AccountInfo GetAccountInfo(const std::string& email) {
+ AccountInfo account_info;
+ account_info.gaia = GetTestGaiaIdForEmail(email);
+ account_info.account_id =
+ identity_manager()->PickAccountIdForAccount(account_info.gaia, email);
+ account_info.email = email;
+ return account_info;
+ }
+
+ void ExpectUnconsentedPrimaryAccountSetEvent(
+ const CoreAccountInfo& expected_primary_account) {
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_TRUE(event.GetPreviousState().primary_account.IsEmpty());
+ EXPECT_EQ(expected_primary_account,
+ event.GetCurrentState().primary_account);
+ observer().Reset();
+ }
+
+ void ExpectUnconsentedPrimaryAccountClearedEvent(
+ const CoreAccountInfo& expected_cleared_account) {
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(expected_cleared_account,
+ event.GetPreviousState().primary_account);
+ EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty());
+ observer().Reset();
+ }
+
+ void ExpectSyncPrimaryAccountSetEvent(
+ const CoreAccountInfo& expected_primary_account) {
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_TRUE(event.GetPreviousState().primary_account.IsEmpty());
+ EXPECT_EQ(expected_primary_account,
+ event.GetCurrentState().primary_account);
+ observer().Reset();
+ }
+
+ IdentityManager* identity_manager() {
+ return identity_test_env_.identity_manager();
+ }
+
+ IdentityTestEnvironment* identity_test_env() { return &identity_test_env_; }
+
+ AccountInfo MakeAccountAvailableWithCookies(const std::string& email) {
+ AccountInfo account = GetAccountInfo(kTestEmail);
+ identity_test_env_.MakeAccountAvailableWithCookies(account.email,
+ account.gaia);
+ EXPECT_FALSE(account.IsEmpty());
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+ return account;
+ }
+
+ AccountInfo MakeSyncAccountAvailableWithCookies(const std::string& email) {
+ AccountInfo account = identity_test_env_.MakePrimaryAccountAvailable(
+ email, signin::ConsentLevel::kSync);
+ identity_test_env_.SetCookieAccounts({{account.email, account.gaia}});
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync));
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccountWithRefreshToken(
+ signin::ConsentLevel::kSync));
+ return account;
+ }
+
+ FakeIdentityManagerObserver& observer() { return observer_; }
+
+ sync_preferences::TestingPrefServiceSyncable prefs_;
+ content::BrowserTaskEnvironment task_environment_;
+ IdentityTestEnvironment identity_test_env_;
+ std::unique_ptr<SigninManager> signin_manger_;
+ FakeIdentityManagerObserver observer_;
+};
+
+TEST_F(
+ SigninManagerTest,
+ UnconsentedPrimaryAccountUpdatedOnItsAccountRefreshTokenUpdateWithValidTokenWhenNoSyncConsent) {
+ // Add an unconsented primary account, incl. proper cookies.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+}
+
+TEST_F(
+ SigninManagerTest,
+ UnconsentedPrimaryAccountUpdatedOnItsAccountRefreshTokenUpdateWithInvalidTokenWhenNoSyncConsent) {
+ // Prerequisite: add an unconsented primary account, incl. proper cookies.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+
+ // Invalid token.
+ SetInvalidRefreshTokenForAccount(identity_manager(), account.account_id);
+ ExpectUnconsentedPrimaryAccountClearedEvent(account);
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+
+ // Update with a valid token.
+ SetRefreshTokenForAccount(identity_manager(), account.account_id, "");
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+ EXPECT_EQ(identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin),
+ account);
+}
+
+TEST_F(
+ SigninManagerTest,
+ UnconsentedPrimaryAccountRemovedOnItsAccountRefreshTokenRemovalWhenNoSyncConsent) {
+ // Prerequisite: Add an unconsented primary account, incl. proper cookies.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+
+ // With no refresh token, there is no unconsented primary account any more.
+ identity_test_env()->RemoveRefreshTokenForAccount(account.account_id);
+ ExpectUnconsentedPrimaryAccountClearedEvent(account);
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+}
+
+TEST_F(SigninManagerTest, UnconsentedPrimaryAccountNotChangedOnSignout) {
+ // Set a primary account at sync consent level.
+ AccountInfo account = MakeSyncAccountAvailableWithCookies(kTestEmail);
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync));
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccountWithRefreshToken(
+ signin::ConsentLevel::kSync));
+
+ // Verify the primary account changed event.
+ ExpectSyncPrimaryAccountSetEvent(account);
+
+ // Tests that sync primary account is cleared, but unconsented account is not.
+ identity_test_env()->RevokeSyncConsent();
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(account, event.GetPreviousState().primary_account);
+ EXPECT_EQ(account, event.GetCurrentState().primary_account);
+}
+
+TEST_F(SigninManagerTest,
+ UnconsentedPrimaryAccountTokenRevokedWithStaleCookies) {
+ // Prerequisite: add an unconsented primary account, incl. proper cookies.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+
+ // Make the cookies stale and remove the account.
+ // Removing the refresh token for the unconsented primary account is
+ // sufficient to clear it.
+ identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(false);
+ identity_test_env()->RemoveRefreshTokenForAccount(account.account_id);
+ ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh);
+
+ // Unconsented account was removed.
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountClearedEvent(account);
+}
+
+TEST_F(SigninManagerTest,
+ UnconsentedPrimaryAccountTokenRevokedWithStaleCookiesMultipleAccounts) {
+ // Add two accounts with cookies.
+ AccountInfo main_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail);
+ AccountInfo secondary_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail2);
+ identity_test_env()->SetCookieAccounts(
+ {{main_account.email, main_account.gaia},
+ {secondary_account.email, secondary_account.gaia}});
+
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+ EXPECT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountSetEvent(main_account);
+
+ // Make the cookies stale and remove the main account.
+ identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(false);
+ identity_test_env()->RemoveRefreshTokenForAccount(main_account.account_id);
+ ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh);
+
+ // Unconsented account was removed.
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountClearedEvent(main_account);
+}
+
+TEST_F(SigninManagerTest, UnconsentedPrimaryAccountDuringLoad) {
+ // Pre-requisite: Add two accounts with cookies.
+ AccountInfo main_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail);
+ AccountInfo secondary_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail2);
+ identity_test_env()->SetCookieAccounts(
+ {{main_account.email, main_account.gaia},
+ {secondary_account.email, secondary_account.gaia}});
+ ASSERT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ ASSERT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+ ExpectUnconsentedPrimaryAccountSetEvent(main_account);
+
+ // Set the token service in "loading" mode.
+ identity_test_env()->ResetToAccountsNotYetLoadedFromDiskState();
+ RecreateSigninManager();
+
+ // Unconsented primary account is available while tokens are not loaded.
+ EXPECT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_TRUE(observer().events().empty());
+
+ // Revoking an unrelated token doesn't change the unconsented primary account.
+ identity_test_env()->RemoveRefreshTokenForAccount(
+ secondary_account.account_id);
+ EXPECT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_TRUE(observer().events().empty());
+
+ // Revoking the token of the unconsented primary account while the tokens
+ // are still loading does not change the unconsented primary account.
+ identity_test_env()->RemoveRefreshTokenForAccount(main_account.account_id);
+ EXPECT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_TRUE(observer().events().empty());
+
+ // Finish the token load should clear the primary account as the token of the
+ // primary account was revoked.
+ identity_test_env()->ReloadAccountsFromDisk();
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountClearedEvent(main_account);
+}
+
+TEST_F(SigninManagerTest,
+ UnconsentedPrimaryAccountUpdatedOnSyncConsentRevoked) {
+ AccountInfo first_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail);
+ AccountInfo second_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail2);
+ identity_test_env()->SetCookieAccounts(
+ {{first_account.email, first_account.gaia},
+ {second_account.email, second_account.gaia}});
+ ASSERT_EQ(first_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountSetEvent(first_account);
+
+ // Set the sync primary account to the second account in cookies.
+ // The unconsented primary account should be updated.
+ identity_test_env()->SetPrimaryAccount(second_account.email,
+ signin::ConsentLevel::kSync);
+ EXPECT_EQ(second_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync));
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(first_account, event.GetPreviousState().primary_account);
+ EXPECT_EQ(second_account, event.GetCurrentState().primary_account);
+ observer().Reset();
+
+ // Clear primary account but do not delete the account. The unconsented
+ // primary account should be updated to be the first account in cookies.
+ identity_test_env()->RevokeSyncConsent();
+ base::RunLoop().RunUntilIdle();
+
+ // Primary account is cleared, but unconsented account is not.
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ EXPECT_EQ(first_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+
+ EXPECT_EQ(2U, observer().events().size());
+ event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(second_account, event.GetPreviousState().primary_account);
+ EXPECT_EQ(second_account, event.GetCurrentState().primary_account);
+
+ event = observer().events()[1];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(second_account, event.GetPreviousState().primary_account);
+ EXPECT_EQ(first_account, event.GetCurrentState().primary_account);
+}
+
+TEST_F(SigninManagerTest, ClearPrimaryAccountAndSignOut) {
+ AccountInfo account = MakeSyncAccountAvailableWithCookies(kTestEmail);
+ ExpectSyncPrimaryAccountSetEvent(account);
+
+ identity_test_env()->ClearPrimaryAccount();
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(account, event.GetPreviousState().primary_account);
+ EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty());
+}
+
+TEST_F(SigninManagerTest,
+ UnconsentedPrimaryAccountClearedWhenSigninDisallowed) {
+ // Prerequisite: add an unconsented primary account.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+
+ prefs_.SetBoolean(prefs::kSigninAllowed, false);
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(account, event.GetPreviousState().primary_account);
+ EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty());
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc
new file mode 100644
index 00000000000..1f0c6a9ae36
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc
@@ -0,0 +1,72 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_profile_attributes_updater.h"
+
+#include <string>
+
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/common/pref_names.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+
+SigninProfileAttributesUpdater::SigninProfileAttributesUpdater(
+ signin::IdentityManager* identity_manager,
+ ProfileAttributesStorage* profile_attributes_storage,
+ const base::FilePath& profile_path,
+ PrefService* prefs)
+ : identity_manager_(identity_manager),
+ profile_attributes_storage_(profile_attributes_storage),
+ profile_path_(profile_path),
+ prefs_(prefs) {
+ DCHECK(identity_manager_);
+ DCHECK(profile_attributes_storage_);
+ identity_manager_observation_.Observe(identity_manager_.get());
+
+ UpdateProfileAttributes();
+}
+
+SigninProfileAttributesUpdater::~SigninProfileAttributesUpdater() = default;
+
+void SigninProfileAttributesUpdater::Shutdown() {
+ identity_manager_observation_.Reset();
+}
+
+void SigninProfileAttributesUpdater::UpdateProfileAttributes() {
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage_->GetProfileAttributesWithPath(profile_path_);
+ if (!entry) {
+ return;
+ }
+
+ CoreAccountInfo account_info =
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+
+ bool clear_profile = account_info.IsEmpty();
+
+ if (account_info.gaia != entry->GetGAIAId() ||
+ !gaia::AreEmailsSame(account_info.email,
+ base::UTF16ToUTF8(entry->GetUserName()))) {
+ // Reset prefs. Note: this will also update the |ProfileAttributesEntry|.
+ prefs_->ClearPref(prefs::kProfileUsingDefaultAvatar);
+ prefs_->ClearPref(prefs::kProfileUsingGAIAAvatar);
+ }
+
+ if (clear_profile) {
+ entry->SetAuthInfo(std::string(), std::u16string(),
+ /*is_consented_primary_account=*/false);
+ } else {
+ entry->SetAuthInfo(
+ account_info.gaia, base::UTF8ToUTF16(account_info.email),
+ identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ }
+}
+
+void SigninProfileAttributesUpdater::OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) {
+ UpdateProfileAttributes();
+}
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater.h b/chromium/chrome/browser/signin/signin_profile_attributes_updater.h
new file mode 100644
index 00000000000..8e189e8e370
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater.h
@@ -0,0 +1,56 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_
+
+#include "base/files/file_path.h"
+#include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+class ProfileAttributesStorage;
+
+// This class listens to various signin events and updates the signin-related
+// fields of ProfileAttributes.
+class SigninProfileAttributesUpdater
+ : public KeyedService,
+ public signin::IdentityManager::Observer {
+ public:
+ SigninProfileAttributesUpdater(
+ signin::IdentityManager* identity_manager,
+ ProfileAttributesStorage* profile_attributes_storage,
+ const base::FilePath& profile_path,
+ PrefService* prefs);
+
+ SigninProfileAttributesUpdater(const SigninProfileAttributesUpdater&) =
+ delete;
+ SigninProfileAttributesUpdater& operator=(
+ const SigninProfileAttributesUpdater&) = delete;
+
+ ~SigninProfileAttributesUpdater() override;
+
+ private:
+ // KeyedService:
+ void Shutdown() override;
+
+ // Updates the profile attributes on signin and signout events.
+ void UpdateProfileAttributes();
+
+ // IdentityManager::Observer:
+ void OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) override;
+
+ raw_ptr<signin::IdentityManager> identity_manager_;
+ raw_ptr<ProfileAttributesStorage> profile_attributes_storage_;
+ const base::FilePath profile_path_;
+ raw_ptr<PrefService> prefs_;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ identity_manager_observation_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc
new file mode 100644
index 00000000000..86c62a2ce6e
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc
@@ -0,0 +1,53 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_profile_attributes_updater_factory.h"
+
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_profile_attributes_updater.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+// static
+SigninProfileAttributesUpdater*
+SigninProfileAttributesUpdaterFactory::GetForProfile(Profile* profile) {
+ return static_cast<SigninProfileAttributesUpdater*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+SigninProfileAttributesUpdaterFactory*
+SigninProfileAttributesUpdaterFactory::GetInstance() {
+ return base::Singleton<SigninProfileAttributesUpdaterFactory>::get();
+}
+
+SigninProfileAttributesUpdaterFactory::SigninProfileAttributesUpdaterFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninProfileAttributesUpdater",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SigninProfileAttributesUpdaterFactory::
+ ~SigninProfileAttributesUpdaterFactory() {}
+
+KeyedService* SigninProfileAttributesUpdaterFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ // Some tests don't have a ProfileManager, disable this service.
+ if (!g_browser_process->profile_manager())
+ return nullptr;
+
+ return new SigninProfileAttributesUpdater(
+ IdentityManagerFactory::GetForProfile(profile),
+ &g_browser_process->profile_manager()->GetProfileAttributesStorage(),
+ profile->GetPath(), profile->GetPrefs());
+}
+
+bool SigninProfileAttributesUpdaterFactory::ServiceIsCreatedWithBrowserContext()
+ const {
+ return true;
+}
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h
new file mode 100644
index 00000000000..78f990abaf8
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h
@@ -0,0 +1,42 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class Profile;
+class SigninProfileAttributesUpdater;
+
+class SigninProfileAttributesUpdaterFactory
+ : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns nullptr if this profile cannot have a
+ // SigninProfileAttributesUpdater (for example, if |profile| is incognito).
+ static SigninProfileAttributesUpdater* GetForProfile(Profile* profile);
+
+ // Returns an instance of the factory singleton.
+ static SigninProfileAttributesUpdaterFactory* GetInstance();
+
+ SigninProfileAttributesUpdaterFactory(
+ const SigninProfileAttributesUpdaterFactory&) = delete;
+ SigninProfileAttributesUpdaterFactory& operator=(
+ const SigninProfileAttributesUpdaterFactory&) = delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<
+ SigninProfileAttributesUpdaterFactory>;
+
+ SigninProfileAttributesUpdaterFactory();
+ ~SigninProfileAttributesUpdaterFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ bool ServiceIsCreatedWithBrowserContext() const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc
new file mode 100644
index 00000000000..01430a350b0
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc
@@ -0,0 +1,224 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_profile_attributes_updater.h"
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/memory/raw_ptr.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/sync_preferences/pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+const char kEmail[] = "example@email.com";
+
+void CheckProfilePrefsReset(PrefService* pref_service,
+ bool expected_using_default_name) {
+ EXPECT_TRUE(pref_service->GetBoolean(prefs::kProfileUsingDefaultAvatar));
+ EXPECT_FALSE(pref_service->GetBoolean(prefs::kProfileUsingGAIAAvatar));
+ EXPECT_EQ(expected_using_default_name,
+ pref_service->GetBoolean(prefs::kProfileUsingDefaultName));
+}
+
+void CheckProfilePrefsSet(PrefService* pref_service,
+ bool expected_is_using_default_name) {
+ EXPECT_FALSE(pref_service->GetBoolean(prefs::kProfileUsingDefaultAvatar));
+ EXPECT_TRUE(pref_service->GetBoolean(prefs::kProfileUsingGAIAAvatar));
+ EXPECT_EQ(expected_is_using_default_name,
+ pref_service->GetBoolean(prefs::kProfileUsingDefaultName));
+}
+
+// Set the prefs to nondefault values.
+void SetProfilePrefs(PrefService* pref_service) {
+ pref_service->SetBoolean(prefs::kProfileUsingDefaultAvatar, false);
+ pref_service->SetBoolean(prefs::kProfileUsingGAIAAvatar, true);
+ pref_service->SetBoolean(prefs::kProfileUsingDefaultName, false);
+
+ CheckProfilePrefsSet(pref_service, false);
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+} // namespace
+
+class SigninProfileAttributesUpdaterTest : public testing::Test {
+ public:
+ SigninProfileAttributesUpdaterTest()
+ : profile_manager_(TestingBrowserProcess::GetGlobal()) {}
+
+ // Recreates |signin_profile_attributes_updater_|. Useful for tests that want
+ // to set up the updater with specific preconditions.
+ void RecreateSigninProfileAttributesUpdater() {
+ signin_profile_attributes_updater_ =
+ std::make_unique<SigninProfileAttributesUpdater>(
+ identity_test_env_.identity_manager(),
+ profile_manager_.profile_attributes_storage(), profile_->GetPath(),
+ profile_->GetPrefs());
+ }
+
+ void SetUp() override {
+ testing::Test::SetUp();
+
+ ASSERT_TRUE(profile_manager_.SetUp());
+ std::string name = "profile_name";
+ profile_ = profile_manager_.CreateTestingProfile(
+ name, /*prefs=*/nullptr, base::UTF8ToUTF16(name), 0, std::string(),
+ TestingProfile::TestingFactories());
+
+ RecreateSigninProfileAttributesUpdater();
+ }
+
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfileManager profile_manager_;
+ raw_ptr<TestingProfile> profile_;
+ signin::IdentityTestEnvironment identity_test_env_;
+ std::unique_ptr<SigninProfileAttributesUpdater>
+ signin_profile_attributes_updater_;
+};
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+// Tests that the browser state info is updated on signin and signout.
+// ChromeOS does not support signout.
+TEST_F(SigninProfileAttributesUpdaterTest, SigninSignout) {
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+ ASSERT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn);
+ EXPECT_FALSE(entry->IsSigninRequired());
+
+ // Signin.
+ identity_test_env_.MakePrimaryAccountAvailable(kEmail,
+ signin::ConsentLevel::kSync);
+ EXPECT_TRUE(entry->IsAuthenticated());
+ EXPECT_EQ(signin::GetTestGaiaIdForEmail(kEmail), entry->GetGAIAId());
+ EXPECT_EQ(kEmail, base::UTF16ToUTF8(entry->GetUserName()));
+
+ // Signout.
+ identity_test_env_.ClearPrimaryAccount();
+ EXPECT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn);
+ EXPECT_FALSE(entry->IsSigninRequired());
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+TEST_F(SigninProfileAttributesUpdaterTest, SigninSignoutResetsProfilePrefs) {
+ PrefService* pref_service = profile_->GetPrefs();
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+
+ // Set profile prefs.
+ CheckProfilePrefsReset(pref_service, true);
+#if !defined(OS_ANDROID)
+ SetProfilePrefs(pref_service);
+
+ // Set UPA should reset profile prefs.
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ "email1@example.com", signin::ConsentLevel::kSignin);
+ EXPECT_FALSE(entry->IsAuthenticated());
+ CheckProfilePrefsReset(pref_service, false);
+ SetProfilePrefs(pref_service);
+ // Signout should reset profile prefs.
+ identity_test_env_.ClearPrimaryAccount();
+ CheckProfilePrefsReset(pref_service, false);
+#endif // !defined(OS_ANDROID)
+
+ SetProfilePrefs(pref_service);
+ // Set primary account should reset profile prefs.
+ AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable(
+ "primary@example.com", signin::ConsentLevel::kSync);
+ CheckProfilePrefsReset(pref_service, false);
+ SetProfilePrefs(pref_service);
+ // Disabling sync should reset profile prefs.
+ identity_test_env_.ClearPrimaryAccount();
+ CheckProfilePrefsReset(pref_service, false);
+}
+
+#if !defined(OS_ANDROID)
+TEST_F(SigninProfileAttributesUpdaterTest,
+ EnablingSyncWithUPAAccountShouldNotResetProfilePrefs) {
+ PrefService* pref_service = profile_->GetPrefs();
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+ // Set UPA.
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ "email1@example.com", signin::ConsentLevel::kSignin);
+ EXPECT_FALSE(entry->IsAuthenticated());
+ SetProfilePrefs(pref_service);
+ // Set primary account to be the same as the UPA.
+ // Given it is the same account, profile prefs should keep the same state.
+ identity_test_env_.SetPrimaryAccount(account_info.email,
+ signin::ConsentLevel::kSync);
+ EXPECT_TRUE(entry->IsAuthenticated());
+ CheckProfilePrefsSet(pref_service, false);
+ identity_test_env_.ClearPrimaryAccount();
+ CheckProfilePrefsReset(pref_service, false);
+}
+
+TEST_F(SigninProfileAttributesUpdaterTest,
+ EnablingSyncWithDifferentAccountThanUPAResetsProfilePrefs) {
+ PrefService* pref_service = profile_->GetPrefs();
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ "email1@example.com", signin::ConsentLevel::kSignin);
+ EXPECT_FALSE(entry->IsAuthenticated());
+ SetProfilePrefs(pref_service);
+ // Set primary account to a different account than the UPA.
+ AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable(
+ "primary@example.com", signin::ConsentLevel::kSync);
+ EXPECT_TRUE(entry->IsAuthenticated());
+ CheckProfilePrefsReset(pref_service, false);
+}
+#endif // !defined(OS_ANDROID)
+
+class SigninProfileAttributesUpdaterWithForceSigninTest
+ : public SigninProfileAttributesUpdaterTest {
+ public:
+ SigninProfileAttributesUpdaterWithForceSigninTest()
+ : forced_signin_setter_(true) {}
+
+ private:
+ signin_util::ScopedForceSigninSetterForTesting forced_signin_setter_;
+};
+
+TEST_F(SigninProfileAttributesUpdaterWithForceSigninTest, IsSigninRequired) {
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+ EXPECT_FALSE(entry->IsAuthenticated());
+ EXPECT_TRUE(entry->IsSigninRequired());
+
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ kEmail, signin::ConsentLevel::kSync);
+
+ EXPECT_TRUE(entry->IsAuthenticated());
+ EXPECT_EQ(signin::GetTestGaiaIdForEmail(kEmail), entry->GetGAIAId());
+ EXPECT_EQ(kEmail, base::UTF16ToUTF8(entry->GetUserName()));
+
+ identity_test_env_.ClearPrimaryAccount();
+ EXPECT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn);
+ EXPECT_TRUE(entry->IsSigninRequired());
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chromium/chrome/browser/signin/signin_promo.cc b/chromium/chrome/browser/signin/signin_promo.cc
new file mode 100644
index 00000000000..9315cb8a924
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo.cc
@@ -0,0 +1,143 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_promo.h"
+
+#include "base/strings/string_number_conversions.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/google/google_brand.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/signin_promo_util.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "components/google/core/common/google_util.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/storage_partition_config.h"
+#include "extensions/browser/guest_view/web_view/web_view_guest.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "net/base/url_util.h"
+#include "url/gurl.h"
+
+#if defined(OS_WIN)
+#include "base/win/windows_version.h"
+#endif
+
+namespace signin {
+
+const char kSignInPromoQueryKeyAccessPoint[] = "access_point";
+const char kSignInPromoQueryKeyAutoClose[] = "auto_close";
+const char kSignInPromoQueryKeyForceKeepData[] = "force_keep_data";
+const char kSignInPromoQueryKeyReason[] = "reason";
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+GURL GetEmbeddedPromoURL(signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ bool auto_close) {
+ CHECK_LT(static_cast<int>(access_point),
+ static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_MAX));
+ CHECK_NE(static_cast<int>(access_point),
+ static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN));
+ CHECK_LE(static_cast<int>(reason),
+ static_cast<int>(signin_metrics::Reason::kMaxValue));
+ CHECK_NE(static_cast<int>(reason),
+ static_cast<int>(signin_metrics::Reason::kUnknownReason));
+
+ GURL url(chrome::kChromeUIChromeSigninURL);
+ url = net::AppendQueryParameter(
+ url, signin::kSignInPromoQueryKeyAccessPoint,
+ base::NumberToString(static_cast<int>(access_point)));
+ url =
+ net::AppendQueryParameter(url, signin::kSignInPromoQueryKeyReason,
+ base::NumberToString(static_cast<int>(reason)));
+ if (auto_close) {
+ url = net::AppendQueryParameter(url, signin::kSignInPromoQueryKeyAutoClose,
+ "1");
+ }
+ return url;
+}
+
+GURL GetEmbeddedReauthURLWithEmail(signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ const std::string& email) {
+ GURL url = GetEmbeddedPromoURL(access_point, reason, /*auto_close=*/true);
+ url = net::AppendQueryParameter(url, "email", email);
+ url = net::AppendQueryParameter(url, "validateEmail", "1");
+ return net::AppendQueryParameter(url, "readOnlyEmail", "1");
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+GURL GetChromeSyncURLForDice(const std::string& email,
+ const std::string& continue_url) {
+ GURL url = GaiaUrls::GetInstance()->signin_chrome_sync_dice();
+ if (!email.empty())
+ url = net::AppendQueryParameter(url, "email_hint", email);
+ if (!continue_url.empty())
+ url = net::AppendQueryParameter(url, "continue", continue_url);
+ return url;
+}
+
+GURL GetAddAccountURLForDice(const std::string& email,
+ const std::string& continue_url) {
+ GURL url = GaiaUrls::GetInstance()->add_account_url();
+ if (!email.empty())
+ url = net::AppendQueryParameter(url, "Email", email);
+ if (!continue_url.empty())
+ url = net::AppendQueryParameter(url, "continue", continue_url);
+ return url;
+}
+
+content::StoragePartition* GetSigninPartition(
+ content::BrowserContext* browser_context) {
+ const auto signin_partition_config = content::StoragePartitionConfig::Create(
+ browser_context, "chrome-signin", /* partition_name= */ "",
+ /* in_memory= */ true);
+ return browser_context->GetStoragePartition(signin_partition_config);
+}
+
+signin_metrics::AccessPoint GetAccessPointForEmbeddedPromoURL(const GURL& url) {
+ std::string value;
+ if (!net::GetValueForKeyInQuery(url, kSignInPromoQueryKeyAccessPoint,
+ &value)) {
+ return signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
+ }
+
+ int access_point = -1;
+ base::StringToInt(value, &access_point);
+ if (access_point <
+ static_cast<int>(
+ signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE) ||
+ access_point >=
+ static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_MAX)) {
+ return signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
+ }
+
+ return static_cast<signin_metrics::AccessPoint>(access_point);
+}
+
+signin_metrics::Reason GetSigninReasonForEmbeddedPromoURL(const GURL& url) {
+ std::string value;
+ if (!net::GetValueForKeyInQuery(url, kSignInPromoQueryKeyReason, &value))
+ return signin_metrics::Reason::kUnknownReason;
+
+ int reason = -1;
+ base::StringToInt(value, &reason);
+ if (reason <
+ static_cast<int>(signin_metrics::Reason::kSigninPrimaryAccount) ||
+ reason > static_cast<int>(signin_metrics::Reason::kMaxValue)) {
+ return signin_metrics::Reason::kUnknownReason;
+ }
+
+ return static_cast<signin_metrics::Reason>(reason);
+}
+
+void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ registry->RegisterIntegerPref(prefs::kDiceSigninUserMenuPromoCount, 0);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/signin_promo.h b/chromium/chrome/browser/signin/signin_promo.h
new file mode 100644
index 00000000000..36cbc05b7c4
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo.h
@@ -0,0 +1,83 @@
+// Copyright 2013 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_
+
+#include <string>
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "components/signin/public/base/signin_metrics.h"
+
+class GURL;
+
+namespace content {
+class BrowserContext;
+class StoragePartition;
+} // namespace content
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+// Utility functions for sign in promos.
+namespace signin {
+
+extern const char kSignInPromoQueryKeyAccessPoint[];
+// TODO(https://crbug.com/1205147): Auto close is unused. Remove it.
+extern const char kSignInPromoQueryKeyAutoClose[];
+extern const char kSignInPromoQueryKeyForceKeepData[];
+extern const char kSignInPromoQueryKeyReason[];
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+// These functions are only used to unlock the profile from the desktop user
+// manager and the windows credential provider.
+
+// Returns the sign in promo URL that can be used in a modal dialog with
+// the given arguments in the query.
+// |access_point| indicates where the sign in is being initiated.
+// |reason| indicates the purpose of using this URL.
+// |auto_close| whether to close the sign in promo automatically when done.
+GURL GetEmbeddedPromoURL(signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ bool auto_close);
+
+// Returns a sign in promo URL specifically for reauthenticating |email| that
+// can be used in a modal dialog.
+GURL GetEmbeddedReauthURLWithEmail(signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ const std::string& email);
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+// Returns the URL to be used to signin and turn on Sync when DICE is enabled.
+// If email is not empty, then it will pass email as hint to the page so that it
+// will be autofilled by Gaia.
+// If |continue_url| is empty, this may redirect to myaccount.
+GURL GetChromeSyncURLForDice(const std::string& email,
+ const std::string& continue_url);
+
+// Returns the URL to be used to add (secondary) account when DICE is enabled.
+// If email is not empty, then it will pass email as hint to the page so that it
+// will be autofilled by Gaia.
+// If |continue_url| is empty, this may redirect to myaccount.
+GURL GetAddAccountURLForDice(const std::string& email,
+ const std::string& continue_url);
+
+// Gets the partition for the embedded sign in frame/webview.
+content::StoragePartition* GetSigninPartition(
+ content::BrowserContext* browser_context);
+
+// Gets the access point from the query portion of the sign in promo URL.
+signin_metrics::AccessPoint GetAccessPointForEmbeddedPromoURL(const GURL& url);
+
+// Gets the sign in reason from the query portion of the sign in promo URL.
+signin_metrics::Reason GetSigninReasonForEmbeddedPromoURL(const GURL& url);
+
+// Registers the preferences the Sign In Promo needs.
+void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_
diff --git a/chromium/chrome/browser/signin/signin_promo_unittest.cc b/chromium/chrome/browser/signin/signin_promo_unittest.cc
new file mode 100644
index 00000000000..c6dc4054af2
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo_unittest.cc
@@ -0,0 +1,56 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_promo.h"
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/common/webui_url_constants.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace signin {
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+TEST(SigninPromoTest, TestPromoURL) {
+ GURL::Replacements replace_query;
+ replace_query.SetQueryStr("access_point=0&reason=0&auto_close=1");
+ EXPECT_EQ(
+ GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query),
+ GetEmbeddedPromoURL(signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE,
+ signin_metrics::Reason::kSigninPrimaryAccount, true));
+ replace_query.SetQueryStr("access_point=15&reason=1");
+ EXPECT_EQ(
+ GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query),
+ GetEmbeddedPromoURL(
+ signin_metrics::AccessPoint::ACCESS_POINT_SIGNIN_PROMO,
+ signin_metrics::Reason::kAddSecondaryAccount, false));
+}
+
+TEST(SigninPromoTest, TestReauthURL) {
+ GURL::Replacements replace_query;
+ replace_query.SetQueryStr(
+ "access_point=0&reason=6&auto_close=1"
+ "&email=example%40domain.com&validateEmail=1"
+ "&readOnlyEmail=1");
+ EXPECT_EQ(
+ GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query),
+ GetEmbeddedReauthURLWithEmail(
+ signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE,
+ signin_metrics::Reason::kFetchLstOnly, "example@domain.com"));
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+TEST(SigninPromoTest, SigninURLForDice) {
+ EXPECT_EQ(
+ "https://accounts.google.com/signin/chrome/sync?ssp=1&"
+ "email_hint=email%40gmail.com&continue=https%3A%2F%2Fcontinue_url%2F",
+ GetChromeSyncURLForDice("email@gmail.com", "https://continue_url/"));
+ EXPECT_EQ(
+ "https://accounts.google.com/AddSession?"
+ "Email=email%40gmail.com&continue=https%3A%2F%2Fcontinue_url%2F",
+ GetAddAccountURLForDice("email@gmail.com", "https://continue_url/"));
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/signin_promo_util.cc b/chromium/chrome/browser/signin/signin_promo_util.cc
new file mode 100644
index 00000000000..4497bbf6b72
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo_util.cc
@@ -0,0 +1,49 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_promo_util.h"
+
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "net/base/network_change_notifier.h"
+
+namespace signin {
+
+bool ShouldShowPromo(Profile* profile) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // There's no need to show the sign in promo on cros since cros users are
+ // already logged in.
+ return false;
+#else
+
+ // Don't bother if we don't have any kind of network connection.
+ if (net::NetworkChangeNotifier::IsOffline())
+ return false;
+
+ // Consider original profile even if an off-the-record profile was
+ // passed to this method as sign-in state is only defined for the
+ // primary profile.
+ Profile* original_profile = profile->GetOriginalProfile();
+
+ // Don't show for supervised child profiles.
+ if (original_profile->IsChild())
+ return false;
+
+ // Don't show if sign-in is not allowed.
+ if (!original_profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed))
+ return false;
+
+ // Display the signin promo if the user is not signed in.
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(original_profile);
+ return !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync);
+#endif
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/signin_promo_util.h b/chromium/chrome/browser/signin/signin_promo_util.h
new file mode 100644
index 00000000000..36a68d39e00
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo_util.h
@@ -0,0 +1,18 @@
+// Copyright 2016 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_
+
+class Profile;
+
+namespace signin {
+
+// Returns true if the sign in promo should be visible.
+// |profile| is the profile of the tab the promo would be shown on.
+bool ShouldShowPromo(Profile* profile);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_
diff --git a/chromium/chrome/browser/signin/signin_ui_util.cc b/chromium/chrome/browser/signin/signin_ui_util.cc
new file mode 100644
index 00000000000..18c541f96a2
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_ui_util.cc
@@ -0,0 +1,608 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_ui_util.h"
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/feature_list.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/metrics/user_metrics.h"
+#include "base/notreached.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_util.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/supports_user_data.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/feature_engagement/tracker_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_navigator.h"
+#include "chrome/browser/ui/browser_navigator_params.h"
+#include "chrome/browser/ui/chrome_pages.h"
+#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/common/pref_names.h"
+#include "components/feature_engagement/public/tracker.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_utils.h"
+#include "third_party/re2/src/re2/re2.h"
+#include "ui/gfx/font_list.h"
+#include "ui/gfx/text_elider.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "components/account_manager_core/account_manager_facade.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#endif
+
+namespace {
+
+// Key for storing animated identity per-profile data.
+const char kAnimatedIdentityKeyName[] = "animated_identity_user_data";
+
+constexpr base::TimeDelta kDelayForCrossWindowAnimationReplay =
+ base::Seconds(5);
+
+// UserData attached to the user profile, keeping track of the last time the
+// animation was shown to the user.
+class AvatarButtonUserData : public base::SupportsUserData::Data {
+ public:
+ ~AvatarButtonUserData() override = default;
+
+ // Returns the last time the animated identity was shown. Returns the null
+ // time if it was never shown.
+ static base::TimeTicks GetAnimatedIdentityLastShown(Profile* profile) {
+ DCHECK(profile);
+ AvatarButtonUserData* data = GetForProfile(profile);
+ if (!data)
+ return base::TimeTicks();
+ return data->animated_identity_last_shown_;
+ }
+
+ // Sets the time when the animated identity was shown.
+ static void SetAnimatedIdentityLastShown(Profile* profile,
+ base::TimeTicks time) {
+ DCHECK(!time.is_null());
+ GetOrCreateForProfile(profile)->animated_identity_last_shown_ = time;
+ }
+
+ private:
+ // Returns nullptr if there is no AvatarButtonUserData attached to the
+ // profile.
+ static AvatarButtonUserData* GetForProfile(Profile* profile) {
+ return static_cast<AvatarButtonUserData*>(
+ profile->GetUserData(kAnimatedIdentityKeyName));
+ }
+
+ // Never returns nullptr.
+ static AvatarButtonUserData* GetOrCreateForProfile(Profile* profile) {
+ DCHECK(profile);
+ AvatarButtonUserData* existing_data = GetForProfile(profile);
+ if (existing_data)
+ return existing_data;
+
+ auto new_data = std::make_unique<AvatarButtonUserData>();
+ auto* new_data_ptr = new_data.get();
+ profile->SetUserData(kAnimatedIdentityKeyName, std::move(new_data));
+ return new_data_ptr;
+ }
+
+ base::TimeTicks animated_identity_last_shown_;
+};
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+void CreateDiceTurnSyncOnHelper(
+ Profile* profile,
+ Browser* browser,
+ signin_metrics::AccessPoint signin_access_point,
+ signin_metrics::PromoAction signin_promo_action,
+ signin_metrics::Reason signin_reason,
+ const CoreAccountId& account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode) {
+ // DiceTurnSyncOnHelper is suicidal (it will delete itself once it finishes
+ // enabling sync).
+ new DiceTurnSyncOnHelper(profile, browser, signin_access_point,
+ signin_promo_action, signin_reason, account_id,
+ signin_aborted_mode);
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+std::string GetReauthAccessPointHistogramSuffix(
+ signin_metrics::ReauthAccessPoint access_point) {
+ switch (access_point) {
+ case signin_metrics::ReauthAccessPoint::kUnknown:
+ NOTREACHED();
+ return std::string();
+ case signin_metrics::ReauthAccessPoint::kAutofillDropdown:
+ return "ToFillPassword";
+ case signin_metrics::ReauthAccessPoint::kPasswordSaveBubble:
+ return "ToSaveOrUpdatePassword";
+ case signin_metrics::ReauthAccessPoint::kPasswordSettings:
+ return "ToManageInSettings";
+ case signin_metrics::ReauthAccessPoint::kGeneratePasswordDropdown:
+ case signin_metrics::ReauthAccessPoint::kGeneratePasswordContextMenu:
+ return "ToGeneratePassword";
+ case signin_metrics::ReauthAccessPoint::kPasswordMoveBubble:
+ return "ToMovePassword";
+ case signin_metrics::ReauthAccessPoint::kPasswordSaveLocallyBubble:
+ return "ToSavePasswordLocallyThenMove";
+ }
+}
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+account_manager::AccountManagerFacade::AccountAdditionSource
+GetAccountReauthSourceFromAccessPoint(
+ signin_metrics::AccessPoint access_point) {
+ switch (access_point) {
+ case signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN:
+ return account_manager::AccountManagerFacade::AccountAdditionSource::
+ kAvatarBubbleReauthAccountButton;
+ default:
+ NOTREACHED() << "Reauth is requested from an unknown access point "
+ << static_cast<int>(access_point);
+ return account_manager::AccountManagerFacade::AccountAdditionSource::
+ kMaxValue;
+ }
+}
+#endif
+
+} // namespace
+
+namespace signin_ui_util {
+
+std::u16string GetAuthenticatedUsername(Profile* profile) {
+ DCHECK(profile);
+ std::string user_display_name;
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ user_display_name =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync)
+ .email;
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // See https://crbug.com/994798 for details.
+ user_manager::User* user =
+ chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
+ // |user| may be null in tests.
+ if (user)
+ user_display_name = user->GetDisplayEmail();
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+ }
+
+ return base::UTF8ToUTF16(user_display_name);
+}
+
+void InitializePrefsForProfile(Profile* profile) {
+ if (profile->IsNewProfile()) {
+ // Suppresses the upgrade tutorial for a new profile.
+ profile->GetPrefs()->SetInteger(prefs::kProfileAvatarTutorialShown,
+ kUpgradeWelcomeTutorialShowMax + 1);
+ }
+}
+
+void ShowSigninErrorLearnMorePage(Profile* profile) {
+ static const char kSigninErrorLearnMoreUrl[] =
+ "https://support.google.com/chrome/answer/1181420?";
+ NavigateParams params(profile, GURL(kSigninErrorLearnMoreUrl),
+ ui::PAGE_TRANSITION_LINK);
+ params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
+ Navigate(&params);
+}
+
+void ShowReauthForPrimaryAccountWithAuthError(
+ Browser* browser,
+ signin_metrics::AccessPoint access_point) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // On ChromeOS, sync errors are fixed by re-signing into the OS.
+ NOTREACHED();
+#elif BUILDFLAG(IS_CHROMEOS_LACROS)
+ internal::ShowReauthForPrimaryAccountWithAuthErrorLacros(
+ browser, access_point,
+ ::GetAccountManagerFacade(browser->profile()->GetPath().value()));
+#else
+ browser->signin_view_controller()->ShowSignin(
+ profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH, access_point);
+#endif
+}
+
+void ShowExtensionSigninPrompt(Profile* profile,
+ bool enable_sync,
+ const std::string& email_hint) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ NOTREACHED();
+#else
+ internal::ShowExtensionSigninPrompt(
+ profile,
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ ::GetAccountManagerFacade(profile->GetPath().value()),
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+ enable_sync, email_hint);
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+}
+
+namespace internal {
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+void ShowReauthForPrimaryAccountWithAuthErrorLacros(
+ Browser* browser,
+ signin_metrics::AccessPoint access_point,
+ account_manager::AccountManagerFacade* account_manager_facade) {
+ Profile* profile = browser->profile();
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ CoreAccountInfo primary_account_info =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ DCHECK(!primary_account_info.IsEmpty());
+ DCHECK(identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
+ primary_account_info.account_id));
+ account_manager_facade->ShowReauthAccountDialog(
+ GetAccountReauthSourceFromAccessPoint(access_point),
+ primary_account_info.email);
+}
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+void ShowExtensionSigninPrompt(
+ Profile* profile,
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ account_manager::AccountManagerFacade* account_manager_facade,
+#endif
+ bool enable_sync,
+ const std::string& email_hint) {
+ // There is no sign-in flow for guest or system profile.
+ if (profile->IsGuestSession() || profile->IsSystemProfile())
+ return;
+ // Locked profile should be unlocked with UserManager only.
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile->GetPath());
+ if (entry && entry->IsSigninRequired()) {
+ return;
+ }
+
+ // This may be called in incognito. Redirect to the original profile.
+ profile = profile->GetOriginalProfile();
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ // There is no sign-in without Mirror.
+ if (!AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile))
+ return;
+
+ if (email_hint.empty()) {
+ // Add a new account.
+ // TODO(https://crbug.com/1260291): add support for signed out profiles.
+ NOTREACHED() << "Lacros doesn't support signed-out profiles yet.";
+ return;
+ }
+
+ // Re-authenticate an existing account.
+ account_manager_facade->ShowReauthAccountDialog(
+ account_manager::AccountManagerFacade::AccountAdditionSource::
+ kChromeExtensionReauth,
+ email_hint);
+#elif BUILDFLAG(ENABLE_DICE_SUPPORT)
+ chrome::ScopedTabbedBrowserDisplayer displayer(profile);
+ Browser* browser = displayer.browser();
+
+ // Cannot sign in if browser cannot be displayed.
+ if (!browser)
+ return;
+
+ if (enable_sync) {
+ // Set a primary account.
+ browser->signin_view_controller()->ShowDiceEnableSyncTab(
+ signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS,
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO, email_hint);
+ } else {
+ // Add an account to the web without setting a primary account.
+ browser->signin_view_controller()->ShowDiceAddAccountTab(
+ signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS, email_hint);
+ }
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+} // namespace internal
+
+void EnableSyncFromSingleAccountPromo(
+ Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point) {
+ EnableSyncFromMultiAccountPromo(browser, account, access_point,
+ /*is_default_promo_account=*/true);
+}
+
+void EnableSyncFromMultiAccountPromo(Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point,
+ bool is_default_promo_account) {
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ internal::EnableSyncFromPromo(browser, account, access_point,
+ is_default_promo_account,
+ base::BindOnce(&CreateDiceTurnSyncOnHelper));
+#else
+ NOTREACHED();
+#endif
+}
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+namespace internal {
+void EnableSyncFromPromo(
+ Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point,
+ bool is_default_promo_account,
+ base::OnceCallback<
+ void(Profile* profile,
+ Browser* browser,
+ signin_metrics::AccessPoint signin_access_point,
+ signin_metrics::PromoAction signin_promo_action,
+ signin_metrics::Reason signin_reason,
+ const CoreAccountId& account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode)>
+ create_dice_turn_sync_on_helper_callback) {
+ DCHECK(browser);
+ DCHECK_NE(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN, access_point);
+ Profile* profile = browser->profile();
+ DCHECK(!profile->IsOffTheRecord());
+
+ if (IdentityManagerFactory::GetForProfile(profile)->HasPrimaryAccount(
+ signin::ConsentLevel::kSync)) {
+ DVLOG(1) << "There is already a primary account.";
+ return;
+ }
+
+ if (account.IsEmpty()) {
+ chrome::ShowBrowserSignin(browser, access_point,
+ signin::ConsentLevel::kSync);
+ return;
+ }
+
+ DCHECK(!account.account_id.empty());
+ DCHECK(!account.email.empty());
+ DCHECK(AccountConsistencyModeManager::IsDiceEnabledForProfile(profile));
+
+ signin_metrics::PromoAction promo_action =
+ is_default_promo_account
+ ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT
+ : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT;
+
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ bool needs_reauth_before_enable_sync =
+ !identity_manager->HasAccountWithRefreshToken(account.account_id) ||
+ identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
+ account.account_id);
+ if (needs_reauth_before_enable_sync) {
+ browser->signin_view_controller()->ShowDiceEnableSyncTab(
+ access_point, promo_action, account.email);
+ return;
+ }
+
+ signin_metrics::LogSigninAccessPointStarted(access_point, promo_action);
+ signin_metrics::RecordSigninUserActionForAccessPoint(access_point,
+ promo_action);
+ std::move(create_dice_turn_sync_on_helper_callback)
+ .Run(profile, browser, access_point, promo_action,
+ signin_metrics::Reason::kSigninPrimaryAccount, account.account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT);
+}
+} // namespace internal
+
+std::vector<AccountInfo> GetAccountsForDicePromos(Profile* profile) {
+ // Fetch account ids for accounts that have a token.
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ std::vector<AccountInfo> accounts_with_tokens =
+ identity_manager->GetExtendedAccountInfoForAccountsWithRefreshToken();
+
+ // Compute the default account.
+ CoreAccountId default_account_id =
+ identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin);
+
+ // Fetch account information for each id and make sure that the first account
+ // in the list matches the unconsented primary account (if available).
+ std::vector<AccountInfo> accounts;
+ for (auto& account_info : accounts_with_tokens) {
+ DCHECK(!account_info.IsEmpty());
+ if (!signin::IsUsernameAllowedByPatternFromPrefs(
+ g_browser_process->local_state(), account_info.email)) {
+ continue;
+ }
+ if (account_info.account_id == default_account_id)
+ accounts.insert(accounts.begin(), std::move(account_info));
+ else
+ accounts.push_back(std::move(account_info));
+ }
+ return accounts;
+}
+
+AccountInfo GetSingleAccountForDicePromos(Profile* profile) {
+ std::vector<AccountInfo> accounts = GetAccountsForDicePromos(profile);
+ if (!accounts.empty())
+ return accounts[0];
+ return AccountInfo();
+}
+
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+std::u16string GetShortProfileIdentityToDisplay(
+ const ProfileAttributesEntry& profile_attributes_entry,
+ Profile* profile) {
+ DCHECK(profile);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ CoreAccountInfo core_info =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ // If there's no unconsented primary account, simply return the name of the
+ // profile according to profile attributes.
+ if (core_info.IsEmpty())
+ return profile_attributes_entry.GetName();
+
+ AccountInfo extended_info =
+ identity_manager->FindExtendedAccountInfoByAccountId(
+ core_info.account_id);
+ // If there's no given name available, return the user email.
+ if (extended_info.given_name.empty())
+ return base::UTF8ToUTF16(core_info.email);
+
+ return base::UTF8ToUTF16(extended_info.given_name);
+}
+
+std::string GetAllowedDomain(std::string signin_pattern) {
+ std::vector<std::string> splitted_signin_pattern = base::SplitString(
+ signin_pattern, "@", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ // There are more than one '@'s in the pattern.
+ if (splitted_signin_pattern.size() != 2)
+ return std::string();
+
+ std::string domain = splitted_signin_pattern[1];
+
+ // Trims tailing '$' if existed.
+ if (!domain.empty() && domain.back() == '$')
+ domain.pop_back();
+
+ // Trims tailing '\E' if existed.
+ if (domain.size() > 1 &&
+ base::EndsWith(domain, "\\E", base::CompareCase::SENSITIVE))
+ domain.erase(domain.size() - 2);
+
+ // Check if there is any special character in the domain. Note that
+ // jsmith@[192.168.2.1] is not supported.
+ if (!re2::RE2::FullMatch(domain, "[a-zA-Z0-9\\-.]+"))
+ return std::string();
+
+ return domain;
+}
+
+bool ShouldShowAnimatedIdentityOnOpeningWindow(
+ const ProfileAttributesStorage& profile_attributes_storage,
+ Profile* profile) {
+ DCHECK(profile);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ DCHECK(identity_manager->AreRefreshTokensLoaded());
+
+ base::TimeTicks animation_last_shown =
+ AvatarButtonUserData::GetAnimatedIdentityLastShown(profile);
+ // When a new window is created, only show the animation if it was never shown
+ // for this profile, or if it was shown in another window in the last few
+ // seconds (because the user may have missed it).
+ if (!animation_last_shown.is_null() &&
+ base::TimeTicks::Now() - animation_last_shown >
+ kDelayForCrossWindowAnimationReplay) {
+ return false;
+ }
+
+ // Show the user identity for users with multiple profiles.
+ if (profile_attributes_storage.GetNumberOfProfiles() > 1) {
+ return true;
+ }
+
+ // Show the user identity for users with multiple signed-in accounts.
+ return identity_manager->GetAccountsWithRefreshTokens().size() > 1;
+}
+
+void RecordAnimatedIdentityTriggered(Profile* profile) {
+ AvatarButtonUserData::SetAnimatedIdentityLastShown(profile,
+ base::TimeTicks::Now());
+}
+
+void RecordAvatarIconHighlighted(Profile* profile) {
+ base::RecordAction(base::UserMetricsAction("AvatarToolbarButtonHighlighted"));
+}
+
+void RecordProfileMenuViewShown(Profile* profile) {
+ base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened"));
+ if (profile->IsRegularProfile()) {
+ base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Regular"));
+ // Record usage for profile switch promo.
+ feature_engagement::TrackerFactory::GetForBrowserContext(profile)
+ ->NotifyEvent("profile_menu_shown");
+ } else if (profile->IsGuestSession()) {
+ base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Guest"));
+ } else if (profile->IsIncognitoProfile()) {
+ base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Incognito"));
+ }
+
+ base::TimeTicks last_shown =
+ AvatarButtonUserData::GetAnimatedIdentityLastShown(profile);
+ if (!last_shown.is_null()) {
+ base::UmaHistogramLongTimes("Profile.Menu.OpenedAfterAvatarAnimation",
+ base::TimeTicks::Now() - last_shown);
+ }
+}
+
+void RecordProfileMenuClick(Profile* profile) {
+ base::RecordAction(
+ base::UserMetricsAction("ProfileMenu_ActionableItemClicked"));
+ if (profile->IsRegularProfile()) {
+ base::RecordAction(
+ base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Regular"));
+ } else if (profile->IsGuestSession()) {
+ base::RecordAction(
+ base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Guest"));
+ } else if (profile->IsIncognitoProfile()) {
+ base::RecordAction(
+ base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Incognito"));
+ }
+}
+
+void RecordTransactionalReauthResult(
+ signin_metrics::ReauthAccessPoint access_point,
+ signin::ReauthResult result) {
+ const char kHistogramName[] = "Signin.TransactionalReauthResult";
+ base::UmaHistogramEnumeration(kHistogramName, result);
+
+ std::string access_point_suffix =
+ GetReauthAccessPointHistogramSuffix(access_point);
+ if (!access_point_suffix.empty()) {
+ std::string suffixed_histogram_name =
+ base::StrCat({kHistogramName, ".", access_point_suffix});
+ base::UmaHistogramEnumeration(suffixed_histogram_name, result);
+ }
+}
+
+void RecordTransactionalReauthUserAction(
+ signin_metrics::ReauthAccessPoint access_point,
+ SigninReauthViewController::UserAction user_action) {
+ const char kHistogramName[] = "Signin.TransactionalReauthUserAction";
+ base::UmaHistogramEnumeration(kHistogramName, user_action);
+
+ std::string access_point_suffix =
+ GetReauthAccessPointHistogramSuffix(access_point);
+ if (!access_point_suffix.empty()) {
+ std::string suffixed_histogram_name =
+ base::StrCat({kHistogramName, ".", access_point_suffix});
+ base::UmaHistogramEnumeration(suffixed_histogram_name, user_action);
+ }
+}
+
+} // namespace signin_ui_util
diff --git a/chromium/chrome/browser/signin/signin_ui_util.h b/chromium/chrome/browser/signin/signin_ui_util.h
new file mode 100644
index 00000000000..075952e959b
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_ui_util.h
@@ -0,0 +1,188 @@
+// Copyright (c) 2013 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_
+
+#include <string>
+#include <vector>
+
+#include "base/callback_forward.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/signin/reauth_result.h"
+#include "chrome/browser/ui/signin_reauth_view_controller.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/base/signin_metrics.h"
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#endif
+
+struct AccountInfo;
+class Browser;
+class Profile;
+class ProfileAttributesEntry;
+class ProfileAttributesStorage;
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+namespace account_manager {
+class AccountManagerFacade;
+}
+#endif
+
+// Utility functions to gather status information from the various signed in
+// services and construct messages suitable for showing in UI.
+namespace signin_ui_util {
+
+// The maximum number of times to show the welcome tutorial for an upgrade user.
+const int kUpgradeWelcomeTutorialShowMax = 1;
+
+// Returns the username of the primary account or an empty string if there is
+// no primary account or the account has not consented to browser sync.
+std::u16string GetAuthenticatedUsername(Profile* profile);
+
+// Initializes signin-related preferences.
+void InitializePrefsForProfile(Profile* profile);
+
+// Shows a learn more page for signin errors.
+void ShowSigninErrorLearnMorePage(Profile* profile);
+
+// Shows a reauth page/dialog to reauthanticate a primary account in error
+// state.
+void ShowReauthForPrimaryAccountWithAuthError(
+ Browser* browser,
+ signin_metrics::AccessPoint access_point);
+
+// Delegates to an existing sign-in tab if one exists. If not, a new sign-in tab
+// is created.
+void ShowExtensionSigninPrompt(Profile* profile,
+ bool enable_sync,
+ const std::string& email_hint);
+
+namespace internal {
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+// Same as `ShowReauthForPrimaryAccountWithAuthError` but with a getter function
+// for AccountManagerFacade so that it can be unit tested.
+void ShowReauthForPrimaryAccountWithAuthErrorLacros(
+ Browser* browser,
+ signin_metrics::AccessPoint access_point,
+ account_manager::AccountManagerFacade* account_manager_facade);
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+void ShowExtensionSigninPrompt(
+ Profile* profile,
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ account_manager::AccountManagerFacade* account_manager_facade,
+#endif
+ bool enable_sync,
+ const std::string& email_hint);
+#endif
+} // namespace internal
+
+// This function is used to enable sync for a given account:
+// * This function does nothing if the user is already signed in to Chrome.
+// * If |account| is empty, then it presents the Chrome sign-in page.
+// * If token service has an invalid refreh token for account |account|,
+// then it presents the Chrome sign-in page with |account.emil| prefilled.
+// * If token service has a valid refresh token for |account|, then it
+// enables sync for |account|.
+void EnableSyncFromSingleAccountPromo(Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point);
+
+// This function is used to enable sync for a given account. It has the same
+// behavior as |EnableSyncFromSingleAccountPromo()| except that it also logs
+// some additional information if the action is started from a promo that
+// supports selecting the account that may be used for sync.
+//
+// |is_default_promo_account| is true if |account| corresponds to the default
+// account in the promo. It is ignored if |account| is empty.
+void EnableSyncFromMultiAccountPromo(Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point,
+ bool is_default_promo_account);
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+// Returns the list of all accounts that have a token. The unconsented primary
+// account will be the first account in the list.
+std::vector<AccountInfo> GetAccountsForDicePromos(Profile* profile);
+
+// Returns single account to use in Dice promos.
+AccountInfo GetSingleAccountForDicePromos(Profile* profile);
+
+#endif
+
+// Returns the short user identity to display for |profile|. It is based on the
+// current unconsented primary account (if exists).
+// TODO(crbug.com/1012179): Move this logic into ProfileAttributesEntry once
+// AvatarToolbarButton becomes an observer of ProfileAttributesStorage and thus
+// ProfileAttributesEntry is up-to-date when AvatarToolbarButton needs it.
+std::u16string GetShortProfileIdentityToDisplay(
+ const ProfileAttributesEntry& profile_attributes_entry,
+ Profile* profile);
+
+// Returns the domain of the policy value of RestrictSigninToPattern. Returns
+// an empty string if the policy is not set or can not be parsed. The parser
+// only supports the policy value that matches [^@]+@[a-zA-Z0-9\-.]+(\\E)?\$?$.
+// Also, the parser does not validate the policy value.
+std::string GetAllowedDomain(std::string signin_pattern);
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+namespace internal {
+// Same as |EnableSyncFromPromo| but with a callback that creates a
+// DiceTurnSyncOnHelper so that it can be unit tested.
+void EnableSyncFromPromo(
+ Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point,
+ bool is_default_promo_account,
+ base::OnceCallback<
+ void(Profile* profile,
+ Browser* browser,
+ signin_metrics::AccessPoint signin_access_point,
+ signin_metrics::PromoAction signin_promo_action,
+ signin_metrics::Reason signin_reason,
+ const CoreAccountId& account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode)>
+ create_dice_turn_sync_on_helper_callback);
+} // namespace internal
+#endif
+
+// Returns whether Chrome should show the identity of the user (using a brief
+// animation) on opening a new window. IdentityManager's refresh tokens must be
+// loaded when this function gets called.
+bool ShouldShowAnimatedIdentityOnOpeningWindow(
+ const ProfileAttributesStorage& profile_attributes_storage,
+ Profile* profile);
+
+// Records that the animated identity was shown for the given profile. This is
+// used for metrics and to decide whether/when the animation can be shown again.
+void RecordAnimatedIdentityTriggered(Profile* profile);
+
+// Records that the avatar icon was highlighted for the given profile. This is
+// used for metrics.
+void RecordAvatarIconHighlighted(Profile* profile);
+
+// Called when the ProfileMenuView is opened. Used for metrics.
+void RecordProfileMenuViewShown(Profile* profile);
+
+// Called when a button/link in the profile menu was clicked.
+void RecordProfileMenuClick(Profile* profile);
+
+// Records the result of a re-auth challenge to finish a transaction (like
+// unlocking the account store for passwords).
+void RecordTransactionalReauthResult(
+ signin_metrics::ReauthAccessPoint access_point,
+ signin::ReauthResult result);
+
+// Records user action performed in a transactional reauth dialog/tab.
+void RecordTransactionalReauthUserAction(
+ signin_metrics::ReauthAccessPoint access_point,
+ SigninReauthViewController::UserAction user_action);
+
+} // namespace signin_ui_util
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_
diff --git a/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc b/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc
new file mode 100644
index 00000000000..299e45539d6
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc
@@ -0,0 +1,85 @@
+// Copyright 2021 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_
+
+#include "chrome/browser/signin/signin_ui_util.h"
+
+#include "base/callback_helpers.h"
+#include "base/test/bind.h"
+#include "build/buildflag.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "content/public/test/browser_test.h"
+
+#if !BUILDFLAG(ENABLE_DICE_SUPPORT)
+#error This file only contains DICE browser tests for now.
+#endif
+
+namespace signin_ui_util {
+
+class DiceSigninUiUtilBrowserTest : public InProcessBrowserTest {
+ public:
+ DiceSigninUiUtilBrowserTest() = default;
+ ~DiceSigninUiUtilBrowserTest() override = default;
+
+ Profile* CreateProfile() {
+ Profile* new_profile = nullptr;
+ base::RunLoop run_loop;
+ ProfileManager::CreateMultiProfileAsync(
+ u"test_profile", /*icon_index=*/0, /*is_hidden=*/false,
+ base::BindLambdaForTesting(
+ [&new_profile, &run_loop](Profile* profile,
+ Profile::CreateStatus status) {
+ ASSERT_NE(status, Profile::CREATE_STATUS_LOCAL_FAIL);
+ if (status == Profile::CREATE_STATUS_INITIALIZED) {
+ new_profile = profile;
+ run_loop.Quit();
+ }
+ }));
+ run_loop.Run();
+ return new_profile;
+ }
+
+ private:
+};
+
+// Tests that `ShowExtensionSigninPrompt()` doesn't crash when it cannot create
+// a new browser. Regression test for https://crbug.com/1273370.
+IN_PROC_BROWSER_TEST_F(DiceSigninUiUtilBrowserTest,
+ ShowExtensionSigninPrompt_NoBrowser) {
+ Profile* new_profile = CreateProfile();
+
+ // New profile should not have any browser windows.
+ EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile));
+
+ ShowExtensionSigninPrompt(new_profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ // `ShowExtensionSigninPrompt()` creates a new browser.
+ Browser* browser = chrome::FindBrowserWithProfile(new_profile);
+ ASSERT_TRUE(browser);
+ EXPECT_EQ(1, browser->tab_strip_model()->count());
+
+ // Profile deletion closes the browser.
+ g_browser_process->profile_manager()->ScheduleProfileForDeletion(
+ new_profile->GetPath(), base::DoNothing());
+ ui_test_utils::WaitForBrowserToClose(browser);
+ EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile));
+
+ // `ShowExtensionSigninPrompt()` does nothing for deleted profile.
+ ShowExtensionSigninPrompt(new_profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile));
+}
+
+} // namespace signin_ui_util
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_
diff --git a/chromium/chrome/browser/signin/signin_ui_util_unittest.cc b/chromium/chrome/browser/signin/signin_ui_util_unittest.cc
new file mode 100644
index 00000000000..43be4319fcc
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_ui_util_unittest.cc
@@ -0,0 +1,736 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_ui_util.h"
+
+#include "base/bind.h"
+#include "base/memory/raw_ptr.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/metrics/user_action_tester.h"
+#include "base/test/task_environment.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile_attributes_init_params.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/signin/signin_promo.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/test/base/browser_with_test_window_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/account_id/account_id.h"
+#include "components/google/core/common/google_util.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "components/account_manager_core/mock_account_manager_facade.h"
+#endif
+
+namespace signin_ui_util {
+
+namespace {
+const char kMainEmail[] = "main_email@example.com";
+const char kMainGaiaID[] = "main_gaia_id";
+const char kSecondaryEmail[] = "secondary_email@example.com";
+const char kSecondaryGaiaID[] = "secondary_gaia_id";
+} // namespace
+
+class GetAllowedDomainTest : public ::testing::Test {};
+
+TEST_F(GetAllowedDomainTest, WithInvalidPattern) {
+ EXPECT_EQ(std::string(), GetAllowedDomain("email"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("email@a@b"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("email@a[b"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("@$"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("@\\E$"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("@\\E$a"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("email@"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("@"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("example@a.com|example@b.com"));
+ EXPECT_EQ(std::string(), GetAllowedDomain(""));
+}
+
+TEST_F(GetAllowedDomainTest, WithValidPattern) {
+ EXPECT_EQ("example.com", GetAllowedDomain("email@example.com"));
+ EXPECT_EQ("example.com", GetAllowedDomain("email@example.com\\E"));
+ EXPECT_EQ("example.com", GetAllowedDomain("email@example.com$"));
+ EXPECT_EQ("example.com", GetAllowedDomain("email@example.com\\E$"));
+ EXPECT_EQ("example.com", GetAllowedDomain("*@example.com\\E$"));
+ EXPECT_EQ("example.com", GetAllowedDomain(".*@example.com\\E$"));
+ EXPECT_EQ("example-1.com", GetAllowedDomain("email@example-1.com"));
+}
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+namespace {
+
+class SigninUiUtilTestBrowserWindow : public TestBrowserWindow {
+ public:
+ SigninUiUtilTestBrowserWindow() = default;
+
+ SigninUiUtilTestBrowserWindow(const SigninUiUtilTestBrowserWindow&) = delete;
+ SigninUiUtilTestBrowserWindow& operator=(
+ const SigninUiUtilTestBrowserWindow&) = delete;
+
+ ~SigninUiUtilTestBrowserWindow() override = default;
+ void set_browser(Browser* browser) { browser_ = browser; }
+
+ void ShowAvatarBubbleFromAvatarButton(
+ AvatarBubbleMode mode,
+ signin_metrics::AccessPoint access_point,
+ bool is_source_keyboard) override {
+ ASSERT_TRUE(browser_);
+ // Simulate what |BrowserView| does for a regular Chrome sign-in flow.
+ browser_->signin_view_controller()->ShowSignin(
+ profiles::BubbleViewMode::BUBBLE_VIEW_MODE_GAIA_SIGNIN, access_point);
+ }
+
+ private:
+ raw_ptr<Browser> browser_ = nullptr;
+};
+
+} // namespace
+
+class DiceSigninUiUtilTest : public BrowserWithTestWindowTest {
+ public:
+ DiceSigninUiUtilTest() = default;
+ ~DiceSigninUiUtilTest() override = default;
+
+ struct CreateDiceTurnSyncOnHelperParams {
+ public:
+ raw_ptr<Profile> profile = nullptr;
+ raw_ptr<Browser> browser = nullptr;
+ signin_metrics::AccessPoint signin_access_point =
+ signin_metrics::AccessPoint::ACCESS_POINT_MAX;
+ signin_metrics::PromoAction signin_promo_action =
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
+ signin_metrics::Reason signin_reason =
+ signin_metrics::Reason::kUnknownReason;
+ CoreAccountId account_id;
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode =
+ DiceTurnSyncOnHelper::SigninAbortedMode::REMOVE_ACCOUNT;
+ };
+
+ void CreateDiceTurnSyncOnHelper(
+ Profile* profile,
+ Browser* browser,
+ signin_metrics::AccessPoint signin_access_point,
+ signin_metrics::PromoAction signin_promo_action,
+ signin_metrics::Reason signin_reason,
+ const CoreAccountId& account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode) {
+ create_dice_turn_sync_on_helper_called_ = true;
+ create_dice_turn_sync_on_helper_params_.profile = profile;
+ create_dice_turn_sync_on_helper_params_.browser = browser;
+ create_dice_turn_sync_on_helper_params_.signin_access_point =
+ signin_access_point;
+ create_dice_turn_sync_on_helper_params_.signin_promo_action =
+ signin_promo_action;
+ create_dice_turn_sync_on_helper_params_.signin_reason = signin_reason;
+ create_dice_turn_sync_on_helper_params_.account_id = account_id;
+ create_dice_turn_sync_on_helper_params_.signin_aborted_mode =
+ signin_aborted_mode;
+ }
+
+ protected:
+ // BrowserWithTestWindowTest:
+ void SetUp() override {
+ BrowserWithTestWindowTest::SetUp();
+ static_cast<SigninUiUtilTestBrowserWindow*>(browser()->window())
+ ->set_browser(browser());
+ }
+
+ // BrowserWithTestWindowTest:
+ TestingProfile::TestingFactories GetTestingFactories() override {
+ return IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories();
+ }
+
+ // BrowserWithTestWindowTest:
+ std::unique_ptr<BrowserWindow> CreateBrowserWindow() override {
+ return std::make_unique<SigninUiUtilTestBrowserWindow>();
+ }
+
+ // Returns the identity manager.
+ signin::IdentityManager* GetIdentityManager() {
+ return IdentityManagerFactory::GetForProfile(profile());
+ }
+
+ void EnableSync(const AccountInfo& account_info,
+ bool is_default_promo_account) {
+ signin_ui_util::internal::EnableSyncFromPromo(
+ browser(), account_info, access_point_, is_default_promo_account,
+ base::BindOnce(&DiceSigninUiUtilTest::CreateDiceTurnSyncOnHelper,
+ base::Unretained(this)));
+ }
+
+ void ExpectNoSigninStartedHistograms(
+ const base::HistogramTester& histogram_tester) {
+ histogram_tester.ExpectTotalCount("Signin.SigninStartedAccessPoint", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ }
+
+ void ExpectOneSigninStartedHistograms(
+ const base::HistogramTester& histogram_tester,
+ signin_metrics::PromoAction expected_promo_action) {
+ histogram_tester.ExpectUniqueSample("Signin.SigninStartedAccessPoint",
+ access_point_, 1);
+ switch (expected_promo_action) {
+ case signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ break;
+ case signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.WithDefault", access_point_, 1);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ break;
+ case signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.NotDefault", access_point_, 1);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ break;
+ case signin_metrics::PromoAction::
+ PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount",
+ access_point_, 1);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ break;
+ case signin_metrics::PromoAction::
+ PROMO_ACTION_NEW_ACCOUNT_EXISTING_ACCOUNT:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount",
+ access_point_, 1);
+ break;
+ }
+ }
+
+ signin_metrics::AccessPoint access_point_ =
+ signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE;
+
+ bool create_dice_turn_sync_on_helper_called_ = false;
+ CreateDiceTurnSyncOnHelperParams create_dice_turn_sync_on_helper_params_;
+};
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncWithExistingAccount) {
+ CoreAccountId account_id =
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ for (bool is_default_promo_account : {true, false}) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(0, user_action_tester.GetActionCount(
+ "Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(
+ GetIdentityManager()->FindExtendedAccountInfoByAccountId(account_id),
+ is_default_promo_account);
+ signin_metrics::PromoAction expected_promo_action =
+ is_default_promo_account
+ ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT
+ : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT;
+ ASSERT_TRUE(create_dice_turn_sync_on_helper_called_);
+ ExpectOneSigninStartedHistograms(histogram_tester, expected_promo_action);
+
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_Signin_FromBookmarkBubble"));
+ if (is_default_promo_account) {
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_SigninWithDefault_FromBookmarkBubble"));
+ } else {
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_SigninNotDefault_FromBookmarkBubble"));
+ }
+
+ // Verify that the helper to enable sync is created with the expected
+ // params.
+ EXPECT_EQ(profile(), create_dice_turn_sync_on_helper_params_.profile);
+ EXPECT_EQ(browser(), create_dice_turn_sync_on_helper_params_.browser);
+ EXPECT_EQ(account_id, create_dice_turn_sync_on_helper_params_.account_id);
+ EXPECT_EQ(signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE,
+ create_dice_turn_sync_on_helper_params_.signin_access_point);
+ EXPECT_EQ(expected_promo_action,
+ create_dice_turn_sync_on_helper_params_.signin_promo_action);
+ EXPECT_EQ(signin_metrics::Reason::kSigninPrimaryAccount,
+ create_dice_turn_sync_on_helper_params_.signin_reason);
+ EXPECT_EQ(DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT,
+ create_dice_turn_sync_on_helper_params_.signin_aborted_mode);
+ }
+}
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncWithAccountThatNeedsReauth) {
+ AddTab(browser(), GURL("http://example.com"));
+ CoreAccountId account_id =
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ // Add an account and then put its refresh token into an error state to
+ // require a reauth before enabling sync.
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ GetIdentityManager(), account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+
+ for (bool is_default_promo_account : {true, false}) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(0, user_action_tester.GetActionCount(
+ "Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(
+ GetIdentityManager()->FindExtendedAccountInfoByAccountId(account_id),
+ is_default_promo_account);
+ ASSERT_FALSE(create_dice_turn_sync_on_helper_called_);
+
+ ExpectOneSigninStartedHistograms(
+ histogram_tester,
+ is_default_promo_account
+ ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT
+ : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT);
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_Signin_FromBookmarkBubble"));
+
+ if (is_default_promo_account) {
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_SigninWithDefault_FromBookmarkBubble"));
+ } else {
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_SigninNotDefault_FromBookmarkBubble"));
+ }
+
+ // Verify that the active tab has the correct DICE sign-in URL.
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ content::WebContents* active_contents = tab_strip->GetActiveWebContents();
+ ASSERT_TRUE(active_contents);
+ EXPECT_EQ(signin::GetChromeSyncURLForDice(kMainEmail,
+ google_util::kGoogleHomepageURL),
+ active_contents->GetVisibleURL());
+ tab_strip->CloseWebContentsAt(
+ tab_strip->GetIndexOfWebContents(active_contents),
+ TabStripModel::CLOSE_USER_GESTURE);
+ }
+}
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithNoTab) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(
+ 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/);
+ ASSERT_FALSE(create_dice_turn_sync_on_helper_called_);
+
+ ExpectOneSigninStartedHistograms(
+ histogram_tester, signin_metrics::PromoAction::
+ PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(1,
+ user_action_tester.GetActionCount(
+ "Signin_SigninNewAccountNoExistingAccount_FromBookmarkBubble"));
+
+ // Verify that the active tab has the correct DICE sign-in URL.
+ content::WebContents* active_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ ASSERT_TRUE(active_contents);
+ EXPECT_EQ(
+ signin::GetChromeSyncURLForDice("", google_util::kGoogleHomepageURL),
+ active_contents->GetVisibleURL());
+}
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithNoTabWithExisting) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(
+ 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/);
+ ASSERT_FALSE(create_dice_turn_sync_on_helper_called_);
+
+ ExpectOneSigninStartedHistograms(
+ histogram_tester,
+ signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_EXISTING_ACCOUNT);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(1,
+ user_action_tester.GetActionCount(
+ "Signin_SigninNewAccountExistingAccount_FromBookmarkBubble"));
+}
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithOneTab) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+ AddTab(browser(), GURL("http://foo/1"));
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(
+ 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/);
+ ASSERT_FALSE(create_dice_turn_sync_on_helper_called_);
+
+ ExpectOneSigninStartedHistograms(
+ histogram_tester, signin_metrics::PromoAction::
+ PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(1,
+ user_action_tester.GetActionCount(
+ "Signin_SigninNewAccountNoExistingAccount_FromBookmarkBubble"));
+
+ // Verify that the active tab has the correct DICE sign-in URL.
+ content::WebContents* active_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ ASSERT_TRUE(active_contents);
+ EXPECT_EQ(
+ signin::GetChromeSyncURLForDice("", google_util::kGoogleHomepageURL),
+ active_contents->GetVisibleURL());
+}
+
+TEST_F(DiceSigninUiUtilTest, GetAccountsForDicePromos) {
+ // Should start off with no accounts.
+ std::vector<AccountInfo> accounts = GetAccountsForDicePromos(profile());
+ EXPECT_TRUE(accounts.empty());
+
+ // TODO(tangltom): Flesh out this test.
+}
+
+TEST_F(DiceSigninUiUtilTest, MergeDiceSigninTab) {
+ base::UserActionTester user_action_tester;
+ EnableSync(AccountInfo(), false);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ // Signin tab is reused.
+ EnableSync(AccountInfo(), false);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ // Give focus to a different tab.
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ ASSERT_EQ(0, tab_strip->active_index());
+ GURL other_url = GURL("http://example.com");
+ AddTab(browser(), other_url);
+ tab_strip->ActivateTabAt(0, {TabStripModel::GestureType::kOther});
+ ASSERT_EQ(other_url, tab_strip->GetActiveWebContents()->GetVisibleURL());
+ ASSERT_EQ(0, tab_strip->active_index());
+
+ // Extensions re-use the tab but do not take focus.
+ access_point_ = signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS;
+ EnableSync(AccountInfo(), false);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(0, tab_strip->active_index());
+
+ // Other access points re-use the tab and take focus.
+ access_point_ = signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS;
+ EnableSync(AccountInfo(), false);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(1, tab_strip->active_index());
+}
+
+TEST_F(DiceSigninUiUtilTest, ShowReauthTab) {
+ AddTab(browser(), GURL("http://example.com"));
+ AccountInfo account_info = signin::MakePrimaryAccountAvailable(
+ GetIdentityManager(), "foo@example.com", signin::ConsentLevel::kSync);
+
+ // Add an account and then put its refresh token into an error state to
+ // require a reauth before enabling sync.
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ GetIdentityManager(), account_info.account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+
+ signin_ui_util::ShowReauthForPrimaryAccountWithAuthError(
+ browser(),
+ signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN);
+
+ // Verify that the active tab has the correct DICE sign-in URL.
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ content::WebContents* active_contents = tab_strip->GetActiveWebContents();
+ ASSERT_TRUE(active_contents);
+ EXPECT_EQ(signin::GetChromeSyncURLForDice(account_info.email,
+ google_util::kGoogleHomepageURL),
+ active_contents->GetVisibleURL());
+}
+
+TEST_F(DiceSigninUiUtilTest,
+ ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsTrueForMultiProfiles) {
+ const char kSecondProfile[] = "SecondProfile";
+ const char16_t kSecondProfile16[] = u"SecondProfile";
+ const base::FilePath profile_path =
+ profile_manager()->profiles_dir().AppendASCII(kSecondProfile);
+ ProfileAttributesInitParams params;
+ params.profile_path = profile_path;
+ params.profile_name = kSecondProfile16;
+ profile_manager()->profile_attributes_storage()->AddProfile(
+ std::move(params));
+
+ EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager()->profile_attributes_storage(), profile()));
+}
+
+TEST_F(DiceSigninUiUtilTest,
+ ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsTrueForMultiSignin) {
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kSecondaryGaiaID, kSecondaryEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager()->profile_attributes_storage(), profile()));
+
+ // The identity can be shown again immediately (which is what happens if there
+ // is multiple windows at startup).
+ RecordAnimatedIdentityTriggered(profile());
+ EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager()->profile_attributes_storage(), profile()));
+}
+
+TEST_F(
+ DiceSigninUiUtilTest,
+ ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsFalseForSingleProfileSingleSignin) {
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ EXPECT_FALSE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager()->profile_attributes_storage(), profile()));
+}
+
+TEST_F(DiceSigninUiUtilTest, ShowExtensionSigninPrompt) {
+ Profile* profile = browser()->profile();
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(1, tab_strip->count());
+ // Calling the function again reuses the tab.
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(1, tab_strip->count());
+
+ content::WebContents* tab = tab_strip->GetWebContentsAt(0);
+ ASSERT_TRUE(tab);
+ EXPECT_TRUE(base::StartsWith(
+ tab->GetVisibleURL().spec(),
+ GaiaUrls::GetInstance()->signin_chrome_sync_dice().spec(),
+ base::CompareCase::INSENSITIVE_ASCII));
+
+ // Changing the parameter opens a new tab.
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/false,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(2, tab_strip->count());
+ // Calling the function again reuses the tab.
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/false,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(2, tab_strip->count());
+ tab = tab_strip->GetWebContentsAt(1);
+ ASSERT_TRUE(tab);
+ EXPECT_TRUE(
+ base::StartsWith(tab->GetVisibleURL().spec(),
+ GaiaUrls::GetInstance()->add_account_url().spec(),
+ base::CompareCase::INSENSITIVE_ASCII));
+}
+
+TEST_F(DiceSigninUiUtilTest, ShowExtensionSigninPrompt_AsLockedProfile) {
+ signin_util::ScopedForceSigninSetterForTesting force_signin_setter(true);
+ Profile* profile = browser()->profile();
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->LockForceSigninProfile(true);
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(0, tab_strip->count());
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/false,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(0, tab_strip->count());
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+class MirrorSigninUiUtilTest : public BrowserWithTestWindowTest {
+ public:
+ MirrorSigninUiUtilTest() = default;
+ ~MirrorSigninUiUtilTest() override = default;
+
+ // BrowserWithTestWindowTest:
+ TestingProfile::TestingFactories GetTestingFactories() override {
+ return IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories();
+ }
+};
+
+TEST_F(MirrorSigninUiUtilTest, ShowReauthDialog) {
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile());
+ const std::string kEmail = "foo@example.com";
+ AccountInfo account_info = signin::MakePrimaryAccountAvailable(
+ identity_manager, kEmail, signin::ConsentLevel::kSync);
+
+ // Add an account and then put its refresh token into an error state to
+ // require a reauth before enabling sync.
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ identity_manager, account_info.account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+
+ account_manager::MockAccountManagerFacade mock_facade;
+
+ EXPECT_CALL(mock_facade,
+ ShowReauthAccountDialog(
+ account_manager::AccountManagerFacade::AccountAdditionSource::
+ kAvatarBubbleReauthAccountButton,
+ kEmail));
+ internal::ShowReauthForPrimaryAccountWithAuthErrorLacros(
+ browser(),
+ signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN,
+ &mock_facade);
+}
+
+TEST_F(MirrorSigninUiUtilTest, ShowExtensionSigninPrompt) {
+ const std::string kEmail = "foo@example.com";
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ account_manager::MockAccountManagerFacade mock_facade;
+
+ EXPECT_CALL(
+ mock_facade,
+ ShowReauthAccountDialog(account_manager::AccountManagerFacade::
+ AccountAdditionSource::kChromeExtensionReauth,
+ kEmail));
+ internal::ShowExtensionSigninPrompt(browser()->profile(), &mock_facade,
+ /*enable_sync=*/true, kEmail);
+ // No tabs should be opened.
+ EXPECT_EQ(0, tab_strip->count());
+}
+
+TEST_F(MirrorSigninUiUtilTest, ShowExtensionSigninPrompt_AsLockedProfile) {
+ signin_util::ScopedForceSigninSetterForTesting force_signin_setter(true);
+ Profile* profile = browser()->profile();
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->LockForceSigninProfile(true);
+
+ const std::string kEmail = "foo@example.com";
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ account_manager::MockAccountManagerFacade mock_facade;
+
+ EXPECT_CALL(mock_facade, ShowReauthAccountDialog(testing::_, testing::_))
+ .Times(0);
+ internal::ShowExtensionSigninPrompt(browser()->profile(), &mock_facade,
+ /*enable_sync=*/true, kEmail);
+ // No dialogs and tabs should be opened.
+ EXPECT_EQ(0, tab_strip->count());
+}
+
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+// This test does not use the DiceSigninUiUtilTest test fixture, because it
+// needs a mock time environment, and BrowserWithTestWindowTest may be flaky
+// when used with mock time (see https://crbug.com/1014790).
+TEST(ShouldShowAnimatedIdentityOnOpeningWindow, ReturnsFalseForNewWindow) {
+ // Setup a testing profile manager with mock time.
+ content::BrowserTaskEnvironment task_environment(
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME);
+ ScopedTestingLocalState local_state(TestingBrowserProcess::GetGlobal());
+ TestingProfileManager profile_manager(TestingBrowserProcess::GetGlobal(),
+ &local_state);
+ ASSERT_TRUE(profile_manager.SetUp());
+ std::string name("testing_profile");
+ TestingProfile* profile = profile_manager.CreateTestingProfile(
+ name, std::unique_ptr<sync_preferences::PrefServiceSyncable>(),
+ base::UTF8ToUTF16(name), 0, std::string(),
+ IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories());
+
+ // Setup accounts.
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ identity_manager->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ identity_manager->GetAccountsMutator()->AddOrUpdateAccount(
+ kSecondaryGaiaID, kSecondaryEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager.profile_attributes_storage(), profile));
+
+ // Animation is shown once.
+ RecordAnimatedIdentityTriggered(profile);
+
+ // Wait a few seconds.
+ task_environment.FastForwardBy(base::Seconds(6));
+
+ // Animation is not shown again in a new window.
+ EXPECT_FALSE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager.profile_attributes_storage(), profile));
+}
+
+} // namespace signin_ui_util
diff --git a/chromium/chrome/browser/signin/signin_util.cc b/chromium/chrome/browser/signin/signin_util.cc
new file mode 100644
index 00000000000..d3ab11f8671
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util.cc
@@ -0,0 +1,382 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_util.h"
+
+#include <memory>
+
+#include "base/bind.h"
+#include "base/feature_list.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/supports_user_data.h"
+#include "base/task/post_task.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/policy/cloud/user_policy_signin_service_internal.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/ui/simple_message_box.h"
+#include "chrome/browser/ui/startup/startup_types.h"
+#include "chrome/browser/ui/webui/profile_helper.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/google/core/common/google_util.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_utils.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \
+ defined(OS_MAC)
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_list_observer.h"
+#include "chrome/browser/ui/browser_window.h"
+#define CAN_DELETE_PROFILE
+#endif
+
+namespace signin_util {
+namespace {
+
+constexpr char kSignoutSettingKey[] = "signout_setting";
+
+#if defined(CAN_DELETE_PROFILE)
+// Manager that presents the profile will be deleted dialog on the first active
+// browser window.
+class DeleteProfileDialogManager : public BrowserListObserver {
+ public:
+ class Delegate {
+ public:
+ // Called when the profile was marked for deletion. It is safe for the
+ // delegate to delete |manager| when this is called.
+ virtual void OnProfileDeleted(DeleteProfileDialogManager* manager) = 0;
+ };
+
+ DeleteProfileDialogManager(std::string primary_account_email,
+ Delegate* delegate)
+ : primary_account_email_(primary_account_email), delegate_(delegate) {}
+
+ DeleteProfileDialogManager(const DeleteProfileDialogManager&) = delete;
+ DeleteProfileDialogManager& operator=(const DeleteProfileDialogManager&) =
+ delete;
+
+ ~DeleteProfileDialogManager() override { BrowserList::RemoveObserver(this); }
+
+ void PresentDialogOnAllBrowserWindows(Profile* profile) {
+ DCHECK(profile);
+ DCHECK(profile_path_.empty());
+ profile_path_ = profile->GetPath();
+
+ BrowserList::AddObserver(this);
+ Browser* active_browser = chrome::FindLastActiveWithProfile(profile);
+ if (active_browser)
+ OnBrowserSetLastActive(active_browser);
+ }
+
+ void OnBrowserSetLastActive(Browser* browser) override {
+ DCHECK(!profile_path_.empty());
+
+ if (profile_path_ != browser->profile()->GetPath())
+ return;
+
+ active_browser_ = browser;
+
+ // Display the dialog on the next run loop as otherwise the dialog can block
+ // browser from displaying because the dialog creates a nested run loop.
+ //
+ // This happens because the browser window is not fully created yet when
+ // OnBrowserSetLastActive() is called. To finish the creation, the code
+ // needs to return from OnBrowserSetLastActive().
+ //
+ // However, if we open a warning dialog from OnBrowserSetLastActive()
+ // synchronously, it will create a nested run loop that will not return
+ // from OnBrowserSetLastActive() until the dialog is dismissed. But the user
+ // cannot dismiss the dialog because the browser is not even shown!
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&DeleteProfileDialogManager::ShowDeleteProfileDialog,
+ weak_factory_.GetWeakPtr(), browser));
+ }
+
+ // Called immediately after a browser becomes not active.
+ void OnBrowserNoLongerActive(Browser* browser) override {
+ if (active_browser_ == browser)
+ active_browser_ = nullptr;
+ }
+
+ void OnBrowserRemoved(Browser* browser) override {
+ if (active_browser_ == browser)
+ active_browser_ = nullptr;
+ }
+
+ private:
+ void ShowDeleteProfileDialog(Browser* browser) {
+ // Block opening dialog from nested task.
+ static bool is_dialog_shown = false;
+ if (is_dialog_shown)
+ return;
+ base::AutoReset<bool> auto_reset(&is_dialog_shown, true);
+
+ // Check that |browser| is still active.
+ if (!active_browser_ || active_browser_ != browser)
+ return;
+
+ // Show the dialog.
+ DCHECK(browser->window()->GetNativeWindow());
+ chrome::MessageBoxResult result = chrome::ShowWarningMessageBox(
+ browser->window()->GetNativeWindow(),
+ l10n_util::GetStringUTF16(IDS_PROFILE_WILL_BE_DELETED_DIALOG_TITLE),
+ l10n_util::GetStringFUTF16(
+ IDS_PROFILE_WILL_BE_DELETED_DIALOG_DESCRIPTION,
+ base::ASCIIToUTF16(primary_account_email_),
+ base::ASCIIToUTF16(
+ gaia::ExtractDomainName(primary_account_email_))));
+
+ switch (result) {
+ case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_NO: {
+ // If the warning dialog is automatically dismissed or the user closed
+ // the dialog by clicking on the close "X" button, then re-present the
+ // dialog (the user should not be able to interact with the browser
+ // window as the profile must be deleted).
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&DeleteProfileDialogManager::ShowDeleteProfileDialog,
+ weak_factory_.GetWeakPtr(), browser));
+ break;
+ }
+ case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_YES:
+ webui::DeleteProfileAtPath(
+ profile_path_,
+ ProfileMetrics::DELETE_PROFILE_PRIMARY_ACCOUNT_NOT_ALLOWED);
+ delegate_->OnProfileDeleted(this);
+ // |this| may be destroyed at this point. Avoid using it.
+ break;
+ case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_DEFERRED:
+ NOTREACHED() << "Message box must not return deferred result when run "
+ "synchronously";
+ break;
+ }
+ }
+
+ std::string primary_account_email_;
+ raw_ptr<Delegate> delegate_;
+ base::FilePath profile_path_;
+ raw_ptr<Browser> active_browser_;
+ base::WeakPtrFactory<DeleteProfileDialogManager> weak_factory_{this};
+};
+#endif // defined(CAN_DELETE_PROFILE)
+
+// Per-profile manager for the signout allowed setting.
+#if defined(CAN_DELETE_PROFILE)
+class UserSignoutSetting : public base::SupportsUserData::Data,
+ public DeleteProfileDialogManager::Delegate {
+#else
+class UserSignoutSetting : public base::SupportsUserData::Data {
+#endif // defined(CAN_DELETE_PROFILE)
+ public:
+ enum class State { kUndefined, kAllowed, kDisallowed };
+
+ // Fetch from Profile. Make and store if not already present.
+ static UserSignoutSetting* GetForProfile(Profile* profile) {
+ UserSignoutSetting* signout_setting = static_cast<UserSignoutSetting*>(
+ profile->GetUserData(kSignoutSettingKey));
+
+ if (!signout_setting) {
+ profile->SetUserData(kSignoutSettingKey,
+ std::make_unique<UserSignoutSetting>());
+ signout_setting = static_cast<UserSignoutSetting*>(
+ profile->GetUserData(kSignoutSettingKey));
+ }
+
+ return signout_setting;
+ }
+
+ State state() const { return state_; }
+ void set_state(State state) { state_ = state; }
+
+#if defined(CAN_DELETE_PROFILE)
+ // Shows the delete profile dialog on the first browser active window.
+ void ShowDeleteProfileDialog(Profile* profile, const std::string& email) {
+ if (delete_profile_dialog_manager_)
+ return;
+ delete_profile_dialog_manager_ =
+ std::make_unique<DeleteProfileDialogManager>(email, this);
+ delete_profile_dialog_manager_->PresentDialogOnAllBrowserWindows(profile);
+ }
+
+ void OnProfileDeleted(DeleteProfileDialogManager* dialog_manager) override {
+ DCHECK_EQ(delete_profile_dialog_manager_.get(), dialog_manager);
+ delete_profile_dialog_manager_.reset();
+ }
+#endif
+
+ private:
+ State state_ = State::kUndefined;
+
+#if defined(CAN_DELETE_PROFILE)
+ std::unique_ptr<DeleteProfileDialogManager> delete_profile_dialog_manager_;
+#endif
+};
+
+enum ForceSigninPolicyCache {
+ NOT_CACHED = 0,
+ ENABLE,
+ DISABLE
+} g_is_force_signin_enabled_cache = NOT_CACHED;
+
+void SetForceSigninPolicy(bool enable) {
+ g_is_force_signin_enabled_cache = enable ? ENABLE : DISABLE;
+}
+
+} // namespace
+
+ScopedForceSigninSetterForTesting::ScopedForceSigninSetterForTesting(
+ bool enable) {
+ SetForceSigninForTesting(enable); // IN-TEST
+}
+
+ScopedForceSigninSetterForTesting::~ScopedForceSigninSetterForTesting() {
+ ResetForceSigninForTesting(); // IN-TEST
+}
+
+bool IsForceSigninEnabled() {
+ if (g_is_force_signin_enabled_cache == NOT_CACHED) {
+ PrefService* prefs = g_browser_process->local_state();
+ if (prefs)
+ SetForceSigninPolicy(prefs->GetBoolean(prefs::kForceBrowserSignin));
+ else
+ return false;
+ }
+ return (g_is_force_signin_enabled_cache == ENABLE);
+}
+
+void SetForceSigninForTesting(bool enable) {
+ SetForceSigninPolicy(enable);
+}
+
+void ResetForceSigninForTesting() {
+ g_is_force_signin_enabled_cache = NOT_CACHED;
+}
+
+bool IsUserSignoutAllowedForProfile(Profile* profile) {
+ return UserSignoutSetting::GetForProfile(profile)->state() ==
+ UserSignoutSetting::State::kAllowed;
+}
+
+void EnsureUserSignoutAllowedIsInitializedForProfile(Profile* profile) {
+ if (UserSignoutSetting::GetForProfile(profile)->state() ==
+ UserSignoutSetting::State::kUndefined) {
+ SetUserSignoutAllowedForProfile(profile, true);
+ }
+}
+
+void SetUserSignoutAllowedForProfile(Profile* profile, bool is_allowed) {
+ UserSignoutSetting::State new_state =
+ is_allowed ? UserSignoutSetting::State::kAllowed
+ : UserSignoutSetting::State::kDisallowed;
+ UserSignoutSetting::GetForProfile(profile)->set_state(new_state);
+}
+
+void EnsurePrimaryAccountAllowedForProfile(Profile* profile) {
+// All primary accounts are allowed on ChromeOS, so this method is a no-op on
+// ChromeOS.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ if (!identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync))
+ return;
+
+ CoreAccountInfo primary_account =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ if (profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed) &&
+ signin::IsUsernameAllowedByPatternFromPrefs(
+ g_browser_process->local_state(), primary_account.email)) {
+ return;
+ }
+
+ UserSignoutSetting* signout_setting =
+ UserSignoutSetting::GetForProfile(profile);
+ switch (signout_setting->state()) {
+ case UserSignoutSetting::State::kUndefined:
+ NOTREACHED();
+ break;
+ case UserSignoutSetting::State::kAllowed: {
+ // Force clear the primary account if it is no longer allowed and if sign
+ // out is allowed.
+ auto* primary_account_mutator =
+ identity_manager->GetPrimaryAccountMutator();
+ primary_account_mutator->ClearPrimaryAccount(
+ signin_metrics::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT,
+ signin_metrics::SignoutDelete::kIgnoreMetric);
+ break;
+ }
+ case UserSignoutSetting::State::kDisallowed:
+#if defined(CAN_DELETE_PROFILE)
+ // Force remove the profile if sign out is not allowed and if the
+ // primary account is no longer allowed.
+ // This may be called while the profile is initializing, so it must be
+ // scheduled for later to allow the profile initialization to complete.
+ CHECK(profiles::IsMultipleProfilesEnabled());
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&UserSignoutSetting::ShowDeleteProfileDialog,
+ base::Unretained(signout_setting), profile,
+ primary_account.email));
+#else
+ CHECK(false) << "Deleting profiles is not supported.";
+#endif // defined(CAN_DELETE_PROFILE)
+ break;
+ }
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+}
+
+#if !defined(OS_ANDROID)
+bool ProfileSeparationEnforcedByPolicy(
+ Profile* profile,
+ const std::string& intercepted_account_level_policy_value) {
+ if (!base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync))
+ return false;
+ std::string current_profile_account_restriction =
+ profile->GetPrefs()->GetString(prefs::kManagedAccountsSigninRestriction);
+
+ bool is_machine_level_policy = profile->GetPrefs()->GetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine);
+
+ // Enforce profile separation for all new signins if any restriction is
+ // applied at a machine level.
+ if (is_machine_level_policy) {
+ return !current_profile_account_restriction.empty() &&
+ current_profile_account_restriction != "none";
+ }
+
+ // Enforce profile separation for all new signins if "primary_account_strict"
+ // is set at the user account level.
+ return current_profile_account_restriction == "primary_account_strict" ||
+ base::StartsWith(intercepted_account_level_policy_value,
+ "primary_account");
+}
+
+void RecordEnterpriseProfileCreationUserChoice(bool enforced_by_policy,
+ bool created) {
+ base::UmaHistogramBoolean(
+ enforced_by_policy
+ ? "Signin.Enterprise.WorkProfile.ProfileCreatedWithPolicySet"
+ : "Signin.Enterprise.WorkProfile.ProfileCreatedwithPolicyUnset",
+ created);
+}
+
+#endif
+
+} // namespace signin_util
diff --git a/chromium/chrome/browser/signin/signin_util.h b/chromium/chrome/browser/signin/signin_util.h
new file mode 100644
index 00000000000..10a436d9ea1
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util.h
@@ -0,0 +1,73 @@
+// Copyright 2017 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_
+
+#include <string>
+
+#include "build/build_config.h"
+
+class Profile;
+
+namespace signin_util {
+
+// This class calls ResetForceSigninForTesting when destroyed, so that
+// ForcedSigning doesn't leak across tests.
+class ScopedForceSigninSetterForTesting {
+ public:
+ explicit ScopedForceSigninSetterForTesting(bool enable);
+ ~ScopedForceSigninSetterForTesting();
+};
+
+// Return whether the force sign in policy is enabled or not.
+// The state of this policy will not be changed without relaunch Chrome.
+bool IsForceSigninEnabled();
+
+// Enable or disable force sign in for testing. Please use
+// ScopedForceSigninSetterForTesting instead, if possible. If not, make sure
+// ResetForceSigninForTesting is called before the test finishes.
+void SetForceSigninForTesting(bool enable);
+
+// Reset force sign in to uninitialized state for testing.
+void ResetForceSigninForTesting();
+
+// Returns true if clearing the primary profile is allowed.
+bool IsUserSignoutAllowedForProfile(Profile* profile);
+
+// Sign-out is allowed by default, but some Chrome profiles (e.g. for cloud-
+// managed enterprise accounts) may wish to disallow user-initiated sign-out.
+// Note that this exempts sign-outs that are not user-initiated (e.g. sign-out
+// triggered when cloud policy no longer allows current email pattern). See
+// ChromeSigninClient::PreSignOut().
+void SetUserSignoutAllowedForProfile(Profile* profile, bool is_allowed);
+
+// Updates the user sign-out state to |true| if is was never initialized.
+// This should be called at the end of the flow to initialize a profile to
+// ensure that the signout allowed flag is updated.
+void EnsureUserSignoutAllowedIsInitializedForProfile(Profile* profile);
+
+// Ensures that the primary account for |profile| is allowed:
+// * If profile does not have any primary account, then this is a no-op.
+// * If |IsUserSignoutAllowedForProfile| is allowed and the primary account
+// is no longer allowed, then this clears the primary account.
+// * If |IsUserSignoutAllowedForProfile| is not allowed and the primary account
+// is not longer allowed, then this removes the profile.
+void EnsurePrimaryAccountAllowedForProfile(Profile* profile);
+
+#if !defined(OS_ANDROID)
+// Returns true if profile separation is enforced by policy.
+bool ProfileSeparationEnforcedByPolicy(
+ Profile* profile,
+ const std::string& intercepted_account_level_policy_value);
+
+// Records a UMA metric if the user accepts or not to create an enterprise
+// profile.
+void RecordEnterpriseProfileCreationUserChoice(bool enforced_by_policy,
+ bool created);
+#endif
+
+} // namespace signin_util
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_
diff --git a/chromium/chrome/browser/signin/signin_util_unittest.cc b/chromium/chrome/browser/signin/signin_util_unittest.cc
new file mode 100644
index 00000000000..80862578339
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util_unittest.cc
@@ -0,0 +1,127 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_util.h"
+
+#include <memory>
+
+#include "base/feature_list.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/prefs/browser_prefs.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/browser_with_test_window_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/prefs/pref_service.h"
+#include "components/prefs/testing_pref_service.h"
+
+class SigninUtilTest : public BrowserWithTestWindowTest {
+ public:
+ void SetUp() override {
+ BrowserWithTestWindowTest::SetUp();
+ signin_util::ResetForceSigninForTesting();
+ }
+
+ void TearDown() override {
+ signin_util::ResetForceSigninForTesting();
+ BrowserWithTestWindowTest::TearDown();
+ }
+};
+
+TEST_F(SigninUtilTest, GetForceSigninPolicy) {
+ EXPECT_FALSE(signin_util::IsForceSigninEnabled());
+
+ g_browser_process->local_state()->SetBoolean(prefs::kForceBrowserSignin,
+ true);
+ signin_util::ResetForceSigninForTesting();
+ EXPECT_TRUE(signin_util::IsForceSigninEnabled());
+ g_browser_process->local_state()->SetBoolean(prefs::kForceBrowserSignin,
+ false);
+ signin_util::ResetForceSigninForTesting();
+ EXPECT_FALSE(signin_util::IsForceSigninEnabled());
+}
+
+#if !BUILDFLAG(IS_CHROMEOS_LACROS)
+class SigninUtilEnterpriseTest : public BrowserWithTestWindowTest {
+ public:
+ SigninUtilEnterpriseTest()
+ : feature_list_(kAccountPoliciesLoadedWithoutSync) {}
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(SigninUtilEnterpriseTest, ProfileSeparationEnforcedByPolicy) {
+ std::unique_ptr<TestingProfile> profile = TestingProfile::Builder().Build();
+
+ // No policy set on the active profile.
+ EXPECT_FALSE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_FALSE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+
+ // Active profile has "primary_account" as a user level policy.
+ profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+ profile->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, false);
+ EXPECT_FALSE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_FALSE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+
+ // Active profile has "primary_account_strict" as a user level policy.
+ profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ profile->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, false);
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_TRUE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+
+ // Active profile has "primary_account" as a machine level policy.
+ profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+ profile->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_TRUE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+
+ // Active profile has "primary_account_strict" as a machine level policy.
+ profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+ profile->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_TRUE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+}
+#endif
diff --git a/chromium/chrome/browser/signin/signin_util_win.cc b/chromium/chrome/browser/signin/signin_util_win.cc
new file mode 100644
index 00000000000..0ffc61db1b6
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util_win.cc
@@ -0,0 +1,332 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/signin_util_win.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/no_destructor.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/win/registry.h"
+#include "base/win/win_util.h"
+#include "base/win/wincrypt_shim.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/first_run/first_run.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_window.h"
+#include "chrome/browser/signin/about_signin_internals_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
+#include "chrome/browser/ui/webui/signin/signin_utils_desktop.h"
+#include "chrome/credential_provider/common/gcp_strings.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/about_signin_internals.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+namespace signin_util {
+
+namespace {
+
+std::unique_ptr<DiceTurnSyncOnHelper::Delegate>*
+GetDiceTurnSyncOnHelperDelegateForTestingStorage() {
+ static base::NoDestructor<std::unique_ptr<DiceTurnSyncOnHelper::Delegate>>
+ delegate;
+ return delegate.get();
+}
+
+std::string DecryptRefreshToken(const std::string& cipher_text) {
+ DATA_BLOB input;
+ input.pbData =
+ const_cast<BYTE*>(reinterpret_cast<const BYTE*>(cipher_text.data()));
+ input.cbData = static_cast<DWORD>(cipher_text.length());
+ DATA_BLOB output;
+ BOOL result = ::CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr,
+ CRYPTPROTECT_UI_FORBIDDEN, &output);
+
+ if (!result)
+ return std::string();
+
+ std::string refresh_token(reinterpret_cast<char*>(output.pbData),
+ output.cbData);
+ ::LocalFree(output.pbData);
+ return refresh_token;
+}
+
+// Finish the process of import credentials. This is either called directly
+// from ImportCredentialsFromProvider() if a browser window for the profile is
+// already available or is delayed until a browser can first be opened.
+void FinishImportCredentialsFromProvider(const CoreAccountId& account_id,
+ Browser* browser,
+ Profile* profile,
+ Profile::CreateStatus status) {
+ // DiceTurnSyncOnHelper deletes itself once done.
+ if (GetDiceTurnSyncOnHelperDelegateForTestingStorage()->get()) {
+ new DiceTurnSyncOnHelper(
+ profile, signin_metrics::AccessPoint::ACCESS_POINT_MACHINE_LOGON,
+ signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT,
+ signin_metrics::Reason::kSigninPrimaryAccount, account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT,
+ std::move(*GetDiceTurnSyncOnHelperDelegateForTestingStorage()),
+ base::DoNothing());
+ } else {
+ if (!browser)
+ browser = chrome::FindLastActiveWithProfile(profile);
+
+ new DiceTurnSyncOnHelper(
+ profile, browser,
+ signin_metrics::AccessPoint::ACCESS_POINT_MACHINE_LOGON,
+ signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT,
+ signin_metrics::Reason::kSigninPrimaryAccount, account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT);
+ }
+}
+
+// Start the process of importing credentials from the credential provider given
+// that all the required information is available. The process depends on
+// having a browser window for the profile. If a browser window exists the
+// profile be signed in and sync will be starting up. If not, the profile will
+// be still be signed in but sync will be started once the browser window is
+// ready.
+void ImportCredentialsFromProvider(Profile* profile,
+ const std::wstring& gaia_id,
+ const std::wstring& email,
+ const std::string& refresh_token,
+ bool turn_on_sync) {
+ // For debugging purposes, record that the credentials for this profile
+ // came from a credential provider.
+ AboutSigninInternals* signin_internals =
+ AboutSigninInternalsFactory::GetInstance()->GetForProfile(profile);
+ signin_internals->OnAuthenticationResultReceived("Credential Provider");
+
+ CoreAccountId account_id =
+ IdentityManagerFactory::GetForProfile(profile)
+ ->GetAccountsMutator()
+ ->AddOrUpdateAccount(base::WideToUTF8(gaia_id),
+ base::WideToUTF8(email), refresh_token,
+ /*is_under_advanced_protection=*/false,
+ signin_metrics::SourceForRefreshTokenOperation::
+ kMachineLogon_CredentialProvider);
+
+ if (turn_on_sync) {
+ Browser* browser = chrome::FindLastActiveWithProfile(profile);
+ if (browser) {
+ FinishImportCredentialsFromProvider(account_id, browser, profile,
+ Profile::CREATE_STATUS_CREATED);
+ } else {
+ // If no active browser exists yet, this profile is in the process of
+ // being created. Wait for the browser to be created before finishing the
+ // sign in. This object deletes itself when done.
+ new profiles::BrowserAddedForProfileObserver(
+ profile, base::BindRepeating(&FinishImportCredentialsFromProvider,
+ account_id, nullptr));
+ }
+ }
+
+ // Mark this profile as having been signed in with the credential provider.
+ profile->GetPrefs()->SetBoolean(prefs::kSignedInWithCredentialProvider, true);
+}
+
+// Extracts the |cred_provider_gaia_id| and |cred_provider_email| for the user
+// signed in throuhg credential provider.
+void ExtractCredentialProviderUser(std::wstring* cred_provider_gaia_id,
+ std::wstring* cred_provider_email) {
+ DCHECK(cred_provider_gaia_id);
+ DCHECK(cred_provider_email);
+
+ cred_provider_gaia_id->clear();
+ cred_provider_email->clear();
+
+ base::win::RegKey key;
+ if (key.Open(HKEY_CURRENT_USER, credential_provider::kRegHkcuAccountsPath,
+ KEY_READ) != ERROR_SUCCESS) {
+ return;
+ }
+
+ base::win::RegistryKeyIterator it(key.Handle(), L"");
+ if (!it.Valid() || it.SubkeyCount() != 1)
+ return;
+
+ base::win::RegKey key_account(key.Handle(), it.Name(), KEY_QUERY_VALUE);
+ if (!key_account.Valid())
+ return;
+
+ std::wstring email;
+ if (key_account.ReadValue(
+ base::UTF8ToWide(credential_provider::kKeyEmail).c_str(), &email) !=
+ ERROR_SUCCESS) {
+ return;
+ }
+
+ *cred_provider_gaia_id = it.Name();
+ *cred_provider_email = email;
+}
+
+// Attempt to sign in with a credentials from a system installed credential
+// provider if available. If |auth_gaia_id| is not empty then the system
+// credential must be for the same account. Starts the process to turn on DICE
+// only if |turn_on_sync| is true.
+bool TrySigninWithCredentialProvider(Profile* profile,
+ const std::wstring& auth_gaia_id,
+ bool turn_on_sync) {
+ base::win::RegKey key;
+ if (key.Open(HKEY_CURRENT_USER, credential_provider::kRegHkcuAccountsPath,
+ KEY_READ) != ERROR_SUCCESS) {
+ return false;
+ }
+
+ base::win::RegistryKeyIterator it(key.Handle(), L"");
+ if (!it.Valid() || it.SubkeyCount() == 0)
+ return false;
+
+ base::win::RegKey key_account(key.Handle(), it.Name(), KEY_READ | KEY_WRITE);
+ if (!key_account.Valid())
+ return false;
+
+ std::wstring gaia_id = it.Name();
+ if (!auth_gaia_id.empty() && auth_gaia_id != gaia_id)
+ return false;
+
+ std::wstring email;
+ if (key_account.ReadValue(
+ base::UTF8ToWide(credential_provider::kKeyEmail).c_str(), &email) !=
+ ERROR_SUCCESS) {
+ return false;
+ }
+
+ // Read the encrypted refresh token. The data is stored in binary format.
+ // No matter what happens, delete the registry entry.
+
+ std::string encrypted_refresh_token;
+ DWORD size = 0;
+ DWORD type;
+ if (key_account.ReadValue(
+ base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str(),
+ nullptr, &size, &type) != ERROR_SUCCESS) {
+ return false;
+ }
+
+ encrypted_refresh_token.resize(size);
+ bool reauth_attempted = false;
+ key_account.ReadValue(
+ base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str(),
+ const_cast<char*>(encrypted_refresh_token.c_str()), &size, &type);
+ if (!gaia_id.empty() && !email.empty() && type == REG_BINARY &&
+ !encrypted_refresh_token.empty()) {
+ std::string refresh_token = DecryptRefreshToken(encrypted_refresh_token);
+ if (!refresh_token.empty()) {
+ reauth_attempted = true;
+ ImportCredentialsFromProvider(profile, gaia_id, email, refresh_token,
+ turn_on_sync);
+ }
+ }
+
+ key_account.DeleteValue(
+ base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str());
+ return reauth_attempted;
+}
+
+} // namespace
+
+void SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate> delegate) {
+ GetDiceTurnSyncOnHelperDelegateForTestingStorage()->swap(delegate);
+}
+
+// Credential provider needs to stick to profile it previously used to import
+// credentials. Thus, if there is another profile that was previously signed in
+// with credential provider regardless of whether user signed in or out,
+// credential provider shouldn't attempt to import credentials into current
+// profile.
+bool IsGCPWUsedInOtherProfile(Profile* profile) {
+ DCHECK(profile);
+
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ if (profile_manager) {
+ std::vector<ProfileAttributesEntry*> entries =
+ profile_manager->GetProfileAttributesStorage()
+ .GetAllProfilesAttributes();
+
+ for (const ProfileAttributesEntry* entry : entries) {
+ if (entry->GetPath() == profile->GetPath())
+ continue;
+
+ if (entry->IsSignedInWithCredentialProvider())
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void SigninWithCredentialProviderIfPossible(Profile* profile) {
+ // This flow is used for first time signin through credential provider. Any
+ // subsequent signin for the credential provider user needs to go through
+ // reauth flow.
+ if (profile->GetPrefs()->GetBoolean(prefs::kSignedInWithCredentialProvider))
+ return;
+
+ std::wstring cred_provider_gaia_id;
+ std::wstring cred_provider_email;
+
+ ExtractCredentialProviderUser(&cred_provider_gaia_id, &cred_provider_email);
+ if (cred_provider_gaia_id.empty() || cred_provider_email.empty())
+ return;
+
+ // Chrome doesn't allow signing into current profile if the same user is
+ // signed in another profile.
+ if (!CanOfferSignin(profile, base::WideToUTF8(cred_provider_gaia_id),
+ base::WideToUTF8(cred_provider_email))
+ .IsOk() ||
+ IsGCPWUsedInOtherProfile(profile)) {
+ return;
+ }
+
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ std::wstring gaia_id;
+ if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ gaia_id = base::UTF8ToWide(
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync)
+ .gaia);
+ }
+
+ TrySigninWithCredentialProvider(profile, gaia_id, gaia_id.empty());
+}
+
+bool ReauthWithCredentialProviderIfPossible(Profile* profile) {
+ // Check to see if auto signin information is available. Only applies if:
+ //
+ // - The profile is marked as having been signed in with a system credential.
+ // - The profile is already signed in.
+ // - The profile is in an auth error state.
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ if (!(profile->GetPrefs()->GetBoolean(
+ prefs::kSignedInWithCredentialProvider) &&
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) &&
+ identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
+ identity_manager->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync)))) {
+ return false;
+ }
+
+ std::wstring gaia_id = base::UTF8ToWide(
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync)
+ .gaia.c_str());
+ return TrySigninWithCredentialProvider(profile, gaia_id, false);
+}
+
+} // namespace signin_util
diff --git a/chromium/chrome/browser/signin/signin_util_win.h b/chromium/chrome/browser/signin/signin_util_win.h
new file mode 100644
index 00000000000..771085f9bbb
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util_win.h
@@ -0,0 +1,31 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_
+
+#include <memory>
+
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+
+class Profile;
+
+namespace signin_util {
+
+// Attempt to sign in with a credentials from a system installed credential
+// provider if available.
+void SigninWithCredentialProviderIfPossible(Profile* profile);
+
+// Attempt to reauthenticate with a credentials from a system installed
+// credential provider if available. If a new authentication token was
+// installed returns true.
+bool ReauthWithCredentialProviderIfPossible(Profile* profile);
+
+// Sets the DiceTurnSyncOnHelper delegate for browser tests.
+void SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate> delegate);
+
+} // namespace signin_util
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_
diff --git a/chromium/chrome/browser/signin/signin_util_win_browsertest.cc b/chromium/chrome/browser/signin/signin_util_win_browsertest.cc
new file mode 100644
index 00000000000..87832ab6e98
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util_win_browsertest.cc
@@ -0,0 +1,698 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <stddef.h>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/command_line.h"
+#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/test_reg_util_win.h"
+#include "base/win/wincrypt_shim.h"
+#include "build/build_config.h"
+#include "chrome/browser/first_run/first_run.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_window.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_util_win.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/credential_provider/common/gcp_strings.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "content/public/test/browser_test.h"
+
+class SigninUIError;
+
+namespace {
+
+class TestDiceTurnSyncOnHelperDelegate : public DiceTurnSyncOnHelper::Delegate {
+ ~TestDiceTurnSyncOnHelperDelegate() override {}
+
+ // DiceTurnSyncOnHelper::Delegate:
+ void ShowLoginError(const SigninUIError& error) override {}
+ void ShowMergeSyncDataConfirmation(
+ const std::string& previous_email,
+ const std::string& new_email,
+ DiceTurnSyncOnHelper::SigninChoiceCallback callback) override {
+ std::move(callback).Run(DiceTurnSyncOnHelper::SIGNIN_CHOICE_CONTINUE);
+ }
+ void ShowEnterpriseAccountConfirmation(
+ const AccountInfo& account_info,
+ DiceTurnSyncOnHelper::SigninChoiceCallback callback) override {
+ std::move(callback).Run(DiceTurnSyncOnHelper::SIGNIN_CHOICE_CONTINUE);
+ }
+ void ShowSyncConfirmation(
+ base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
+ callback) override {
+ std::move(callback).Run(LoginUIService::SYNC_WITH_DEFAULT_SETTINGS);
+ }
+ void ShowSyncDisabledConfirmation(
+ bool is_managed_account,
+ base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
+ callback) override {}
+ void ShowSyncSettings() override {}
+ void SwitchToProfile(Profile* new_profile) override {}
+};
+
+struct SigninUtilWinBrowserTestParams {
+ SigninUtilWinBrowserTestParams(bool is_first_run,
+ const std::wstring& gaia_id,
+ const std::wstring& email,
+ const std::string& refresh_token,
+ bool expect_is_started)
+ : is_first_run(is_first_run),
+ gaia_id(gaia_id),
+ email(email),
+ refresh_token(refresh_token),
+ expect_is_started(expect_is_started) {}
+
+ bool is_first_run = false;
+ std::wstring gaia_id;
+ std::wstring email;
+ std::string refresh_token;
+ bool expect_is_started = false;
+};
+
+void AssertSigninStarted(bool expect_is_started, Profile* profile) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+
+ ProfileAttributesStorage& storage =
+ profile_manager->GetProfileAttributesStorage();
+
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(profile->GetPath());
+
+ ASSERT_NE(entry, nullptr);
+
+ ASSERT_EQ(expect_is_started, entry->IsSignedInWithCredentialProvider());
+}
+
+} // namespace
+
+class BrowserTestHelper {
+ public:
+ BrowserTestHelper(const std::wstring& gaia_id,
+ const std::wstring& email,
+ const std::string& refresh_token)
+ : gaia_id_(gaia_id), email_(email), refresh_token_(refresh_token) {}
+
+ protected:
+ void CreateRegKey(base::win::RegKey* key) {
+ if (!gaia_id_.empty()) {
+ EXPECT_EQ(
+ ERROR_SUCCESS,
+ key->Create(HKEY_CURRENT_USER,
+ credential_provider::kRegHkcuAccountsPath, KEY_WRITE));
+ EXPECT_EQ(ERROR_SUCCESS, key->CreateKey(gaia_id_.c_str(), KEY_WRITE));
+ }
+ }
+
+ void WriteRefreshToken(base::win::RegKey* key,
+ const std::string& refresh_token) {
+ EXPECT_TRUE(key->Valid());
+ DATA_BLOB plaintext;
+ plaintext.pbData =
+ reinterpret_cast<BYTE*>(const_cast<char*>(refresh_token.c_str()));
+ plaintext.cbData = static_cast<DWORD>(refresh_token.length());
+
+ DATA_BLOB ciphertext;
+ ASSERT_TRUE(::CryptProtectData(&plaintext, L"Gaia refresh token", nullptr,
+ nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN,
+ &ciphertext));
+ std::string encrypted_data(reinterpret_cast<char*>(ciphertext.pbData),
+ ciphertext.cbData);
+ EXPECT_EQ(
+ ERROR_SUCCESS,
+ key->WriteValue(
+ base::ASCIIToWide(credential_provider::kKeyRefreshToken).c_str(),
+ encrypted_data.c_str(), encrypted_data.length(), REG_BINARY));
+ LocalFree(ciphertext.pbData);
+ }
+
+ void ExpectRefreshTokenExists(bool exists) {
+ base::win::RegKey key;
+ EXPECT_EQ(ERROR_SUCCESS,
+ key.Open(HKEY_CURRENT_USER,
+ credential_provider::kRegHkcuAccountsPath, KEY_READ));
+ EXPECT_EQ(ERROR_SUCCESS, key.OpenKey(gaia_id_.c_str(), KEY_READ));
+ EXPECT_EQ(
+ exists,
+ key.HasValue(
+ base::ASCIIToWide(credential_provider::kKeyRefreshToken).c_str()));
+ }
+
+ public:
+ void SetSigninUtilRegistry() {
+ base::win::RegKey key;
+ CreateRegKey(&key);
+
+ if (!email_.empty()) {
+ EXPECT_TRUE(key.Valid());
+ EXPECT_EQ(ERROR_SUCCESS,
+ key.WriteValue(
+ base::ASCIIToWide(credential_provider::kKeyEmail).c_str(),
+ email_.c_str()));
+ }
+
+ if (!refresh_token_.empty())
+ WriteRefreshToken(&key, refresh_token_);
+ }
+
+ bool IsPreTest() {
+ std::string test_name =
+ ::testing::UnitTest::GetInstance()->current_test_info()->name();
+ LOG(INFO) << "PRE_ test_name " << test_name;
+ return test_name.find("PRE_") != std::string::npos;
+ }
+
+ bool IsPrePreTest() {
+ std::string test_name =
+ ::testing::UnitTest::GetInstance()->current_test_info()->name();
+ LOG(INFO) << "PRE_PRE_ test_name " << test_name;
+ return test_name.find("PRE_PRE_") != std::string::npos;
+ }
+
+ private:
+ std::wstring gaia_id_;
+ std::wstring email_;
+ std::string refresh_token_;
+};
+
+class SigninUtilWinBrowserTest
+ : public BrowserTestHelper,
+ public InProcessBrowserTest,
+ public testing::WithParamInterface<SigninUtilWinBrowserTestParams> {
+ public:
+ SigninUtilWinBrowserTest()
+ : BrowserTestHelper(GetParam().gaia_id,
+ GetParam().email,
+ GetParam().refresh_token) {}
+
+ protected:
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ command_line->AppendSwitch(GetParam().is_first_run
+ ? switches::kForceFirstRun
+ : switches::kNoFirstRun);
+ }
+
+ bool SetUpUserDataDirectory() override {
+ registry_override_.OverrideRegistry(HKEY_CURRENT_USER);
+
+ signin_util::SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate>(
+ new TestDiceTurnSyncOnHelperDelegate()));
+
+ SetSigninUtilRegistry();
+
+ return InProcessBrowserTest::SetUpUserDataDirectory();
+ }
+
+ private:
+ registry_util::RegistryOverrideManager registry_override_;
+};
+
+IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, Run) {
+ ASSERT_EQ(GetParam().is_first_run, first_run::IsChromeFirstRun());
+
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+ ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName());
+
+ Browser* browser = chrome::FindLastActiveWithProfile(profile);
+ ASSERT_NE(nullptr, browser);
+
+ AssertSigninStarted(GetParam().expect_is_started, profile);
+
+ // If a refresh token was specified and a sign in attempt was expected, make
+ // sure the refresh token was removed from the registry.
+ if (!GetParam().refresh_token.empty() && GetParam().expect_is_started)
+ ExpectRefreshTokenExists(false);
+}
+
+IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, ReauthNoop) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ // Whether the profile was signed in with the credential provider or not,
+ // reauth should be a noop.
+ ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+}
+
+IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, NoReauthAfterSignout) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ if (GetParam().expect_is_started) {
+ // Write a new refresh token.
+ base::win::RegKey key;
+ CreateRegKey(&key);
+ WriteRefreshToken(&key, "lst-new");
+ ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+
+ // Sign user out of browser.
+ auto* primary_account_mutator =
+ IdentityManagerFactory::GetForProfile(profile)
+ ->GetPrimaryAccountMutator();
+ primary_account_mutator->RevokeSyncConsent(
+ signin_metrics::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST,
+ signin_metrics::SignoutDelete::kDeleted);
+
+ // Even with a refresh token available, no reauth happens if the profile
+ // is signed out.
+ ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+ }
+}
+
+IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, FixReauth) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ if (GetParam().expect_is_started) {
+ // Write a new refresh token. This time reauth should work.
+ base::win::RegKey key;
+ CreateRegKey(&key);
+ WriteRefreshToken(&key, "lst-new");
+ ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+
+ // Make sure the profile stays signed in, but in an auth error state.
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ identity_manager,
+ identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSync),
+ GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
+ GoogleServiceAuthError::InvalidGaiaCredentialsReason::
+ CREDENTIALS_REJECTED_BY_SERVER));
+
+ // If the profile remains signed in but is in an auth error state,
+ // reauth should happen.
+ ASSERT_TRUE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest1,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/false,
+ /*gaia_id=*/std::wstring(),
+ /*email=*/std::wstring(),
+ /*refresh_token=*/std::string(),
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest2,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/true,
+ /*gaia_id=*/std::wstring(),
+ /*email=*/std::wstring(),
+ /*refresh_token=*/std::string(),
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest3,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/true,
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/std::wstring(),
+ /*refresh_token=*/std::string(),
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest4,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/true,
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/std::string(),
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest5,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/true,
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest6,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/false,
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*expect_is_started=*/true)));
+
+struct ExistingWinBrowserSigninUtilTestParams : SigninUtilWinBrowserTestParams {
+ ExistingWinBrowserSigninUtilTestParams(
+ const std::wstring& gaia_id,
+ const std::wstring& email,
+ const std::string& refresh_token,
+ const std::wstring& existing_email,
+ bool expect_is_started)
+ : SigninUtilWinBrowserTestParams(false,
+ gaia_id,
+ email,
+ refresh_token,
+ expect_is_started),
+ existing_email(existing_email) {}
+
+ std::wstring existing_email;
+};
+
+class ExistingWinBrowserSigninUtilTest
+ : public BrowserTestHelper,
+ public InProcessBrowserTest,
+ public testing::WithParamInterface<
+ ExistingWinBrowserSigninUtilTestParams> {
+ public:
+ ExistingWinBrowserSigninUtilTest()
+ : BrowserTestHelper(GetParam().gaia_id,
+ GetParam().email,
+ GetParam().refresh_token) {}
+
+ protected:
+ bool SetUpUserDataDirectory() override {
+ registry_override_.OverrideRegistry(HKEY_CURRENT_USER);
+
+ signin_util::SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate>(
+ new TestDiceTurnSyncOnHelperDelegate()));
+ if (!IsPreTest())
+ SetSigninUtilRegistry();
+
+ return InProcessBrowserTest::SetUpUserDataDirectory();
+ }
+
+ private:
+ registry_util::RegistryOverrideManager registry_override_;
+};
+
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserSigninUtilTest,
+ PRE_ExistingWinBrowser) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName());
+
+ if (!GetParam().existing_email.empty()) {
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+
+ ASSERT_TRUE(identity_manager);
+
+ signin::MakePrimaryAccountAvailable(
+ identity_manager, base::WideToUTF8(GetParam().existing_email),
+ signin::ConsentLevel::kSync);
+
+ ASSERT_TRUE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ }
+}
+
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserSigninUtilTest, ExistingWinBrowser) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+ ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName());
+
+ AssertSigninStarted(GetParam().expect_is_started, profile);
+
+ // If a refresh token was specified and a sign in attempt was expected, make
+ // sure the refresh token was removed from the registry.
+ if (!GetParam().refresh_token.empty() && GetParam().expect_is_started)
+ ExpectRefreshTokenExists(false);
+}
+
+INSTANTIATE_TEST_SUITE_P(AllowSubsequentRun,
+ ExistingWinBrowserSigninUtilTest,
+ testing::Values(ExistingWinBrowserSigninUtilTestParams(
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*existing_email=*/std::wstring(),
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(OnlyAllowProfileWithNoPrimaryAccount,
+ ExistingWinBrowserSigninUtilTest,
+ testing::Values(ExistingWinBrowserSigninUtilTestParams(
+ /*gaia_id=*/L"gaia_id_for_foo_gmail.com",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*existing_email=*/L"bar@gmail.com",
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(AllowProfileWithPrimaryAccount_DifferentUser,
+ ExistingWinBrowserSigninUtilTest,
+ testing::Values(ExistingWinBrowserSigninUtilTestParams(
+ /*gaia_id=*/L"gaia_id_for_foo_gmail.com",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*existing_email=*/L"bar@gmail.com",
+ /*expect_is_started=*/false)));
+
+
+INSTANTIATE_TEST_SUITE_P(AllowProfileWithPrimaryAccount_SameUser,
+ ExistingWinBrowserSigninUtilTest,
+ testing::Values(ExistingWinBrowserSigninUtilTestParams(
+ /*gaia_id=*/L"gaia_id_for_foo_gmail.com",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*existing_email=*/L"foo@gmail.com",
+ /*expect_is_started=*/true)));
+
+void UnblockOnProfileInitialized(base::OnceClosure quit_closure,
+ Profile* profile,
+ Profile::CreateStatus status) {
+ // If the status is CREATE_STATUS_CREATED, then the function will be called
+ // again with CREATE_STATUS_INITIALIZED.
+ if (status == Profile::CREATE_STATUS_CREATED)
+ return;
+
+ EXPECT_EQ(Profile::CREATE_STATUS_INITIALIZED, status);
+ std::move(quit_closure).Run();
+}
+
+void CreateAndSwitchToProfile(const std::string& basepath) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_TRUE(profile_manager);
+
+ base::FilePath path = profile_manager->user_data_dir().AppendASCII(basepath);
+ base::RunLoop run_loop;
+ profile_manager->CreateProfileAsync(
+ path, base::BindRepeating(&UnblockOnProfileInitialized,
+ run_loop.QuitClosure()));
+ // Run the message loop to allow profile initialization to take place; the
+ // loop is terminated by UnblockOnProfileInitialized.
+ run_loop.Run();
+
+ profiles::SwitchToProfile(path, false, ProfileManager::CreateCallback());
+}
+
+struct ExistingWinBrowserProfilesSigninUtilTestParams {
+ ExistingWinBrowserProfilesSigninUtilTestParams(
+ const std::wstring& email_in_other_profile,
+ bool cred_provider_used_other_profile,
+ const std::wstring& current_profile,
+ const std::wstring& email_in_current_profile,
+ bool expect_is_started)
+ : email_in_other_profile(email_in_other_profile),
+ cred_provider_used_other_profile(cred_provider_used_other_profile),
+ current_profile(current_profile),
+ email_in_current_profile(email_in_current_profile),
+ expect_is_started(expect_is_started) {}
+
+ std::wstring email_in_other_profile;
+ bool cred_provider_used_other_profile;
+ std::wstring current_profile;
+ std::wstring email_in_current_profile;
+ bool expect_is_started;
+};
+
+class ExistingWinBrowserProfilesSigninUtilTest
+ : public BrowserTestHelper,
+ public InProcessBrowserTest,
+ public testing::WithParamInterface<
+ ExistingWinBrowserProfilesSigninUtilTestParams> {
+ public:
+ ExistingWinBrowserProfilesSigninUtilTest()
+ : BrowserTestHelper(L"gaia_id_for_foo_gmail.com",
+ L"foo@gmail.com",
+ "lst-123456") {}
+
+ protected:
+ bool SetUpUserDataDirectory() override {
+ registry_override_.OverrideRegistry(HKEY_CURRENT_USER);
+
+ signin_util::SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate>(
+ new TestDiceTurnSyncOnHelperDelegate()));
+ if (!IsPreTest()) {
+ SetSigninUtilRegistry();
+ } else if (IsPrePreTest() && GetParam().cred_provider_used_other_profile) {
+ BrowserTestHelper(L"gaia_id_for_bar_gmail.com", L"bar@gmail.com",
+ "lst-123456")
+ .SetSigninUtilRegistry();
+ }
+
+ return InProcessBrowserTest::SetUpUserDataDirectory();
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ registry_util::RegistryOverrideManager registry_override_;
+};
+
+// In PRE_PRE_Run, browser starts for the first time with the initial profile
+// dir. If needed by the test, this step can set |email_in_other_profile| as the
+// primary account in the profile or it can sign in with credential provider,
+// but before this step ends, |current_profile| is created and browser switches
+// to that profile just to prepare the browser for the next step.
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, PRE_PRE_Run) {
+ g_browser_process->local_state()->SetBoolean(
+ prefs::kBrowserShowProfilePickerOnStartup, false);
+
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName());
+
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ ASSERT_TRUE(identity_manager);
+ ASSERT_TRUE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) ==
+ GetParam().cred_provider_used_other_profile);
+
+ if (!GetParam().cred_provider_used_other_profile &&
+ !GetParam().email_in_other_profile.empty()) {
+ signin::MakePrimaryAccountAvailable(
+ identity_manager, base::WideToUTF8(GetParam().email_in_other_profile),
+ signin::ConsentLevel::kSync);
+
+ ASSERT_TRUE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ }
+
+ CreateAndSwitchToProfile(base::WideToUTF8(GetParam().current_profile));
+}
+
+// Browser starts with the |current_profile| profile created in the previous
+// step. If needed by the test, this step can set |email_in_current_profile| as
+// the primary account in the profile.
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, PRE_Run) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ ASSERT_EQ(GetParam().current_profile, profile->GetBaseName().value());
+
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ ASSERT_TRUE(identity_manager);
+ ASSERT_FALSE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync));
+
+ if (!GetParam().email_in_current_profile.empty()) {
+ signin::MakePrimaryAccountAvailable(
+ identity_manager, base::WideToUTF8(GetParam().email_in_current_profile),
+ signin::ConsentLevel::kSync);
+
+ ASSERT_TRUE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ }
+}
+
+// Before this step runs, refresh token is written into fake registry. Browser
+// starts with the |current_profile| profile. Depending on the test case,
+// profile may have a primary account. Similarly the other profile(initial
+// profile in this case) may have a primary account as well.
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, Run) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ ASSERT_EQ(GetParam().current_profile, profile->GetBaseName().value());
+ AssertSigninStarted(GetParam().expect_is_started, profile);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ AllowCurrentProfile_NoUserSignedIn,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"",
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(
+ AllowCurrentProfile_SameUserSignedIn,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"foo@gmail.com",
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(
+ DisallowCurrentProfile_DifferentUserSignedIn,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"bar@gmail.com",
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(
+ DisallowCurrentProfile_SameUserSignedInDefaultProfile,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"foo@gmail.com",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"",
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(
+ AllowCurrentProfile_DifferentUserSignedInDefaultProfile,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"bar@gmail.com",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"",
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(
+ DisallowCurrentProfile_CredProviderUsedDefaultProfile,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"",
+ /*cred_provider_used_other_profile*/ true,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"",
+ /*expect_is_started=*/false)));
diff --git a/chromium/chrome/browser/signin/test_signin_client_builder.cc b/chromium/chrome/browser/signin/test_signin_client_builder.cc
new file mode 100644
index 00000000000..9795a5e7e85
--- /dev/null
+++ b/chromium/chrome/browser/signin/test_signin_client_builder.cc
@@ -0,0 +1,18 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/test_signin_client_builder.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "components/signin/public/base/test_signin_client.h"
+
+namespace signin {
+
+std::unique_ptr<KeyedService> BuildTestSigninClient(
+ content::BrowserContext* context) {
+ return std::make_unique<TestSigninClient>(
+ static_cast<Profile*>(context)->GetPrefs());
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/test_signin_client_builder.h b/chromium/chrome/browser/signin/test_signin_client_builder.h
new file mode 100644
index 00000000000..3863c510150
--- /dev/null
+++ b/chromium/chrome/browser/signin/test_signin_client_builder.h
@@ -0,0 +1,26 @@
+// Copyright 2014 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_
+#define CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_
+
+#include <memory>
+
+class KeyedService;
+
+namespace content {
+class BrowserContext;
+}
+
+namespace signin {
+
+// Method to be used by the |ChromeSigninClientFactory| to create a test version
+// of the SigninClient
+std::unique_ptr<KeyedService> BuildTestSigninClient(
+ content::BrowserContext* context);
+
+} // namespace signin
+
+
+#endif // CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_
diff --git a/chromium/chrome/browser/signin/token_revoker_test_utils.cc b/chromium/chrome/browser/signin/token_revoker_test_utils.cc
new file mode 100644
index 00000000000..69b26c881b1
--- /dev/null
+++ b/chromium/chrome/browser/signin/token_revoker_test_utils.cc
@@ -0,0 +1,36 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/signin/token_revoker_test_utils.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/net/system_network_context_manager.h"
+#include "content/public/test/test_utils.h"
+#include "google_apis/gaia/gaia_auth_fetcher.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+
+namespace token_revoker_test_utils {
+
+RefreshTokenRevoker::RefreshTokenRevoker()
+ : gaia_fetcher_(this,
+ gaia::GaiaSource::kChrome,
+ g_browser_process->system_network_context_manager()
+ ->GetSharedURLLoaderFactory()) {}
+
+RefreshTokenRevoker::~RefreshTokenRevoker() {
+}
+
+void RefreshTokenRevoker::Revoke(const std::string& token) {
+ DVLOG(1) << "Starting RefreshTokenRevoker for token: " << token;
+ gaia_fetcher_.StartRevokeOAuth2Token(token);
+ message_loop_runner_ = new content::MessageLoopRunner;
+ message_loop_runner_->Run();
+}
+
+void RefreshTokenRevoker::OnOAuth2RevokeTokenCompleted(
+ GaiaAuthConsumer::TokenRevocationStatus status) {
+ DVLOG(1) << "TokenRevoker OnOAuth2RevokeTokenCompleted";
+ message_loop_runner_->Quit();
+}
+
+} // namespace token_revoker_test_utils
diff --git a/chromium/chrome/browser/signin/token_revoker_test_utils.h b/chromium/chrome/browser/signin/token_revoker_test_utils.h
new file mode 100644
index 00000000000..2c1b08a257a
--- /dev/null
+++ b/chromium/chrome/browser/signin/token_revoker_test_utils.h
@@ -0,0 +1,42 @@
+// Copyright 2016 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_
+#define CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_
+
+#include "base/memory/ref_counted.h"
+#include "google_apis/gaia/gaia_auth_fetcher.h"
+
+namespace content {
+class MessageLoopRunner;
+}
+
+namespace token_revoker_test_utils {
+
+// A helper class that takes care of asynchronously revoking a refresh token.
+class RefreshTokenRevoker : public GaiaAuthConsumer {
+ public:
+ RefreshTokenRevoker();
+
+ RefreshTokenRevoker(const RefreshTokenRevoker&) = delete;
+ RefreshTokenRevoker& operator=(const RefreshTokenRevoker&) = delete;
+
+ ~RefreshTokenRevoker() override;
+
+ // Sends a request to Gaia servers to revoke the refresh token. Blocks until
+ // it is revoked, i.e. until OnOAuth2RevokeTokenCompleted is fired.
+ void Revoke(const std::string& token);
+
+ // Called when token is revoked.
+ void OnOAuth2RevokeTokenCompleted(
+ GaiaAuthConsumer::TokenRevocationStatus status) override;
+
+ private:
+ GaiaAuthFetcher gaia_fetcher_;
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner_;
+};
+
+} // namespace token_revoker_test_utils
+
+#endif // CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_