// Copyright 2015 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/signin/internal/identity_manager/mutable_profile_oauth2_token_service_delegate.h"

#include <stddef.h>

#include <algorithm>
#include <map>
#include <optional>
#include <string>

#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/containers/map_util.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/signin/internal/identity_manager/profile_oauth2_token_service.h"
#include "components/signin/internal/identity_manager/token_binding_helper.h"
#include "components/signin/internal/identity_manager/token_binding_oauth2_access_token_fetcher.h"
#include "components/signin/public/base/binding_key_registration_token_result.h"
#include "components/signin/public/base/device_id_helper.h"
#include "components/signin/public/base/hybrid_encryption_key.h"
#include "components/signin/public/base/signin_client.h"
#include "components/signin/public/base/signin_metrics.h"
#include "components/signin/public/base/signin_pref_names.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/webdata/token_service_table.h"
#include "components/signin/public/webdata/token_web_data.h"
#include "components/unexportable_keys/features.h"
#include "components/version_info/version_info.h"
#include "components/webdata/common/web_data_service_base.h"
#include "crypto/process_bound_string.h"
#include "google_apis/gaia/core_account_id.h"
#include "google_apis/gaia/gaia_access_token_fetcher.h"
#include "google_apis/gaia/gaia_auth_fetcher.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "google_apis/gaia/gaia_config.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_id.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/oauth2_access_token_fetcher.h"
#include "google_apis/gaia/oauth2_access_token_fetcher_immediate_error.h"
#include "google_apis/gaia/oauth2_mint_access_token_fetcher_adapter.h"
#include "google_apis/google_api_keys.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"

namespace {

constexpr base::TimeDelta kWebWrappedKeyFetchDelay = base::Minutes(2);

bool g_ignore_non_official_api_keys_for_testing = false;

const char kAccountIdPrefix[] = "AccountId-";

// Temporary kill-switch in case the unreadable tokens clearing logic needs to
// be disabled.
BASE_FEATURE(kClearUnreadableTokensUponAddingNewCredential,
             base::FEATURE_ENABLED_BY_DEFAULT);

// Kill-switch for revoking tokens containing invalid characters on load.
BASE_FEATURE(kRevokeTokensWithInvalidCharactersOnLoad,
             base::FEATURE_ENABLED_BY_DEFAULT);

// Returns true if `token` contains invalid characters and thus it cannot
// be a valid refresh or access token. Most likely this happens due to a failed
// OSCrypt decryption.
// This check is performed on database load to prevent a crash when the token is
// later used in an Authorization header.
// See https://crbug.com/485885766#comment2.
bool HasInvalidCharacters(std::string_view token) {
  if (!base::FeatureList::IsEnabled(kRevokeTokensWithInvalidCharactersOnLoad)) {
    return false;
  }

  return !std::ranges::all_of(token, [](char c) {
    // The token must consist of printable ASCII characters.
    // https://www.rfc-editor.org/rfc/rfc6749.html#appendix-A.17
    // refresh-token = 1*VSCHAR
    // VSCHAR = %x20-7E
    return c >= 0x20 && c <= 0x7E;
  });
}

std::string ApplyAccountIdPrefix(const std::string& account_id) {
  return kAccountIdPrefix + account_id;
}

// Checks that |prefixed_account_id| starts with the expected prefix
// (|prefixed_account_id|) and returns the non-prefixed account id or an empty
// account id if |prefixed_account_id| is not correctly prefixed.
CoreAccountId RemoveAccountIdPrefix(const std::string& prefixed_account_id) {
  if (!base::StartsWith(prefixed_account_id, kAccountIdPrefix)) {
    return CoreAccountId();
  }

  return CoreAccountId::FromString(
      prefixed_account_id.substr(/*pos=*/strlen(kAccountIdPrefix)));
}

signin::LoadCredentialsState LoadCredentialsStateFromTokenResult(
    TokenServiceTable::Result token_result) {
  switch (token_result) {
    case TokenServiceTable::TOKEN_DB_RESULT_SQL_INVALID_STATEMENT:
    case TokenServiceTable::TOKEN_DB_RESULT_BAD_ENTRY:
      return signin::LoadCredentialsState::
          LOAD_CREDENTIALS_FINISHED_WITH_DB_ERRORS;
    case TokenServiceTable::TOKEN_DB_RESULT_DECRYPT_ERROR:
      return signin::LoadCredentialsState::
          LOAD_CREDENTIALS_FINISHED_WITH_DECRYPT_ERRORS;
    case TokenServiceTable::TOKEN_DB_RESULT_SUCCESS:
      return signin::LoadCredentialsState::
          LOAD_CREDENTIALS_FINISHED_WITH_SUCCESS;
  }
  NOTREACHED();
}

constexpr std::string_view kTokenBindingAssertionDestinationUrl =
    "https://accounts.google.com/accountmanager";

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange(BoundTokenPrevalence)
enum class BoundTokenPrevalence {
  kEmpty = 0,
  kZeroTokensBound = 1,
  kSomeTokensBoundSomeUnbound = 2,
  kAllTokensBound = 3,
  kMaxValue = kAllTokensBound
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/signin/enums.xml:TokenBindingBoundTokenPrevalence)

void RecordTokenBindingHistogramsOnCredentialsLoaded(
    TokenBindingHelper* token_binding_helper,
    size_t token_count) {
  size_t bound_token_count =
      token_binding_helper ? token_binding_helper->GetBoundTokenCount() : 0;
  CHECK_LE(bound_token_count, token_count);

  BoundTokenPrevalence prevalence = [token_count, bound_token_count] {
    if (token_count == 0) {
      return BoundTokenPrevalence::kEmpty;
    } else if (bound_token_count == 0) {
      return BoundTokenPrevalence::kZeroTokensBound;
    } else if (token_count == bound_token_count) {
      return BoundTokenPrevalence::kAllTokensBound;
    } else {
      return BoundTokenPrevalence::kSomeTokensBoundSomeUnbound;
    }
  }();

  base::UmaHistogramEnumeration("Signin.TokenBinding.BoundTokenPrevalence",
                                prevalence);
  if (bound_token_count > 1) {
    CHECK(token_binding_helper);
    base::UmaHistogramBoolean("Signin.TokenBinding.BoundToTheSameKey",
                              token_binding_helper->AreAllBindingKeysSame());
  }
}

// Determines whether `account_id` can be moved to `destination_service`.
// An account cannot be moved if `destination_service` has accounts bound to a
// different binding key.
bool CanMoveAccountToService(
    const ProfileOAuth2TokenService& destination_service,
    const CoreAccountId& account_id,
    const std::vector<uint8_t>& wrapped_binding_key) {
  if (wrapped_binding_key.empty()) {
    // Unbound refresh tokens can always be moved.
    return true;
  }

  for (const auto& account : destination_service.GetAccounts()) {
    if (account == account_id) {
      // Ignore `account_id` as it will get a new refresh token after the move.
      continue;
    }
    std::vector<uint8_t> other_wrapped_binding_key =
        destination_service.GetWrappedBindingKey(account);
    if (!other_wrapped_binding_key.empty() &&
        other_wrapped_binding_key != wrapped_binding_key) {
      return false;
    }
  }
  return true;
}

bool ShouldUseIssueTokenForUnboundTokens() {
  if (GaiaConfig* gaia_config = GaiaConfig::GetInstance()) {
    std::optional<bool> enable_issue_token_fetch =
        gaia_config->GetFlagIfExists("enable_issue_token_fetch");
    if (enable_issue_token_fetch.has_value()) {
      return *enable_issue_token_fetch;
    }
  }

  // IssueToken can be used only with official Google API keys.
  if (!google_apis::IsGoogleChromeAPIKeyUsed() &&
      !g_ignore_non_official_api_keys_for_testing) {
    return false;
  }

  return true;
}

}  // namespace

