// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/sync/service/sync_auth_manager.h"

#include <utility>

#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "components/signin/public/base/oauth_consumer_id.h"
#include "components/signin/public/identity_manager/access_token_fetcher.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/account_managed_status_finder.h"
#include "components/sync/base/features.h"
#include "components/sync/base/stop_source.h"
#include "components/sync/engine/sync_credentials.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/google_service_auth_error.h"

namespace syncer {

namespace {

constexpr net::BackoffEntry::Policy
    kIgnoreFirstErrorRequestAccessTokenBackoffPolicy = {
        // Number of initial errors (in sequence) to ignore before applying
        // exponential back-off rules.
        1,

        // 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/40320443): 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,
};

SyncAccountInfo DetermineAccountToUse(
    const signin::IdentityManager* identity_manager) {
  // TODO(crbug.com/40246339): During signout, it can happen that the primary
  // account temporarily doesn't have a refresh token (before the account
  // itself gets removed). As a workaround for crbug.com/1383912 /
  // crbug.com/897628, do *not* use the account for Sync in this case. This
  // ensures that Sync metadata gets properly cleared during signout.
  if (identity_manager->AreRefreshTokensLoaded() &&
      !identity_manager->HasPrimaryAccountWithRefreshToken(
          signin::ConsentLevel::kSignin)) {
    return SyncAccountInfo();
  }

  // TODO(crbug.com/40066949): Simplify once kSync becomes unreachable or is
  // deleted from the codebase. See ConsentLevel::kSync documentation for
  // details.
  return {.account_info = identity_manager->GetPrimaryAccountInfo(
              signin::ConsentLevel::kSignin),
          .is_sync_consented =
              identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)};
}

}  // namespace

SyncAuthManager::ActiveAccount::ActiveAccount(
    signin::IdentityManager* identity_manager,
    base::TimeDelta managed_status_finder_timeout,
    base::RepeatingClosure account_changed_callback)
    : identity_manager_(identity_manager),
      managed_status_finder_timeout_(managed_status_finder_timeout),
      account_changed_callback_(std::move(account_changed_callback)) {}

SyncAuthManager::ActiveAccount::~ActiveAccount() = default;

void SyncAuthManager::ActiveAccount::Set(const SyncAccountInfo& new_account) {
  account_info_ = new_account;
  StartDeterminingAccountType();
}

const SyncAccountInfo& SyncAuthManager::ActiveAccount::Get() const {
  return account_info_;
}

void SyncAuthManager::ActiveAccount::StartDeterminingAccountType() {
  if (account_info_.account_info.account_id.empty()) {
    managed_status_finder_.reset();
    return;
  }
  // If there wasn't an account previously, or the account has changed,
  // recreate the managed-status finder.
  if (!managed_status_finder_ ||
      managed_status_finder_->GetAccountInfo().account_id !=
          account_info_.account_info.account_id) {
    managed_status_finder_start_time_ = base::Time::Now();
    managed_status_finder_ =
        std::make_unique<signin::AccountManagedStatusFinder>(
            identity_manager_, account_info_.account_info,
            base::BindOnce(&SyncAuthManager::ActiveAccount::
                               AccountTypeDeterminedAsynchronously,
                           base::Unretained(this)),
            managed_status_finder_timeout_);
    base::UmaHistogramEnumeration("Sync.AccountManagedStatusSynchronousOutcome",
                                  managed_status_finder_->GetOutcome());
  }
  // Whether the `AccountManagedStatusFinder` was recreated or not, re-populate
  // the managed status. It's possible the managed status is not known yet, in
  // which case it'll be updated later in
  // `AccountTypeDeterminedAsynchronously()`.
  account_info_.managed_status = managed_status_finder_->GetOutcome();
}

void SyncAuthManager::ActiveAccount::AccountTypeDeterminedAsynchronously() {
  account_info_.managed_status = managed_status_finder_->GetOutcome();

  const base::TimeDelta duration =
      base::Time::Now() - managed_status_finder_start_time_;

  base::UmaHistogramEnumeration("Sync.AccountManagedStatusAsynchronousOutcome",
                                managed_status_finder_->GetOutcome());
  base::UmaHistogramTimes("Sync.AccountManagedStatusDuration", duration);

  account_changed_callback_.Run();
}

