// 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/signin/public/identity_manager/access_token_fetcher.h"

#include <utility>

#include "base/check_op.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/access_token_restriction.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

namespace {

// LINT.IfChange(ErrorToString)
std::string ErrorToString(GoogleServiceAuthError::State error_state) {
  switch (error_state) {
    case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS:
      return "InvalidGaiaCredentials";
    case GoogleServiceAuthError::ACCOUNT_NOT_FOUND:
      return "AccountNotFound";
    case GoogleServiceAuthError::CONNECTION_FAILED:
      return "ConnectionFailed";
    case GoogleServiceAuthError::SERVICE_UNAVAILABLE:
      return "ServiceUnavailable";
    case GoogleServiceAuthError::REQUEST_CANCELED:
      return "RequestCanceled";
    case GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE:
      return "UnexpectedServiceResponse";
    case GoogleServiceAuthError::SERVICE_ERROR:
      return "ServiceError";
    case GoogleServiceAuthError::SCOPE_LIMITED_UNRECOVERABLE_ERROR:
      return "ScopeLimitedUnrecoverableError";
    case GoogleServiceAuthError::CHALLENGE_RESPONSE_REQUIRED:
      return "ChallengeResponseRequired";
    case GoogleServiceAuthError::DEVICE_MANAGEMENT_ERROR:
      return "DeviceManagementError";
    default:
      NOTREACHED() << "Unexpected error state: " << error_state;
  }
}
// LINT.ThenChange(//tools/metrics/histograms/metadata/signin/histograms.xml:AccessTokenFetchFailureError)

}  // namespace

