blob: 8ff2fe907000ecaab0eb16372c6da3b2a493ad26 [file] [log] [blame]
// 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 "components/browser_sync/sync_auth_manager.h"
#include <utility>
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "components/browser_sync/profile_sync_service.h"
#include "components/sync/base/stop_source.h"
#include "components/sync/base/sync_prefs.h"
#include "components/sync/engine/sync_credentials.h"
#include "google_apis/gaia/gaia_constants.h"
#include "services/identity/public/cpp/primary_account_access_token_fetcher.h"
namespace browser_sync {
namespace {
constexpr char kSyncOAuthConsumerName[] = "sync";
constexpr net::BackoffEntry::Policy kRequestAccessTokenBackoffPolicy = {
// Number of initial errors (in sequence) to ignore before applying
// exponential back-off rules.
0,
// Initial delay for exponential back-off in ms.
2000,
// Factor by which the waiting time will be multiplied.
2,
// Fuzzing percentage. ex: 10% will spread requests randomly
// between 90%-100% of the calculated time.
0.2, // 20%
// Maximum amount of time we are willing to delay our request in ms.
// TODO(crbug.com/246686): We should retry RequestAccessToken on connection
// state change after backoff.
1000 * 3600 * 4, // 4 hours.
// Time to keep an entry from being discarded even when it
// has no significant state, -1 to never discard.
-1,
// Don't use initial delay unless the last request was an error.
false,
};
} // namespace
SyncAuthManager::SyncAuthManager(ProfileSyncService* sync_service,
syncer::SyncPrefs* sync_prefs,
identity::IdentityManager* identity_manager,
OAuth2TokenService* token_service)
: sync_service_(sync_service),
sync_prefs_(sync_prefs),
identity_manager_(identity_manager),
token_service_(token_service),
registered_for_auth_notifications_(false),
is_auth_in_progress_(false),
request_access_token_backoff_(&kRequestAccessTokenBackoffPolicy),
weak_ptr_factory_(this) {
DCHECK(sync_service_);
DCHECK(sync_prefs_);
// |identity_manager_| and |token_service_| can be null if local Sync is
// enabled.
}
SyncAuthManager::~SyncAuthManager() {
if (registered_for_auth_notifications_) {
token_service_->RemoveObserver(this);
identity_manager_->RemoveObserver(this);
}
}
void SyncAuthManager::RegisterForAuthNotifications() {
DCHECK(!registered_for_auth_notifications_);
identity_manager_->AddObserver(this);
token_service_->AddObserver(this);
registered_for_auth_notifications_ = true;
}
AccountInfo SyncAuthManager::GetAuthenticatedAccountInfo() const {
return identity_manager_ ? identity_manager_->GetPrimaryAccountInfo()
: AccountInfo();
}
bool SyncAuthManager::RefreshTokenIsAvailable() const {
std::string account_id = GetAuthenticatedAccountInfo().account_id;
return !account_id.empty() &&
token_service_->RefreshTokenIsAvailable(account_id);
}
const syncer::SyncTokenStatus& SyncAuthManager::GetSyncTokenStatus() const {
return token_status_;
}
syncer::SyncCredentials SyncAuthManager::GetCredentials() const {
syncer::SyncCredentials credentials;
const AccountInfo account_info = GetAuthenticatedAccountInfo();
credentials.account_id = account_info.account_id;
credentials.email = account_info.email;
credentials.sync_token = access_token_;
credentials.scope_set.insert(GaiaConstants::kChromeSyncOAuth2Scope);
return credentials;
}
void SyncAuthManager::ConnectionStatusChanged(syncer::ConnectionStatus status) {
token_status_.connection_status_update_time = base::Time::Now();
token_status_.connection_status = status;
switch (status) {
case syncer::CONNECTION_AUTH_ERROR:
// Sync server returned error indicating that access token is invalid. It
// could be either expired or access is revoked. Let's request another
// access token and if access is revoked then request for token will fail
// with corresponding error. If access token is repeatedly reported
// invalid, there may be some issues with server, e.g. authentication
// state is inconsistent on sync and token server. In that case, we
// backoff token requests exponentially to avoid hammering token server
// too much and to avoid getting same token due to token server's caching
// policy. |request_access_token_retry_timer_| is used to backoff request
// triggered by both auth error and failure talking to GAIA server.
// Therefore, we're likely to reach the backoff ceiling more quickly than
// you would expect from looking at the BackoffPolicy if both types of
// errors happen. We shouldn't receive two errors back-to-back without
// attempting a token/sync request in between, thus crank up request delay
// unnecessary. This is because we won't make a sync request if we hit an
// error until GAIA succeeds at sending a new token, and we won't request
// a new token unless sync reports a token failure. But to be safe, don't
// schedule request if this happens.
if (request_access_token_retry_timer_.IsRunning()) {
// The timer to perform a request later is already running; nothing
// further needs to be done at this point.
} else if (request_access_token_backoff_.failure_count() == 0) {
// First time request without delay. Currently invalid token is used
// to initialize sync engine and we'll always end up here. We don't
// want to delay initialization.
request_access_token_backoff_.InformOfRequest(false);
RequestAccessToken();
} else {
request_access_token_backoff_.InformOfRequest(false);
base::TimeDelta delay =
request_access_token_backoff_.GetTimeUntilRelease();
token_status_.next_token_request_time = base::Time::Now() + delay;
request_access_token_retry_timer_.Start(
FROM_HERE, delay,
base::BindRepeating(&SyncAuthManager::RequestAccessToken,
weak_ptr_factory_.GetWeakPtr()));
}
break;
case syncer::CONNECTION_OK:
// Reset backoff time after successful connection.
// Request shouldn't be scheduled at this time. But if it is, it's
// possible that sync flips between OK and auth error states rapidly,
// thus hammers token server. To be safe, only reset backoff delay when
// no scheduled request.
if (!request_access_token_retry_timer_.IsRunning()) {
request_access_token_backoff_.Reset();
}
ClearAuthError();
break;
case syncer::CONNECTION_SERVER_ERROR:
UpdateAuthErrorState(
GoogleServiceAuthError(GoogleServiceAuthError::CONNECTION_FAILED));
break;
case syncer::CONNECTION_NOT_ATTEMPTED:
// The connection status should never change to "not attempted".
NOTREACHED();
break;
}
}
void SyncAuthManager::UpdateAuthErrorState(
const GoogleServiceAuthError& error) {
is_auth_in_progress_ = false;
last_auth_error_ = error;
}
void SyncAuthManager::ClearAuthError() {
UpdateAuthErrorState(GoogleServiceAuthError::AuthErrorNone());
}
void SyncAuthManager::ClearAccessTokenAndRequest() {
access_token_.clear();
request_access_token_retry_timer_.Stop();
token_status_.next_token_request_time = base::Time();
ongoing_access_token_fetch_.reset();
weak_ptr_factory_.InvalidateWeakPtrs();
}
void SyncAuthManager::Clear() {
ClearAuthError();
ClearAccessTokenAndRequest();
}
void SyncAuthManager::OnPrimaryAccountSet(
const AccountInfo& primary_account_info) {
// Track the fact that we're still waiting for auth to complete.
DCHECK(!is_auth_in_progress_);
is_auth_in_progress_ = true;
sync_service_->OnPrimaryAccountSet();
if (token_service_->RefreshTokenIsAvailable(
primary_account_info.account_id)) {
OnRefreshTokenAvailable(primary_account_info.account_id);
}
}
void SyncAuthManager::OnPrimaryAccountCleared(
const AccountInfo& previous_primary_account_info) {
UMA_HISTOGRAM_ENUMERATION("Sync.StopSource", syncer::SIGN_OUT,
syncer::STOP_SOURCE_LIMIT);
is_auth_in_progress_ = false;
sync_service_->OnPrimaryAccountCleared();
}
void SyncAuthManager::OnRefreshTokenAvailable(const std::string& account_id) {
if (account_id != GetAuthenticatedAccountInfo().account_id) {
return;
}
GoogleServiceAuthError token_error =
token_service_->GetAuthError(GetAuthenticatedAccountInfo().account_id);
if (token_error == GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
GoogleServiceAuthError::InvalidGaiaCredentialsReason::
CREDENTIALS_REJECTED_BY_CLIENT)) {
is_auth_in_progress_ = false;
// When the refresh token is replaced by a new token with a
// CREDENTIALS_REJECTED_BY_CLIENT error, Sync must be stopped immediately,
// even if the current access token is still valid. This happens e.g. when
// the user signs out of the web with Dice enabled.
// It is not necessary to do this when the refresh token is
// CREDENTIALS_REJECTED_BY_SERVER, because in that case the access token
// will be rejected by the server too.
// We only do this in OnRefreshTokensLoaded(), as opposed to
// OAuth2TokenService::Observer::OnAuthErrorChanged(), because
// CREDENTIALS_REJECTED_BY_CLIENT is only set by the signin component when
// the refresh token is created.
ClearAccessTokenAndRequest();
// TODO(treib): Should we also set our auth error state?
sync_service_->OnRefreshTokenRevoked();
// TODO(treib): We can probably early-out here - no point in also calling
// OnRefreshTokenAvailable on the ProfileSyncService.
}
sync_service_->OnRefreshTokenAvailable();
}
void SyncAuthManager::OnRefreshTokenRevoked(const std::string& account_id) {
if (account_id != GetAuthenticatedAccountInfo().account_id) {
return;
}
UpdateAuthErrorState(
GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED));
ClearAccessTokenAndRequest();
sync_service_->OnRefreshTokenRevoked();
}
void SyncAuthManager::OnRefreshTokensLoaded() {
// This notification gets fired when OAuth2TokenService loads the tokens from
// storage. Initialize the engine if sync is enabled. If the sync token was
// not loaded, GetCredentials() will generate invalid credentials to cause the
// engine to generate an auth error (https://crbug.com/121755).
// TODO(treib): Is this necessary? Either we actually have a refresh token, in
// which case this was already called from OnRefreshTokenAvailable above, or
// there is no refresh token, in which case Sync can't start anyway.
sync_service_->OnRefreshTokenAvailable();
}
bool SyncAuthManager::IsRetryingAccessTokenFetchForTest() const {
return request_access_token_retry_timer_.IsRunning();
}
void SyncAuthManager::RequestAccessToken() {
// Only one active request at a time.
if (ongoing_access_token_fetch_) {
return;
}
request_access_token_retry_timer_.Stop();
token_status_.next_token_request_time = base::Time();
OAuth2TokenService::ScopeSet oauth2_scopes;
oauth2_scopes.insert(GaiaConstants::kChromeSyncOAuth2Scope);
// Invalidate previous token, otherwise token service will return the same
// token again.
if (!access_token_.empty()) {
identity_manager_->RemoveAccessTokenFromCache(GetAuthenticatedAccountInfo(),
oauth2_scopes, access_token_);
}
access_token_.clear();
token_status_.token_request_time = base::Time::Now();
token_status_.token_receive_time = base::Time();
token_status_.next_token_request_time = base::Time();
ongoing_access_token_fetch_ =
identity_manager_->CreateAccessTokenFetcherForPrimaryAccount(
kSyncOAuthConsumerName, oauth2_scopes,
base::BindOnce(&SyncAuthManager::AccessTokenFetched,
base::Unretained(this)),
identity::PrimaryAccountAccessTokenFetcher::Mode::kImmediate);
}
void SyncAuthManager::AccessTokenFetched(const GoogleServiceAuthError& error,
const std::string& access_token) {
DCHECK(ongoing_access_token_fetch_);
std::unique_ptr<identity::PrimaryAccountAccessTokenFetcher> fetcher_deleter(
std::move(ongoing_access_token_fetch_));
access_token_ = access_token;
token_status_.last_get_token_error = error;
switch (error.state()) {
case GoogleServiceAuthError::NONE:
token_status_.token_receive_time = base::Time::Now();
sync_prefs_->SetSyncAuthError(false);
ClearAuthError();
break;
case GoogleServiceAuthError::CONNECTION_FAILED:
case GoogleServiceAuthError::REQUEST_CANCELED:
case GoogleServiceAuthError::SERVICE_ERROR:
case GoogleServiceAuthError::SERVICE_UNAVAILABLE:
// Transient error. Retry after some time.
request_access_token_backoff_.InformOfRequest(false);
token_status_.next_token_request_time =
base::Time::Now() +
request_access_token_backoff_.GetTimeUntilRelease();
request_access_token_retry_timer_.Start(
FROM_HERE, request_access_token_backoff_.GetTimeUntilRelease(),
base::BindRepeating(&SyncAuthManager::RequestAccessToken,
weak_ptr_factory_.GetWeakPtr()));
break;
case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS:
sync_prefs_->SetSyncAuthError(true);
UpdateAuthErrorState(error);
break;
default:
LOG(ERROR) << "Unexpected persistent error: " << error.ToString();
UpdateAuthErrorState(error);
}
sync_service_->AccessTokenFetched(error);
}
} // namespace browser_sync