SyncAuthManager::SyncAuthManager(
    signin::IdentityManager* identity_manager,
    Delegate* delegate,
    base::TimeDelta account_managed_status_finder_timeout)
    : identity_manager_(identity_manager),
      delegate_(delegate),
      sync_account_(
          identity_manager,
          account_managed_status_finder_timeout,
          base::BindRepeating(&SyncAuthManager::AccountManagednessDetermined,
                              base::Unretained(this))),
      request_access_token_backoff_(
          &kIgnoreFirstErrorRequestAccessTokenBackoffPolicy) {
  CHECK(delegate_);
  // `identity_manager_` can be null if local Sync is enabled.
}

SyncAuthManager::~SyncAuthManager() {
  if (registered_for_auth_notifications_) {
    identity_manager_observation_.Reset();
  }
}

void SyncAuthManager::RegisterForAuthNotifications() {
  DCHECK(!registered_for_auth_notifications_);
  DCHECK(sync_account_.Get().account_info.account_id.empty());

  identity_manager_observation_.Observe(identity_manager_);
  registered_for_auth_notifications_ = true;

  // Initialize the sync account, without (immediately) notifying the delegate
  // aka the SyncService. (If the account's managed-ness isn't known yet and
  // gets determined later, that should and will notify the delegate.)
  sync_account_.Set(DetermineAccountToUse(identity_manager_));

  // If there's already a persistent auth error, also propagate that into our
  // local state. Note that (as of 2021-01) this shouldn't happen in practice:
  // Auth errors are not persisted, so it's unlikely that at this point in time
  // (early during browser startup) an auth error has already been detected.
  const GoogleServiceAuthError token_error =
      identity_manager_->GetErrorStateOfRefreshTokenForAccount(
          sync_account_.Get().account_info.account_id);
  if (token_error.IsPersistentError()) {
    SetLastAuthError(token_error);
  }
}

bool SyncAuthManager::IsActiveAccountInfoFullyLoaded() const {
  // The result of DetermineAccountToUse() is influenced by refresh tokens being
  // loaded due to how IdentityManager::ComputeUnconsentedPrimaryAccountInfo()
  // is implemented, which requires a refresh token.
  return identity_manager_->AreRefreshTokensLoaded();
}

SyncAccountInfo SyncAuthManager::GetActiveAccountInfo() const {
  // Note: `sync_account_` should generally be identical to the result of a
  // DetermineAccountToUse() call, but there are a few edge cases when it isn't:
  // E.g. when another identity observer gets notified before us and calls in
  // here, or when we're currently switching accounts in
  // UpdateSyncAccountIfNecessary(). So unfortunately we can't verify this.
  return sync_account_.Get();
}

GoogleServiceAuthError SyncAuthManager::GetLastAuthError() const {
  DCHECK(!last_auth_error_.IsTransientError());
  return last_auth_error_;
}

base::Time SyncAuthManager::GetLastAuthErrorTime() const {
  // See GetLastAuthError().
  if (partial_token_status_.connection_status == CONNECTION_SERVER_ERROR) {
    return partial_token_status_.connection_status_update_time;
  }
  return last_auth_error_time_;
}

bool SyncAuthManager::IsSyncPaused() const {
  DCHECK(!GetLastAuthError().IsTransientError());
  return GetLastAuthError() != GoogleServiceAuthError::AuthErrorNone();
}

SyncTokenStatus SyncAuthManager::GetSyncTokenStatus() const {
  DCHECK(partial_token_status_.next_token_request_time.is_null());

  SyncTokenStatus token_status = partial_token_status_;
  token_status.has_token = !access_token_info_.token.empty();
  if (request_access_token_retry_timer_.IsRunning()) {
    base::TimeDelta delta =
        request_access_token_retry_timer_.desired_run_time() -
        base::TimeTicks::Now();
    token_status.next_token_request_time = base::Time::Now() + delta;
  }
  return token_status;
}

SyncCredentials SyncAuthManager::GetCredentials() const {
  return {.access_token_info = access_token_info_};
}

void SyncAuthManager::FetchAccessToken(
    base::OnceCallback<void(signin::AccessTokenInfo)> callback) {
  CHECK(registered_for_auth_notifications_);
  CHECK(base::FeatureList::IsEnabled(kSyncUsePropagatedAccessToken));

  // This method is called by the sync engine, which only exists and requests
  // access tokens while the connection is open.
  CHECK(connection_open_);

  if (sync_account_.Get().account_info.IsEmpty() || IsSyncPaused()) {
    std::move(callback).Run(signin::AccessTokenInfo());
    return;
  }

  if (!access_token_info_.token.empty()) {
    std::move(callback).Run(access_token_info_);
    return;
  }

  access_token_callbacks_.push_back(std::move(callback));

  // Verifies an ongoing access token fetch internally.
  RequestAccessToken();
}

