chromium/ash/webui/projector_app/projector_oauth_token_fetcher.cc

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

#include "ash/webui/projector_app/projector_oauth_token_fetcher.h"

#include "ash/public/cpp/projector/projector_controller.h"
#include "ash/webui/projector_app/projector_app_client.h"
#include "base/containers/contains.h"
#include "base/containers/flat_tree.h"
#include "base/time/time.h"
#include "components/signin/public/base/consent_level.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_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/oauth2_access_token_manager.h"

namespace {

// The cached OAuth token needs to be valid at least until base::Time::Now()
// + `kBufferTime`. The buffer time will be useful to ensure that we don't send
// soon to expire tokens to the Projector app.
const base::TimeDelta kBufferTime = base::Seconds(4);

signin::IdentityManager* GetIdentityManager() {
  return ash::ProjectorAppClient::Get()->GetIdentityManager();
}

OAuth2AccessTokenManager::ScopeSet GetScopeSet() {
  return OAuth2AccessTokenManager::ScopeSet{GaiaConstants::kDriveOAuth2Scope};
}

}  // namespace

namespace ash {

AccessTokenRequests::AccessTokenRequests() = default;

AccessTokenRequests::AccessTokenRequests(AccessTokenRequests&&) = default;

AccessTokenRequests& AccessTokenRequests::operator=(AccessTokenRequests&&) =
    default;

AccessTokenRequests::~AccessTokenRequests() = default;

ProjectorOAuthTokenFetcher::ProjectorOAuthTokenFetcher() = default;

ProjectorOAuthTokenFetcher::~ProjectorOAuthTokenFetcher() = default;

// static
std::vector<AccountInfo> ProjectorOAuthTokenFetcher::GetAccounts() {
  return GetIdentityManager()
      ->GetExtendedAccountInfoForAccountsWithRefreshToken();
}

// static
CoreAccountInfo ProjectorOAuthTokenFetcher::GetPrimaryAccountInfo() {
  return GetIdentityManager()->GetPrimaryAccountInfo(
      signin::ConsentLevel::kSignin);
}

void ProjectorOAuthTokenFetcher::GetAccessTokenFor(
    const std::string& email,
    AccessTokenRequestCallback callback) {
  if (base::Contains(fetched_access_tokens_, email)) {
    const auto& access_token_info = fetched_access_tokens_[email];
    if (base::Time::Now() + kBufferTime < access_token_info.expiration_time) {
      std::move(callback).Run(
          email, GoogleServiceAuthError(GoogleServiceAuthError::NONE),
          access_token_info);
      return;
    }

    // Else the stored value is expired. Let's remove its entry.
    fetched_access_tokens_.erase(email);
  }

  // If there is a pending fetch for the email, then append the callback to
  // the pending callbacks.
  if (base::Contains(pending_oauth_token_fetch_, email)) {
    pending_oauth_token_fetch_[email].callbacks.push_back(std::move(callback));
    return;
  }

  InitiateAccessTokenFetchFor(email, std::move(callback));
}

// Removed by token instead of email because the token value stored in
//`fetched_access_tokens_` might be updated to the valid value before this
// function get called.
void ProjectorOAuthTokenFetcher::InvalidateToken(const std::string& token) {
  base::EraseIf(fetched_access_tokens_, [&token](const auto& pair) -> bool {
    return pair.second.token == token;
  });
  GetIdentityManager()->RemoveAccessTokenFromCache(
      GetIdentityManager()->GetPrimaryAccountId(signin::ConsentLevel::kSignin),
      GetScopeSet(), token);
}

bool ProjectorOAuthTokenFetcher::HasCachedTokenForTest(
    const std::string& email) {
  return base::Contains(fetched_access_tokens_, email);
}

bool ProjectorOAuthTokenFetcher::HasPendingRequestForTest(
    const std::string& email) {
  return base::Contains(pending_oauth_token_fetch_, email);
}

void ProjectorOAuthTokenFetcher::InitiateAccessTokenFetchFor(
    const std::string& email,
    AccessTokenRequestCallback callback) {
  DCHECK(!base::Contains(pending_oauth_token_fetch_, email));

  // There is no pending fetch for the email. Let's create a new fetch.
  // Let's start creating the oauth2 access token request.

  // kImmediate makes a one-shot immediate request.
  const auto mode = signin::AccessTokenFetcher::Mode::kImmediate;

  // Create the fetcher via |identity_manager|.
  auto* identity_manager = GetIdentityManager();
  std::unique_ptr<signin::AccessTokenFetcher> access_token_fetcher =
      identity_manager->CreateAccessTokenFetcherForAccount(
          identity_manager->FindExtendedAccountInfoByEmailAddress(email)
              .account_id,
          /*oauth_consumer_name=*/"ProjectorOAuthTokenFetcher", GetScopeSet(),
          base::BindOnce(
              &ProjectorOAuthTokenFetcher::OnAccessTokenRequestCompleted,
              // It is safe to use base::Unretained as |this| owns
              // |access_token_fetcher_|.
              base::Unretained(this), email),
          mode);
  AccessTokenRequests& entry = pending_oauth_token_fetch_[email];
  entry.access_token_fetcher = std::move(access_token_fetcher);
  entry.callbacks.push_back(std::move(callback));
}

void ProjectorOAuthTokenFetcher::OnAccessTokenRequestCompleted(
    const std::string& email,
    GoogleServiceAuthError error,
    signin::AccessTokenInfo info) {
  if (!base::Contains(pending_oauth_token_fetch_, email))
    return;

  for (auto& callback : pending_oauth_token_fetch_[email].callbacks)
    std::move(callback).Run(email, error, info);

  if (error.state() == GoogleServiceAuthError::State::NONE)
    fetched_access_tokens_[email] = std::move(info);

  pending_oauth_token_fetch_.erase(email);
}

}  // namespace ash