blob: a6e1c1dcb1ad4828a014b59296510bd277a0e55c [file] [log] [blame]
// 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/histogram_functions.h"
#include "base/optional.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/password_manager/chrome_password_manager_client.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/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
#include "chrome/browser/ui/signin/profile_colors_util.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/search/generated_colors_info.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/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
constexpr char kProfileCreationInterceptionDeclinedPref[] =
"signin.ProfileCreationInterceptionDeclinedPref";
void RecordSigninInterceptionHeuristicOutcome(
SigninInterceptionHeuristicOutcome outcome) {
base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome);
}
bool IsProfileCreationAllowed() {
PrefService* service = g_browser_process->local_state();
DCHECK(service);
return service->GetBoolean(prefs::kBrowserAddPersonEnabled);
}
// 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::kNotRequired);
if (primary_core_account_info.IsEmpty())
return AccountInfo();
base::Optional<AccountInfo> primary_account_info =
manager->FindExtendedAccountInfoForAccountWithRefreshToken(
primary_core_account_info);
if (primary_account_info)
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;
}
bool GuestOptionAvailable() {
return Profile::IsEphemeralGuestProfileEnabled() &&
!ProfileManager::GuestProfileExists() &&
g_browser_process->local_state()->GetBoolean(
prefs::kBrowserGuestModeEnabled);
}
} // 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);
}
base::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;
}
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 (!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 base::nullopt;
}
void DiceWebSigninInterceptor::MaybeInterceptWebSignin(
content::WebContents* web_contents,
CoreAccountId account_id,
bool is_new_account,
bool is_sync_signin) {
if (!base::FeatureList::IsEnabled(kDiceWebSigninInterceptionFeature))
return;
if (is_interception_in_progress_) {
// Multiple concurrent interceptions are not supported.
RecordSigninInterceptionHeuristicOutcome(
SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress);
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;
}
base::Optional<AccountInfo> account_info =
identity_manager_
->FindExtendedAccountInfoForAccountWithRefreshTokenByAccountId(
account_id);
DCHECK(account_info) << "Intercepting unknown account.";
const ProfileAttributesEntry* entry = nullptr;
base::Optional<SigninInterceptionHeuristicOutcome> heuristic_outcome =
GetHeuristicOutcome(is_new_account, is_sync_signin, account_info->email,
&entry);
account_id_ = account_id;
is_interception_in_progress_ = true;
Observe(web_contents);
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), 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::TimeDelta::FromSeconds(5));
account_info_update_observer_.Add(identity_manager_);
}
}
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() {
Observe(/*web_contents=*/nullptr);
account_info_update_observer_.RemoveAll();
on_account_info_update_timeout_.Cancel();
is_interception_in_progress_ = false;
account_id_ = CoreAccountId();
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();
}
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::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::kNotRequired);
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;
base::Optional<AccountInfo> primary_account_info =
identity_manager_->FindExtendedAccountInfoForAccountWithRefreshToken(
primary_core_account_info);
return primary_account_info && primary_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::OnExtendedAccountInfoUpdated(
const AccountInfo& info) {
if (info.account_id != account_id_)
return;
if (!info.IsValid())
return;
account_info_update_observer_.RemoveAll();
on_account_info_update_timeout_.Cancel();
base::UmaHistogramTimes(
"Signin.Intercept.AccountInfoFetchDuration",
base::TimeTicks::Now() - account_info_fetch_start_time_);
base::Optional<SigninInterceptionType> interception_type;
if (ShouldShowEnterpriseBubble(info))
interception_type = SigninInterceptionType::kEnterprise;
else if (ShouldShowMultiUserBubble(info))
interception_type = SigninInterceptionType::kMultiUser;
if (!interception_type) {
// Signin should not be intercepted.
RecordSigninInterceptionHeuristicOutcome(
SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible);
Reset();
return;
}
ProfileAttributesEntry* entry;
g_browser_process->profile_manager()
->GetProfileAttributesStorage()
.GetProfileAttributesWithPath(profile_->GetPath(), &entry);
SkColor profile_color = GenerateNewProfileColor(entry).color;
Delegate::BubbleParameters bubble_parameters{
*interception_type, info, GetPrimaryAccountInfo(identity_manager_),
GetAutogeneratedThemeColors(profile_color).frame_color,
GuestOptionAvailable()};
interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble(
web_contents(), bubble_parameters,
base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice,
base::Unretained(this), info, profile_color));
was_interception_ui_displayed_ = true;
RecordSigninInterceptionHeuristicOutcome(
*interception_type == SigninInterceptionType::kEnterprise
? SigninInterceptionHeuristicOutcome::kInterceptEnterprise
: SigninInterceptionHeuristicOutcome::kInterceptMultiUser);
}
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();
base::string16 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 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), base::nullopt));
}
void DiceWebSigninInterceptor::OnNewSignedInProfileCreated(
base::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);
if (!new_profile->IsEphemeralGuestProfile()) {
// 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_);
}
// 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(), std::move(interception_bubble_handle_),
is_new_profile);
Reset();
}
void DiceWebSigninInterceptor::OnNewBrowserCreated(bool is_new_profile) {
DCHECK(interception_bubble_handle_);
interception_bubble_handle_.reset(); // Close the bubble now.
session_startup_helper_.reset();
if (is_new_profile && !profile_->IsEphemeralGuestProfile()) {
Browser* browser = chrome::FindBrowserWithProfile(profile_);
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);
base::Optional<int> declined_count = update->FindIntKey(key);
update->SetIntKey(
key, declined_count.has_value() ? declined_count.value() + 1 : 1);
}
bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation(
const std::string& email) const {
const base::DictionaryValue* pref_data = profile_->GetPrefs()->GetDictionary(
kProfileCreationInterceptionDeclinedPref);
base::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;
}