// This class sends a request to GAIA to revoke the given refresh token from
// the server.  This is a best effort attempt only.  This class deletes itself
// when done successfully or otherwise.
class MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken
    : public GaiaAuthConsumer {
 public:
  RevokeServerRefreshToken(
      MutableProfileOAuth2TokenServiceDelegate* token_service_delegate,
      SigninClient* client,
      const std::string& refresh_token,
      int attempt);

  RevokeServerRefreshToken(const RevokeServerRefreshToken&) = delete;
  RevokeServerRefreshToken& operator=(const RevokeServerRefreshToken&) = delete;

  ~RevokeServerRefreshToken() override;

 private:
  // Starts the network request.
  void Start();
  // Returns true if the request should be retried.
  bool ShouldRetry(GaiaAuthConsumer::TokenRevocationStatus status);
  // GaiaAuthConsumer overrides:
  void OnOAuth2RevokeTokenCompleted(
      GaiaAuthConsumer::TokenRevocationStatus status) override;

  raw_ptr<MutableProfileOAuth2TokenServiceDelegate> token_service_delegate_;
  GaiaAuthFetcher fetcher_;
  std::string refresh_token_;
  int attempt_;
  base::WeakPtrFactory<RevokeServerRefreshToken> weak_ptr_factory_{this};
};

MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken::
    RevokeServerRefreshToken(
        MutableProfileOAuth2TokenServiceDelegate* token_service_delegate,
        SigninClient* client,
        const std::string& refresh_token,
        int attempt)
    : token_service_delegate_(token_service_delegate),
      fetcher_(this,
               gaia::GaiaSource::kChrome,
               token_service_delegate_->GetURLLoaderFactory()),
      refresh_token_(refresh_token),
      attempt_(attempt) {
  client->DelayNetworkCall(
      base::BindRepeating(&MutableProfileOAuth2TokenServiceDelegate::
                              RevokeServerRefreshToken::Start,
                          weak_ptr_factory_.GetWeakPtr()));
}

void MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken::
    Start() {
  fetcher_.StartRevokeOAuth2Token(refresh_token_);
}

MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken::
    ~RevokeServerRefreshToken() = default;

bool MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken::
    ShouldRetry(GaiaAuthConsumer::TokenRevocationStatus status) {
  // Token revocation can be retried up to 3 times.
  if (attempt_ >= 2) {
    return false;
  }

  switch (status) {
    case GaiaAuthConsumer::TokenRevocationStatus::kServerError:
    case GaiaAuthConsumer::TokenRevocationStatus::kConnectionFailed:
    case GaiaAuthConsumer::TokenRevocationStatus::kConnectionTimeout:
    case GaiaAuthConsumer::TokenRevocationStatus::kConnectionCanceled:
      return true;
    case GaiaAuthConsumer::TokenRevocationStatus::kSuccess:
    case GaiaAuthConsumer::TokenRevocationStatus::kInvalidToken:
    case GaiaAuthConsumer::TokenRevocationStatus::kInvalidRequest:
    case GaiaAuthConsumer::TokenRevocationStatus::kUnknownError:
      return false;
  }
}

void MutableProfileOAuth2TokenServiceDelegate::RevokeServerRefreshToken::
    OnOAuth2RevokeTokenCompleted(
        GaiaAuthConsumer::TokenRevocationStatus status) {
  if (ShouldRetry(status)) {
    token_service_delegate_->server_revokes_.push_back(
        std::make_unique<RevokeServerRefreshToken>(
            token_service_delegate_, token_service_delegate_->client_,
            refresh_token_, attempt_ + 1));
  }
  // |this| pointer will be deleted when removed from the vector, so don't
  // access any members after call to erase().
  token_service_delegate_->server_revokes_.erase(std::ranges::find(
      token_service_delegate_->server_revokes_, this,
      &std::unique_ptr<MutableProfileOAuth2TokenServiceDelegate::
                           RevokeServerRefreshToken>::get));
}

