1 // Copyright 2014 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 "components/signin/core/browser/account_reconcilor.h"
6
7 #include <algorithm>
8
9 #include "base/bind.h"
10 #include "base/json/json_reader.h"
11 #include "base/logging.h"
12 #include "base/message_loop/message_loop.h"
13 #include "base/message_loop/message_loop_proxy.h"
14 #include "base/strings/string_number_conversions.h"
15 #include "base/time/time.h"
16 #include "components/signin/core/browser/profile_oauth2_token_service.h"
17 #include "components/signin/core/browser/signin_client.h"
18 #include "components/signin/core/browser/signin_metrics.h"
19 #include "components/signin/core/common/profile_management_switches.h"
20 #include "google_apis/gaia/gaia_auth_fetcher.h"
21 #include "google_apis/gaia/gaia_auth_util.h"
22 #include "google_apis/gaia/gaia_constants.h"
23 #include "google_apis/gaia/gaia_oauth_client.h"
24 #include "google_apis/gaia/gaia_urls.h"
25 #include "net/cookies/canonical_cookie.h"
26
27
28 namespace {
29
30 class EmailEqualToFunc : public std::equal_to<std::pair<std::string, bool> > {
31 public:
32 bool operator()(const std::pair<std::string, bool>& p1,
33 const std::pair<std::string, bool>& p2) const;
34 };
35
operator ()(const std::pair<std::string,bool> & p1,const std::pair<std::string,bool> & p2) const36 bool EmailEqualToFunc::operator()(
37 const std::pair<std::string, bool>& p1,
38 const std::pair<std::string, bool>& p2) const {
39 return p1.second == p2.second && gaia::AreEmailsSame(p1.first, p2.first);
40 }
41
42 class AreEmailsSameFunc : public std::equal_to<std::string> {
43 public:
44 bool operator()(const std::string& p1,
45 const std::string& p2) const;
46 };
47
operator ()(const std::string & p1,const std::string & p2) const48 bool AreEmailsSameFunc::operator()(
49 const std::string& p1,
50 const std::string& p2) const {
51 return gaia::AreEmailsSame(p1, p2);
52 }
53
54 } // namespace
55
56
AccountReconcilor(ProfileOAuth2TokenService * token_service,SigninManagerBase * signin_manager,SigninClient * client)57 AccountReconcilor::AccountReconcilor(ProfileOAuth2TokenService* token_service,
58 SigninManagerBase* signin_manager,
59 SigninClient* client)
60 : token_service_(token_service),
61 signin_manager_(signin_manager),
62 client_(client),
63 merge_session_helper_(token_service_,
64 client->GetURLRequestContext(),
65 this),
66 registered_with_token_service_(false),
67 is_reconcile_started_(false),
68 first_execution_(true),
69 are_gaia_accounts_set_(false) {
70 VLOG(1) << "AccountReconcilor::AccountReconcilor";
71 }
72
~AccountReconcilor()73 AccountReconcilor::~AccountReconcilor() {
74 VLOG(1) << "AccountReconcilor::~AccountReconcilor";
75 // Make sure shutdown was called first.
76 DCHECK(!registered_with_token_service_);
77 }
78
Initialize(bool start_reconcile_if_tokens_available)79 void AccountReconcilor::Initialize(bool start_reconcile_if_tokens_available) {
80 VLOG(1) << "AccountReconcilor::Initialize";
81 RegisterWithSigninManager();
82
83 // If this user is not signed in, the reconcilor should do nothing but
84 // wait for signin.
85 if (IsProfileConnected()) {
86 RegisterForCookieChanges();
87 RegisterWithTokenService();
88
89 // Start a reconcile if the tokens are already loaded.
90 if (start_reconcile_if_tokens_available &&
91 token_service_->GetAccounts().size() > 0) {
92 StartReconcile();
93 }
94 }
95 }
96
Shutdown()97 void AccountReconcilor::Shutdown() {
98 VLOG(1) << "AccountReconcilor::Shutdown";
99 merge_session_helper_.CancelAll();
100 merge_session_helper_.RemoveObserver(this);
101 gaia_fetcher_.reset();
102 get_gaia_accounts_callbacks_.clear();
103 UnregisterWithSigninManager();
104 UnregisterWithTokenService();
105 UnregisterForCookieChanges();
106 }
107
AddMergeSessionObserver(MergeSessionHelper::Observer * observer)108 void AccountReconcilor::AddMergeSessionObserver(
109 MergeSessionHelper::Observer* observer) {
110 merge_session_helper_.AddObserver(observer);
111 }
112
RemoveMergeSessionObserver(MergeSessionHelper::Observer * observer)113 void AccountReconcilor::RemoveMergeSessionObserver(
114 MergeSessionHelper::Observer* observer) {
115 merge_session_helper_.RemoveObserver(observer);
116 }
117
RegisterForCookieChanges()118 void AccountReconcilor::RegisterForCookieChanges() {
119 // First clear any existing registration to avoid DCHECKs that can otherwise
120 // go off in some embedders on reauth (e.g., ChromeSigninClient).
121 UnregisterForCookieChanges();
122 cookie_changed_subscription_ = client_->AddCookieChangedCallback(
123 base::Bind(&AccountReconcilor::OnCookieChanged, base::Unretained(this)));
124 }
125
UnregisterForCookieChanges()126 void AccountReconcilor::UnregisterForCookieChanges() {
127 cookie_changed_subscription_.reset();
128 }
129
RegisterWithSigninManager()130 void AccountReconcilor::RegisterWithSigninManager() {
131 signin_manager_->AddObserver(this);
132 }
133
UnregisterWithSigninManager()134 void AccountReconcilor::UnregisterWithSigninManager() {
135 signin_manager_->RemoveObserver(this);
136 }
137
RegisterWithTokenService()138 void AccountReconcilor::RegisterWithTokenService() {
139 VLOG(1) << "AccountReconcilor::RegisterWithTokenService";
140 // During re-auth, the reconcilor will get a callback about successful signin
141 // even when the profile is already connected. Avoid re-registering
142 // with the token service since this will DCHECK.
143 if (registered_with_token_service_)
144 return;
145
146 token_service_->AddObserver(this);
147 registered_with_token_service_ = true;
148 }
149
UnregisterWithTokenService()150 void AccountReconcilor::UnregisterWithTokenService() {
151 if (!registered_with_token_service_)
152 return;
153
154 token_service_->RemoveObserver(this);
155 registered_with_token_service_ = false;
156 }
157
IsProfileConnected()158 bool AccountReconcilor::IsProfileConnected() {
159 return signin_manager_->IsAuthenticated();
160 }
161
OnCookieChanged(const net::CanonicalCookie * cookie)162 void AccountReconcilor::OnCookieChanged(const net::CanonicalCookie* cookie) {
163 if (cookie->Name() == "LSID" &&
164 cookie->Domain() == GaiaUrls::GetInstance()->gaia_url().host() &&
165 cookie->IsSecure() && cookie->IsHttpOnly()) {
166 VLOG(1) << "AccountReconcilor::OnCookieChanged: LSID changed";
167
168 // It is possible that O2RT is not available at this moment.
169 if (!token_service_->GetAccounts().size()) {
170 VLOG(1) << "AccountReconcilor::OnCookieChanged: cookie change is ingored"
171 "because O2RT is not available yet.";
172 return;
173 }
174
175 StartReconcile();
176 }
177 }
178
OnEndBatchChanges()179 void AccountReconcilor::OnEndBatchChanges() {
180 VLOG(1) << "AccountReconcilor::OnEndBatchChanges";
181 StartReconcile();
182 }
183
GoogleSigninSucceeded(const std::string & account_id,const std::string & username,const std::string & password)184 void AccountReconcilor::GoogleSigninSucceeded(const std::string& account_id,
185 const std::string& username,
186 const std::string& password) {
187 VLOG(1) << "AccountReconcilor::GoogleSigninSucceeded: signed in";
188 RegisterForCookieChanges();
189 RegisterWithTokenService();
190 }
191
GoogleSignedOut(const std::string & account_id,const std::string & username)192 void AccountReconcilor::GoogleSignedOut(const std::string& account_id,
193 const std::string& username) {
194 VLOG(1) << "AccountReconcilor::GoogleSignedOut: signed out";
195 gaia_fetcher_.reset();
196 get_gaia_accounts_callbacks_.clear();
197 AbortReconcile();
198 UnregisterWithTokenService();
199 UnregisterForCookieChanges();
200 PerformLogoutAllAccountsAction();
201 }
202
PerformMergeAction(const std::string & account_id)203 void AccountReconcilor::PerformMergeAction(const std::string& account_id) {
204 if (!switches::IsEnableAccountConsistency()) {
205 MarkAccountAsAddedToCookie(account_id);
206 return;
207 }
208 VLOG(1) << "AccountReconcilor::PerformMergeAction: " << account_id;
209 merge_session_helper_.LogIn(account_id);
210 }
211
PerformLogoutAllAccountsAction()212 void AccountReconcilor::PerformLogoutAllAccountsAction() {
213 if (!switches::IsEnableAccountConsistency())
214 return;
215 VLOG(1) << "AccountReconcilor::PerformLogoutAllAccountsAction";
216 merge_session_helper_.LogOutAllAccounts();
217 }
218
StartReconcile()219 void AccountReconcilor::StartReconcile() {
220 if (!IsProfileConnected() || is_reconcile_started_ ||
221 get_gaia_accounts_callbacks_.size() > 0 ||
222 merge_session_helper_.is_running())
223 return;
224
225 is_reconcile_started_ = true;
226
227 StartFetchingExternalCcResult();
228
229 // Reset state for validating gaia cookie.
230 are_gaia_accounts_set_ = false;
231 gaia_accounts_.clear();
232 GetAccountsFromCookie(base::Bind(
233 &AccountReconcilor::ContinueReconcileActionAfterGetGaiaAccounts,
234 base::Unretained(this)));
235
236 // Reset state for validating oauth2 tokens.
237 primary_account_.clear();
238 chrome_accounts_.clear();
239 add_to_cookie_.clear();
240 ValidateAccountsFromTokenService();
241 }
242
GetAccountsFromCookie(GetAccountsFromCookieCallback callback)243 void AccountReconcilor::GetAccountsFromCookie(
244 GetAccountsFromCookieCallback callback) {
245 get_gaia_accounts_callbacks_.push_back(callback);
246 if (!gaia_fetcher_) {
247 // There is no list account request in flight.
248 gaia_fetcher_.reset(new GaiaAuthFetcher(
249 this, GaiaConstants::kChromeSource, client_->GetURLRequestContext()));
250 gaia_fetcher_->StartListAccounts();
251 }
252 }
253
StartFetchingExternalCcResult()254 void AccountReconcilor::StartFetchingExternalCcResult() {
255 merge_session_helper_.StartFetchingExternalCcResult();
256 }
257
OnListAccountsSuccess(const std::string & data)258 void AccountReconcilor::OnListAccountsSuccess(const std::string& data) {
259 gaia_fetcher_.reset();
260
261 // Get account information from response data.
262 std::vector<std::pair<std::string, bool> > gaia_accounts;
263 bool valid_json = gaia::ParseListAccountsData(data, &gaia_accounts);
264 if (!valid_json) {
265 VLOG(1) << "AccountReconcilor::OnListAccountsSuccess: parsing error";
266 } else if (gaia_accounts.size() > 0) {
267 VLOG(1) << "AccountReconcilor::OnListAccountsSuccess: "
268 << "Gaia " << gaia_accounts.size() << " accounts, "
269 << "Primary is '" << gaia_accounts[0].first << "'";
270 } else {
271 VLOG(1) << "AccountReconcilor::OnListAccountsSuccess: No accounts";
272 }
273
274 // There must be at least one callback waiting for result.
275 DCHECK(!get_gaia_accounts_callbacks_.empty());
276
277 GoogleServiceAuthError error =
278 !valid_json ? GoogleServiceAuthError(
279 GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE)
280 : GoogleServiceAuthError::AuthErrorNone();
281 get_gaia_accounts_callbacks_.front().Run(error, gaia_accounts);
282 get_gaia_accounts_callbacks_.pop_front();
283
284 MayBeDoNextListAccounts();
285 }
286
OnListAccountsFailure(const GoogleServiceAuthError & error)287 void AccountReconcilor::OnListAccountsFailure(
288 const GoogleServiceAuthError& error) {
289 gaia_fetcher_.reset();
290 VLOG(1) << "AccountReconcilor::OnListAccountsFailure: " << error.ToString();
291 std::vector<std::pair<std::string, bool> > empty_accounts;
292
293 // There must be at least one callback waiting for result.
294 DCHECK(!get_gaia_accounts_callbacks_.empty());
295
296 get_gaia_accounts_callbacks_.front().Run(error, empty_accounts);
297 get_gaia_accounts_callbacks_.pop_front();
298
299 MayBeDoNextListAccounts();
300 }
301
MayBeDoNextListAccounts()302 void AccountReconcilor::MayBeDoNextListAccounts() {
303 if (!get_gaia_accounts_callbacks_.empty()) {
304 gaia_fetcher_.reset(new GaiaAuthFetcher(
305 this, GaiaConstants::kChromeSource, client_->GetURLRequestContext()));
306 gaia_fetcher_->StartListAccounts();
307 }
308 }
309
ContinueReconcileActionAfterGetGaiaAccounts(const GoogleServiceAuthError & error,const std::vector<std::pair<std::string,bool>> & accounts)310 void AccountReconcilor::ContinueReconcileActionAfterGetGaiaAccounts(
311 const GoogleServiceAuthError& error,
312 const std::vector<std::pair<std::string, bool> >& accounts) {
313 if (error.state() == GoogleServiceAuthError::NONE) {
314 gaia_accounts_ = accounts;
315 are_gaia_accounts_set_ = true;
316 FinishReconcile();
317 } else {
318 AbortReconcile();
319 }
320 }
321
ValidateAccountsFromTokenService()322 void AccountReconcilor::ValidateAccountsFromTokenService() {
323 primary_account_ = signin_manager_->GetAuthenticatedAccountId();
324 DCHECK(!primary_account_.empty());
325
326 chrome_accounts_ = token_service_->GetAccounts();
327
328 VLOG(1) << "AccountReconcilor::ValidateAccountsFromTokenService: "
329 << "Chrome " << chrome_accounts_.size() << " accounts, "
330 << "Primary is '" << primary_account_ << "'";
331 }
332
OnNewProfileManagementFlagChanged(bool new_flag_status)333 void AccountReconcilor::OnNewProfileManagementFlagChanged(
334 bool new_flag_status) {
335 if (new_flag_status) {
336 // The reconciler may have been newly created just before this call, or may
337 // have already existed and in mid-reconcile. To err on the safe side, force
338 // a restart.
339 Shutdown();
340 Initialize(true);
341 } else {
342 Shutdown();
343 }
344 }
345
FinishReconcile()346 void AccountReconcilor::FinishReconcile() {
347 VLOG(1) << "AccountReconcilor::FinishReconcile";
348 DCHECK(are_gaia_accounts_set_);
349 DCHECK(add_to_cookie_.empty());
350 int number_gaia_accounts = gaia_accounts_.size();
351 bool are_primaries_equal = number_gaia_accounts > 0 &&
352 gaia::AreEmailsSame(primary_account_, gaia_accounts_[0].first);
353
354 // If there are any accounts in the gaia cookie but not in chrome, then
355 // those accounts need to be removed from the cookie. This means we need
356 // to blow the cookie away.
357 int removed_from_cookie = 0;
358 for (size_t i = 0; i < gaia_accounts_.size(); ++i) {
359 const std::string& gaia_account = gaia_accounts_[i].first;
360 if (gaia_accounts_[i].second &&
361 chrome_accounts_.end() ==
362 std::find_if(chrome_accounts_.begin(),
363 chrome_accounts_.end(),
364 std::bind1st(AreEmailsSameFunc(), gaia_account))) {
365 ++removed_from_cookie;
366 }
367 }
368
369 bool rebuild_cookie = !are_primaries_equal || removed_from_cookie > 0;
370 std::vector<std::pair<std::string, bool> > original_gaia_accounts =
371 gaia_accounts_;
372 if (rebuild_cookie) {
373 VLOG(1) << "AccountReconcilor::FinishReconcile: rebuild cookie";
374 // Really messed up state. Blow away the gaia cookie completely and
375 // rebuild it, making sure the primary account as specified by the
376 // SigninManager is the first session in the gaia cookie.
377 PerformLogoutAllAccountsAction();
378 gaia_accounts_.clear();
379 }
380
381 // Create a list of accounts that need to be added to the gaia cookie.
382 // The primary account must be first to make sure it becomes the default
383 // account in the case where chrome is completely rebuilding the cookie.
384 add_to_cookie_.push_back(primary_account_);
385 for (size_t i = 0; i < chrome_accounts_.size(); ++i) {
386 if (chrome_accounts_[i] != primary_account_)
387 add_to_cookie_.push_back(chrome_accounts_[i]);
388 }
389
390 // For each account known to chrome, PerformMergeAction() if the account is
391 // not already in the cookie jar or its state is invalid, or signal merge
392 // completed otherwise. Make a copy of |add_to_cookie_| since calls to
393 // SignalComplete() will change the array.
394 std::vector<std::string> add_to_cookie_copy = add_to_cookie_;
395 int added_to_cookie = 0;
396 bool external_cc_result_completed =
397 !merge_session_helper_.StillFetchingExternalCcResult();
398 for (size_t i = 0; i < add_to_cookie_copy.size(); ++i) {
399 if (gaia_accounts_.end() !=
400 std::find_if(gaia_accounts_.begin(),
401 gaia_accounts_.end(),
402 std::bind1st(EmailEqualToFunc(),
403 std::make_pair(add_to_cookie_copy[i],
404 true)))) {
405 merge_session_helper_.SignalComplete(
406 add_to_cookie_copy[i],
407 GoogleServiceAuthError::AuthErrorNone());
408 } else {
409 PerformMergeAction(add_to_cookie_copy[i]);
410 if (original_gaia_accounts.end() ==
411 std::find_if(original_gaia_accounts.begin(),
412 original_gaia_accounts.end(),
413 std::bind1st(EmailEqualToFunc(),
414 std::make_pair(add_to_cookie_copy[i],
415 true)))) {
416 added_to_cookie++;
417 }
418 }
419 }
420
421 // Log whether the external connection checks were completed when we tried
422 // to add the accounts to the cookie.
423 if (rebuild_cookie || added_to_cookie > 0)
424 signin_metrics::LogExternalCcResultFetches(external_cc_result_completed);
425
426 signin_metrics::LogSigninAccountReconciliation(chrome_accounts_.size(),
427 added_to_cookie,
428 removed_from_cookie,
429 are_primaries_equal,
430 first_execution_,
431 number_gaia_accounts);
432 first_execution_ = false;
433 CalculateIfReconcileIsDone();
434 ScheduleStartReconcileIfChromeAccountsChanged();
435 }
436
AbortReconcile()437 void AccountReconcilor::AbortReconcile() {
438 VLOG(1) << "AccountReconcilor::AbortReconcile: we'll try again later";
439 add_to_cookie_.clear();
440 CalculateIfReconcileIsDone();
441 }
442
CalculateIfReconcileIsDone()443 void AccountReconcilor::CalculateIfReconcileIsDone() {
444 is_reconcile_started_ = !add_to_cookie_.empty();
445 if (!is_reconcile_started_)
446 VLOG(1) << "AccountReconcilor::CalculateIfReconcileIsDone: done";
447 }
448
ScheduleStartReconcileIfChromeAccountsChanged()449 void AccountReconcilor::ScheduleStartReconcileIfChromeAccountsChanged() {
450 if (is_reconcile_started_)
451 return;
452
453 // Start a reconcile as the token accounts have changed.
454 VLOG(1) << "AccountReconcilor::StartReconcileIfChromeAccountsChanged";
455 std::vector<std::string> reconciled_accounts(chrome_accounts_);
456 std::vector<std::string> new_chrome_accounts(token_service_->GetAccounts());
457 std::sort(reconciled_accounts.begin(), reconciled_accounts.end());
458 std::sort(new_chrome_accounts.begin(), new_chrome_accounts.end());
459 if (reconciled_accounts != new_chrome_accounts) {
460 base::MessageLoop::current()->PostTask(
461 FROM_HERE,
462 base::Bind(&AccountReconcilor::StartReconcile, base::Unretained(this)));
463 }
464 }
465
466 // Remove the account from the list that is being merged.
MarkAccountAsAddedToCookie(const std::string & account_id)467 bool AccountReconcilor::MarkAccountAsAddedToCookie(
468 const std::string& account_id) {
469 for (std::vector<std::string>::iterator i = add_to_cookie_.begin();
470 i != add_to_cookie_.end();
471 ++i) {
472 if (account_id == *i) {
473 add_to_cookie_.erase(i);
474 return true;
475 }
476 }
477 return false;
478 }
479
MergeSessionCompleted(const std::string & account_id,const GoogleServiceAuthError & error)480 void AccountReconcilor::MergeSessionCompleted(
481 const std::string& account_id,
482 const GoogleServiceAuthError& error) {
483 VLOG(1) << "AccountReconcilor::MergeSessionCompleted: account_id="
484 << account_id << " error=" << error.ToString();
485
486 if (MarkAccountAsAddedToCookie(account_id)) {
487 CalculateIfReconcileIsDone();
488 ScheduleStartReconcileIfChromeAccountsChanged();
489 }
490 }
491