void SyncAuthManager::ConnectionOpened() {
  DCHECK(registered_for_auth_notifications_);
  DCHECK(!connection_open_);

  connection_open_ = true;

  // At this point, we must not already have an access token or an attempt to
  // get one.
  DCHECK(access_token_info_.token.empty());
  DCHECK(!ongoing_access_token_fetch_);
  DCHECK(!request_access_token_retry_timer_.IsRunning());

  RequestAccessToken();
}

void SyncAuthManager::ConnectionStatusChanged(ConnectionStatus status) {
  DCHECK(registered_for_auth_notifications_);
  DCHECK(connection_open_);

  partial_token_status_.connection_status_update_time = base::Time::Now();
  partial_token_status_.connection_status = status;

  switch (status) {
    case 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 (ongoing_access_token_fetch_) {
        // A request is already in flight; nothing further needs to be done at
        // this point.
        DCHECK(access_token_info_.token.empty());
        DCHECK(!request_access_token_retry_timer_.IsRunning());
      } else 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.
        DCHECK(access_token_info_.token.empty());
      } else {
        // Drop any access token here, to maintain the invariant that only one
        // of a token OR a pending request OR a pending retry can exist at any
        // time.
        InvalidateAccessToken();
        request_access_token_backoff_.InformOfRequest(false);
        ScheduleAccessTokenRequest();
      }
      break;
    case 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();
      }
      break;
    case CONNECTION_SERVER_ERROR:
      // Not an auth error, nothing to do.
      break;
    case CONNECTION_NOT_ATTEMPTED:
      // The connection status should never change to "not attempted".
      NOTREACHED();
  }
}

void SyncAuthManager::InvalidateAccessToken() {
  DCHECK(registered_for_auth_notifications_);

  if (access_token_info_.token.empty()) {
    return;
  }

  identity_manager_->RemoveAccessTokenFromCache(
      sync_account_.Get().account_info.account_id,
      signin::OAuthConsumerId::kSync, access_token_info_.token);

  access_token_info_ = signin::AccessTokenInfo();
  delegate_->SyncAuthCredentialsChanged();
}

void SyncAuthManager::ClearAccessTokenAndRequest() {
  access_token_info_ = signin::AccessTokenInfo();
  request_access_token_retry_timer_.Stop();
  ongoing_access_token_fetch_.reset();
  weak_ptr_factory_.InvalidateWeakPtrs();
  NotifyAccessTokenCallbacks(signin::AccessTokenInfo());
}

void SyncAuthManager::ScheduleAccessTokenRequest() {
  DCHECK(access_token_info_.token.empty());
  DCHECK(!ongoing_access_token_fetch_);
  DCHECK(!request_access_token_retry_timer_.IsRunning());

  request_access_token_retry_timer_.Start(
      FROM_HERE, request_access_token_backoff_.GetTimeUntilRelease(),
      base::BindRepeating(&SyncAuthManager::RequestAccessToken,
                          weak_ptr_factory_.GetWeakPtr()));
}

void SyncAuthManager::ConnectionClosed() {
  DCHECK(registered_for_auth_notifications_);
  DCHECK(connection_open_);

  partial_token_status_ = SyncTokenStatus();
  ClearAccessTokenAndRequest();

  connection_open_ = false;
}

void SyncAuthManager::OnPrimaryAccountChanged(
    const signin::PrimaryAccountChangeEvent& event) {
  UpdateSyncAccountIfNecessary();
}