MutableProfileOAuth2TokenServiceDelegate::TokenData::TokenData(
    crypto::ProcessBoundString refresh_token,
    bool mtls_token_binding)
    : refresh_token(std::move(refresh_token)),
      mtls_token_binding(mtls_token_binding) {}

MutableProfileOAuth2TokenServiceDelegate::TokenData::TokenData(
    const TokenData&) = default;
MutableProfileOAuth2TokenServiceDelegate::TokenData&
MutableProfileOAuth2TokenServiceDelegate::TokenData::operator=(
    const TokenData&) = default;
MutableProfileOAuth2TokenServiceDelegate::TokenData::TokenData(TokenData&&) =
    default;
MutableProfileOAuth2TokenServiceDelegate::TokenData&
MutableProfileOAuth2TokenServiceDelegate::TokenData::operator=(TokenData&&) =
    default;

MutableProfileOAuth2TokenServiceDelegate::TokenData::~TokenData() = default;

MutableProfileOAuth2TokenServiceDelegate::
    MutableProfileOAuth2TokenServiceDelegate(
        SigninClient* client,
        AccountTrackerService* account_tracker_service,
        network::NetworkConnectionTracker* network_connection_tracker,
        scoped_refptr<TokenWebData> token_web_data,
        RevokeAllTokensOnLoad revoke_all_tokens_on_load,
        std::unique_ptr<TokenBindingHelper> token_binding_helper,
        FixRequestErrorCallback fix_request_error_callback)
    : ProfileOAuth2TokenServiceDelegate(/*use_backoff=*/true),
      web_data_service_request_(0),
      client_(client),
      account_tracker_service_(account_tracker_service),
      network_connection_tracker_(network_connection_tracker),
      token_web_data_(token_web_data),
      revoke_all_tokens_on_load_(revoke_all_tokens_on_load),
      token_binding_helper_(std::move(token_binding_helper)),
      fix_request_error_callback_(fix_request_error_callback) {
  VLOG(1) << "MutablePO2TS::MutablePO2TS";
  DCHECK(client);
  DCHECK(account_tracker_service_);
  DCHECK(network_connection_tracker_);
  network_connection_tracker_->AddNetworkConnectionObserver(this);
  if (token_binding_helper_) {
    // `base::Unretained()` is safe because `this` owns `token_binding_helper`.
    token_binding_helper_->SetSaveBindingKeyCallback(base::BindRepeating(
        &MutableProfileOAuth2TokenServiceDelegate::UpdateRefreshTokenBindingKey,
        base::Unretained(this)));
  }
}

MutableProfileOAuth2TokenServiceDelegate::
    ~MutableProfileOAuth2TokenServiceDelegate() {
  VLOG(1) << "MutablePO2TS::~MutablePO2TS";
  DCHECK(server_revokes_.empty());
  network_connection_tracker_->RemoveNetworkConnectionObserver(this);
}

std::unique_ptr<OAuth2AccessTokenFetcher>
MutableProfileOAuth2TokenServiceDelegate::CreateAccessTokenFetcher(
    const CoreAccountId& account_id,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    OAuth2AccessTokenConsumer* consumer,
    const std::string& token_binding_challenge) {
  ValidateAccountId(account_id);
  // check whether the account has persistent error.
  GoogleServiceAuthError auth_error = GetAuthError(account_id);
  if (auth_error.IsPersistentError()) {
    VLOG(1) << "Request for token has been rejected due to persistent error #"
            << auth_error.state();
    return std::make_unique<OAuth2AccessTokenFetcherImmediateError>(consumer,
                                                                    auth_error);
  }
  if (BackoffEntry()->ShouldRejectRequest()) {
    VLOG(1) << "Request for token has been rejected due to backoff rules from"
            << " previous error #" << BackOffError().state();
    return std::make_unique<OAuth2AccessTokenFetcherImmediateError>(
        consumer, BackOffError());
  }
  std::string refresh_token = GetRefreshToken(account_id);
  bool mtls_token_binding = IsRefreshTokenBoundToMtls(account_id);
  DCHECK(!refresh_token.empty());
  bool is_refresh_token_bound = IsRefreshTokenBoundToKey(account_id);
  if (is_refresh_token_bound || ShouldUseIssueTokenForUnboundTokens()) {
    // `CoreAccountId` is always equal to Gaia ID on DICE platforms.
    // We cannot get Gaia ID from `account_tracker_service_` as it's sometimes
    // unknown and the only way of getting it requires an access token, which
    // requires a known Gaia ID (see https://crbug.com/386841916).
    const GaiaId gaia_id(account_id.ToString());
    const std::string device_id =
        signin::GetSigninScopedDeviceId(client_->GetPrefs());
    // `GaiaAccessTokenFetcher` doesn't support bound refresh tokens.
    auto fetcher = std::make_unique<OAuth2MintAccessTokenFetcherAdapter>(
        consumer, url_loader_factory, gaia_id, refresh_token,
        mtls_token_binding, is_refresh_token_bound, device_id,
        std::string(version_info::GetVersionNumber()),
        std::string(
            version_info::GetChannelString(client_->GetClientChannel())));
    if (token_binding_helper_ &&
        token_binding_helper_->IsRegistrationKeyReady() &&
        base::FeatureList::IsEnabled(
            switches::kEnableChromeRefreshTokenBindingUpgrade)) {
      fetcher->EnableTokenUpgradeEligibility(
          base::BindOnce(&TokenBindingHelper::PerformTokenBindingUpgrade,
                         token_binding_helper_->GetWeakPtr(), account_id,
                         refresh_token, url_loader_factory, device_id));
    }
    if (token_binding_challenge.empty() || !is_refresh_token_bound) {
      return fetcher;
    }
    // `fetcher_wrapper` makes `fetcher` wait until a binding key assertion is
    // generated before sending a network request.
    auto fetcher_wrapper =
        std::make_unique<TokenBindingOAuth2AccessTokenFetcher>(
            std::move(fetcher));
    HybridEncryptionKey ephemeral_key;
    std::string ephemeral_public_key = ephemeral_key.ExportPublicKey();
    token_binding_helper_->GenerateBindingKeyAssertion(
        account_id, token_binding_challenge, ephemeral_public_key,
        GURL(kTokenBindingAssertionDestinationUrl),
        base::BindOnce(
            &TokenBindingOAuth2AccessTokenFetcher::SetBindingKeyAssertion,
            fetcher_wrapper->GetWeakPtr(), std::move(ephemeral_key)));
    return fetcher_wrapper;
  }
  return GaiaAccessTokenFetcher::
      CreateExchangeRefreshTokenForAccessTokenInstance(
          consumer, url_loader_factory, refresh_token);
}

