| // 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/federated_auth_request_impl.h" |
| #include "content/browser/webid/request_page_data.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 = FederatedAuthRequestImpl::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); |
| } |
| |
| FederatedAuthRequestImpl* FedCmHandler::GetFederatedAuthRequest() { |
| webid::RequestPageData* page_data = GetPageData(); |
| if (!page_data) { |
| return nullptr; |
| } |
| return page_data->PendingWebIdentityRequest(); |
| } |
| |
| const std::vector<IdentityProviderDataPtr>* |
| FedCmHandler::GetIdentityProviderData(FederatedAuthRequestImpl* 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( |
| FederatedAuthRequestImpl* 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 |