namespace signin {

AccessTokenFetcher::AccessTokenFetcher(
    const CoreAccountId& account_id,
    OAuthConsumerId oauth_consumer_id,
    const OAuthConsumer& oauth_consumer,
    ProfileOAuth2TokenService* token_service,
    PrimaryAccountManager* primary_account_manager,
    TokenCallback callback,
    Mode mode,
    Source token_source)
    : AccessTokenFetcher(account_id,
                         oauth_consumer_id,
                         oauth_consumer,
                         token_service,
                         primary_account_manager,
                         /*url_loader_factory=*/nullptr,
                         std::move(callback),
                         mode,
                         token_source) {}

AccessTokenFetcher::AccessTokenFetcher(
    const CoreAccountId& account_id,
    OAuthConsumerId oauth_consumer_id,
    const OAuthConsumer& oauth_consumer,
    ProfileOAuth2TokenService* token_service,
    PrimaryAccountManager* primary_account_manager,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    TokenCallback callback,
    Mode mode,
    Source token_source)
    : OAuth2AccessTokenManager::Consumer(oauth_consumer.GetName()),
      account_id_(account_id),
      token_service_(token_service),
      primary_account_manager_(primary_account_manager),
      url_loader_factory_(std::move(url_loader_factory)),
      scopes_(oauth_consumer.GetScopes()),
      callback_(std::move(callback)),
      mode_(mode),
      token_source_(token_source),
      oauth_consumer_id_(oauth_consumer_id) {
  if (mode_ == Mode::kImmediate || IsRefreshTokenAvailable()) {
    StartAccessTokenRequest();
    return;
  }

  // Start observing the IdentityManager. This observer will be removed either
  // when a refresh token is obtained and an access token request is started or
  // when this object is destroyed.
  token_service_observation_.Observe(token_service_.get());
}

AccessTokenFetcher::~AccessTokenFetcher() = default;

void AccessTokenFetcher::VerifyScopeAccess() {
  if (account_id_.empty()) {
    // Fetching access tokens for an empty account should fail, but not crash.
    // Verifying the OAuth scopes based on the consent level is thus not needed.
    return;
  }

  // The consumer has privileged access to all scopes, return early.
  if (IsPrivilegedOAuth2Consumer(oauth_consumer_id_)) {
    VLOG(1) << id() << " has access rights to scopes: "
            << base::JoinString(
                   std::vector<std::string>(scopes_.begin(), scopes_.end()),
                   ",");
    return;
  }

  bool is_signed_in =
      primary_account_manager_->HasPrimaryAccount(ConsentLevel::kSignin);

  // A consumer is allowed to access the requested scopes if EVERY scope is
  // either allowlisted for this consumer, unrestricted, or satisfies the
  // sign-in requirements. A mix of allowlisted and unrestricted scopes is
  // allowed, but any forbidden scope will trigger a CHECK or NOTREACHED.
  for (const std::string& scope : scopes_) {
    if (IsConsumerAllowlistedForScope(oauth_consumer_id_, scope)) {
      continue;
    }
    OAuth2ScopeRestriction restriction = GetOAuth2ScopeRestriction(scope);
    switch (restriction) {
      case OAuth2ScopeRestriction::kNoRestriction:
        continue;

      case OAuth2ScopeRestriction::kSignedIn:
        CHECK(is_signed_in) << base::StringPrintf(
            "Consumer '%s' is requesting scope '%s' that requires user "
            "to be signed in to the browser. "
            "Please check that the user is signed in to the browser before "
            "using this API.",
            id().c_str(), scope.c_str());
        break;

      case OAuth2ScopeRestriction::kPrivilegedOAuth2Consumer:
        NOTREACHED() << base::StringPrintf(
            "You are attempting to access a privileged scope '%s' without the "
            "required access, please file a bug for access at "
            "https://bugs.chromium.org/p/chromium/issues/"
            "list?q=component:Services>SignIn.",
            scope.c_str());
    }
  }

  VLOG(1) << id() << " has access rights to scopes: "
          << base::JoinString(
                 std::vector<std::string>(scopes_.begin(), scopes_.end()), ",");
}

bool AccessTokenFetcher::IsRefreshTokenAvailable() const {
  DCHECK_EQ(Mode::kWaitUntilRefreshTokenAvailable, mode_);

  switch (token_source_) {
    case Source::kProfile:
      return token_service_->RefreshTokenIsAvailable(account_id_);
#if BUILDFLAG(IS_IOS)
    case Source::kDevice:
      return token_service_->RefreshTokenIsAvailableOnDevice(account_id_);
#endif
  }
}

void AccessTokenFetcher::StartAccessTokenRequest() {
  DCHECK(mode_ == Mode::kImmediate || IsRefreshTokenAvailable());

  // By the time of starting an access token request, we should no longer be
  // listening for signin-related events.
  DCHECK(!token_service_observation_.IsObservingSource(token_service_.get()));

  // Note: We might get here even in cases where we know that there's no refresh
  // token. We're requesting an access token anyway, so that the token service
  // will generate an appropriate error code that we can return to the client.
  DCHECK(!access_token_request_);

  // Ensure that the client has the appropriate user consent for accessing the
  // OAuth API scopes in this request.
  VerifyScopeAccess();

  if (url_loader_factory_) {
    access_token_request_ = token_service_->StartRequestWithContext(
        account_id_, url_loader_factory_, scopes_, this);
    return;
  }

  switch (token_source_) {
    case Source::kProfile:
      access_token_request_ =
          token_service_->StartRequest(account_id_, scopes_, this);
      return;
#if BUILDFLAG(IS_IOS)
    case Source::kDevice:
      token_service_->GetRefreshTokenFromDevice(
          account_id_, scopes_,
          base::BindOnce(&AccessTokenFetcher::RunCallbackAndMaybeDie,
                         base::Unretained(this)));
      return;
#endif
  }
}

void AccessTokenFetcher::OnRefreshTokenAvailable(
    const CoreAccountId& account_id) {
  DCHECK_EQ(Mode::kWaitUntilRefreshTokenAvailable, mode_);

  if (!IsRefreshTokenAvailable()) {
    return;
  }

  DCHECK(token_service_observation_.IsObservingSource(token_service_.get()));
  token_service_observation_.Reset();

  StartAccessTokenRequest();
}

void AccessTokenFetcher::OnGetTokenSuccess(
    const OAuth2AccessTokenManager::Request* request,
    const OAuth2AccessTokenConsumer::TokenResponse& token_response) {
  DCHECK_EQ(request, access_token_request_.get());
  std::unique_ptr<OAuth2AccessTokenManager::Request> request_deleter(
      std::move(access_token_request_));

  base::UmaHistogramEnumeration("Signin.AccessTokenFetch.Success",
                                oauth_consumer_id_);
  RunCallbackAndMaybeDie(
      GoogleServiceAuthError::AuthErrorNone(),
      AccessTokenInfo(token_response.access_token,
                      token_response.expiration_time, token_response.id_token));

  // Potentially dead after the above invocation; nothing to do except return.
}

void AccessTokenFetcher::OnGetTokenFailure(
    const OAuth2AccessTokenManager::Request* request,
    const GoogleServiceAuthError& error) {
  DCHECK_EQ(request, access_token_request_.get());
  std::unique_ptr<OAuth2AccessTokenManager::Request> request_deleter(
      std::move(access_token_request_));

  CHECK(error.state() != GoogleServiceAuthError::NONE);
  std::string error_str = ErrorToString(error.state());
  base::UmaHistogramEnumeration("Signin.AccessTokenFetch.Failure." + error_str,
                                oauth_consumer_id_);
  RunCallbackAndMaybeDie(error, AccessTokenInfo());

  // Potentially dead after the above invocation; nothing to do except return.
}

void AccessTokenFetcher::RunCallbackAndMaybeDie(
    GoogleServiceAuthError error,
    AccessTokenInfo access_token_info) {
  // Per the contract of this class, it is allowed for consumers to delete this
  // object from within the callback that is run below. Hence, it is not safe to
  // add any code below this call.
  std::move(callback_).Run(std::move(error), std::move(access_token_info));
}

}  // namespace signin