std::string MutableProfileOAuth2TokenServiceDelegate::GetTokenForMultilogin(
    const CoreAccountId& account_id) const {
  auto iter = refresh_tokens_.find(account_id);
  if (iter == refresh_tokens_.end() ||
      GetAuthError(account_id) != GoogleServiceAuthError::AuthErrorNone()) {
    return std::string();
  }
  const auto refresh_token = iter->second.refresh_token.value();
  DCHECK(!refresh_token.empty());
  return refresh_token;
}

bool MutableProfileOAuth2TokenServiceDelegate::RefreshTokenIsAvailable(
    const CoreAccountId& account_id) const {
  VLOG(1) << "MutablePO2TS::RefreshTokenIsAvailable";
  return !GetRefreshToken(account_id).empty();
}

std::string MutableProfileOAuth2TokenServiceDelegate::GetRefreshToken(
    const CoreAccountId& account_id) const {
  auto iter = refresh_tokens_.find(account_id);
  if (iter != refresh_tokens_.end()) {
    const std::string refresh_token = iter->second.refresh_token.value();
    DCHECK(!refresh_token.empty());
    return refresh_token;
  }
  return std::string();
}


bool MutableProfileOAuth2TokenServiceDelegate::
    GenerateBindingKeyRegistrationToken(
        base::span<const crypto::SignatureVerifier::SignatureAlgorithm>
            supported_algorithms,
        std::string_view auth_code,
        base::OnceCallback<
            void(std::optional<signin::BindingKeyRegistrationTokenResult>)>
            callback) {
  if (!token_binding_helper_) {
    return false;
  }
  token_binding_helper_->GenerateBindingKeyRegistrationToken(
      supported_algorithms,
      signin::TokenBindingAuthCode(std::string(auth_code)),
      std::move(callback));
  return true;
}

bool MutableProfileOAuth2TokenServiceDelegate::IsRefreshTokenBoundToKey(
    const CoreAccountId& account_id) const {
  return token_binding_helper_ &&
         token_binding_helper_->HasBindingKey(account_id);
}

std::vector<uint8_t>
MutableProfileOAuth2TokenServiceDelegate::GetWrappedBindingKey(
    const CoreAccountId& account_id) const {
  if (!token_binding_helper_) {
    return {};
  }

  return token_binding_helper_->GetWrappedBindingKey(account_id);
}

bool MutableProfileOAuth2TokenServiceDelegate::IsRefreshTokenBoundToMtls(
    const CoreAccountId& account_id) const {
  const auto* token_data = base::FindOrNull(refresh_tokens_, account_id);
  return token_data && token_data->mtls_token_binding &&
         base::FeatureList::IsEnabled(switches::kEnableMtlsTokenBinding);
}

bool MutableProfileOAuth2TokenServiceDelegate::
    AllBoundTokensShareSameBindingKey() const {
  if (!token_binding_helper_) {
    return true;
  }
  return token_binding_helper_->AreAllBindingKeysSame();
}

void MutableProfileOAuth2TokenServiceDelegate::
    GenerateRefreshTokenBindingKeyAssertionForMultilogin(
        const CoreAccountId& account_id,
        std::string_view challenge,
        std::string_view ephemeral_public_key,
        TokenBindingHelper::GenerateAssertionCallback callback) {
  if (!token_binding_helper_ || GetTokenForMultilogin(account_id).empty()) {
    std::move(callback).Run(std::string());
  }

  token_binding_helper_->GenerateBindingKeyAssertion(
      account_id, challenge, ephemeral_public_key,
      GURL(kTokenBindingAssertionDestinationUrl), std::move(callback));
}

std::string MutableProfileOAuth2TokenServiceDelegate::GetRefreshTokenForTest(
    const CoreAccountId& account_id) const {
  return GetRefreshToken(account_id);
}

base::AutoReset<bool>
MutableProfileOAuth2TokenServiceDelegate::SetIgnoreNonOfficialApiKeysForTesting(
    bool ignore) {
  CHECK_IS_TEST();
  return base::AutoReset<bool>(&g_ignore_non_official_api_keys_for_testing,
                               ignore);
}

void MutableProfileOAuth2TokenServiceDelegate::AddBindingKeyToService(
    base::span<const uint8_t> wrapped_binding_key) {
  if (token_binding_helper_ && !wrapped_binding_key.empty()) {
    token_binding_helper_->CopyBindingKeyFromAnotherTokenService(
        wrapped_binding_key);
  }
}