void SyncAuthManager::OnRefreshTokenUpdatedForAccount(
    const CoreAccountInfo& account_info) {
  if (UpdateSyncAccountIfNecessary()) {
    // If the syncing account was updated as a result of this, then all that's
    // necessary has been handled; nothing else to be done here.
    return;
  }

  if (account_info.account_id != sync_account_.Get().account_info.account_id) {
    return;
  }

  // Compute the validity of the new refresh token: The identity code sets an
  // account's refresh token to be invalid if the user signs out of that account
  // on the web.
  const GoogleServiceAuthError token_error =
      identity_manager_->GetErrorStateOfRefreshTokenForAccount(
          account_info.account_id);

  // GetErrorStateOfRefreshTokenForAccount() only reports persistent errors.
  DCHECK(!token_error.IsTransientError());

  if (token_error != GoogleServiceAuthError::AuthErrorNone()) {
    DCHECK(token_error.IsPersistentError());

    // When the refresh token is replaced by an invalid token, 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.
    ClearAccessTokenAndRequest();

    // Set the last auth error. Usually this happens in AccessTokenFetched(...)
    // if the fetch failed, but since we just canceled any access token request,
    // that's not going to happen in this case.
    SetLastAuthError(token_error);

    delegate_->SyncAuthCredentialsChanged();
  } else if (last_auth_error_ != GoogleServiceAuthError::AuthErrorNone()) {
    DCHECK(last_auth_error_.IsPersistentError());
    // Conversely, if we just exited the paused state, we need to reset the last
    // auth error and tell our client (i.e. the SyncService) so that it'll know
    // to resume syncing (if appropriate).
    SetLastAuthError(GoogleServiceAuthError::AuthErrorNone());
    delegate_->SyncAuthCredentialsChanged();

    // If we have an open connection to the server, then also get a new access
    // token now.
    if (connection_open_) {
      RequestAccessToken();
    }
  } else if (!access_token_info_.token.empty() ||
             request_access_token_retry_timer_.IsRunning()) {
    // If we already have an access token or previously failed to retrieve one
    // (and hence the retry timer is running), then request a fresh access token
    // now. This will also drop the current access token.
    DCHECK(!ongoing_access_token_fetch_);
    RequestAccessToken();
  }
}

void SyncAuthManager::OnRefreshTokenRemovedForAccount(
    const CoreAccountId& account_id) {
  // If we're syncing to a different account, then this doesn't affect us.
  if (account_id != sync_account_.Get().account_info.account_id) {
    return;
  }

  bool changed = UpdateSyncAccountIfNecessary();
  // This should have removed the syncing account.
  DCHECK(changed);
  DCHECK(sync_account_.Get().account_info.IsEmpty());
}

void SyncAuthManager::OnErrorStateOfRefreshTokenUpdatedForAccount(
    const CoreAccountInfo& account_info,
    const GoogleServiceAuthError& error,
    signin_metrics::SourceForRefreshTokenOperation token_operation_source) {
  OnRefreshTokenUpdatedForAccount(account_info);
}

void SyncAuthManager::OnRefreshTokensLoaded() {
  DCHECK(IsActiveAccountInfoFullyLoaded());

  if (UpdateSyncAccountIfNecessary()) {
    // `delegate_->SyncAuthAccountStateChanged()` has already been called, no
    // need to consider calling it again.
    return;
  }

  if (sync_account_.Get().account_info.account_id.empty()) {
    // Nothing actually changed, so `delegate_->SyncAuthAccountStateChanged()`
    // hasn't been called yet. However, this is the first time we can reliably
    // tell the user is signed out, exposed via
    // IsActiveAccountInfoFullyLoaded(), so let's treat it as account state
    // change.
    delegate_->SyncAuthAccountStateChanged();
  }
}

void SyncAuthManager::OnIdentityManagerShutdown(
    signin::IdentityManager* identity_manager) {
  // Needs to be destroyed before `IdentityManager`, similar to
  // `SyncServiceImpl::OnIdentityManagerShutdown()`.
  NOTREACHED();
}

bool SyncAuthManager::IsRetryingAccessTokenFetchForTest() const {
  return request_access_token_retry_timer_.IsRunning();
}

void SyncAuthManager::ResetRequestAccessTokenBackoffForTest() {
  request_access_token_backoff_.Reset();
}

