sync_auth_manager.cc 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. // Copyright 2018 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. #include "components/sync/driver/sync_auth_manager.h"
  5. #include <utility>
  6. #include "base/bind.h"
  7. #include "base/metrics/histogram_macros.h"
  8. #include "base/time/time.h"
  9. #include "components/signin/public/identity_manager/access_token_fetcher.h"
  10. #include "components/signin/public/identity_manager/access_token_info.h"
  11. #include "components/signin/public/identity_manager/scope_set.h"
  12. #include "components/sync/base/stop_source.h"
  13. #include "components/sync/base/sync_prefs.h"
  14. #include "components/sync/engine/sync_credentials.h"
  15. #include "google_apis/gaia/gaia_constants.h"
  16. namespace syncer {
  17. namespace {
  18. constexpr char kSyncOAuthConsumerName[] = "sync";
  19. constexpr net::BackoffEntry::Policy
  20. kIgnoreFirstErrorRequestAccessTokenBackoffPolicy = {
  21. // Number of initial errors (in sequence) to ignore before applying
  22. // exponential back-off rules.
  23. 1,
  24. // Initial delay for exponential back-off in ms.
  25. 2000,
  26. // Factor by which the waiting time will be multiplied.
  27. 2,
  28. // Fuzzing percentage. ex: 10% will spread requests randomly
  29. // between 90%-100% of the calculated time.
  30. 0.2, // 20%
  31. // Maximum amount of time we are willing to delay our request in ms.
  32. // TODO(crbug.com/246686): We should retry RequestAccessToken on
  33. // connection state change after backoff.
  34. 1000 * 3600 * 4, // 4 hours.
  35. // Time to keep an entry from being discarded even when it
  36. // has no significant state, -1 to never discard.
  37. -1,
  38. // Don't use initial delay unless the last request was an error.
  39. false,
  40. };
  41. } // namespace
  42. SyncAuthManager::SyncAuthManager(
  43. signin::IdentityManager* identity_manager,
  44. const AccountStateChangedCallback& account_state_changed,
  45. const CredentialsChangedCallback& credentials_changed)
  46. : identity_manager_(identity_manager),
  47. account_state_changed_callback_(account_state_changed),
  48. credentials_changed_callback_(credentials_changed),
  49. request_access_token_backoff_(
  50. &kIgnoreFirstErrorRequestAccessTokenBackoffPolicy) {
  51. // |identity_manager_| can be null if local Sync is enabled.
  52. }
  53. SyncAuthManager::~SyncAuthManager() {
  54. if (registered_for_auth_notifications_) {
  55. identity_manager_->RemoveObserver(this);
  56. }
  57. }
  58. void SyncAuthManager::RegisterForAuthNotifications() {
  59. DCHECK(!registered_for_auth_notifications_);
  60. DCHECK(sync_account_.account_info.account_id.empty());
  61. identity_manager_->AddObserver(this);
  62. registered_for_auth_notifications_ = true;
  63. // Also initialize the sync account here, but *without* notifying the
  64. // SyncService.
  65. sync_account_ = DetermineAccountToUse();
  66. // If there's already a persistent auth error, also propagate that into our
  67. // local state. Note that (as of 2021-01) this shouldn't happen in practice:
  68. // Auth errors are not persisted, so it's unlikely that at this point in time
  69. // (early during browser startup) an auth error has already been detected.
  70. GoogleServiceAuthError token_error =
  71. identity_manager_->GetErrorStateOfRefreshTokenForAccount(
  72. sync_account_.account_info.account_id);
  73. if (token_error.IsPersistentError()) {
  74. SetLastAuthError(token_error);
  75. }
  76. }
  77. bool SyncAuthManager::IsActiveAccountInfoFullyLoaded() const {
  78. // The result of DetermineAccountToUse() is influenced by refresh tokens being
  79. // loaded due to how IdentityManager::ComputeUnconsentedPrimaryAccountInfo()
  80. // is implemented, which requires a refresh token.
  81. return identity_manager_->AreRefreshTokensLoaded();
  82. }
  83. SyncAccountInfo SyncAuthManager::GetActiveAccountInfo() const {
  84. // Note: |sync_account_| should generally be identical to the result of a
  85. // DetermineAccountToUse() call, but there are a few edge cases when it isn't:
  86. // E.g. when another identity observer gets notified before us and calls in
  87. // here, or when we're currently switching accounts in
  88. // UpdateSyncAccountIfNecessary(). So unfortunately we can't verify this.
  89. return sync_account_;
  90. }
  91. GoogleServiceAuthError SyncAuthManager::GetLastAuthError() const {
  92. // TODO(crbug.com/921553): Which error should take precedence?
  93. if (partial_token_status_.connection_status == CONNECTION_SERVER_ERROR) {
  94. // TODO(crbug.com/921553): Verify whether CONNECTION_FAILED is really an
  95. // appropriate auth error here; maybe SERVICE_ERROR would be better? Or
  96. // maybe we shouldn't expose this case as an auth error at all?
  97. return GoogleServiceAuthError(GoogleServiceAuthError::CONNECTION_FAILED);
  98. }
  99. return last_auth_error_;
  100. }
  101. base::Time SyncAuthManager::GetLastAuthErrorTime() const {
  102. // See GetLastAuthError().
  103. if (partial_token_status_.connection_status == CONNECTION_SERVER_ERROR) {
  104. return partial_token_status_.connection_status_update_time;
  105. }
  106. return last_auth_error_time_;
  107. }
  108. bool SyncAuthManager::IsSyncPaused() const {
  109. return IsWebSignout(GetLastAuthError());
  110. }
  111. SyncTokenStatus SyncAuthManager::GetSyncTokenStatus() const {
  112. DCHECK(partial_token_status_.next_token_request_time.is_null());
  113. SyncTokenStatus token_status = partial_token_status_;
  114. token_status.has_token = !access_token_.empty();
  115. if (request_access_token_retry_timer_.IsRunning()) {
  116. base::TimeDelta delta =
  117. request_access_token_retry_timer_.desired_run_time() -
  118. base::TimeTicks::Now();
  119. token_status.next_token_request_time = base::Time::Now() + delta;
  120. }
  121. return token_status;
  122. }
  123. SyncCredentials SyncAuthManager::GetCredentials() const {
  124. const CoreAccountInfo& account_info = sync_account_.account_info;
  125. SyncCredentials credentials;
  126. credentials.email = account_info.email;
  127. credentials.access_token = access_token_;
  128. return credentials;
  129. }
  130. void SyncAuthManager::ConnectionOpened() {
  131. DCHECK(registered_for_auth_notifications_);
  132. DCHECK(!connection_open_);
  133. connection_open_ = true;
  134. // At this point, we must not already have an access token or an attempt to
  135. // get one.
  136. DCHECK(access_token_.empty());
  137. DCHECK(!ongoing_access_token_fetch_);
  138. DCHECK(!request_access_token_retry_timer_.IsRunning());
  139. RequestAccessToken();
  140. }
  141. void SyncAuthManager::ConnectionStatusChanged(ConnectionStatus status) {
  142. DCHECK(registered_for_auth_notifications_);
  143. DCHECK(connection_open_);
  144. partial_token_status_.connection_status_update_time = base::Time::Now();
  145. partial_token_status_.connection_status = status;
  146. switch (status) {
  147. case CONNECTION_AUTH_ERROR:
  148. // Sync server returned error indicating that access token is invalid. It
  149. // could be either expired or access is revoked. Let's request another
  150. // access token and if access is revoked then request for token will fail
  151. // with corresponding error. If access token is repeatedly reported
  152. // invalid, there may be some issues with server, e.g. authentication
  153. // state is inconsistent on sync and token server. In that case, we
  154. // backoff token requests exponentially to avoid hammering token server
  155. // too much and to avoid getting same token due to token server's caching
  156. // policy. |request_access_token_retry_timer_| is used to backoff request
  157. // triggered by both auth error and failure talking to GAIA server.
  158. // Therefore, we're likely to reach the backoff ceiling more quickly than
  159. // you would expect from looking at the BackoffPolicy if both types of
  160. // errors happen. We shouldn't receive two errors back-to-back without
  161. // attempting a token/sync request in between, thus crank up request delay
  162. // unnecessary. This is because we won't make a sync request if we hit an
  163. // error until GAIA succeeds at sending a new token, and we won't request
  164. // a new token unless sync reports a token failure. But to be safe, don't
  165. // schedule request if this happens.
  166. if (ongoing_access_token_fetch_) {
  167. // A request is already in flight; nothing further needs to be done at
  168. // this point.
  169. DCHECK(access_token_.empty());
  170. DCHECK(!request_access_token_retry_timer_.IsRunning());
  171. } else if (request_access_token_retry_timer_.IsRunning()) {
  172. // The timer to perform a request later is already running; nothing
  173. // further needs to be done at this point.
  174. DCHECK(access_token_.empty());
  175. } else {
  176. // Drop any access token here, to maintain the invariant that only one
  177. // of a token OR a pending request OR a pending retry can exist at any
  178. // time.
  179. InvalidateAccessToken();
  180. request_access_token_backoff_.InformOfRequest(false);
  181. ScheduleAccessTokenRequest();
  182. }
  183. break;
  184. case CONNECTION_OK:
  185. // Reset backoff time after successful connection.
  186. // Request shouldn't be scheduled at this time. But if it is, it's
  187. // possible that sync flips between OK and auth error states rapidly,
  188. // thus hammers token server. To be safe, only reset backoff delay when
  189. // no scheduled request.
  190. if (!request_access_token_retry_timer_.IsRunning()) {
  191. request_access_token_backoff_.Reset();
  192. }
  193. break;
  194. case CONNECTION_SERVER_ERROR:
  195. // Note: This case will be exposed as an auth error, due to the
  196. // |connection_status| in |partial_token_status_|.
  197. DCHECK(GetLastAuthError().IsTransientError());
  198. break;
  199. case CONNECTION_NOT_ATTEMPTED:
  200. // The connection status should never change to "not attempted".
  201. NOTREACHED();
  202. break;
  203. }
  204. }
  205. void SyncAuthManager::InvalidateAccessToken() {
  206. DCHECK(registered_for_auth_notifications_);
  207. if (access_token_.empty()) {
  208. return;
  209. }
  210. identity_manager_->RemoveAccessTokenFromCache(
  211. sync_account_.account_info.account_id,
  212. signin::ScopeSet{GaiaConstants::kChromeSyncOAuth2Scope}, access_token_);
  213. access_token_.clear();
  214. credentials_changed_callback_.Run();
  215. }
  216. void SyncAuthManager::ClearAccessTokenAndRequest() {
  217. access_token_.clear();
  218. request_access_token_retry_timer_.Stop();
  219. ongoing_access_token_fetch_.reset();
  220. weak_ptr_factory_.InvalidateWeakPtrs();
  221. }
  222. void SyncAuthManager::ScheduleAccessTokenRequest() {
  223. DCHECK(access_token_.empty());
  224. DCHECK(!ongoing_access_token_fetch_);
  225. DCHECK(!request_access_token_retry_timer_.IsRunning());
  226. request_access_token_retry_timer_.Start(
  227. FROM_HERE, request_access_token_backoff_.GetTimeUntilRelease(),
  228. base::BindRepeating(&SyncAuthManager::RequestAccessToken,
  229. weak_ptr_factory_.GetWeakPtr()));
  230. }
  231. void SyncAuthManager::ConnectionClosed() {
  232. DCHECK(registered_for_auth_notifications_);
  233. DCHECK(connection_open_);
  234. partial_token_status_ = SyncTokenStatus();
  235. ClearAccessTokenAndRequest();
  236. connection_open_ = false;
  237. }
  238. void SyncAuthManager::OnPrimaryAccountChanged(
  239. const signin::PrimaryAccountChangeEvent& event) {
  240. if (event.GetEventTypeFor(signin::ConsentLevel::kSync) ==
  241. signin::PrimaryAccountChangeEvent::Type::kCleared) {
  242. UMA_HISTOGRAM_ENUMERATION("Sync.StopSource", SIGN_OUT, STOP_SOURCE_LIMIT);
  243. }
  244. UpdateSyncAccountIfNecessary();
  245. }
  246. void SyncAuthManager::OnRefreshTokenUpdatedForAccount(
  247. const CoreAccountInfo& account_info) {
  248. if (UpdateSyncAccountIfNecessary()) {
  249. // If the syncing account was updated as a result of this, then all that's
  250. // necessary has been handled; nothing else to be done here.
  251. return;
  252. }
  253. if (account_info.account_id != sync_account_.account_info.account_id) {
  254. return;
  255. }
  256. // Compute the validity of the new refresh token: The identity code sets an
  257. // account's refresh token to be invalid if the user signs out of that account
  258. // on the web.
  259. // TODO(blundell): Hide this logic inside IdentityManager.
  260. GoogleServiceAuthError token_error =
  261. identity_manager_->GetErrorStateOfRefreshTokenForAccount(
  262. account_info.account_id);
  263. if (IsWebSignout(token_error)) {
  264. // When the refresh token is replaced by an invalid token, Sync must be
  265. // stopped immediately, even if the current access token is still valid.
  266. // This happens e.g. when the user signs out of the web with Dice enabled.
  267. ClearAccessTokenAndRequest();
  268. // Set the last auth error. Usually this happens in AccessTokenFetched(...)
  269. // if the fetch failed, but since we just canceled any access token request,
  270. // that's not going to happen in this case.
  271. // TODO(blundell): Long-term, it would be nicer if Sync didn't have to
  272. // cache signin-level authentication errors.
  273. SetLastAuthError(token_error);
  274. credentials_changed_callback_.Run();
  275. } else if (IsWebSignout(last_auth_error_)) {
  276. // Conversely, if we just exited the web-signout state, we need to reset the
  277. // last auth error and tell our client (i.e. the SyncService) so that it'll
  278. // know to resume syncing (if appropriate).
  279. // TODO(blundell): Long-term, it would be nicer if Sync didn't have to
  280. // cache signin-level authentication errors.
  281. SetLastAuthError(token_error);
  282. credentials_changed_callback_.Run();
  283. // If we have an open connection to the server, then also get a new access
  284. // token now.
  285. if (connection_open_) {
  286. RequestAccessToken();
  287. }
  288. } else if (!access_token_.empty() ||
  289. request_access_token_retry_timer_.IsRunning()) {
  290. // If we already have an access token or previously failed to retrieve one
  291. // (and hence the retry timer is running), then request a fresh access token
  292. // now. This will also drop the current access token.
  293. DCHECK(!ongoing_access_token_fetch_);
  294. RequestAccessToken();
  295. } else if (last_auth_error_ != GoogleServiceAuthError::AuthErrorNone() &&
  296. connection_open_) {
  297. // If we were in an auth error state, then now's also a good time to try
  298. // again. In this case it's possible that there is already a pending
  299. // request, in which case RequestAccessToken will simply do nothing.
  300. // Note: This is necessary to recover if the refresh token was previously
  301. // removed.
  302. RequestAccessToken();
  303. }
  304. }
  305. void SyncAuthManager::OnRefreshTokenRemovedForAccount(
  306. const CoreAccountId& account_id) {
  307. // If we're syncing to a different account, then this doesn't affect us.
  308. if (account_id != sync_account_.account_info.account_id) {
  309. return;
  310. }
  311. if (UpdateSyncAccountIfNecessary()) {
  312. // If the syncing account was updated as a result of this, then all that's
  313. // necessary has been handled; nothing else to be done here.
  314. return;
  315. }
  316. // If we're still here, then that means Chrome is still signed in to this
  317. // account. Keep Sync alive but set an auth error.
  318. // TODO(crbug.com/1156584): Should we stop Sync in this case?
  319. DCHECK_EQ(
  320. sync_account_.account_info.account_id,
  321. identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSignin));
  322. // Note: It's possible that we're in the middle of a signout, and the "refresh
  323. // token removed" event just arrived before the "signout" event. In that case,
  324. // OnPrimaryAccountChanged() will get called momentarily and stop sync.
  325. // TODO(crbug.com/839834): REQUEST_CANCELED doesn't seem like the right auth
  326. // error to use here. Maybe INVALID_GAIA_CREDENTIALS?
  327. SetLastAuthError(
  328. GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED));
  329. ClearAccessTokenAndRequest();
  330. credentials_changed_callback_.Run();
  331. }
  332. void SyncAuthManager::OnRefreshTokensLoaded() {
  333. DCHECK(IsActiveAccountInfoFullyLoaded());
  334. if (UpdateSyncAccountIfNecessary()) {
  335. // |account_state_changed_callback_| has already been called, no need to
  336. // consider calling it again.
  337. return;
  338. }
  339. if (sync_account_.account_info.account_id.empty()) {
  340. // Nothing actually changed, so |account_state_changed_callback_| hasn't
  341. // been called yet. However, this is the first time we can reliably tell the
  342. // user is signed out, exposed via IsActiveAccountInfoFullyLoaded(), so
  343. // let's treat it as account state change.
  344. account_state_changed_callback_.Run();
  345. }
  346. }
  347. bool SyncAuthManager::IsRetryingAccessTokenFetchForTest() const {
  348. return request_access_token_retry_timer_.IsRunning();
  349. }
  350. void SyncAuthManager::ResetRequestAccessTokenBackoffForTest() {
  351. request_access_token_backoff_.Reset();
  352. }
  353. SyncAccountInfo SyncAuthManager::DetermineAccountToUse() const {
  354. DCHECK(registered_for_auth_notifications_);
  355. return syncer::DetermineAccountToUse(identity_manager_);
  356. }
  357. bool SyncAuthManager::UpdateSyncAccountIfNecessary() {
  358. DCHECK(registered_for_auth_notifications_);
  359. SyncAccountInfo new_account = DetermineAccountToUse();
  360. if (new_account.account_info.account_id ==
  361. sync_account_.account_info.account_id) {
  362. // We're already using this account (or there was and is no account to use).
  363. // If the |is_sync_consented| bit hasn't changed either, then there's
  364. // nothing to do.
  365. if (new_account.is_sync_consented == sync_account_.is_sync_consented) {
  366. return false;
  367. }
  368. // The |is_sync_consented| bit *has* changed, so update our state and
  369. // notify.
  370. sync_account_ = new_account;
  371. account_state_changed_callback_.Run();
  372. return true;
  373. }
  374. // Something has changed: Either this is a sign-in or sign-out, or the account
  375. // changed.
  376. // Sign out of the old account (if any).
  377. if (!sync_account_.account_info.account_id.empty()) {
  378. sync_account_ = SyncAccountInfo();
  379. // Let the client (SyncService) know of the removed account *before*
  380. // throwing away the access token, so it can do "unregister" tasks.
  381. account_state_changed_callback_.Run();
  382. // Also clear any pending request or auth errors we might have, since they
  383. // aren't meaningful anymore.
  384. partial_token_status_ = SyncTokenStatus();
  385. ClearAccessTokenAndRequest();
  386. SetLastAuthError(GoogleServiceAuthError::AuthErrorNone());
  387. }
  388. // Sign in to the new account (if any).
  389. if (!new_account.account_info.account_id.empty()) {
  390. DCHECK_EQ(GoogleServiceAuthError::NONE, last_auth_error_.state());
  391. sync_account_ = new_account;
  392. account_state_changed_callback_.Run();
  393. }
  394. return true;
  395. }
  396. void SyncAuthManager::RequestAccessToken() {
  397. DCHECK(registered_for_auth_notifications_);
  398. DCHECK(connection_open_);
  399. // Only one active request at a time.
  400. if (ongoing_access_token_fetch_) {
  401. DCHECK(access_token_.empty());
  402. DCHECK(!request_access_token_retry_timer_.IsRunning());
  403. return;
  404. }
  405. // If a request is scheduled for later, abandon that now since we'll send one
  406. // immediately.
  407. if (request_access_token_retry_timer_.IsRunning()) {
  408. request_access_token_retry_timer_.Stop();
  409. }
  410. // Invalidate any previous token, otherwise the token service will return the
  411. // same token again.
  412. InvalidateAccessToken();
  413. // Finally, kick off a new access token fetch.
  414. partial_token_status_.token_request_time = base::Time::Now();
  415. partial_token_status_.token_response_time = base::Time();
  416. ongoing_access_token_fetch_ =
  417. identity_manager_->CreateAccessTokenFetcherForAccount(
  418. sync_account_.account_info.account_id, kSyncOAuthConsumerName,
  419. {GaiaConstants::kChromeSyncOAuth2Scope},
  420. base::BindOnce(&SyncAuthManager::AccessTokenFetched,
  421. base::Unretained(this)),
  422. signin::AccessTokenFetcher::Mode::kWaitUntilRefreshTokenAvailable);
  423. }
  424. void SyncAuthManager::AccessTokenFetched(
  425. GoogleServiceAuthError error,
  426. signin::AccessTokenInfo access_token_info) {
  427. DCHECK(registered_for_auth_notifications_);
  428. DCHECK(ongoing_access_token_fetch_);
  429. ongoing_access_token_fetch_.reset();
  430. DCHECK(!request_access_token_retry_timer_.IsRunning());
  431. // Retry without backoff when the request is canceled for the first time. For
  432. // more details, see inline comments of
  433. // PrimaryAccountAccessTokenFetcher::OnAccessTokenFetchComplete.
  434. if (error.state() == GoogleServiceAuthError::REQUEST_CANCELED &&
  435. !access_token_retried_) {
  436. access_token_retried_ = true;
  437. RequestAccessToken();
  438. return;
  439. }
  440. access_token_ = access_token_info.token;
  441. partial_token_status_.token_response_time = base::Time::Now();
  442. partial_token_status_.last_get_token_error = error;
  443. DCHECK_EQ(access_token_.empty(),
  444. error.state() != GoogleServiceAuthError::NONE);
  445. switch (error.state()) {
  446. case GoogleServiceAuthError::NONE:
  447. SetLastAuthError(GoogleServiceAuthError::AuthErrorNone());
  448. break;
  449. case GoogleServiceAuthError::CONNECTION_FAILED:
  450. case GoogleServiceAuthError::REQUEST_CANCELED:
  451. case GoogleServiceAuthError::SERVICE_ERROR:
  452. case GoogleServiceAuthError::SERVICE_UNAVAILABLE:
  453. // Transient error. Retry after some time.
  454. // TODO(crbug.com/839834): SERVICE_ERROR is actually considered a
  455. // persistent error. Should we use .IsTransientError() instead of manually
  456. // listing cases here?
  457. request_access_token_backoff_.InformOfRequest(false);
  458. ScheduleAccessTokenRequest();
  459. break;
  460. case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS:
  461. SetLastAuthError(error);
  462. break;
  463. case GoogleServiceAuthError::USER_NOT_SIGNED_UP:
  464. case GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE:
  465. case GoogleServiceAuthError::SCOPE_LIMITED_UNRECOVERABLE_ERROR:
  466. DLOG(ERROR) << "Unexpected persistent error: " << error.ToString();
  467. SetLastAuthError(error);
  468. break;
  469. case GoogleServiceAuthError::NUM_STATES:
  470. NOTREACHED();
  471. break;
  472. }
  473. credentials_changed_callback_.Run();
  474. }
  475. void SyncAuthManager::SetLastAuthError(const GoogleServiceAuthError& error) {
  476. if (last_auth_error_ == error) {
  477. return;
  478. }
  479. last_auth_error_ = error;
  480. last_auth_error_time_ = base::Time::Now();
  481. }
  482. } // namespace syncer