TokenBindingHelper::SaveBindingKeyResult
MutableProfileOAuth2TokenServiceDelegate::UpdateRefreshTokenBindingKey(
    const CoreAccountId& account_id,
    std::string_view refresh_token,
    std::vector<uint8_t> wrapped_binding_key) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  auto iter = refresh_tokens_.find(account_id);
  if (iter == refresh_tokens_.end() ||
      iter->second.refresh_token.value() != refresh_token) {
    return TokenBindingHelper::SaveBindingKeyResult::kRefreshTokenNotFound;
  }

  CHECK(token_binding_helper_);

  token_binding_helper_->SetBindingKey(account_id, wrapped_binding_key);
  signin::TokenBindingInfo token_binding_info(std::move(wrapped_binding_key),
                                              iter->second.mtls_token_binding);
  // TODO(crbug.com/514242898): Wait until `PersistCredentials()` completes
  // successfully before resuming the upgrade flow.
  PersistCredentials(account_id, iter->second.refresh_token.value(),
                     token_binding_info);
  return TokenBindingHelper::SaveBindingKeyResult::kSuccess;
}

std::vector<CoreAccountId>
MutableProfileOAuth2TokenServiceDelegate::GetAccounts() const {
  std::vector<CoreAccountId> account_ids;
  for (auto& token : refresh_tokens_) {
    account_ids.push_back(token.first);
  }
  return account_ids;
}

scoped_refptr<network::SharedURLLoaderFactory>
MutableProfileOAuth2TokenServiceDelegate::GetURLLoaderFactory() const {
  return client_->GetURLLoaderFactory();
}

void MutableProfileOAuth2TokenServiceDelegate::InvalidateTokenForMultilogin(
    const CoreAccountId& failed_account) {
  UpdateAuthError(failed_account,
                  GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
                      GoogleServiceAuthError::InvalidGaiaCredentialsReason::
                          CREDENTIALS_REJECTED_BY_SERVER));
}

void MutableProfileOAuth2TokenServiceDelegate::LoadCredentialsInternal(
    const CoreAccountId& primary_account_id) {
  if (load_credentials_state() ==
      signin::LoadCredentialsState::LOAD_CREDENTIALS_IN_PROGRESS) {
    VLOG(1) << "Load credentials operation already in progress";
    return;
  }

  set_load_credentials_state(
      signin::LoadCredentialsState::LOAD_CREDENTIALS_IN_PROGRESS);

  if (!primary_account_id.empty()) {
    ValidateAccountId(primary_account_id);
  }
  DCHECK(loading_primary_account_id_.empty());
  DCHECK_EQ(0, web_data_service_request_);

  refresh_tokens_.clear();
  ClearAuthError(std::nullopt);
  if (token_binding_helper_) {
    token_binding_helper_->ClearAllKeys();
  }

  if (!token_web_data_) {
    // This case only exists in unit tests that do not care about loading
    // credentials.
    set_load_credentials_state(
        signin::LoadCredentialsState::
            LOAD_CREDENTIALS_FINISHED_WITH_UNKNOWN_ERRORS);
    FinishLoadingCredentials();
    return;
  }

  loading_primary_account_id_ = primary_account_id;
  web_data_service_request_ = token_web_data_->GetAllTokens(this);

  if (!base::FeatureList::IsEnabled(
          unexportable_keys::kUnexportableKeyDeletion)) {
    return;
  }

  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(
          &MutableProfileOAuth2TokenServiceDelegate::StartWebWrappedKeyFetch,
          weak_ptr_factory_.GetWeakPtr()),
      kWebWrappedKeyFetchDelay);
}

void MutableProfileOAuth2TokenServiceDelegate::OnWebDataServiceRequestDone(
    WebDataServiceBase::Handle handle,
    std::unique_ptr<WDTypedResult> result) {
  VLOG(1) << "MutablePO2TS::OnWebDataServiceRequestDone. Result type: "
          << (result.get() == nullptr ? -1
                                      : static_cast<int>(result->GetType()));

  if (handle == web_data_service_request_for_gc_) {
    OnWebWrappedKeyFetchDone(std::move(result));
    return;
  }

  DCHECK_EQ(web_data_service_request_, handle);
  web_data_service_request_ = 0;
  ScopedBatchChange batch(this);

  if (result) {
    DCHECK(result->GetType() == TOKEN_RESULT);
    const WDResult<TokenResult>* token_result =
        static_cast<const WDResult<TokenResult>*>(result.get());
    LoadAllCredentialsIntoMemory(token_result->GetValue().tokens,
                                 token_result->GetValue().should_reencrypt);
    set_load_credentials_state(LoadCredentialsStateFromTokenResult(
        token_result->GetValue().db_result));
  } else {
    set_load_credentials_state(
        signin::LoadCredentialsState::
            LOAD_CREDENTIALS_FINISHED_WITH_DB_CANNOT_BE_OPENED);
  }

  // Make sure that we have an entry for |loading_primary_account_id_| in the
  // map.  The entry could be missing if there is a corruption in the token DB
  // while this profile is connected to an account.
  if (!loading_primary_account_id_.empty() &&
      refresh_tokens_.count(loading_primary_account_id_) == 0) {
    if (load_credentials_state() ==
        signin::LoadCredentialsState::LOAD_CREDENTIALS_FINISHED_WITH_SUCCESS) {
      set_load_credentials_state(
          signin::LoadCredentialsState::
              LOAD_CREDENTIALS_FINISHED_WITH_NO_TOKEN_FOR_PRIMARY_ACCOUNT);
    }
    UpdateCredentialsInMemory(
        loading_primary_account_id_, GaiaConstants::kInvalidRefreshToken,
        signin::TokenBindingInfo(),
        GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
            GoogleServiceAuthError::InvalidGaiaCredentialsReason::
                CREDENTIALS_MISSING));
    FireRefreshTokenAvailable(loading_primary_account_id_);
  }

#ifndef NDEBUG
  for (auto& token : refresh_tokens_) {
    DCHECK(RefreshTokenIsAvailable(token.first))
        << "Missing token for " << token.first;
  }
#endif

  loading_primary_account_id_ = CoreAccountId();
  FinishLoadingCredentials();
}

