910e62b5创建于 1月15日历史提交
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/devtools/protocol/fedcm_handler.h"

#include <optional>

#include "base/strings/string_number_conversions.h"
#include "content/browser/devtools/devtools_agent_host_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/webid/request_page_data.h"
#include "content/browser/webid/request_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/webid/federated_identity_api_permission_context_delegate.h"
#include "content/public/browser/webid/identity_request_dialog_controller.h"

namespace content {
namespace {
namespace FedCm = protocol::FedCm;

using DialogType = webid::RequestService::DialogType;

FedCm::DialogType ConvertDialogType(DialogType type) {
  switch (type) {
    case DialogType::kNone:
      NOTREACHED() << "This should only be called if there is a dialog";
    case DialogType::kLoginToIdpPopup:
    case DialogType::kContinueOnPopup:
    case DialogType::kErrorUrlPopup:
      NOTREACHED()
          << "These dialog types are not currently exposed to automation";
    case DialogType::kSelectAccount:
      return FedCm::DialogTypeEnum::AccountChooser;
    case DialogType::kAutoReauth:
      return FedCm::DialogTypeEnum::AutoReauthn;
    case DialogType::kConfirmIdpLogin:
      return FedCm::DialogTypeEnum::ConfirmIdpLogin;
    case DialogType::kError:
      return FedCm::DialogTypeEnum::Error;
  }
}

}  // namespace

namespace protocol {

FedCmHandler::FedCmHandler()
    : DevToolsDomainHandler(FedCm::Metainfo::domainName) {}

FedCmHandler::~FedCmHandler() = default;

// static
std::vector<FedCmHandler*> FedCmHandler::ForAgentHost(
    DevToolsAgentHostImpl* host) {
  return host->HandlersByName<FedCmHandler>(FedCm::Metainfo::domainName);
}

void FedCmHandler::SetRenderer(int process_host_id,
                               RenderFrameHostImpl* frame_host) {
  frame_host_ = frame_host;
}

void FedCmHandler::Wire(UberDispatcher* dispatcher) {
  frontend_ = std::make_unique<FedCm::Frontend>(dispatcher->channel());
  FedCm::Dispatcher::wire(dispatcher, this);
}

DispatchResponse FedCmHandler::Enable(
    std::optional<bool> in_disableRejectionDelay) {
  auto* auth_request = GetFederatedAuthRequest();
  bool was_enabled = enabled_;
  enabled_ = true;
  disable_delay_ = in_disableRejectionDelay.value_or(false);

  // OnDialogShown should have been called previously if was_enabled is true.
  // This could happen if FedCmHandler::Enable was called to enable/disable the
  // rejection delay.
  if (!was_enabled && auth_request &&
      auth_request->GetDialogType() != DialogType::kNone) {
    DidShowDialog();
  }

  return DispatchResponse::Success();
}

DispatchResponse FedCmHandler::Disable() {
  enabled_ = false;
  return DispatchResponse::Success();
}

void FedCmHandler::DidShowDialog() {
  DCHECK(frontend_);
  if (!enabled_) {
    return;
  }

  static int next_dialog_id_ = 0;
  dialog_id_ = base::NumberToString(next_dialog_id_++);

  auto* auth_request = GetFederatedAuthRequest();
  const auto* accounts = GetAccounts(auth_request);
  // `accounts` can be empty if this is an IDP Signin Confirmation dialog.
  auto accounts_array = std::make_unique<Array<FedCm::Account>>();
  if (accounts) {
    for (const auto& account : *accounts) {
      FedCm::LoginState login_state;
      std::optional<std::string> tos_url;
      std::optional<std::string> pp_url;
      switch (account->idp_claimed_login_state.value_or(
          account->browser_trusted_login_state)) {
        case IdentityRequestAccount::LoginState::kSignUp:
          login_state = FedCm::LoginStateEnum::SignUp;
          // Because TOS and PP URLs are only used when the login state is
          // sign up, we only populate them in that case.
          pp_url = account->identity_provider->client_metadata
                       .privacy_policy_url.spec();
          tos_url = account->identity_provider->client_metadata
                        .terms_of_service_url.spec();
          break;
        case IdentityRequestAccount::LoginState::kSignIn:
          login_state = FedCm::LoginStateEnum::SignIn;
          break;
      }
      std::unique_ptr<FedCm::Account> entry =
          FedCm::Account::Create()
              .SetAccountId(account->id)
              .SetEmail(account->display_identifier)
              .SetName(account->display_name)
              .SetGivenName(account->given_name)
              .SetPictureUrl(account->picture.spec())
              .SetIdpConfigUrl(
                  account->identity_provider->idp_metadata.config_url.spec())
              .SetIdpLoginUrl(
                  account->identity_provider->idp_metadata.idp_login_url.spec())
              .SetLoginState(login_state)
              .Build();
      if (pp_url) {
        entry->SetPrivacyPolicyUrl(*pp_url);
      }
      if (tos_url) {
        entry->SetTermsOfServiceUrl(*tos_url);
      }
      accounts_array->push_back(std::move(entry));
    }
  }
  IdentityRequestDialogController* dialog = auth_request->GetDialogController();
  CHECK(dialog);

  FedCm::DialogType dialog_type =
      ConvertDialogType(auth_request->GetDialogType());
  std::optional<String> maybe_subtitle;
  std::optional<std::string> subtitle = dialog->GetSubtitle();
  if (subtitle) {
    maybe_subtitle = *subtitle;
  }
  frontend_->DialogShown(dialog_id_, dialog_type, std::move(accounts_array),
                         dialog->GetTitle(), std::move(maybe_subtitle));
}

void FedCmHandler::DidCloseDialog() {
  CHECK(frontend_);
  if (!enabled_) {
    return;
  }
  frontend_->DialogClosed(dialog_id_);
}

DispatchResponse FedCmHandler::SelectAccount(const String& in_dialogId,
                                             int in_accountIndex) {
  if (in_dialogId != dialog_id_) {
    return DispatchResponse::InvalidParams(
        "Dialog ID does not match current dialog");
  }

  auto* auth_request = GetFederatedAuthRequest();
  if (!GetIdentityProviderData(auth_request)) {
    return DispatchResponse::ServerError(
        "selectAccount called while no FedCm dialog is shown");
  }
  const auto* accounts = GetAccounts(auth_request);
  if (!accounts || in_accountIndex < 0 ||
      static_cast<size_t>(in_accountIndex) >= accounts->size()) {
    return DispatchResponse::InvalidParams("Invalid account index");
  }

  const auto& account = accounts->at(in_accountIndex);
  auth_request->AcceptAccountsDialogForDevtools(
      account->identity_provider->idp_metadata.config_url, *account);
  return DispatchResponse::Success();
}

DispatchResponse FedCmHandler::OpenUrl(
    const String& in_dialogId,
    int in_accountIndex,
    const FedCm::AccountUrlType& in_accountUrlType) {
  if (in_dialogId != dialog_id_) {
    return DispatchResponse::InvalidParams(
        "Dialog ID does not match current dialog");
  }

  auto* auth_request = GetFederatedAuthRequest();
  if (!GetIdentityProviderData(auth_request)) {
    return DispatchResponse::ServerError(
        "openUrl called while no FedCm dialog is shown");
  }

  const auto* accounts = GetAccounts(auth_request);
  if (!accounts || in_accountIndex < 0 ||
      static_cast<size_t>(in_accountIndex) >= accounts->size()) {
    return DispatchResponse::InvalidParams("Invalid account index");
  }

  const auto& account = accounts->at(in_accountIndex);
  IdentityRequestDialogController::LinkType type;
  GURL url;
  if (in_accountUrlType == FedCm::AccountUrlTypeEnum::TermsOfService) {
    type = IdentityRequestDialogController::LinkType::TERMS_OF_SERVICE;
    url = account->identity_provider->client_metadata.terms_of_service_url;
  } else if (in_accountUrlType == FedCm::AccountUrlTypeEnum::PrivacyPolicy) {
    type = IdentityRequestDialogController::LinkType::PRIVACY_POLICY;
    url = account->identity_provider->client_metadata.privacy_policy_url;
  } else {
    return DispatchResponse::InvalidParams("Invalid account URL type");
  }
  if (!url.is_valid() || account->fields.empty()) {
    return DispatchResponse::InvalidParams(
        "Account does not have requested URL");
  }
  auth_request->GetDialogController()->ShowUrl(type, url);
  return DispatchResponse::Success();
}

DispatchResponse FedCmHandler::ClickDialogButton(
    const String& in_dialogId,
    const FedCm::DialogButton& in_dialogButton) {
  if (in_dialogId != dialog_id_) {
    return DispatchResponse::InvalidParams(
        "Dialog ID does not match current dialog");
  }

  auto* auth_request = GetFederatedAuthRequest();
  if (!auth_request) {
    return DispatchResponse::ServerError(
        "clickDialogButton called while no FedCm dialog is shown");
  }

  DialogType type = auth_request->GetDialogType();
  if (in_dialogButton == FedCm::DialogButtonEnum::ConfirmIdpLoginContinue) {
    switch (type) {
      case DialogType::kConfirmIdpLogin:
        auth_request->AcceptConfirmIdpLoginDialogForDevtools();
        return DispatchResponse::Success();
      case DialogType::kSelectAccount: {
        const auto* idp_data = GetIdentityProviderData(auth_request);
        CHECK(idp_data) << "kSelectAccount should always have IDP data";
        CHECK(!idp_data->empty());
        if (idp_data->size() > 1) {
          return DispatchResponse::ServerError(
              "Multi-IDP not supported for ConfirmIdpLogin yet "
              "(crbug.com/328115461)");
        }
        if (!auth_request->UseAnotherAccountForDevtools(*idp_data->at(0))) {
          return DispatchResponse::ServerError(
              "'Use another account' not supported for this IDP");
        }
        return DispatchResponse::Success();
      }
      default:
        return DispatchResponse::ServerError(
            "clickDialogButton called with ConfirmIdpLoginContinue while no "
            "confirm IDP login dialog is shown");
    }
  } else if (in_dialogButton == FedCm::DialogButtonEnum::ErrorGotIt) {
    if (type != DialogType::kError) {
      return DispatchResponse::ServerError(
          "clickDialogButton called with ErrorGotIt while no error dialog is "
          "shown");
    }
    auth_request->ClickErrorDialogGotItForDevtools();
    return DispatchResponse::Success();
  } else if (in_dialogButton == FedCm::DialogButtonEnum::ErrorMoreDetails) {
    if (type != DialogType::kError) {
      return DispatchResponse::ServerError(
          "clickDialogButton called with ErrorMoreDetails while no error "
          "dialog is shown");
    } else if (!auth_request->HasMoreDetailsButtonForDevtools()) {
      return DispatchResponse::ServerError(
          "clickDialogButton called with ErrorMoreDetails but more details "
          "button is not shown");
    }
    auth_request->ClickErrorDialogMoreDetailsForDevtools();
    return DispatchResponse::Success();
  }
  return DispatchResponse::InvalidParams("Invalid dialog button");
}

DispatchResponse FedCmHandler::DismissDialog(
    const String& in_dialogId,
    std::optional<bool> in_triggerCooldown) {
  if (in_dialogId != dialog_id_) {
    return DispatchResponse::InvalidParams(
        "Dialog ID does not match current dialog");
  }

  auto* auth_request = GetFederatedAuthRequest();
  if (!auth_request){
    return DispatchResponse::ServerError(
        "dismissDialog called while no FedCm dialog is shown");
  }

  DialogType type = auth_request->GetDialogType();
  if (type == DialogType::kConfirmIdpLogin) {
    auth_request->DismissConfirmIdpLoginDialogForDevtools();
    return DispatchResponse::Success();
  }
  if (type == DialogType::kError) {
    auth_request->DismissErrorDialogForDevtools();
    return DispatchResponse::Success();
  }
  const auto* idp_data = GetIdentityProviderData(auth_request);
  if (!idp_data) {
    return DispatchResponse::ServerError(
        "cancelDialog called while no FedCm dialog is shown");
  }

  auth_request->DismissAccountsDialogForDevtools(
      in_triggerCooldown.value_or(false));
  return DispatchResponse::Success();
}

DispatchResponse FedCmHandler::ResetCooldown() {
  auto* context = GetApiPermissionContext();
  if (!context) {
    return DispatchResponse::ServerError("no frame host");
  }
  context->RemoveEmbargoAndResetCounts(GetEmbeddingOrigin());
  return DispatchResponse::Success();
}

url::Origin FedCmHandler::GetEmbeddingOrigin() {
  CHECK(frame_host_);
  CHECK(frame_host_->GetMainFrame());
  return frame_host_->GetMainFrame()->GetLastCommittedOrigin();
}

webid::RequestPageData* FedCmHandler::GetPageData() {
  if (!frame_host_) {
    return nullptr;
  }
  Page& page = frame_host_->GetPage();
  return PageUserData<webid::RequestPageData>::GetOrCreateForPage(page);
}

webid::RequestService* FedCmHandler::GetFederatedAuthRequest() {
  webid::RequestPageData* page_data = GetPageData();
  if (!page_data) {
    return nullptr;
  }
  return page_data->PendingWebIdentityRequest();
}

const std::vector<IdentityProviderDataPtr>*
FedCmHandler::GetIdentityProviderData(webid::RequestService* auth_request) {
  if (!auth_request) {
    return nullptr;
  }
  const auto& idp_data = auth_request->GetSortedIdpData();
  // idp_data is empty iff no dialog is shown.
  if (idp_data.empty()) {
    return nullptr;
  }
  return &idp_data;
}

const std::vector<IdentityRequestAccountPtr>* FedCmHandler::GetAccounts(
    webid::RequestService* auth_request) {
  if (!auth_request) {
    return nullptr;
  }
  const auto& accounts = auth_request->GetAccounts();
  if (accounts.empty()) {
    return nullptr;
  }
  return &accounts;
}

FederatedIdentityApiPermissionContextDelegate*
FedCmHandler::GetApiPermissionContext() {
  if (!frame_host_) {
    return nullptr;
  }
  return frame_host_->GetBrowserContext()
      ->GetFederatedIdentityApiPermissionContext();
}

}  // namespace protocol
}  // namespace content