1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/signin/dice_web_signin_interceptor.h"
6 
7 #include <string>
8 
9 #include "base/check.h"
10 #include "base/hash/hash.h"
11 #include "base/i18n/case_conversion.h"
12 #include "base/metrics/histogram_functions.h"
13 #include "base/optional.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/threading/thread_task_runner_handle.h"
16 #include "base/time/time.h"
17 #include "chrome/browser/browser_process.h"
18 #include "chrome/browser/password_manager/chrome_password_manager_client.h"
19 #include "chrome/browser/profiles/profile_attributes_entry.h"
20 #include "chrome/browser/profiles/profile_attributes_storage.h"
21 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
22 #include "chrome/browser/profiles/profile_manager.h"
23 #include "chrome/browser/profiles/profile_metrics.h"
24 #include "chrome/browser/profiles/profiles_state.h"
25 #include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
26 #include "chrome/browser/signin/dice_signed_in_profile_creator.h"
27 #include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
28 #include "chrome/browser/signin/identity_manager_factory.h"
29 #include "chrome/browser/signin/signin_features.h"
30 #include "chrome/browser/themes/theme_service.h"
31 #include "chrome/browser/themes/theme_service_factory.h"
32 #include "chrome/browser/ui/browser_finder.h"
33 #include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
34 #include "chrome/browser/ui/signin/profile_colors_util.h"
35 #include "chrome/common/pref_names.h"
36 #include "chrome/common/search/generated_colors_info.h"
37 #include "chrome/common/themes/autogenerated_theme_util.h"
38 #include "components/password_manager/core/browser/password_manager.h"
39 #include "components/password_manager/core/common/password_manager_ui.h"
40 #include "components/pref_registry/pref_registry_syncable.h"
41 #include "components/prefs/pref_service.h"
42 #include "components/prefs/scoped_user_pref_update.h"
43 #include "components/signin/public/identity_manager/identity_manager.h"
44 #include "google_apis/gaia/gaia_auth_util.h"
45 #include "ui/base/l10n/l10n_util.h"
46 
47 namespace {
48 
49 constexpr char kProfileCreationInterceptionDeclinedPref[] =
50     "signin.ProfileCreationInterceptionDeclinedPref";
51 
RecordSigninInterceptionHeuristicOutcome(SigninInterceptionHeuristicOutcome outcome)52 void RecordSigninInterceptionHeuristicOutcome(
53     SigninInterceptionHeuristicOutcome outcome) {
54   base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome);
55 }
56 
IsProfileCreationAllowed()57 bool IsProfileCreationAllowed() {
58   PrefService* service = g_browser_process->local_state();
59   DCHECK(service);
60   return service->GetBoolean(prefs::kBrowserAddPersonEnabled);
61 }
62 
63 // Helper function to return the primary account info. The returned info is
64 // empty if there is no primary account, and non-empty otherwise. Extended
65 // fields may be missing if they are not available.
GetPrimaryAccountInfo(signin::IdentityManager * manager)66 AccountInfo GetPrimaryAccountInfo(signin::IdentityManager* manager) {
67   CoreAccountInfo primary_core_account_info =
68       manager->GetPrimaryAccountInfo(signin::ConsentLevel::kNotRequired);
69   if (primary_core_account_info.IsEmpty())
70     return AccountInfo();
71 
72   base::Optional<AccountInfo> primary_account_info =
73       manager->FindExtendedAccountInfoForAccountWithRefreshToken(
74           primary_core_account_info);
75 
76   if (primary_account_info)
77     return *primary_account_info;
78 
79   // Return an AccountInfo without extended fields, based on the core info.
80   AccountInfo account_info;
81   account_info.gaia = primary_core_account_info.gaia;
82   account_info.email = primary_core_account_info.email;
83   account_info.account_id = primary_core_account_info.account_id;
84   return account_info;
85 }
86 
HasNoBrowser(content::WebContents * web_contents)87 bool HasNoBrowser(content::WebContents* web_contents) {
88   return chrome::FindBrowserWithWebContents(web_contents) == nullptr;
89 }
90 
91 }  // namespace
92 
SigninInterceptionHeuristicOutcomeIsSuccess(SigninInterceptionHeuristicOutcome outcome)93 bool SigninInterceptionHeuristicOutcomeIsSuccess(
94     SigninInterceptionHeuristicOutcome outcome) {
95   return outcome == SigninInterceptionHeuristicOutcome::kInterceptEnterprise ||
96          outcome == SigninInterceptionHeuristicOutcome::kInterceptMultiUser ||
97          outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
98 }
99 
DiceWebSigninInterceptor(Profile * profile,std::unique_ptr<Delegate> delegate)100 DiceWebSigninInterceptor::DiceWebSigninInterceptor(
101     Profile* profile,
102     std::unique_ptr<Delegate> delegate)
103     : profile_(profile),
104       identity_manager_(IdentityManagerFactory::GetForProfile(profile)),
105       delegate_(std::move(delegate)) {
106   DCHECK(profile_);
107   DCHECK(identity_manager_);
108   DCHECK(delegate_);
109 }
110 
111 DiceWebSigninInterceptor::~DiceWebSigninInterceptor() = default;
112 
113 // static
RegisterProfilePrefs(user_prefs::PrefRegistrySyncable * registry)114 void DiceWebSigninInterceptor::RegisterProfilePrefs(
115     user_prefs::PrefRegistrySyncable* registry) {
116   registry->RegisterDictionaryPref(kProfileCreationInterceptionDeclinedPref);
117   registry->RegisterBooleanPref(prefs::kSigninInterceptionEnabled, true);
118 }
119 
120 base::Optional<SigninInterceptionHeuristicOutcome>
GetHeuristicOutcome(bool is_new_account,bool is_sync_signin,const std::string & email,const ProfileAttributesEntry ** entry) const121 DiceWebSigninInterceptor::GetHeuristicOutcome(
122     bool is_new_account,
123     bool is_sync_signin,
124     const std::string& email,
125     const ProfileAttributesEntry** entry) const {
126   if (!profile_->GetPrefs()->GetBoolean(prefs::kSigninInterceptionEnabled))
127     return SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled;
128 
129   if (is_sync_signin) {
130     // Do not intercept signins from the Sync startup flow.
131     // Note: |is_sync_signin| is an approximation, and in rare cases it may be
132     // true when in fact the signin was not a sync signin. In this case the
133     // interception is missed.
134     return SigninInterceptionHeuristicOutcome::kAbortSyncSignin;
135   }
136   if (!is_new_account) {
137     // Do not intercept reauth.
138     return SigninInterceptionHeuristicOutcome::kAbortAccountNotNew;
139   }
140 
141   const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble(
142       email,
143       &g_browser_process->profile_manager()->GetProfileAttributesStorage());
144   if (switch_to_entry) {
145     if (entry)
146       *entry = switch_to_entry;
147     return SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
148   }
149 
150   // From this point the remaining possible interceptions involve creating a new
151   // profile.
152   if (!IsProfileCreationAllowed()) {
153     return SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed;
154   }
155 
156   std::vector<CoreAccountInfo> accounts_in_chrome =
157       identity_manager_->GetAccountsWithRefreshTokens();
158   if (accounts_in_chrome.size() == 0 ||
159       (accounts_in_chrome.size() == 1 &&
160        gaia::AreEmailsSame(email, accounts_in_chrome[0].email))) {
161     // Enterprise and multi-user bubbles are only shown if there are multiple
162     // accounts. The intercepted account may not be added to chrome yet.
163     return SigninInterceptionHeuristicOutcome::kAbortSingleAccount;
164   }
165 
166   if (HasUserDeclinedProfileCreation(email)) {
167     return SigninInterceptionHeuristicOutcome::
168         kAbortUserDeclinedProfileForAccount;
169   }
170 
171   return base::nullopt;
172 }
173 
MaybeInterceptWebSignin(content::WebContents * web_contents,CoreAccountId account_id,bool is_new_account,bool is_sync_signin)174 void DiceWebSigninInterceptor::MaybeInterceptWebSignin(
175     content::WebContents* web_contents,
176     CoreAccountId account_id,
177     bool is_new_account,
178     bool is_sync_signin) {
179   if (!base::FeatureList::IsEnabled(kDiceWebSigninInterceptionFeature))
180     return;
181 
182   if (is_interception_in_progress_) {
183     // Multiple concurrent interceptions are not supported.
184     RecordSigninInterceptionHeuristicOutcome(
185         SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress);
186     return;
187   }
188 
189   if (HasNoBrowser(web_contents)) {
190     // Do not intercept from the profile creation flow.
191     RecordSigninInterceptionHeuristicOutcome(
192         SigninInterceptionHeuristicOutcome::kAbortNoBrowser);
193     return;
194   }
195 
196   // Do not show the interception UI if a password update is required: both
197   // bubbles cannot be shown at the same time and the password update is more
198   // important.
199   ChromePasswordManagerClient* password_manager_client =
200       ChromePasswordManagerClient::FromWebContents(web_contents);
201   if (password_manager_client && password_manager_client->GetPasswordManager()
202                                      ->IsFormManagerPendingPasswordUpdate()) {
203     RecordSigninInterceptionHeuristicOutcome(
204         SigninInterceptionHeuristicOutcome::kAbortPasswordUpdatePending);
205     return;
206   }
207 
208   ManagePasswordsUIController* password_controller =
209       ManagePasswordsUIController::FromWebContents(web_contents);
210   if (password_controller &&
211       password_controller->GetState() ==
212           password_manager::ui::State::PENDING_PASSWORD_UPDATE_STATE) {
213     RecordSigninInterceptionHeuristicOutcome(
214         SigninInterceptionHeuristicOutcome::kAbortPasswordUpdate);
215     return;
216   }
217 
218   base::Optional<AccountInfo> account_info =
219       identity_manager_
220           ->FindExtendedAccountInfoForAccountWithRefreshTokenByAccountId(
221               account_id);
222   DCHECK(account_info) << "Intercepting unknown account.";
223   const ProfileAttributesEntry* entry = nullptr;
224   base::Optional<SigninInterceptionHeuristicOutcome> heuristic_outcome =
225       GetHeuristicOutcome(is_new_account, is_sync_signin, account_info->email,
226                           &entry);
227   account_id_ = account_id;
228   is_interception_in_progress_ = true;
229   Observe(web_contents);
230 
231   if (heuristic_outcome) {
232     RecordSigninInterceptionHeuristicOutcome(*heuristic_outcome);
233     if (*heuristic_outcome ==
234         SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch) {
235       DCHECK(entry);
236       Delegate::BubbleParameters bubble_parameters{
237           SigninInterceptionType::kProfileSwitch, *account_info,
238           GetPrimaryAccountInfo(identity_manager_),
239           entry->GetProfileThemeColors().profile_highlight_color};
240       delegate_->ShowSigninInterceptionBubble(
241           web_contents, bubble_parameters,
242           base::BindOnce(&DiceWebSigninInterceptor::OnProfileSwitchChoice,
243                          base::Unretained(this), entry->GetPath()));
244       was_interception_ui_displayed_ = true;
245     } else {
246       // Interception is aborted.
247       DCHECK(!SigninInterceptionHeuristicOutcomeIsSuccess(*heuristic_outcome));
248       Reset();
249     }
250     return;
251   }
252 
253   account_info_fetch_start_time_ = base::TimeTicks::Now();
254   if (account_info->IsValid()) {
255     OnExtendedAccountInfoUpdated(*account_info);
256   } else {
257     on_account_info_update_timeout_.Reset(base::BindOnce(
258         &DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout,
259         base::Unretained(this)));
260     base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
261         FROM_HERE, on_account_info_update_timeout_.callback(),
262         base::TimeDelta::FromSeconds(5));
263     account_info_update_observer_.Add(identity_manager_);
264   }
265 }
266 
CreateBrowserAfterSigninInterception(CoreAccountId account_id,content::WebContents * intercepted_contents,bool show_customization_bubble)267 void DiceWebSigninInterceptor::CreateBrowserAfterSigninInterception(
268     CoreAccountId account_id,
269     content::WebContents* intercepted_contents,
270     bool show_customization_bubble) {
271   DCHECK(!session_startup_helper_);
272   session_startup_helper_ =
273       std::make_unique<DiceInterceptedSessionStartupHelper>(
274           profile_, account_id, intercepted_contents);
275   session_startup_helper_->Startup(
276       base::Bind(&DiceWebSigninInterceptor::OnNewBrowserCreated,
277                  base::Unretained(this), show_customization_bubble));
278 }
279 
Shutdown()280 void DiceWebSigninInterceptor::Shutdown() {
281   if (is_interception_in_progress_ && !was_interception_ui_displayed_) {
282     RecordSigninInterceptionHeuristicOutcome(
283         SigninInterceptionHeuristicOutcome::kAbortShutdown);
284   }
285   Reset();
286 }
287 
Reset()288 void DiceWebSigninInterceptor::Reset() {
289   Observe(/*web_contents=*/nullptr);
290   account_info_update_observer_.RemoveAll();
291   on_account_info_update_timeout_.Cancel();
292   is_interception_in_progress_ = false;
293   account_id_ = CoreAccountId();
294   dice_signed_in_profile_creator_.reset();
295   was_interception_ui_displayed_ = false;
296   account_info_fetch_start_time_ = base::TimeTicks();
297   profile_creation_start_time_ = base::TimeTicks();
298 }
299 
300 const ProfileAttributesEntry*
ShouldShowProfileSwitchBubble(const std::string & intercepted_email,ProfileAttributesStorage * profile_attribute_storage) const301 DiceWebSigninInterceptor::ShouldShowProfileSwitchBubble(
302     const std::string& intercepted_email,
303     ProfileAttributesStorage* profile_attribute_storage) const {
304   // Check if there is already an existing profile with this account.
305   base::FilePath profile_path = profile_->GetPath();
306   for (const auto* entry :
307        profile_attribute_storage->GetAllProfilesAttributes()) {
308     if (entry->GetPath() == profile_path)
309       continue;
310     if (gaia::AreEmailsSame(intercepted_email,
311                             base::UTF16ToUTF8(entry->GetUserName()))) {
312       return entry;
313     }
314   }
315   return nullptr;
316 }
317 
ShouldShowEnterpriseBubble(const AccountInfo & intercepted_account_info) const318 bool DiceWebSigninInterceptor::ShouldShowEnterpriseBubble(
319     const AccountInfo& intercepted_account_info) const {
320   DCHECK(intercepted_account_info.IsValid());
321   // Check if the intercepted account or the primary account is managed.
322   CoreAccountInfo primary_core_account_info =
323       identity_manager_->GetPrimaryAccountInfo(
324           signin::ConsentLevel::kNotRequired);
325 
326   if (primary_core_account_info.IsEmpty() ||
327       primary_core_account_info.account_id ==
328           intercepted_account_info.account_id) {
329     return false;
330   }
331 
332   if (intercepted_account_info.hosted_domain != kNoHostedDomainFound)
333     return true;
334 
335   base::Optional<AccountInfo> primary_account_info =
336       identity_manager_->FindExtendedAccountInfoForAccountWithRefreshToken(
337           primary_core_account_info);
338   if (!primary_account_info || !primary_account_info->IsValid())
339     return false;
340 
341   return primary_account_info->hosted_domain != kNoHostedDomainFound;
342 }
343 
ShouldShowMultiUserBubble(const AccountInfo & intercepted_account_info) const344 bool DiceWebSigninInterceptor::ShouldShowMultiUserBubble(
345     const AccountInfo& intercepted_account_info) const {
346   DCHECK(intercepted_account_info.IsValid());
347   if (identity_manager_->GetAccountsWithRefreshTokens().size() <= 1u)
348     return false;
349   // Check if the account has the same name as another account in the profile.
350   for (const auto& account_info :
351        identity_manager_->GetExtendedAccountInfoForAccountsWithRefreshToken()) {
352     if (account_info.account_id == intercepted_account_info.account_id)
353       continue;
354     // Case-insensitve comparison supporting non-ASCII characters.
355     if (base::i18n::FoldCase(base::UTF8ToUTF16(account_info.given_name)) ==
356         base::i18n::FoldCase(
357             base::UTF8ToUTF16(intercepted_account_info.given_name))) {
358       return false;
359     }
360   }
361   return true;
362 }
363 
OnExtendedAccountInfoUpdated(const AccountInfo & info)364 void DiceWebSigninInterceptor::OnExtendedAccountInfoUpdated(
365     const AccountInfo& info) {
366   if (info.account_id != account_id_)
367     return;
368   if (!info.IsValid())
369     return;
370 
371   account_info_update_observer_.RemoveAll();
372   on_account_info_update_timeout_.Cancel();
373   base::UmaHistogramTimes(
374       "Signin.Intercept.AccountInfoFetchDuration",
375       base::TimeTicks::Now() - account_info_fetch_start_time_);
376 
377   base::Optional<SigninInterceptionType> interception_type;
378 
379   if (ShouldShowEnterpriseBubble(info))
380     interception_type = SigninInterceptionType::kEnterprise;
381   else if (ShouldShowMultiUserBubble(info))
382     interception_type = SigninInterceptionType::kMultiUser;
383 
384   if (!interception_type) {
385     // Signin should not be intercepted.
386     RecordSigninInterceptionHeuristicOutcome(
387         SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible);
388     Reset();
389     return;
390   }
391 
392   ProfileAttributesEntry* entry;
393   g_browser_process->profile_manager()
394       ->GetProfileAttributesStorage()
395       .GetProfileAttributesWithPath(profile_->GetPath(), &entry);
396   SkColor profile_color = GenerateNewProfileColor(entry).color;
397   Delegate::BubbleParameters bubble_parameters{
398       *interception_type, info, GetPrimaryAccountInfo(identity_manager_),
399       GetAutogeneratedThemeColors(profile_color).frame_color};
400   delegate_->ShowSigninInterceptionBubble(
401       web_contents(), bubble_parameters,
402       base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice,
403                      base::Unretained(this), info, profile_color));
404   was_interception_ui_displayed_ = true;
405   RecordSigninInterceptionHeuristicOutcome(
406       *interception_type == SigninInterceptionType::kEnterprise
407           ? SigninInterceptionHeuristicOutcome::kInterceptEnterprise
408           : SigninInterceptionHeuristicOutcome::kInterceptMultiUser);
409 }
410 
OnExtendedAccountInfoFetchTimeout()411 void DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout() {
412   RecordSigninInterceptionHeuristicOutcome(
413       SigninInterceptionHeuristicOutcome::kAbortAccountInfoTimeout);
414   Reset();
415 }
416 
OnProfileCreationChoice(const AccountInfo & account_info,SkColor profile_color,SigninInterceptionResult create)417 void DiceWebSigninInterceptor::OnProfileCreationChoice(
418     const AccountInfo& account_info,
419     SkColor profile_color,
420     SigninInterceptionResult create) {
421   if (create != SigninInterceptionResult::kAccepted) {
422     if (create == SigninInterceptionResult::kDeclined)
423       RecordProfileCreationDeclined(account_info.email);
424     Reset();
425     return;
426   }
427 
428   profile_creation_start_time_ = base::TimeTicks::Now();
429   base::string16 profile_name;
430   profile_name = profiles::GetDefaultNameForNewSignedInProfile(account_info);
431 
432   DCHECK(!dice_signed_in_profile_creator_);
433   // Unretained is fine because the profile creator is owned by this.
434   dice_signed_in_profile_creator_ =
435       std::make_unique<DiceSignedInProfileCreator>(
436           profile_, account_id_, profile_name,
437           profiles::GetPlaceholderAvatarIndex(),
438           base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
439                          base::Unretained(this), profile_color));
440 }
441 
OnProfileSwitchChoice(const base::FilePath & profile_path,SigninInterceptionResult switch_profile)442 void DiceWebSigninInterceptor::OnProfileSwitchChoice(
443     const base::FilePath& profile_path,
444     SigninInterceptionResult switch_profile) {
445   if (switch_profile != SigninInterceptionResult::kAccepted) {
446     Reset();
447     return;
448   }
449 
450   profile_creation_start_time_ = base::TimeTicks::Now();
451   DCHECK(!dice_signed_in_profile_creator_);
452   // Unretained is fine because the profile creator is owned by this.
453   dice_signed_in_profile_creator_ =
454       std::make_unique<DiceSignedInProfileCreator>(
455           profile_, account_id_, profile_path,
456           base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
457                          base::Unretained(this), base::nullopt));
458 }
459 
OnNewSignedInProfileCreated(base::Optional<SkColor> profile_color,Profile * new_profile)460 void DiceWebSigninInterceptor::OnNewSignedInProfileCreated(
461     base::Optional<SkColor> profile_color,
462     Profile* new_profile) {
463   DCHECK(dice_signed_in_profile_creator_);
464   dice_signed_in_profile_creator_.reset();
465 
466   if (!new_profile) {
467     Reset();
468     return;
469   }
470 
471   bool show_customization_bubble = false;
472   if (profile_color.has_value()) {
473     // The profile color is defined only when the profile has just been created
474     // (with interception type kMultiUser or kEnterprise). If the profile is not
475     // new (kProfileSwitch), then the color is not updated.
476     base::UmaHistogramTimes(
477         "Signin.Intercept.ProfileCreationDuration",
478         base::TimeTicks::Now() - profile_creation_start_time_);
479     ProfileMetrics::LogProfileAddNewUser(
480         ProfileMetrics::ADD_NEW_USER_SIGNIN_INTERCEPTION);
481     // Apply the new color to the profile.
482     ThemeServiceFactory::GetForProfile(new_profile)
483         ->BuildAutogeneratedThemeFromColor(*profile_color);
484     // Show the customization UI to allow changing the color.
485     show_customization_bubble = true;
486   } else {
487     base::UmaHistogramTimes(
488         "Signin.Intercept.ProfileSwitchDuration",
489         base::TimeTicks::Now() - profile_creation_start_time_);
490   }
491 
492   // Work is done in this profile, the flow continues in the
493   // DiceWebSigninInterceptor that is attached to the new profile.
494   DiceWebSigninInterceptorFactory::GetForProfile(new_profile)
495       ->CreateBrowserAfterSigninInterception(account_id_, web_contents(),
496                                              show_customization_bubble);
497   Reset();
498 }
499 
OnNewBrowserCreated(bool show_customization_bubble)500 void DiceWebSigninInterceptor::OnNewBrowserCreated(
501     bool show_customization_bubble) {
502   session_startup_helper_.reset();
503   if (show_customization_bubble) {
504     Browser* browser = chrome::FindBrowserWithProfile(profile_);
505     DCHECK(browser);
506     delegate_->ShowProfileCustomizationBubble(browser);
507   }
508 }
509 
510 // static
GetPersistentEmailHash(const std::string & email)511 std::string DiceWebSigninInterceptor::GetPersistentEmailHash(
512     const std::string& email) {
513   int hash = base::PersistentHash(
514                  gaia::CanonicalizeEmail(gaia::SanitizeEmail(email))) &
515              0xFF;
516   return base::StringPrintf("email_%i", hash);
517 }
518 
RecordProfileCreationDeclined(const std::string & email)519 void DiceWebSigninInterceptor::RecordProfileCreationDeclined(
520     const std::string& email) {
521   DictionaryPrefUpdate update(profile_->GetPrefs(),
522                               kProfileCreationInterceptionDeclinedPref);
523   std::string key = GetPersistentEmailHash(email);
524   base::Optional<int> declined_count = update->FindIntKey(key);
525   update->SetIntKey(
526       key, declined_count.has_value() ? declined_count.value() + 1 : 1);
527 }
528 
HasUserDeclinedProfileCreation(const std::string & email) const529 bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation(
530     const std::string& email) const {
531   const base::DictionaryValue* pref_data = profile_->GetPrefs()->GetDictionary(
532       kProfileCreationInterceptionDeclinedPref);
533   base::Optional<int> declined_count =
534       pref_data->FindIntKey(GetPersistentEmailHash(email));
535   // Check if the user declined 3 times.
536   constexpr int kMaxProfileCreationDeclinedCount = 3;
537   return declined_count &&
538          declined_count.value() >= kMaxProfileCreationDeclinedCount;
539 }
540