void MutableProfileOAuth2TokenServiceDelegate::LoadAllCredentialsIntoMemory(
    const std::map<std::string, TokenServiceTable::TokenWithBindingInfo>&
        db_tokens,
    bool should_reencrypt) {
  VLOG(1) << "MutablePO2TS::LoadAllCredentialsIntoMemory; " << db_tokens.size()
          << " credential(s).";
  ScopedBatchChange batch(this);
  for (const auto& [prefixed_account_id, token_with_binding_info] : db_tokens) {
    LoadTokenFromDBStatus load_token_status =
        LoadTokenFromDBStatus::kTokenLoaded;
    absl::Cleanup record_histogram = [&load_token_status] {
      base::UmaHistogramEnumeration("Signin.LoadTokenFromDB",
                                    load_token_status);
    };

    std::string refresh_token = token_with_binding_info.token;
    signin::TokenBindingInfo token_binding_info(
        token_with_binding_info.wrapped_binding_key,
        token_with_binding_info.mtls_token_binding);

    CoreAccountId account_id = RemoveAccountIdPrefix(prefixed_account_id);
    if (account_id.empty()) {
      load_token_status = LoadTokenFromDBStatus::kTokenRevokedInvalidAccountId;
      if (token_web_data_) {
        VLOG(1) << "MutablePO2TS remove refresh token for invalid account id ["
                << prefixed_account_id << "]";
        token_web_data_->RemoveTokenForService(prefixed_account_id);
      }
      continue;
    }

    DCHECK(!account_id.IsEmail())
        << "Account id should be a Gaia id [account_id = " << account_id << "]";
    DCHECK(!refresh_token.empty());

    const bool is_primary_account = account_id == loading_primary_account_id_;
    const bool has_invalid_characters = HasInvalidCharacters(refresh_token);

    const bool revoke_token_on_load = [&] {
      switch (revoke_all_tokens_on_load_) {
        case RevokeAllTokensOnLoad::kNo:
          return false;
        case RevokeAllTokensOnLoad::kDeleteSiteDataOnExit:
          // Tokens are not revoked when clearing cookies if the user
          // is signed in.
          return loading_primary_account_id_.empty();
        case RevokeAllTokensOnLoad::kExplicitRevoke:
          return true;
      }
      NOTREACHED();
    }();

    const bool revoke_token = [&] {
      if (revoke_token_on_load) {
        load_token_status = LoadTokenFromDBStatus::kTokenRevokedOnLoad;
        return true;
      }
      if (has_invalid_characters) {
        load_token_status =
            LoadTokenFromDBStatus::kTokenRevokedInvalidTokenCharacters;
        return true;
      }

      return false;
    }();

    if (revoke_token) {
      RevokeCredentialsOnServer(refresh_token);
      if (is_primary_account) {
        // If the primary token needs to be revoked, replace it with
        // `GaiaConstants::kInvalidRefreshToken`.
        refresh_token = GaiaConstants::kInvalidRefreshToken;
        token_binding_info = signin::TokenBindingInfo();
        PersistCredentials(account_id, refresh_token, token_binding_info);
      } else {
        ClearPersistedCredentials(account_id);
        FireRefreshTokenRevoked(account_id);
        continue;
      }
    }

    if (!revoke_token && should_reencrypt) {
      PersistCredentials(account_id, refresh_token, token_binding_info);
    }

    RecordAccountAvailabilityStartup(account_id, refresh_token);
    UpdateCredentialsInMemory(account_id, refresh_token, token_binding_info);
    FireRefreshTokenAvailable(account_id);
  }
  RecordTokenBindingHistogramsOnCredentialsLoaded(
      token_binding_helper_.get(),
      std::ranges::count_if(refresh_tokens_, [](const auto& kv_pair) {
        return kv_pair.second.refresh_token.value() !=
               GaiaConstants::kInvalidRefreshToken;
      }));
}

void MutableProfileOAuth2TokenServiceDelegate::UpdateCredentialsInternal(
    const CoreAccountId& account_id,
    const std::string& refresh_token,
    const signin::TokenBindingInfo& token_binding_info) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(!account_id.empty());
  DCHECK(!refresh_token.empty());

  ValidateAccountId(account_id);
  if (GetRefreshToken(account_id) != refresh_token) {
    UpdateCredentialsInMemory(account_id, refresh_token, token_binding_info);
    PersistCredentials(account_id, refresh_token, token_binding_info);
    if (base::FeatureList::IsEnabled(
            kClearUnreadableTokensUponAddingNewCredential)) {
      // If any tokens failed to read from the DB, discard them now to avoid
      // mixing them with the new token in the next session.
      ClearUnreadableCredentials();
    }
    FireRefreshTokenAvailable(account_id);
  }
}