bool SyncAuthManager::UpdateSyncAccountIfNecessary() {
  DCHECK(registered_for_auth_notifications_);

  const SyncAccountInfo new_account = DetermineAccountToUse(identity_manager_);

  if (sync_account_.Get().account_info.account_id !=
      new_account.account_info.account_id) {
    // Either this is a sign-in or sign-out, or the account changed.

    // Sign out of the old account (if any).
    if (!sync_account_.Get().account_info.account_id.empty()) {
      sync_account_.Set(SyncAccountInfo());
      // Let the client (SyncService) know of the removed account *before*
      // throwing away the access token, so it can do "unregister" tasks.
      delegate_->SyncAuthAccountStateChanged();
      // Also clear any pending request or auth errors we might have, since they
      // aren't meaningful anymore.
      partial_token_status_ = SyncTokenStatus();
      ClearAccessTokenAndRequest();
      SetLastAuthError(GoogleServiceAuthError::AuthErrorNone());
    }

    // Sign in to the new account (if any).
    if (!new_account.account_info.account_id.empty()) {
      DCHECK_EQ(GoogleServiceAuthError::NONE, last_auth_error_.state());
      sync_account_.Set(new_account);
      delegate_->SyncAuthAccountStateChanged();
    }

    return true;
  }
  // Else: The account is still the same, but `is_sync_consented` and/or
  // `managed_status` may have changed.
  const SyncAccountInfo old_account = sync_account_.Get();
  sync_account_.Set(new_account);

  if (old_account == sync_account_.Get()) {
    return false;
  }

  delegate_->SyncAuthAccountStateChanged();
  return true;
}

void SyncAuthManager::AccountManagednessDetermined() {
  DCHECK(registered_for_auth_notifications_);
  delegate_->SyncAuthAccountStateChanged();
}

void SyncAuthManager::RequestAccessToken() {
  DCHECK(registered_for_auth_notifications_);
  DCHECK(connection_open_);

  // Only one active request at a time.
  if (ongoing_access_token_fetch_) {
    DCHECK(access_token_info_.token.empty());
    DCHECK(!request_access_token_retry_timer_.IsRunning());
    return;
  }

  // If a request is scheduled for later, abandon that now since we'll send one
  // immediately.
  if (request_access_token_retry_timer_.IsRunning()) {
    request_access_token_retry_timer_.Stop();
  }

  // Invalidate any previous token, otherwise the token service will return the
  // same token again.
  InvalidateAccessToken();

  // Finally, kick off a new access token fetch.
  partial_token_status_.token_request_time = base::Time::Now();
  partial_token_status_.token_response_time = base::Time();
  ongoing_access_token_fetch_ =
      identity_manager_->CreateAccessTokenFetcherForAccount(
          sync_account_.Get().account_info.account_id,
          signin::OAuthConsumerId::kSync,
          base::BindOnce(&SyncAuthManager::AccessTokenFetched,
                         base::Unretained(this)),
          signin::AccessTokenFetcher::Mode::kWaitUntilRefreshTokenAvailable);
}

void SyncAuthManager::AccessTokenFetched(
    GoogleServiceAuthError error,
    signin::AccessTokenInfo access_token_info) {
  DCHECK(registered_for_auth_notifications_);

  DCHECK(ongoing_access_token_fetch_);
  ongoing_access_token_fetch_.reset();
  DCHECK(!request_access_token_retry_timer_.IsRunning());

  // Retry without backoff when the request is canceled for the first time. For
  // more details, see inline comments of
  // PrimaryAccountAccessTokenFetcher::OnAccessTokenFetchComplete.
  if (error.state() == GoogleServiceAuthError::REQUEST_CANCELED &&
      !access_token_retried_) {
    access_token_retried_ = true;
    RequestAccessToken();
    return;
  }

  access_token_info_ = access_token_info;
  partial_token_status_.token_response_time = base::Time::Now();
  partial_token_status_.last_get_token_error = error;

  DCHECK_EQ(access_token_info_.token.empty(),
            error.state() != GoogleServiceAuthError::NONE);

  if (error.IsTransientError()) {
    // Transient error. Retry after some time.
    request_access_token_backoff_.InformOfRequest(false);
    ScheduleAccessTokenRequest();
  } else {
    SetLastAuthError(error);
  }

  NotifyAccessTokenCallbacks(access_token_info_);

  delegate_->SyncAuthCredentialsChanged();
}

void SyncAuthManager::SetLastAuthError(const GoogleServiceAuthError& error) {
  DCHECK(!error.IsTransientError());
  if (last_auth_error_ == error) {
    return;
  }
  last_auth_error_ = error;
  last_auth_error_time_ = base::Time::Now();
}

void SyncAuthManager::NotifyAccessTokenCallbacks(
    const signin::AccessTokenInfo& token) {
  for (base::OnceCallback<void(signin::AccessTokenInfo)>& callback :
       access_token_callbacks_) {
    std::move(callback).Run(token);
  }
  access_token_callbacks_.clear();
}

}  // namespace syncer