void MutableProfileOAuth2TokenServiceDelegate::UpdateCredentialsInMemory(
    const CoreAccountId& account_id,
    const std::string& refresh_token,
    const signin::TokenBindingInfo& token_binding_info,
    base::optional_ref<const GoogleServiceAuthError> error_for_invalid_token) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(!account_id.empty());
  DCHECK(!refresh_token.empty());

  bool is_refresh_token_invalidated =
      refresh_token == GaiaConstants::kInvalidRefreshToken;
  GoogleServiceAuthError error = [&] {
    if (!is_refresh_token_invalidated) {
      return GoogleServiceAuthError::AuthErrorNone();
    }

    if (error_for_invalid_token.has_value()) {
      return *error_for_invalid_token;
    }

    return GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
        GoogleServiceAuthError::InvalidGaiaCredentialsReason::
            CREDENTIALS_REJECTED_BY_CLIENT);
  }();

  bool refresh_token_present = refresh_tokens_.count(account_id) > 0;
  // If token present, and different from the new one, cancel its requests,
  // and clear the entries in cache related to that account.
  if (refresh_token_present) {
    DCHECK_NE(refresh_token,
              refresh_tokens_.at(account_id).refresh_token.value());
    VLOG(1) << "MutablePO2TS::UpdateCredentials; Refresh Token was present. "
            << "account_id=" << account_id;

    // The old refresh token must be revoked on the server only when it is
    // invalidated.
    //
    // The refresh token is updated to a new valid one in case of reauth.
    // In the reauth case the old and the new refresh tokens have the same
    // device ID. When revoking a refresh token on the server, Gaia revokes
    // all the refresh tokens that have the same device ID.
    // Therefore, the old refresh token must not be revoked on the server
    // when it is updated to a new valid one (otherwise the new refresh token
    // would also be invalidated server-side).
    // See http://crbug.com/865189 for more information about this regression.
    if (is_refresh_token_invalidated) {
      RevokeCredentialsOnServer(
          refresh_tokens_.at(account_id).refresh_token.value());
    }
  } else {
    VLOG(1) << "MutablePO2TS::UpdateCredentials; Refresh Token was absent. "
            << "account_id=" << account_id;
  }

  refresh_tokens_.insert_or_assign(
      account_id, TokenData(crypto::ProcessBoundString(refresh_token),
                            token_binding_info.mtls_token_binding));

  if (token_binding_helper_) {
    token_binding_helper_->SetBindingKey(
        account_id, token_binding_info.wrapped_binding_key);
  }
  UpdateAuthError(account_id, error, /*fire_auth_error_changed=*/false);
}

void MutableProfileOAuth2TokenServiceDelegate::PersistCredentials(
    const CoreAccountId& account_id,
    const std::string& refresh_token,
    const signin::TokenBindingInfo& token_binding_info) {
  DCHECK(!account_id.empty());
  DCHECK(!refresh_token.empty());
  if (token_web_data_) {
    VLOG(1) << "MutablePO2TS::PersistCredentials for account_id=" << account_id;
    token_web_data_->SetTokenForService(
        ApplyAccountIdPrefix(account_id.ToString()), refresh_token,
        token_binding_info.wrapped_binding_key,
        token_binding_info.mtls_token_binding);
  }
}

void MutableProfileOAuth2TokenServiceDelegate::ClearUnreadableCredentials() {
  if (!token_web_data_) {
    return;
  }

  // No need to clear the DB if credentials hasn't finished loading yet or there
  // were no credential loading errors.
  using enum signin::LoadCredentialsState;
  if (load_credentials_state() != LOAD_CREDENTIALS_FINISHED_WITH_DB_ERRORS &&
      load_credentials_state() !=
          LOAD_CREDENTIALS_FINISHED_WITH_DECRYPT_ERRORS) {
    return;
  }

  // No need to clear credentials more than once.
  if (has_cleared_unreadable_credentials_) {
    return;
  }
  has_cleared_unreadable_credentials_ = true;

  std::vector<std::string> accounts_to_keep;
  for (const auto& [account_id, data] : refresh_tokens_) {
    accounts_to_keep.push_back(ApplyAccountIdPrefix(account_id.ToString()));
  }
  token_web_data_->RemoveOtherTokens(accounts_to_keep);
}

void MutableProfileOAuth2TokenServiceDelegate::RevokeAllCredentialsInternal(
    signin_metrics::SourceForRefreshTokenOperation source) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  VLOG(1) << "MutablePO2TS::RevokeAllCredentials";

  ScopedBatchChange batch(this);
  if (load_credentials_state() ==
      signin::LoadCredentialsState::LOAD_CREDENTIALS_IN_PROGRESS) {
    VLOG(1) << "MutablePO2TS::RevokeAllCredentials before tokens are loaded.";
    // If |RevokeAllCredentials| is called while credentials are being loaded,
    // then the tokens should be revoked on load.
    revoke_all_tokens_on_load_ = RevokeAllTokensOnLoad::kExplicitRevoke;
    loading_primary_account_id_ = CoreAccountId();
  }

  // Make a temporary copy of the account ids.
  std::vector<CoreAccountId> accounts;
  for (const auto& token : refresh_tokens_) {
    accounts.push_back(token.first);
  }
  for (const auto& account : accounts) {
    RevokeCredentials(account, source);
  }

  DCHECK_EQ(0u, refresh_tokens_.size());

  // Make sure all tokens are removed from storage.
  if (token_web_data_) {
    token_web_data_->RemoveAllTokens();
  }
}

void MutableProfileOAuth2TokenServiceDelegate::RevokeCredentialsInternal(
    const CoreAccountId& account_id) {
  RevokeCredentialsImpl(account_id, /*revoke_on_server=*/true);
}

void MutableProfileOAuth2TokenServiceDelegate::ClearPersistedCredentials(
    const CoreAccountId& account_id) {
  DCHECK(!account_id.empty());
  if (token_web_data_) {
    VLOG(1) << "MutablePO2TS::ClearPersistedCredentials for account_id="
            << account_id;
    token_web_data_->RemoveTokenForService(
        ApplyAccountIdPrefix(account_id.ToString()));
  }
}

void MutableProfileOAuth2TokenServiceDelegate::RevokeCredentialsOnServer(
    const std::string& refresh_token) {
  DCHECK(!refresh_token.empty());

  if (refresh_token == GaiaConstants::kInvalidRefreshToken ||
      HasInvalidCharacters(refresh_token)) {
    return;
  }

  // Keep track or all server revoke requests.  This way they can be deleted
  // before the token service is shutdown and won't outlive the profile.
  server_revokes_.push_back(std::make_unique<RevokeServerRefreshToken>(
      this, client_, refresh_token, 0));
}

void MutableProfileOAuth2TokenServiceDelegate::StartWebWrappedKeyFetch() {
  if (!token_web_data_) {
    return;
  }

  web_data_service_request_for_gc_ =
      token_web_data_->GetAllWrappedBindingKeys(this);
}

void MutableProfileOAuth2TokenServiceDelegate::OnWebWrappedKeyFetchDone(
    std::unique_ptr<WDTypedResult> result) {
  web_data_service_request_for_gc_.reset();
  if (!result) {
    return;
  }

  CHECK_EQ(result->GetType(), WRAPPED_BINDING_KEYS_RESULT);
  if (token_binding_helper_) {
    token_binding_helper_->StartGarbageCollection(
        static_cast<WDResult<absl::flat_hash_set<std::vector<uint8_t>>>&>(
            *result)
            .GetValue());
  }
}

void MutableProfileOAuth2TokenServiceDelegate::CancelWebFetches() {
  if (web_data_service_request_ != 0) {
    DCHECK(token_web_data_);
    token_web_data_->CancelRequest(web_data_service_request_);
    web_data_service_request_ = 0;
  }

  if (web_data_service_request_for_gc_.has_value()) {
    CHECK_DEREF(token_web_data_)
        .CancelRequest(*std::exchange(web_data_service_request_for_gc_, {}));
  }
}

void MutableProfileOAuth2TokenServiceDelegate::ExtractCredentialsInternal(
    ProfileOAuth2TokenService* to_service,
    const CoreAccountId& account_id) {
  bool should_update_credentials = true;
  std::string refresh_token = GetRefreshToken(account_id);
  bool mtls_token_binding = IsRefreshTokenBoundToMtls(account_id);
  AccountMoveDecision move_decision =
      AccountMoveDecision::kCanMoveWithRefreshToken;
  signin::TokenBindingInfo token_binding_info(GetWrappedBindingKey(account_id),
                                              mtls_token_binding);
  if (!CanMoveAccountToService(*to_service, account_id,
                               token_binding_info.wrapped_binding_key)) {
    if (to_service->HasRefreshToken(account_id)) {
      // `to_service` already has this account. Do not override the existing,
      // potentially valid token.
      should_update_credentials = false;
      move_decision = AccountMoveDecision::kCannotMoveAlreadyExists;
    } else {
      // Insert an account without a token.
      refresh_token = GaiaConstants::kInvalidRefreshToken;
      token_binding_info = signin::TokenBindingInfo();
      move_decision = AccountMoveDecision::kCannotMoveInsertWithoutRefreshToken;
    }
  }
  base::UmaHistogramEnumeration("Signin.MoveAccount.CanMoveToService",
                                move_decision);

  if (should_update_credentials) {
    to_service->UpdateCredentials(
        account_id, refresh_token,
        signin_metrics::SourceForRefreshTokenOperation::
            kTokenService_ExtractCredentials,
        token_binding_info);

    to_service->GetDelegate()->AddBindingKeyToService(
        token_binding_info.wrapped_binding_key);
  }

  RevokeCredentialsImpl(account_id, /*revoke_on_server=*/false);
}

void MutableProfileOAuth2TokenServiceDelegate::Shutdown() {
  VLOG(1) << "MutablePO2TS::Shutdown";
  server_revokes_.clear();
  CancelWebFetches();
  refresh_tokens_.clear();
  if (token_binding_helper_) {
    token_binding_helper_->ClearAllKeys();
  }
  ProfileOAuth2TokenServiceDelegate::Shutdown();
}

void MutableProfileOAuth2TokenServiceDelegate::OnConnectionChanged(
    net::NetworkChangeNotifier::ConnectionType type) {
  // If our network has changed, reset the backoff timer so that errors caused
  // by a previous lack of network connectivity don't prevent new requests.
  ResetBackOffEntry();
}

bool MutableProfileOAuth2TokenServiceDelegate::FixAccountErrorIfPossible() {
  return !fix_request_error_callback_.is_null() &&
         fix_request_error_callback_.Run();
}

void MutableProfileOAuth2TokenServiceDelegate::FinishLoadingCredentials() {
  if (token_binding_helper_) {
    token_binding_helper_->OnAllCredentialsLoaded(!refresh_tokens_.empty());
  }
  FireRefreshTokensLoaded();
}

void MutableProfileOAuth2TokenServiceDelegate::RevokeCredentialsImpl(
    const CoreAccountId& account_id,
    bool revoke_on_server) {
  ValidateAccountId(account_id);
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (refresh_tokens_.count(account_id) > 0) {
    VLOG(1) << "MutablePO2TS::RevokeCredentials for account_id=" << account_id;
    if (revoke_on_server) {
      RevokeCredentialsOnServer(
          refresh_tokens_.at(account_id).refresh_token.value());
    }
    refresh_tokens_.erase(account_id);
    ClearAuthError(account_id);
    if (token_binding_helper_) {
      token_binding_helper_->SetBindingKey(account_id, {});
    }
    ClearPersistedCredentials(account_id);
    FireRefreshTokenRevoked(account_id);
  }
}

void MutableProfileOAuth2TokenServiceDelegate::RecordAccountAvailabilityStartup(
    const CoreAccountId& account_id,
    const std::string& refresh_token) {
  constexpr const char kAccountAvailabilityStartupHistogramBase[] =
      "Signin.AccountInPref.StartupState.";

  bool known_account =
      !account_tracker_service_->GetAccountInfo(account_id).IsEmpty();
  bool refersh_token_valid =
      refresh_token != GaiaConstants::kInvalidRefreshToken;

  AccountStartupState startup_state = AccountStartupState::kUnknownInvalidToken;
  if (known_account && refersh_token_valid) {
    startup_state = AccountStartupState::kKnownValidToken;
  } else if (known_account) {
    startup_state = AccountStartupState::kKnownInvalidToken;
  } else if (refersh_token_valid) {
    startup_state = AccountStartupState::kUnknownValidToken;
  }

  base::UmaHistogramEnumeration(
      base::StrCat({kAccountAvailabilityStartupHistogramBase,
                    loading_primary_account_id_ == account_id ? "Primary"
                                                              : "Secondary"}),
      startup_state);
}
