| // Copyright 2020 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/webid/request.h" |
| |
| #include <algorithm> |
| #include <random> |
| #include <vector> |
| |
| #include "base/barrier_closure.h" |
| #include "base/base64url.h" |
| #include "base/command_line.h" |
| #include "base/containers/span.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/rand_util.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "content/browser/bad_message.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/browser/webid/fake_identity_request_dialog_controller.h" |
| #include "content/browser/webid/flags.h" |
| #include "content/browser/webid/identity_registry.h" |
| #include "content/browser/webid/idp_network_request_manager.h" |
| #include "content/browser/webid/mappers.h" |
| #include "content/browser/webid/request_page_data.h" |
| #include "content/browser/webid/request_service.h" |
| #include "content/browser/webid/url_computations.h" |
| #include "content/browser/webid/user_info_request.h" |
| #include "content/browser/webid/webid_utils.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/webid/federated_embedder_login_request.h" |
| #include "content/public/browser/webid/federated_identity_api_permission_context_delegate.h" |
| #include "content/public/browser/webid/federated_identity_auto_reauthn_permission_context_delegate.h" |
| #include "content/public/browser/webid/federated_identity_permission_context_delegate.h" |
| #include "content/public/browser/webid/identity_request_account.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/page_visibility_state.h" |
| #include "mojo/public/cpp/base/values_mojom_traits.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "third_party/blink/public/common/webid/login_status_account.h" |
| #include "third_party/blink/public/common/webid/login_status_options.h" |
| #include "third_party/blink/public/mojom/webid/federated_request.mojom.h" |
| #include "ui/base/page_transition_types.h" |
| #include "url/gurl.h" |
| |
| namespace content::webid { |
| |
| using CompleteRequestWithErrorCallback = |
| base::OnceCallback<void(blink::mojom::FederatedRequestResult, |
| std::optional<RequestIdTokenStatus>, |
| bool)>; |
| using ErrorDialogType = IdpNetworkRequestManager::FedCmErrorDialogType; |
| using ErrorUrlType = IdpNetworkRequestManager::FedCmErrorUrlType; |
| using FederatedApiPermissionStatus = |
| FederatedIdentityApiPermissionContextDelegate::PermissionStatus; |
| using IdentityProviderDataPtr = scoped_refptr<IdentityProviderData>; |
| using IdentityProviderGetInfo = AccountsFetcher::IdentityProviderGetInfo; |
| using IdentityRequestAccountPtr = scoped_refptr<IdentityRequestAccount>; |
| using LoginState = IdentityRequestAccount::LoginState; |
| using MediationRequirement = ::password_manager::CredentialMediationRequirement; |
| using RpMode = blink::mojom::RpMode; |
| using SignInMode = IdentityRequestAccount::SignInMode; |
| using TokenError = IdentityCredentialTokenError; |
| using TokenResponseType = IdpNetworkRequestManager::FedCmTokenResponseType; |
| using TokenStatus = RequestIdTokenStatus; |
| using base::Value; |
| using blink::mojom::FederatedRequestResult; |
| using blink::mojom::IdentityProviderConfig; |
| using blink::mojom::IdentityProviderGetParametersPtr; |
| using blink::mojom::IdentityProviderRequestOptionsPtr; |
| using blink::mojom::RegisterIdpStatus; |
| using blink::mojom::RequestTokenStatus; |
| using blink::mojom::RequestUserInfoStatus; |
| |
| namespace { |
| static constexpr base::TimeDelta kTokenRequestDelay = base::Seconds(3); |
| static constexpr base::TimeDelta kMaxRejectionTime = base::Seconds(60); |
| |
| // Users spend less time on Android to dismiss the UI. Given the difference, we |
| // use two set of values. The values are calculated based on UMA data to follow |
| // lognormal distribution. |
| #if BUILDFLAG(IS_ANDROID) |
| static constexpr double kRejectionLogNormalMu = 7.4; |
| static constexpr double kRejectionLogNormalSigma = 1.24; |
| #else |
| static constexpr double kRejectionLogNormalMu = 8.6; |
| static constexpr double kRejectionLogNormalSigma = 1.4; |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // The time from when the accounts dialog is shown to when a user explicitly |
| // closes it follows normal distribution. To make the random failures |
| // indistinguishable from user declines, we use lognormal distribution to |
| // generate the random number. |
| base::TimeDelta GetRandomRejectionTime() { |
| base::RandomBitGenerator generator; |
| std::lognormal_distribution<double> distribution(kRejectionLogNormalMu, |
| kRejectionLogNormalSigma); |
| |
| base::TimeDelta rejection_time = |
| base::Seconds(distribution(generator) / 1000); |
| |
| return std::min(kMaxRejectionTime, rejection_time); |
| } |
| |
| std::string FormatOriginForDisplay(const url::Origin& origin) { |
| return FormatUrlToSite(origin.GetURL()); |
| } |
| |
| std::string GetTopFrameOriginForDisplay(const url::Origin& top_frame_origin) { |
| return FormatOriginForDisplay(top_frame_origin); |
| } |
| |
| bool IsFrameActive(RenderFrameHost* frame) { |
| return frame && frame->IsActive(); |
| } |
| |
| bool IsFrameVisible(RenderFrameHost* frame) { |
| return frame && frame->IsActive() && |
| frame->GetVisibilityState() == PageVisibilityState::kVisible; |
| } |
| |
| bool CanBypassPermissionStatusCheck( |
| const blink::mojom::RpMode& rp_mode, |
| const MediationRequirement& mediation_requirement) { |
| // Embargo or browser settings should not affect active mode. Since |
| // conditional flow isn't intrusive which was the main reason we added such |
| // controls, we can bypass the check for it as well. |
| return rp_mode == RpMode::kActive || |
| (IsAutofillEnabled() && |
| mediation_requirement == MediationRequirement::kConditional); |
| } |
| |
| } // namespace |
| |
| Request::FetchData::FetchData() = default; |
| Request::FetchData::~FetchData() = default; |
| |
| Request::AutoReauthnInfo::AutoReauthnInfo() = default; |
| Request::AutoReauthnInfo::~AutoReauthnInfo() = default; |
| Request::AutoReauthnInfo::AutoReauthnInfo(const AutoReauthnInfo&) = default; |
| Request::AutoReauthnInfo& Request::AutoReauthnInfo::operator=( |
| const AutoReauthnInfo&) = default; |
| |
| Request::Request(RenderFrameHost* rfh, RequestService& request_service) |
| : render_frame_host_(rfh), |
| request_service_(request_service), |
| perfetto_track_(CreatePerfettoTrackForFedCM(this)) { |
| receivers_.set_disconnect_handler( |
| base::BindRepeating(&Request::OnConnectionError, base::Unretained(this))); |
| } |
| |
| Request::~Request() { |
| // Ensures key data members are destructed in proper order and resolves any |
| // pending promise. |
| if (request_token_callback_) { |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| TokenStatus::kUnhandledRequest, |
| /*should_delay_callback=*/false); |
| } |
| } |
| |
| void Request::BindReceiver( |
| mojo::PendingReceiver<blink::mojom::FederatedRequest> pending_receiver) { |
| receivers_.Add(this, std::move(pending_receiver)); |
| } |
| |
| void Request::Abort() { |
| if (!request_token_callback_) { |
| // This can happen if the renderer requested an abort() after the browser |
| // invoked the callback but before the renderer received the callback. |
| return; |
| } |
| |
| // Dialog will be hidden by the destructor of the dialog controller in |
| // RequestService, triggered by CompleteRequest. |
| |
| CompleteRequestWithError(FederatedRequestResult::kCanceled, |
| TokenStatus::kAborted, |
| /*should_delay_callback=*/false); |
| } |
| |
| void Request::OnConnectionError() { |
| // If the renderer disconnected the FederatedRequest pipe without calling |
| // Abort(), it means the frame was detached, the tab was closed, or the |
| // renderer crashed. We complete the request with an error, which will |
| // correctly record a `kUnhandledRequest` fallback metric and trigger the |
| // service-level cleanup. |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| TokenStatus::kUnhandledRequest, |
| /*should_delay_callback=*/false); |
| } |
| |
| std::vector<IdentityProviderRequestOptionsPtr> |
| Request::MaybeAddRegisteredProviders( |
| std::vector<IdentityProviderRequestOptionsPtr>& providers) { |
| std::vector<IdentityProviderRequestOptionsPtr> result; |
| |
| std::vector<GURL> registered_config_urls = |
| permission_delegate()->GetRegisteredIdPs(); |
| |
| // TODO(crbug.com/40252825): we insert the registered IdPs to |
| // the list of IdPs in a reverse chronological order: |
| // first IdPs to be registered goes first. It is not clear |
| // yet what's the right order, but this seems like a reasonable |
| // starting point. |
| std::ranges::reverse(registered_config_urls); |
| |
| for (auto& provider : providers) { |
| if (!provider->config->from_idp_registration_api) { |
| result.emplace_back(provider->Clone()); |
| continue; |
| } |
| |
| for (auto& configURL : registered_config_urls) { |
| IdentityProviderRequestOptionsPtr idp = provider->Clone(); |
| // Keep `from_idp_registration_api` so it is clear this is a registered |
| // provider. |
| idp->config->config_url = configURL; |
| result.emplace_back(std::move(idp)); |
| } |
| } |
| |
| // TODO(crbug.com/40252825): Consider removing duplicate |
| // IdPs in case they were present in the registry as well |
| // as added individually. |
| |
| return result; |
| } |
| |
| bool Request::RequestToken( |
| std::vector<IdentityProviderGetParametersPtr> idp_get_params_ptrs, |
| MediationRequirement requirement, |
| NavigationHandle* navigation_handle, |
| const GURL& intercepted_url, |
| RequestTokenCallback callback) { |
| CHECK(!HasPendingRequest()); |
| bool intercept = false; |
| bool should_complete_request_immediately = false; |
| devtools_instrumentation::WillSendFedCmRequest( |
| render_frame_host(), &intercept, &should_complete_request_immediately); |
| should_complete_request_immediately = |
| (intercept && should_complete_request_immediately) || |
| api_permission_delegate()->ShouldCompleteRequestImmediately(); |
| |
| // Expand the providers list with registered providers. |
| if (IsIdPRegistrationEnabled()) { |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| std::vector<IdentityProviderRequestOptionsPtr> providers = |
| MaybeAddRegisteredProviders(idp_get_params_ptr->providers); |
| if (providers.empty()) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "No identity providers are registered."); |
| base::TimeDelta delay; |
| if (!should_complete_request_immediately) { |
| delay = GetRandomRejectionTime(); |
| } |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), RequestTokenStatus::kError, |
| std::nullopt, std::nullopt, |
| /*error=*/nullptr, |
| /*is_auto_selected=*/false), |
| delay); |
| return true; |
| } |
| idp_get_params_ptr->providers = std::move(providers); |
| } |
| } |
| |
| if (!render_frame_host().GetPage().IsPrimary()) { |
| // This should not be possible but seems to be happening, so we log |
| // the lifecycle state for further investigation. |
| RenderFrameHostImpl* host_impl = |
| static_cast<RenderFrameHostImpl*>(&render_frame_host()); |
| |
| RecordLifecycleStateFailureReason( |
| LifecycleStateImplLifecycleStateImplToFedCmLifecycleStateFailureReason( |
| host_impl->lifecycle_state())); |
| std::move(callback).Run(RequestTokenStatus::kError, std::nullopt, |
| std::nullopt, |
| /*error=*/nullptr, |
| /*is_auto_selected=*/false); |
| return false; |
| } |
| |
| can_accept_redirect_to_ = |
| request_service_->force_allow_redirect_to_for_testing() || |
| ((IsNavigationInterceptionEnabled() || |
| HasEmbedderLoginRequest(&render_frame_host())) && |
| navigation_handle != nullptr); |
| |
| had_transient_user_activation_ = |
| (navigation_handle && |
| DidNavigationHandleHaveActivation(navigation_handle)) || |
| render_frame_host().HasTransientUserActivation(); |
| if (navigation_handle) { |
| intercepted_url_ = intercepted_url; |
| } |
| |
| idp_order_ = {}; |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| for (auto& idp_ptr : idp_get_params_ptr->providers) { |
| idp_order_.push_back(idp_ptr->config->config_url); |
| } |
| } |
| |
| // From here on out, all failures go through CompleteRequest, so this is |
| // where we start the trace event. |
| TRACE_EVENT_BEGIN("content.fedcm", "FedCM get", perfetto_track_); |
| |
| should_complete_request_immediately_ = should_complete_request_immediately; |
| mediation_requirement_ = requirement; |
| request_token_callback_ = std::move(callback); |
| GetPageData(render_frame_host().GetPage()) |
| ->SetPendingWebIdentityRequest(this); |
| network_manager_ = request_service_->CreateNetworkManager(); |
| |
| start_time_ = base::TimeTicks::Now(); |
| if (!fedcm_metrics_) { |
| fedcm_metrics_ = CreateFedCmMetrics(); |
| } |
| std::set<GURL> idps_with_nonce; |
| std::set<GURL> idps_with_nonce_outside_params_only; |
| for (const auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| for (const auto& idp_ptr : idp_get_params_ptr->providers) { |
| if (!idp_ptr->nonce.empty()) { |
| idps_with_nonce.insert(idp_ptr->config->config_url); |
| |
| bool has_nonce_in_params = false; |
| if (idp_ptr->params_json) { |
| std::optional<base::Value> params = base::JSONReader::Read( |
| *idp_ptr->params_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS); |
| if (params && params->is_dict()) { |
| if (params->GetDict().contains("nonce")) { |
| has_nonce_in_params = true; |
| } |
| } |
| } |
| if (!has_nonce_in_params) { |
| idps_with_nonce_outside_params_only.insert( |
| idp_ptr->config->config_url); |
| } |
| } |
| } |
| } |
| fedcm_metrics_->RecordHasNonce(idps_with_nonce); |
| fedcm_metrics_->RecordHasNonceOutsideParamsOnly( |
| idps_with_nonce_outside_params_only); |
| |
| if (idp_get_params_ptrs[0]->mode == blink::mojom::RpMode::kActive) { |
| rp_mode_ = RpMode::kActive; |
| if (!had_transient_user_activation_) { |
| CompleteRequestWithError( |
| FederatedRequestResult::kMissingTransientUserActivation, |
| TokenStatus::kMissingTransientUserActivation, |
| /*should_delay_callback=*/false); |
| return false; |
| } |
| } else { |
| rp_mode_ = RpMode::kPassive; |
| } |
| |
| if (origin().opaque()) { |
| CompleteRequestWithError( |
| FederatedRequestResult::kRelyingPartyOriginIsOpaque, |
| TokenStatus::kRpOriginIsOpaque, |
| /*should_delay_callback=*/false); |
| return false; |
| } |
| |
| FederatedApiPermissionStatus permission_status = GetApiPermissionStatus(); |
| |
| if (!CanBypassPermissionStatusCheck(rp_mode_, mediation_requirement_)) { |
| if (permission_status != FederatedApiPermissionStatus::GRANTED) { |
| std::pair<FederatedRequestResult, TokenStatus> resultAndTokenStatus = |
| PermissionStatusToRequestResultAndTokenStatus(permission_status); |
| CompleteRequestWithError(resultAndTokenStatus.first, |
| resultAndTokenStatus.second, |
| /*should_delay_callback=*/true); |
| return true; |
| } |
| } |
| |
| request_service_->IncrementNumRequests(); |
| |
| std::set<GURL> unique_idps; |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| for (auto& idp_ptr : idp_get_params_ptr->providers) { |
| // Throw an error if duplicate IDPs are specified. |
| const bool is_unique_idp = |
| unique_idps.insert(idp_ptr->config->config_url).second; |
| if (!is_unique_idp) { |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| /*token_status=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return false; |
| } |
| |
| url::Origin idp_origin = url::Origin::Create(idp_ptr->config->config_url); |
| if (!network::IsOriginPotentiallyTrustworthy(idp_origin)) { |
| CompleteRequestWithError( |
| FederatedRequestResult::kIdpNotPotentiallyTrustworthy, |
| TokenStatus::kIdpNotPotentiallyTrustworthy, |
| /*should_delay_callback=*/false); |
| return false; |
| } |
| } |
| } |
| |
| bool any_idp_has_custom_scopes = false; |
| bool any_idp_has_parameters = false; |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| for (auto& idp_ptr : idp_get_params_ptr->providers) { |
| bool has_failing_idp_signin_status = |
| ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp( |
| idp_ptr->config->config_url, permission_delegate()); |
| |
| if (has_failing_idp_signin_status) { |
| if (idp_get_params_ptr->mode == blink::mojom::RpMode::kPassive) { |
| // In the multi IDP case, we do not want to complete the request |
| // right away as there are other IDPs which may be logged in. But we |
| // also do not want to fetch this IDP. |
| unique_idps.erase(idp_ptr->config->config_url); |
| continue; |
| } else if (idp_get_params_ptr->mode == blink::mojom::RpMode::kActive) { |
| // We fail sooner before, but just to double check, we assert that |
| // we are inside a user gesture here again. |
| CHECK(had_transient_user_activation_); |
| } |
| } |
| if (ShouldFailBeforeFetchingAccounts(idp_ptr->config->config_url)) { |
| // In the multi IDP case, we do not want to complete the request right |
| // away as there are other IDPs which may be logged in. But we also do |
| // not want to fetch this IDP. |
| unique_idps.erase(idp_ptr->config->config_url); |
| continue; |
| } |
| |
| any_idp_has_custom_scopes = any_idp_has_custom_scopes || |
| GetDisclosureFields(idp_ptr->fields).empty(); |
| any_idp_has_parameters = any_idp_has_parameters || idp_ptr->params_json; |
| |
| blink::mojom::RpContext rp_context = idp_get_params_ptr->context; |
| blink::mojom::RpMode rp_mode = idp_get_params_ptr->mode; |
| const GURL& idp_config_url = idp_ptr->config->config_url; |
| std::optional<blink::mojom::Format> format = |
| IsDelegationEnabled() ? idp_ptr->format : std::nullopt; |
| token_request_get_infos_.emplace( |
| idp_config_url, IdentityProviderGetInfo(std::move(idp_ptr), |
| rp_context, rp_mode, format)); |
| } |
| } |
| |
| if (any_idp_has_parameters || any_idp_has_custom_scopes) { |
| RpParameters parameters; |
| if (any_idp_has_custom_scopes && any_idp_has_parameters) { |
| parameters = RpParameters::kHasParametersAndNonDefaultScope; |
| } else if (any_idp_has_parameters) { |
| parameters = RpParameters::kHasParameters; |
| } else { |
| DCHECK(any_idp_has_custom_scopes); |
| parameters = RpParameters::kHasNonDefaultScope; |
| } |
| fedcm_metrics_->RecordRpParameters(parameters); |
| } |
| |
| if (unique_idps.empty()) { |
| // At this point either all IDPs are signed out or mediation:silent was used |
| // and there are no returning accounts. |
| auto result = mediation_requirement_ == MediationRequirement::kSilent |
| ? FederatedRequestResult::kSilentMediationFailure |
| : FederatedRequestResult::kNotSignedInWithIdp; |
| auto token_status = mediation_requirement_ == MediationRequirement::kSilent |
| ? TokenStatus::kSilentMediationFailure |
| : TokenStatus::kNotSignedInWithIdp; |
| CompleteRequestWithError(result, token_status, |
| /*should_delay_callback=*/true); |
| return true; |
| } |
| |
| // Show loading dialog while fetching endpoints if it is a active flow. This |
| // is needed even if the LoginStatus is "logged-out" because we need to fetch |
| // the config file to get the login_url which may take some time. |
| if (rp_mode_ == RpMode::kActive) { |
| CHECK_GT(idp_order_.size(), 0u); |
| // If there is more than 1 IDP, do not show the IDP info in the loading |
| // dialog. |
| std::string idp_for_display = |
| idp_order_.size() == 1u |
| ? FormatOriginForDisplay(url::Origin::Create(idp_order_[0])) |
| : ""; |
| blink::mojom::RpContext rp_context = idp_get_params_ptrs[0]->context; |
| if (!GetDialogController()->ShowLoadingDialog( |
| CreateRpData(/*client_metadata_received=*/false), idp_for_display, |
| rp_context, rp_mode_, |
| base::BindOnce(&Request::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| return false; |
| } |
| did_show_ui_ = true; |
| } |
| |
| fedcm_metrics_->RecordIdentityProvidersCount(idp_order_.size()); |
| |
| CHECK(!unique_idps.empty()); |
| if (rp_mode_ == RpMode::kPassive && idp_order_.size() == 1u) { |
| GetDialogController()->GetPassiveDialogVolume( |
| base::BindOnce(&Request::OnGetPassiveDialogVolume, |
| weak_ptr_factory_.GetWeakPtr(), std::move(unique_idps))); |
| return true; |
| } |
| FetchEndpointsForIdps(std::move(unique_idps)); |
| return true; |
| } |
| |
| void Request::OnIdpSigninStatusReceived(const url::Origin& idp_config_origin, |
| bool idp_signin_status) { |
| if (!idp_signin_status) { |
| return; |
| } |
| |
| for (const auto& [get_idp_config_url, get_info] : token_request_get_infos_) { |
| if (url::Origin::Create(get_idp_config_url) == idp_config_origin) { |
| permission_delegate()->RemoveIdpSigninStatusObserver(this); |
| idps_user_tried_to_signin_to_.insert(get_idp_config_url); |
| FetchEndpointsForIdps({get_idp_config_url}); |
| break; |
| } |
| } |
| } |
| |
| bool Request::HasPendingRequest() const { |
| RequestPageData* page_data = GetPageData(render_frame_host().GetPage()); |
| bool has_pending_request = page_data->PendingWebIdentityRequest() != nullptr; |
| DCHECK(has_pending_request || !request_token_callback_); |
| return has_pending_request; |
| } |
| |
| void Request::FetchEndpointsForIdps(const std::set<GURL>& idp_config_urls) { |
| int icon_ideal_size = GetDialogController()->GetBrandIconIdealSize(rp_mode_); |
| int icon_minimum_size = |
| GetDialogController()->GetBrandIconMinimumSize(rp_mode_); |
| std::set<GURL> pending_idps = std::move(fetch_data_.pending_idps); |
| pending_idps.insert(idp_config_urls.begin(), idp_config_urls.end()); |
| fetch_data_ = FetchData(); |
| fetch_data_.pending_idps = std::move(pending_idps); |
| |
| std::vector<ConfigFetcher::FetchRequest> idps; |
| for (const auto& idp : idp_config_urls) { |
| auto idp_get = token_request_get_infos_.find(idp); |
| CHECK(idp_get != token_request_get_infos_.end()); |
| idps.emplace_back( |
| idp, idp_get->second.provider->config->from_idp_registration_api); |
| } |
| |
| fedcm_accounts_fetcher_ = std::make_unique<AccountsFetcher>( |
| render_frame_host(), network_manager_.get(), api_permission_delegate(), |
| permission_delegate(), |
| AccountsFetcher::FedCmFetchingParams( |
| rp_mode_, icon_ideal_size, icon_minimum_size, mediation_requirement_), |
| base::BindOnce(&Request::OnAccountsResultsReceived, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // When retrying (e.g. after IDP sign-in failure popup), there is only 1 IDP |
| // requested and its .well-known and config endpoints/metadata are already |
| // cached in `idp_infos_`. In this case, bypass ConfigFetcher and directly |
| // fetch accounts for the cached IDP. |
| if (idps.size() == 1u) { |
| auto it = idp_infos_.find(idps[0].identity_provider_config_url); |
| if (it != idp_infos_.end() && it->second) { |
| std::vector<std::unique_ptr<IdentityProviderInfo>> cached_idp_infos; |
| cached_idp_infos.push_back( |
| std::make_unique<IdentityProviderInfo>(*it->second)); |
| fedcm_accounts_fetcher_->FetchAccountsForIdps( |
| cached_idp_infos, token_request_get_infos_, fedcm_metrics_.get(), |
| GetEmbeddingOrigin(), |
| base::BindRepeating(&Request::FilterAccounts, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| } |
| |
| fedcm_accounts_fetcher_->FetchEndpointsForIdps( |
| idps, token_request_get_infos_, fedcm_metrics_.get(), |
| GetEmbeddingOrigin(), |
| base::BindRepeating(&Request::FilterAccounts, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void Request::FilterAccounts(const GURL& idp_config_url, |
| const GURL& idp_login_url, |
| std::vector<IdentityRequestAccountPtr>& accounts) { |
| auto filter = [](const IdentityRequestAccountPtr& account) { |
| return account->is_filtered_out; |
| }; |
| if (idps_user_tried_to_signin_to_.find(idp_config_url) == |
| idps_user_tried_to_signin_to_.end() || |
| login_url_ != idp_login_url) { |
| std::erase_if(accounts, filter); |
| } else { |
| // If the user is logging in to new accounts, only show filtered |
| // accounts if there are no new unfiltered accounts. This includes in |
| // particular the case where all accounts are filtered out. |
| size_t new_unfiltered = |
| std::count_if(accounts.begin(), accounts.end(), |
| [&](const IdentityRequestAccountPtr& account) { |
| return !account->is_filtered_out && |
| account_ids_before_login_.find(account->id) == |
| account_ids_before_login_.end(); |
| }); |
| if (new_unfiltered > 0u) { |
| std::erase_if(accounts, filter); |
| } |
| } |
| } |
| |
| void Request::OnAccountsResultsReceived( |
| base::TimeTicks well_known_and_config_fetched_time, |
| std::vector<AccountsFetcher::Result> results) { |
| if (!well_known_and_config_fetched_time.is_null()) { |
| SetWellKnownAndConfigFetchedTime(well_known_and_config_fetched_time); |
| } |
| |
| for (auto& result : results) { |
| if (result.idp_info) { |
| SetIdpLoginInfo(result.idp_info->metadata.idp_login_url, |
| result.idp_info->provider->login_hint, |
| result.idp_info->provider->domain_hint); |
| } |
| if (result.accounts_fetched_time != base::TimeTicks()) { |
| SetAccountsFetchedTime(result.accounts_fetched_time); |
| } |
| if (result.client_metadata_fetched_time != base::TimeTicks()) { |
| SetClientMetadataFetchedTime(result.client_metadata_fetched_time); |
| } |
| |
| if (result.show_active_mode_modal_dialog) { |
| MaybeShowActiveModeModalDialog(result.idp_config_url, |
| result.idp_info->metadata.idp_login_url); |
| continue; |
| } |
| |
| if (result.error) { |
| OnFetchDataForIdpFailed(std::move(result.idp_info), *result.error, |
| result.token_status, |
| result.should_delay_callback); |
| continue; |
| } |
| |
| if (result.is_mismatch) { |
| OnIdpMismatch(std::move(result.idp_info)); |
| continue; |
| } |
| |
| // Success |
| CHECK(result.accounts.has_value()); |
| idp_filtered_accounts_[result.idp_config_url] = |
| std::move(result.filtered_accounts); |
| OnFetchDataForIdpSucceeded(std::move(*result.accounts), |
| std::move(result.idp_info)); |
| } |
| } |
| |
| bool Request::CanShowContinueOnPopup() const { |
| if (mediation_requirement_ == MediationRequirement::kConditional) { |
| // Because conditional mediation always requires a user gesture to sign in, |
| // we can always allow the continuation popup. |
| return true; |
| } |
| |
| if (mediation_requirement_ == MediationRequirement::kSilent) { |
| return false; |
| } |
| |
| if (mediation_requirement_ == MediationRequirement::kRequired) { |
| // In this case, we always have a user gesture (the user had to choose |
| // an account), so we can show a popup. |
| return true; |
| } |
| |
| if (identity_selection_type_ == kExplicit) { |
| return true; |
| } |
| |
| return had_transient_user_activation_; |
| } |
| |
| UseOtherAccountResult Request::ComputeUseOtherAccountResult( |
| blink::mojom::FederatedRequestResult result, |
| const std::optional<GURL>& selected_idp_config_url) { |
| if (result != FederatedRequestResult::kSuccess) { |
| return UseOtherAccountResult::kUserDoesNotSignIn; |
| } |
| |
| CHECK(selected_idp_config_url); |
| if (IsEndpointSameOrigin(*selected_idp_config_url, login_url_) && |
| !account_ids_before_login_.contains(account_id_)) { |
| return UseOtherAccountResult::kUserSignsInWithNewAccount; |
| } |
| return UseOtherAccountResult::kUserSignsInWithExistingAccount; |
| } |
| |
| void Request::OnFetchDataForIdpSucceeded( |
| IdpNetworkRequestManager::AccountsResponse accounts, |
| std::unique_ptr<IdentityProviderInfo> idp_info) { |
| fetch_data_.did_succeed_for_at_least_one_idp = true; |
| |
| const GURL& idp_config_url = idp_info->provider->config->config_url; |
| // If the IDP data existed before, we need to remove the old accounts data. |
| // This can happen with the 'use other account' feature. |
| if (idp_infos_.find(idp_config_url) != idp_infos_.end()) { |
| std::erase_if(accounts_, [&idp_config_url](const auto& account) { |
| return account->identity_provider->idp_metadata.config_url == |
| idp_config_url; |
| }); |
| std::erase_if(filtered_accounts_, [&idp_config_url](const auto& account) { |
| return account->identity_provider->idp_metadata.config_url == |
| idp_config_url; |
| }); |
| } |
| idp_infos_[idp_config_url] = std::move(idp_info); |
| idp_accounts_[idp_config_url] = std::move(accounts.accounts); |
| |
| fetch_data_.pending_idps.erase(idp_config_url); |
| MaybeShowAccountsDialog(); |
| } |
| |
| void Request::SetIdpLoginInfo(const GURL& idp_login_url, |
| const std::string& login_hint, |
| const std::string& domain_hint) { |
| idp_login_infos_[idp_login_url] = {login_hint, domain_hint}; |
| } |
| |
| void Request::SetWellKnownAndConfigFetchedTime(base::TimeTicks time) { |
| well_known_and_config_fetched_time_ = time; |
| fedcm_metrics_->RecordWellKnownAndConfigFetchTime( |
| well_known_and_config_fetched_time_ - start_time_); |
| } |
| |
| void Request::OnFetchDataForIdpFailed( |
| const std::unique_ptr<IdentityProviderInfo> idp_info, |
| blink::mojom::FederatedRequestResult result, |
| std::optional<RequestIdTokenStatus> token_status, |
| bool should_delay_callback) { |
| const GURL& idp_config_url = idp_info->provider->config->config_url; |
| fetch_data_.pending_idps.erase(idp_config_url); |
| |
| if (fetch_data_.pending_idps.empty() && |
| !fetch_data_.did_succeed_for_at_least_one_idp) { |
| CompleteRequestWithError(result, token_status, should_delay_callback); |
| return; |
| } |
| |
| AddDevToolsIssue(result); |
| AddConsoleErrorMessage(result); |
| |
| // We do not call both OnFetchDataForIdpFailed() after OnFetchDataSucceeded() |
| // for the same IDP. |
| DCHECK(idp_infos_.find(idp_config_url) == idp_infos_.end()); |
| MaybeShowAccountsDialog(); |
| } |
| |
| const std::optional<std::vector<IdentityRequestAccountPtr>> |
| Request::GetAutofillSuggestions() const { |
| // Requires conditional FedCM to be enabled. |
| if (!IsAutofillEnabled()) { |
| return std::nullopt; |
| } |
| |
| // There isn't a request hanging. |
| if (!HasPendingRequest()) { |
| return std::nullopt; |
| } |
| |
| // We only augment autofill when it is a conditional mediation request. |
| if (mediation_requirement_ != MediationRequirement::kConditional) { |
| return std::nullopt; |
| } |
| |
| return GetAccounts(); |
| } |
| |
| void Request::AssembleAndSortAccounts() { |
| idp_data_for_display_.clear(); |
| filtered_accounts_.clear(); |
| |
| for (const auto& idp : idp_order_) { |
| auto idp_info_it = idp_infos_.find(idp); |
| if (idp_info_it != idp_infos_.end() && idp_info_it->second->data) { |
| idp_info_it->second->data->idp_metadata.has_filtered_out_account = false; |
| idp_data_for_display_.push_back(idp_info_it->second->data); |
| } |
| auto accounts_it = idp_accounts_.find(idp); |
| if (accounts_it != idp_accounts_.end()) { |
| accounts_.insert(accounts_.end(), |
| std::make_move_iterator(accounts_it->second.begin()), |
| std::make_move_iterator(accounts_it->second.end())); |
| } |
| auto filtered_it = idp_filtered_accounts_.find(idp); |
| if (filtered_it != idp_filtered_accounts_.end()) { |
| filtered_accounts_.insert( |
| filtered_accounts_.end(), |
| std::make_move_iterator(filtered_it->second.begin()), |
| std::make_move_iterator(filtered_it->second.end())); |
| } |
| } |
| idp_accounts_.clear(); |
| idp_filtered_accounts_.clear(); |
| |
| std::stable_sort( |
| accounts_.begin(), accounts_.end(), |
| [&](const auto& account1, const auto& account2) { |
| // Show filtered accounts after valid ones. |
| if (account1->is_filtered_out || account2->is_filtered_out) { |
| return !account1->is_filtered_out; |
| } |
| // Show newly logged in accounts, if any. |
| bool is_account1_new = IsNewlyLoggedIn(*account1); |
| bool is_account2_new = IsNewlyLoggedIn(*account2); |
| if (is_account1_new || is_account2_new) { |
| return !is_account2_new; |
| } |
| // Show returning accounts before non-returning. |
| if (account1->idp_claimed_login_state.value_or( |
| account1->browser_trusted_login_state) == LoginState::kSignUp || |
| account2->idp_claimed_login_state.value_or( |
| account2->browser_trusted_login_state) == LoginState::kSignUp) { |
| return account1->idp_claimed_login_state.value_or( |
| account1->browser_trusted_login_state) == |
| LoginState::kSignIn; |
| } |
| // Within returning accounts, prefer those with last used |
| // timestamp. |
| if (!account1->last_used_timestamp || !account2->last_used_timestamp) { |
| return !!account1->last_used_timestamp; |
| } |
| // If both have last used timestamp, prefer the latest. |
| return *account1->last_used_timestamp > *account2->last_used_timestamp; |
| }); |
| // Set the display priority for newly logged in accounts. |
| for (const auto& account : accounts_) { |
| if (IsNewlyLoggedIn(*account)) { |
| account->display_priority = IdentityRequestAccount::DisplayPriority::kNew; |
| } else { |
| account->display_priority = |
| IdentityRequestAccount::DisplayPriority::kRegular; |
| } |
| if (account->is_filtered_out) { |
| account->identity_provider->idp_metadata.has_filtered_out_account = true; |
| } |
| } |
| } |
| |
| Request::AutoReauthnInfo Request::CheckAutoReauthnEligibility() { |
| AutoReauthnInfo result; |
| |
| // TODO(crbug.com/40246099): Handle auto_reauthn_ for multi IDP. |
| // TODO(crbug.com/380367784): Handle auto_reauthn_ for delegated IdP. |
| bool auto_reauthn_enabled = |
| mediation_requirement_ != MediationRequirement::kRequired; |
| |
| if (!auto_reauthn_enabled) { |
| return result; |
| } |
| |
| bool is_auto_reauthn_setting_enabled = |
| auto_reauthn_permission_delegate()->IsAutoReauthnSettingEnabled(); |
| bool is_auto_reauthn_embargoed = |
| auto_reauthn_permission_delegate()->IsAutoReauthnEmbargoed( |
| GetEmbeddingOrigin()); |
| bool is_auto_reauthn_blocked_by_embedder = |
| auto_reauthn_permission_delegate()->IsAutoReauthnDisabledByEmbedder( |
| WebContents::FromRenderFrameHost(&render_frame_host())); |
| |
| if (is_auto_reauthn_embargoed) { |
| // See `kFederatedIdentityAutoReauthnEmbargoDuration`. |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kInfo, |
| "Auto re-authn was previously triggered less than 10 minutes ago. " |
| "Only one auto re-authn request can be made every 10 minutes."); |
| } |
| bool requires_user_mediation = RequiresUserMediation(); |
| // Auto signs in returning users if they have a single returning account and |
| // are signing in. |
| IdentityProviderDataPtr auto_reauthn_idp = nullptr; |
| IdentityRequestAccountPtr auto_reauthn_account = nullptr; |
| bool has_single_returning_account = |
| GetAccountForAutoReauthn(&auto_reauthn_idp, &auto_reauthn_account); |
| |
| bool is_eligible = |
| !requires_user_mediation && is_auto_reauthn_setting_enabled && |
| !is_auto_reauthn_embargoed && has_single_returning_account && |
| !is_auto_reauthn_blocked_by_embedder; |
| |
| fedcm_metrics_->RecordAutoReauthnMetrics( |
| has_single_returning_account, auto_reauthn_account.get(), is_eligible, |
| !is_auto_reauthn_setting_enabled, is_auto_reauthn_embargoed, |
| is_auto_reauthn_blocked_by_embedder, requires_user_mediation); |
| |
| if (is_eligible) { |
| result.is_eligible = true; |
| result.idp = auto_reauthn_idp; |
| result.account = auto_reauthn_account; |
| } |
| return result; |
| } |
| |
| void Request::MaybeShowAccountsDialog() { |
| if (!fetch_data_.pending_idps.empty()) { |
| return; |
| } |
| |
| // The accounts fetch could be delayed for legitimate reasons. A user may be |
| // able to disable FedCM API (e.g. via settings or dismissing another FedCM UI |
| // on the same RP origin) before the browser receives the accounts response. |
| // We should exit early without showing any UI. |
| if (!CanBypassPermissionStatusCheck(rp_mode_, mediation_requirement_) && |
| GetApiPermissionStatus() != FederatedApiPermissionStatus::GRANTED) { |
| CompleteRequestWithError(FederatedRequestResult::kDisabledInSettings, |
| TokenStatus::kDisabledInSettings, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| AssembleAndSortAccounts(); |
| |
| // Conditional mediation doesn't display the account chooser when called, |
| // it instead waits for another UI surface (say, autofill) to trigger the |
| // account chooser. |
| if (mediation_requirement_ == MediationRequirement::kConditional) { |
| GetDialogController()->NotifyAutofillSourceReadyForTesting(); |
| return; |
| } |
| |
| AutoReauthnInfo auto_reauthn = CheckAutoReauthnEligibility(); |
| |
| if (!auto_reauthn.is_eligible && |
| mediation_requirement_ == MediationRequirement::kSilent) { |
| // By this moment we know that the user has granted permission in the past |
| // for the RP/IdP. Because otherwise we have returned already in |
| // `ShouldFailBeforeFetchingAccounts`. It means that we don't need to show |
| // any UI to respect `mediation: silent`. |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: the user has used FedCM with multiple " |
| "accounts on this site."); |
| CompleteRequestWithError(FederatedRequestResult::kSilentMediationFailure, |
| TokenStatus::kSilentMediationFailure, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| if (auto_reauthn.is_eligible) { |
| dialog_type_ = DialogType::kAutoReauth; |
| accounts_ = {auto_reauthn.account}; |
| idp_data_for_display_ = {auto_reauthn.idp}; |
| accounts_[0]->identity_provider = idp_data_for_display_[0]; |
| |
| identity_selection_type_ = (rp_mode_ == blink::mojom::RpMode::kPassive) |
| ? kAutoPassive |
| : kAutoActive; |
| } else { |
| dialog_type_ = DialogType::kSelectAccount; |
| identity_selection_type_ = kExplicit; |
| } |
| |
| // The RenderFrameHost may be alive but not visible in the following |
| // situations: |
| // Situation #1: User switched tabs |
| // Situation #2: User navigated the page with bfcache |
| // |
| // - If this fetch is as a result of an IdP sign-in status change, the FedCM |
| // dialog is either visible or temporarily hidden. Update the contents of |
| // the dialog. |
| // - If the FedCM dialog has not already been shown, do not show the dialog |
| // if the RenderFrameHost is hidden because the user does not seem interested |
| // in the contents of the current page. |
| if (idps_user_tried_to_signin_to_.empty()) { |
| bool is_active = IsFrameActive(render_frame_host().GetMainFrame()); |
| fedcm_metrics_->RecordWebContentsStatusUponReadyToShowDialog( |
| IsFrameVisible(render_frame_host().GetMainFrame()), is_active); |
| |
| if (!is_active) { |
| CompleteRequestWithError(FederatedRequestResult::kRpPageNotVisible, |
| TokenStatus::kRpPageNotVisible, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| ready_to_display_accounts_dialog_time_ = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordShowAccountsDialogTime( |
| idp_data_for_display_, |
| ready_to_display_accounts_dialog_time_ - start_time_); |
| |
| fedcm_metrics_->RecordShowAccountsDialogTimeBreakdown( |
| well_known_and_config_fetched_time_ - start_time_, |
| accounts_fetched_time_ - well_known_and_config_fetched_time_, |
| client_metadata_fetched_time_ != base::TimeTicks() |
| ? client_metadata_fetched_time_ - accounts_fetched_time_ |
| : base::TimeDelta()); |
| } |
| bool did_succeed_for_at_least_one_idp = |
| fetch_data_.did_succeed_for_at_least_one_idp; |
| |
| fetch_data_ = FetchData(); |
| |
| // RenderFrameHost should be in the primary page (ex not in the BFCache). |
| DCHECK(render_frame_host().GetPage().IsPrimary()); |
| |
| bool intercept = false; |
| // In tests (content_shell or when --use-fake-ui-for-fedcm is used), the |
| // dialog controller will immediately select an account. But if browser |
| // automation is enabled, we don't want that to happen because automation |
| // should be able to choose which account to select or to cancel. |
| // So we use this call to see whether interception is enabled. |
| // It is not needed in regular Chrome even when automation is used because |
| // there, the dialog will wait for user input anyway. |
| devtools_instrumentation::WillShowFedCmDialog(render_frame_host(), |
| &intercept); |
| // Since we don't reuse the controller for each request, and intercept |
| // defaults to false, we only need to call this if intercept is true. |
| if (intercept) { |
| GetDialogController()->SetIsInterceptionEnabled(intercept); |
| } |
| |
| if (identity_selection_type_ != kExplicit) { |
| OnAccountSelected(accounts_[0]->identity_provider->idp_metadata.config_url, |
| accounts_[0]->id, /*is_sign_in=*/true); |
| if (!GetDialogController()->ShowVerifyingDialog( |
| CreateRpData(/*client_metadata_received=*/true), auto_reauthn.idp, |
| accounts_[0], SignInMode::kAuto, rp_mode_, |
| base::BindOnce(&Request::OnAccountsDisplayed, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| return; |
| } |
| } else { |
| if (!GetDialogController()->ShowAccountsDialog( |
| CreateRpData(/*client_metadata_received=*/true), |
| idp_data_for_display_, accounts_, filtered_accounts_, rp_mode_, |
| base::BindOnce(&Request::OnAccountSelected, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindRepeating(&Request::LoginToIdP, |
| weak_ptr_factory_.GetWeakPtr(), |
| /*can_append_hints=*/false), |
| base::BindOnce(&Request::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&Request::OnAccountsDisplayed, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| return; |
| } |
| } |
| AfterAccountsDialogShown(did_succeed_for_at_least_one_idp); |
| } |
| |
| void Request::OnGetPassiveDialogVolume( |
| const std::set<GURL>& unique_idps, |
| IdentityRequestDialogController::PassiveDialogVolume dialog_volume) { |
| passive_dialog_volume_ = dialog_volume; |
| FetchEndpointsForIdps(std::move(unique_idps)); |
| } |
| |
| void Request::AfterAccountsDialogShown(bool did_succeed_for_at_least_one_idp) { |
| devtools_instrumentation::DidShowFedCmDialog(render_frame_host()); |
| |
| if (identity_selection_type_ == kExplicit && |
| did_succeed_for_at_least_one_idp) { |
| // We omit recording the accounts dialog shown metric for auto re-authn |
| // because the metric is used to detect IDPs flashing UI. Auto re-authn |
| // verifying UI cannot be flashed since it is destroyed automatically after |
| // 3 seconds and cannot be destroyed earlier for a11y reasons. |
| accounts_dialog_shown_time_ = base::TimeTicks::Now(); |
| } |
| |
| // Note that accounts dialog shown after mismatch dialog is also recorded. |
| // Although not useful for catching malicious IDPs, it should only be a very |
| // small percentage of the samples recorded. |
| fedcm_metrics_->RecordAccountsDialogShown(idp_data_for_display_); |
| fedcm_metrics_->RecordRpUrlHasPath( |
| render_frame_host().GetMainFrame()->GetLastCommittedURL().GetPath() != |
| "/"); |
| } |
| |
| void Request::NotifyAutofillSuggestionAccepted( |
| const GURL& idp, |
| const std::string& account_id, |
| bool show_modal, |
| OnFederatedTokenReceivedCallback callback) { |
| token_received_callback_for_autofill_ = std::move(callback); |
| |
| auto get_info_it = token_request_get_infos_.find(idp); |
| auto idp_info_it = idp_infos_.find(idp); |
| bool is_account_id_valid = |
| std::ranges::any_of(accounts_, [&](const auto& account) { |
| return account->identity_provider->idp_metadata.config_url == idp && |
| account->id == account_id; |
| }); |
| |
| if (get_info_it == token_request_get_infos_.end() || |
| idp_info_it == idp_infos_.end() || !is_account_id_valid) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(token_received_callback_for_autofill_), |
| false)); |
| return; |
| } |
| |
| // Currently the verified email flow opens a modal UI upon notification and |
| // the autofill dropdown UI gets dismissed immediately. i.e. it doesn't need a |
| // valid callback. However, if a user is presented a full federated account, |
| // upon the account selection we'd proceed with fetching tokens directly and |
| // update he autofill dropdown UI to a loading UI. |
| if (!show_modal) { |
| OnAccountSelected(idp, account_id, true); |
| return; |
| } |
| // TODO(crbug.com/380367784): The third argument of OnAccountSelected checks |
| // if this is a sign-in or a sign-up moment. In delegation, however, by |
| // design, the IdP doesn't get to learn about the presentations, so wouldn't |
| // know whether this is a sign-in or sign-up moment (e.g. wouldn't have a |
| // approved_clients array). We should figure out how to reconcile these two |
| // modes. |
| |
| // TODO(crbug.com/412640661): Currently, in order to skip the account chooser |
| // and go straight to the disclosure UI, we have to call ShowLoadingDialog() |
| // before we can call ShowAccountsDialog() to create the internal state |
| // necessary in the dialog controller. We should probably be able to create |
| // the internal state on demand in case it isn't available. |
| if (!GetDialogController()->ShowLoadingDialog( |
| CreateRpData(/*client_metadata_received=*/true), |
| FormatOriginForDisplay(url::Origin::Create(idp)), |
| get_info_it->second.rp_context, blink::mojom::RpMode::kActive, |
| base::BindOnce(&Request::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| return; |
| } |
| did_show_ui_ = true; |
| |
| std::vector<IdentityRequestAccountPtr> selected; |
| |
| for (auto account : accounts_) { |
| if (account->identity_provider->idp_metadata.config_url == idp && |
| account->id == account_id) { |
| selected.push_back(account); |
| } |
| } |
| |
| // TODO(crbug.com/412640661): in order to skip the account chooser, we |
| // set the display priority to `kNew`. We should probably refactor the API to |
| // support this use case, rather than overload an unintended use. |
| for (const auto& account : selected) { |
| account->display_priority = IdentityRequestAccount::DisplayPriority::kNew; |
| } |
| if (!GetDialogController()->ShowAccountsDialog( |
| CreateRpData(/*client_metadata_received=*/true), |
| idp_data_for_display_, selected, filtered_accounts_, |
| blink::mojom::RpMode::kActive, |
| base::BindOnce(&Request::OnAccountSelected, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindRepeating(&Request::LoginToIdP, |
| weak_ptr_factory_.GetWeakPtr(), |
| /*can_append_hints=*/false), |
| base::BindOnce(&Request::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&Request::OnAccountsDisplayed, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| return; |
| } |
| // TODO(crbug.com/435216589): Should we call AfterAccountsDialogShown here? |
| } |
| |
| void Request::OnAccountsDisplayed() { |
| accounts_dialog_display_time_ = base::TimeTicks::Now(); |
| did_show_ui_ = true; |
| } |
| |
| void Request::OnIdpMismatch(std::unique_ptr<IdentityProviderInfo> idp_info) { |
| const GURL& idp_config_url = idp_info->provider->config->config_url; |
| |
| idp_infos_[idp_config_url] = std::move(idp_info); |
| |
| fetch_data_.pending_idps.erase(idp_config_url); |
| if (!fetch_data_.pending_idps.empty()) { |
| return; |
| } |
| |
| // Invoke the accounts dialog flow if there is at least one account or more |
| // than one IDP for which we should show the mismatch dialog. |
| // TODO(crbug.com/331426009): make this code clearer by creating a separate |
| // method for showing multiple mismatch UI. |
| if (fetch_data_.did_succeed_for_at_least_one_idp || idp_infos_.size() > 1u) { |
| MaybeShowAccountsDialog(); |
| // If there are no successful IDPs, this is the multi IDP case where all are |
| // mismatch. |
| if (!fetch_data_.did_succeed_for_at_least_one_idp) { |
| mismatch_dialog_shown_time_ = base::TimeTicks::Now(); |
| has_shown_mismatch_ = true; |
| devtools_instrumentation::DidShowFedCmDialog(render_frame_host()); |
| } |
| return; |
| } |
| |
| if (rp_mode_ == RpMode::kActive) { |
| MaybeShowActiveModeModalDialog( |
| idp_config_url, idp_infos_[idp_config_url]->metadata.idp_login_url); |
| return; |
| } |
| |
| ShowSingleIdpFailureDialog(); |
| } |
| |
| void Request::ShowSingleIdpFailureDialog() { |
| CHECK_EQ(idp_infos_.size(), 1u); |
| IdentityProviderInfo* idp_info = idp_infos_.begin()->second.get(); |
| url::Origin idp_origin = |
| url::Origin::Create(idp_info->provider->config->config_url); |
| // RenderFrameHost should be in the primary page (ex not in the BFCache). |
| DCHECK(render_frame_host().GetPage().IsPrimary()); |
| |
| fetch_data_ = FetchData(); |
| |
| // Set `idp_data_for_display_` so it is always the case that we can rely on it |
| // to know which IDPs have been seen in the UI. |
| CHECK(idp_info->data); |
| idp_data_for_display_ = {idp_info->data}; |
| |
| // If IdP login status mismatch dialog is already visible, calling |
| // ShowFailureDialog() a 2nd time should notify the user that login |
| // failed. |
| dialog_type_ = DialogType::kConfirmIdpLogin; |
| config_url_ = idp_info->provider->config->config_url; |
| login_url_ = idp_info->metadata.idp_login_url; |
| |
| // Store variables used in RecordMismatchDialogShown since they may be cleaned |
| // up in ShowFailureDialog(). |
| bool has_shown_mismatch = has_shown_mismatch_; |
| bool has_hints = !idp_info->provider->login_hint.empty() || |
| !idp_info->provider->domain_hint.empty() || |
| !idp_info->metadata.requested_label.empty(); |
| |
| if (!GetDialogController()->ShowFailureDialog( |
| CreateRpData(/*client_metadata_received=*/true), |
| FormatOriginForDisplay(idp_origin), idp_info->rp_context, rp_mode_, |
| idp_info->metadata, filtered_accounts_, |
| base::BindOnce(&Request::OnDismissFailureDialog, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindRepeating(&Request::LoginToIdP, |
| weak_ptr_factory_.GetWeakPtr(), |
| /*can_append_hints=*/true))) { |
| return; |
| } |
| did_show_ui_ = true; |
| |
| CHECK_EQ(idp_data_for_display_.size(), 1u); |
| fedcm_metrics_->RecordSingleIdpMismatchDialogShown( |
| *idp_data_for_display_[0], has_shown_mismatch, has_hints); |
| mismatch_dialog_shown_time_ = base::TimeTicks::Now(); |
| has_shown_mismatch_ = true; |
| devtools_instrumentation::DidShowFedCmDialog(render_frame_host()); |
| } |
| |
| void Request::OnAccountSelected(const GURL& idp_config_url, |
| const std::string& account_id, |
| bool is_sign_in) { |
| DCHECK(!account_id.empty()); |
| const IdentityProviderInfo& idp_info = *idp_infos_[idp_config_url]; |
| |
| // Check if the user has disabled the FedCM API after the FedCM UI is |
| // displayed. This ensures that requests are not wrongfully sent to IDPs when |
| // settings are changed while an existing FedCM UI is displayed. Ideally, we |
| // should enforce this check before all requests but users typically won't |
| // have time to disable the FedCM API in other types of requests. |
| // Note that for the active flow is not affected by the permission status. |
| if (!CanBypassPermissionStatusCheck(rp_mode_, mediation_requirement_) && |
| GetApiPermissionStatus() != FederatedApiPermissionStatus::GRANTED) { |
| CompleteRequestWithError(FederatedRequestResult::kDisabledInSettings, |
| TokenStatus::kDisabledInSettings, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| if (identity_selection_type_ != kExplicit) { |
| // Embargo auto re-authn to mitigate a deadloop where an auto |
| // re-authenticated user gets auto re-authenticated again soon after logging |
| // out of the active session. |
| auto_reauthn_permission_delegate()->RecordEmbargoForAutoReauthn( |
| GetEmbeddingOrigin()); |
| } else { |
| // Once a user has explicitly selected an account, there is no need to block |
| // auto re-authn with embargo. |
| auto_reauthn_permission_delegate()->RemoveEmbargoForAutoReauthn( |
| GetEmbeddingOrigin()); |
| |
| // Record page scroll Y-axis position upon account selection to analyse |
| // for intrusion. Do not record for auto re-authn because we want to detect |
| // whether users scroll the webpage before choosing to sign-in. |
| RenderFrameHostImpl* host_impl = static_cast<RenderFrameHostImpl*>( |
| render_frame_host().GetOutermostMainFrame()); |
| host_impl->GetAssociatedLocalFrame()->GetScrollPosition( |
| base::BindOnce(&RecordAccountSelectionScrollPosition, |
| render_frame_host().GetPageUkmSourceId(), |
| fedcm_metrics_->GetSessionID())); |
| } |
| |
| fedcm_metrics_->RecordIsSignInUser(is_sign_in); |
| |
| api_permission_delegate()->RemoveEmbargoAndResetCounts(GetEmbeddingOrigin()); |
| |
| account_id_ = account_id; |
| select_account_time_ = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordContinueOnPopupTime( |
| idp_config_url, select_account_time_ - accounts_dialog_display_time_); |
| |
| IdpNetworkRequestManager::ContinueOnCallback continue_on = base::BindOnce( |
| &Request::OnContinueOnResponseReceived, weak_ptr_factory_.GetWeakPtr(), |
| idp_info.provider->Clone()); |
| |
| IdpNetworkRequestManager::RedirectToCallback redirect_to; |
| if (can_accept_redirect_to_) { |
| redirect_to = base::BindOnce(&Request::OnRedirectToResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp_info.provider->Clone()); |
| } |
| |
| std::vector<std::string> disclosure_shown_for; |
| if (!is_sign_in) { |
| disclosure_shown_for = |
| DisclosureFieldsToStringList(idp_info.data->disclosure_fields); |
| } |
| |
| CHECK(idp_info.data); |
| |
| has_sent_token_request_ = true; |
| |
| bool idp_blindness = |
| idp_info.provider->format && |
| *idp_info.provider->format == blink::mojom::Format::kSdJwt; |
| |
| GURL endpoint; |
| std::string query; |
| if (idp_blindness) { |
| // Checked previously. |
| DCHECK(IsDelegationEnabled()); |
| |
| endpoint = idp_info.endpoints.issuance; |
| federated_sdjwt_handler_ = std::make_unique<FederatedSdJwtHandler>( |
| idp_info.provider, render_frame_host(), this); |
| query = federated_sdjwt_handler_->ComputeUrlEncodedTokenPostDataForIssuers( |
| account_id); |
| } else { |
| endpoint = idp_info.endpoints.token; |
| query = ComputeUrlEncodedTokenPostData( |
| render_frame_host(), idp_info.provider->config->client_id, |
| idp_info.provider->nonce, account_id, |
| identity_selection_type_ != kExplicit, rp_mode_, |
| idp_info.provider->fields, disclosure_shown_for, |
| idp_info.provider->params_json.value_or(""), |
| idp_info.provider->config->type); |
| } |
| |
| network_manager_->SendTokenRequest( |
| endpoint, account_id_, query, idp_blindness, |
| base::BindOnce(&Request::OnTokenResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp_info.provider->Clone()), |
| std::move(continue_on), std::move(redirect_to), |
| base::BindOnce(&Request::RecordErrorMetrics, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp_info.provider->Clone())); |
| } |
| |
| void Request::OnDismissFailureDialog( |
| IdentityRequestDialogController::DismissReason dismiss_reason) { |
| // Clicking the close active and swiping away the account chooser are more |
| // intentional than other ways of dismissing the account chooser such as |
| // the virtual keyboard showing on Android. Dismissal through closing the |
| // pop-up window is not embargoed since the user has taken some action to |
| // continue to open the pop-up window. |
| bool should_embargo = |
| dismiss_reason == |
| IdentityRequestDialogController::DismissReason::kCloseButton || |
| dismiss_reason == IdentityRequestDialogController::DismissReason::kSwipe; |
| fedcm_metrics_->RecordCancelReason(dismiss_reason); |
| |
| should_embargo &= rp_mode_ == RpMode::kPassive && !IsUsingAmbient(); |
| if (should_embargo) { |
| api_permission_delegate()->RecordDismissAndEmbargo(GetEmbeddingOrigin()); |
| } |
| |
| CompleteRequestWithError(should_embargo |
| ? FederatedRequestResult::kShouldEmbargo |
| : FederatedRequestResult::kUiDismissedNoEmbargo, |
| should_embargo ? TokenStatus::kShouldEmbargo |
| : TokenStatus::kNotSignedInWithIdp, |
| |
| /*should_delay_callback=*/false); |
| } |
| |
| void Request::OnDismissErrorDialog( |
| const GURL& idp_config_url, |
| FetchStatus status, |
| IdentityRequestDialogController::DismissReason dismiss_reason) { |
| bool has_url = token_error_ && !token_error_->url.is_empty(); |
| ErrorDialogResult result = |
| DismissReasonToErrorDialogResult(dismiss_reason, has_url); |
| fedcm_metrics_->RecordErrorDialogResult(result, idp_config_url); |
| |
| CompleteTokenRequest(idp_config_url, status, /*token=*/std::nullopt, |
| token_error_, /*should_delay_callback=*/false); |
| } |
| |
| void Request::OnDialogDismissed( |
| IdentityRequestDialogController::DismissReason dismiss_reason) { |
| // If the request has already completed, ignore any subsequent dismissals. |
| if (!request_token_callback_) { |
| return; |
| } |
| |
| // Ignore dismissals triggered during the synchronous execution of RedirectTo. |
| if (in_redirect_to_) { |
| return; |
| } |
| |
| if (has_sent_token_request_) { |
| verifying_dialog_result_ = identity_selection_type_ == kExplicit |
| ? VerifyingDialogResult::kCancelExplicit |
| : VerifyingDialogResult::kCancelAutoReauthn; |
| } |
| |
| if (dialog_type_ == DialogType::kContinueOnPopup) { |
| fedcm_metrics_->RecordContinueOnPopupResult( |
| ContinueOnPopupResult::kWindowClosed); |
| // Popups always get dismissed with reason kOther, so we never embargo. |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| TokenStatus::kContinuationPopupClosedByUser, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| // Clicking the close active and swiping away the account chooser are more |
| // intentional than other ways of dismissing the account chooser such as |
| // the virtual keyboard showing on Android. |
| bool should_embargo = |
| dismiss_reason == |
| IdentityRequestDialogController::DismissReason::kCloseButton || |
| dismiss_reason == IdentityRequestDialogController::DismissReason::kSwipe; |
| if (should_embargo) { |
| base::TimeTicks dismiss_dialog_time = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordCancelOnDialogTime( |
| idp_data_for_display_, |
| dismiss_dialog_time - accounts_dialog_display_time_); |
| } |
| fedcm_metrics_->RecordCancelReason(dismiss_reason); |
| |
| should_embargo &= rp_mode_ == RpMode::kPassive && !IsUsingAmbient(); |
| if (should_embargo) { |
| api_permission_delegate()->RecordDismissAndEmbargo(GetEmbeddingOrigin()); |
| } |
| |
| TokenStatus token_status; |
| FederatedRequestResult result; |
| if (should_embargo) { |
| token_status = TokenStatus::kShouldEmbargo; |
| result = FederatedRequestResult::kShouldEmbargo; |
| } else { |
| token_status = TokenStatus::kNotSelectAccount; |
| result = FederatedRequestResult::kUiDismissedNoEmbargo; |
| } |
| |
| // Reject the promise immediately if the UI is dismissed without selecting |
| // an account. Meanwhile, we fuzz the rejection time for other failures to |
| // make it indistinguishable. |
| CompleteRequestWithError(result, token_status, |
| /*should_delay_callback=*/false); |
| } |
| |
| void Request::ShowModalDialog(DialogType dialog_type, |
| const GURL& idp_config_url, |
| const GURL& url_to_show) { |
| // Reset dialog type, since we are typically not showing a FedCM dialog while |
| // the popup window is open. When using the active flow the dialog may |
| // still be up in some cases, but we do not expect that browser automation |
| // needs to interact with the account chooser in this case. |
| if (dialog_type_ != DialogType::kNone && dialog_type_ != dialog_type) { |
| // This call ensures that we send a dialogClosed event if an account |
| // chooser or mismatch dialog is open. |
| devtools_instrumentation::DidCloseFedCmDialog(render_frame_host()); |
| } |
| // TODO(crbug.com/336815315): Should we notify browser automation of this |
| // dialog? |
| if (dialog_type_ != dialog_type) { |
| UMA_HISTOGRAM_ENUMERATION("Blink.FedCm.Popup.DialogType", dialog_type); |
| } |
| dialog_type_ = dialog_type; |
| config_url_ = idp_config_url; |
| |
| auto create_registry_async = [](base::WeakPtr<Request> weak_this, |
| const GURL& idp_config_url, |
| WebContents* web_contents) { |
| if (web_contents && weak_this) { |
| IdentityRegistry::CreateForWebContents(web_contents, weak_this, |
| idp_config_url); |
| } |
| }; |
| |
| WebContents* web_contents = GetDialogController()->ShowModalDialog( |
| url_to_show, rp_mode_, |
| base::BindOnce(&Request::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(create_registry_async, weak_ptr_factory_.GetWeakPtr(), |
| idp_config_url), |
| base::BindOnce(&Request::OnNativeAppResult, |
| weak_ptr_factory_.GetWeakPtr(), dialog_type, |
| idp_config_url)); |
| did_show_ui_ = true; |
| // This may be null on Android, as the method cannot return the WebContents of |
| // the CCT that will be created. |
| // If the showing of the model dialog was deferred, this will be null, and |
| // we'll get the future WebContents via `create_registry_async`. |
| if (web_contents) { |
| IdentityRegistry::CreateForWebContents( |
| web_contents, weak_ptr_factory_.GetWeakPtr(), idp_config_url); |
| } |
| |
| // Samples are at most 10 minutes. This metric is used to determine a |
| // reasonable minimum duration for the mismatch dialog to be shown to prevent |
| // abuse through flashing UI. When users trigger the IDP sign-in flow, the |
| // mismatch dialog is hidden so we record this metric upon user triggering the |
| // flow. |
| if (mismatch_dialog_shown_time_.has_value()) { |
| fedcm_metrics_->RecordMismatchDialogShownDuration( |
| idp_data_for_display_, |
| base::TimeTicks::Now() - mismatch_dialog_shown_time_.value()); |
| mismatch_dialog_shown_time_ = std::nullopt; |
| } |
| } |
| |
| void Request::OnContinueOnResponseReceived( |
| IdentityProviderRequestOptionsPtr idp, |
| FetchStatus status, |
| const GURL& continue_on) { |
| id_assertion_response_time_ = base::TimeTicks::Now(); |
| |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), blink::mojom::WebFeature::kFedCmContinueOnResponse); |
| |
| url::Origin idp_origin = url::Origin::Create(idp->config->config_url); |
| // We only allow loading continue_on urls that are same-origin |
| // with the IdP. |
| // This isn't necessarily final, but seemed like a safer |
| // and sufficient default for now. |
| // This behavior may change in https://crbug.com/1429083 |
| bool is_same_origin = |
| url::Origin::Create(continue_on).IsSameOriginWith(idp_origin); |
| |
| bool can_show_popup = CanShowContinueOnPopup(); |
| if (!is_same_origin || !can_show_popup) { |
| if (!is_same_origin && !can_show_popup) { |
| fedcm_metrics_->RecordContinueOnPopupStatus( |
| ContinueOnPopupStatus::kUrlNotSameOriginAndPopupNotAllowed); |
| } else if (!is_same_origin) { |
| fedcm_metrics_->RecordContinueOnPopupStatus( |
| ContinueOnPopupStatus::kUrlNotSameOrigin); |
| } else if (!can_show_popup) { |
| fedcm_metrics_->RecordContinueOnPopupStatus( |
| ContinueOnPopupStatus::kPopupNotAllowed); |
| } |
| |
| CompleteRequestWithError(FederatedRequestResult::kIdTokenInvalidResponse, |
| TokenStatus::kIdTokenInvalidResponse, |
| |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| fedcm_metrics_->RecordContinueOnPopupStatus( |
| ContinueOnPopupStatus::kPopupOpened); |
| ShowModalDialog(DialogType::kContinueOnPopup, idp->config->config_url, |
| continue_on); |
| } |
| |
| void Request::OnRedirectToResponseReceived( |
| IdentityProviderRequestOptionsPtr idp, |
| FetchStatus status, |
| blink::mojom::RedirectParams::Tag method, |
| const GURL& redirect_to, |
| const std::string& request_body) { |
| RedirectTo(idp->config->config_url, method, redirect_to, request_body); |
| } |
| |
| void Request::RedirectTo(const GURL& idp_config_url, |
| blink::mojom::RedirectParams::Tag method, |
| const GURL& redirect_to, |
| const std::string& request_body) { |
| // Navigate the top-level frame to the URL specified by the IdP. |
| // |
| // This is done here rather than in the callers of the Request because |
| // that allows us to have a consistent experience regardless of how the token |
| // was requested (e.g. via an interception or via the renderer process call). |
| if (!can_accept_redirect_to_ || !redirect_to.SchemeIsHTTPOrHTTPS()) { |
| CompleteRequestWithError(FederatedRequestResult::kIdTokenInvalidResponse, |
| TokenStatus::kIdTokenInvalidResponse, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| // can_accept_redirect_to_ is only true for primary main frames. |
| DCHECK(render_frame_host().IsInPrimaryMainFrame()); |
| |
| WebContentsImpl* web_contents = static_cast<WebContentsImpl*>( |
| WebContents::FromRenderFrameHost(&render_frame_host())); |
| |
| if (!web_contents) { |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| /*token_status=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| NavigationController::LoadURLParams params(redirect_to); |
| params.transition_type = ui::PAGE_TRANSITION_LINK; |
| params.initiator_frame_token = render_frame_host().GetFrameToken(); |
| params.initiator_process_id = render_frame_host().GetProcess()->GetID(); |
| params.initiator_origin = origin(); |
| params.initiator_navigation_state = |
| RenderFrameHostImpl::From(&render_frame_host()) |
| ->CreateInitiatorStateFromCurrentFrame(); |
| params.source_site_instance = render_frame_host().GetSiteInstance(); |
| params.referrer = |
| Referrer(intercepted_url_, network::mojom::ReferrerPolicy::kDefault); |
| // Pretend this was renderer initiated like the load we intercepted. |
| params.is_renderer_initiated = true; |
| params.has_user_gesture = had_transient_user_activation_; |
| // This is used for "Request Desktop Site" on Android. |
| if (web_contents->ShouldOverrideUserAgentForRendererInitiatedNavigation()) { |
| params.override_user_agent = NavigationController::UA_OVERRIDE_TRUE; |
| } else { |
| params.override_user_agent = NavigationController::UA_OVERRIDE_FALSE; |
| } |
| if (method == blink::mojom::RedirectParams::Tag::kPost) { |
| params.transition_type = ui::PAGE_TRANSITION_FORM_SUBMIT; |
| params.load_type = NavigationController::LOAD_TYPE_HTTP_POST; |
| // It is very important that we only allow bytes in the post data, so that |
| // it is not possible to trigger file uploads that bypass security checks. |
| params.post_data = network::ResourceRequestBody::CreateFromCopyOfBytes( |
| base::as_byte_span(request_body)); |
| params.extra_headers = |
| "Content-Type: application/x-www-form-urlencoded\r\n"; |
| } |
| in_redirect_to_ = true; |
| web_contents->GetController().LoadURLWithParams(params); |
| |
| CompleteRequest(FederatedRequestResult::kSuccess, |
| TokenStatus::kSuccessUsingRedirectTo, |
| /*token_error=*/std::nullopt, idp_config_url, |
| /*token_data=*/base::Value(), |
| /*should_delay_callback=*/false); |
| } |
| |
| void Request::ShowErrorDialog(const GURL& idp_config_url, |
| FetchStatus status, |
| std::optional<TokenError> token_error) { |
| CHECK(idp_infos_.find(idp_config_url) != idp_infos_.end()); |
| |
| dialog_type_ = DialogType::kError; |
| config_url_ = idp_config_url; |
| token_request_status_ = status; |
| token_error_ = token_error; |
| |
| // TODO(crbug.com/40282657): Refactor IdentityCredentialTokenError |
| if (!GetDialogController()->ShowErrorDialog( |
| CreateRpData(/*client_metadata_received=*/true), |
| FormatOriginForDisplay(url::Origin::Create(idp_config_url)), |
| idp_infos_[idp_config_url]->rp_context, rp_mode_, |
| idp_infos_[idp_config_url]->metadata, token_error, |
| base::BindOnce(&Request::OnDismissErrorDialog, |
| weak_ptr_factory_.GetWeakPtr(), idp_config_url, |
| status), |
| token_error && !token_error->url.is_empty() |
| ? base::BindOnce( |
| &Request::ShowModalDialog, weak_ptr_factory_.GetWeakPtr(), |
| DialogType::kErrorUrlPopup, config_url_, token_error->url) |
| : base::NullCallback())) { |
| return; |
| } |
| did_show_ui_ = true; |
| devtools_instrumentation::DidShowFedCmDialog(render_frame_host()); |
| } |
| |
| void Request::OnTokenResponseReceived( |
| IdentityProviderRequestOptionsPtr idp, |
| FetchStatus status, |
| IdpNetworkRequestManager::TokenResult&& result) { |
| CHECK(result.token.has_value() || result.error.has_value()); |
| |
| verifying_dialog_result_ = identity_selection_type_ == kExplicit |
| ? VerifyingDialogResult::kSuccessExplicit |
| : VerifyingDialogResult::kSuccessAutoReauthn; |
| |
| bool should_show_error_ui = |
| result.error || status.parse_status != ParseStatus::kSuccess; |
| auto complete_request_callback = |
| should_show_error_ui |
| ? base::BindOnce(&Request::ShowErrorDialog, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp->config->config_url, status, result.error) |
| : base::BindOnce(&Request::CompleteTokenRequest, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp->config->config_url, status, |
| std::move(result.token), result.error, |
| /*should_delay_callback=*/false); |
| |
| // When fetching id tokens we show a "Verify" sheet to users in case fetching |
| // takes a long time due to latency etc. In case that the fetching process is |
| // fast, we still want to show the "Verify" sheet for at least |
| // `kTokenRequestDelay` seconds for better UX. |
| // Note that for active flow, conditional flow, or when an error occurs we can |
| // complete without delay. |
| id_assertion_response_time_ = base::TimeTicks::Now(); |
| base::TimeDelta fetch_time = |
| id_assertion_response_time_ - select_account_time_; |
| if (should_complete_request_immediately_ || rp_mode_ == RpMode::kActive || |
| mediation_requirement_ == MediationRequirement::kConditional || |
| should_show_error_ui || fetch_time >= kTokenRequestDelay) { |
| std::move(complete_request_callback).Run(); |
| return; |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, std::move(complete_request_callback), |
| kTokenRequestDelay - fetch_time); |
| } |
| |
| void Request::MarkUserAsSignedIn(const GURL& idp_config_url, |
| const std::string& account_id) { |
| CHECK(!account_id_.empty()); |
| // Auto re-authentication can only be triggered when there's already a |
| // sharing permission OR the IdP is exempted with 3PC access. Either way |
| // we shouldn't explicitly grant permission here. |
| if (identity_selection_type_ == kAutoPassive || |
| identity_selection_type_ == kAutoActive) { |
| permission_delegate()->RefreshExistingSharingPermission( |
| origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url), |
| account_id); |
| } else { |
| // A sharing permission should only be granted after we explicitly ask for |
| // user permission to sign in. It has a high bar because its extensive |
| // capability such as storage access auto-grant. If a login request is |
| // initiated by the embedder, a deemed sign-in user may have not granted |
| // such permission via a FedCM flow yet so we skip granting the sharing |
| // permission in this case. |
| CHECK_EQ(identity_selection_type_, kExplicit); |
| FederatedEmbedderLoginRequest* embedder_login_request = |
| FederatedEmbedderLoginRequest::Get( |
| WebContents::FromRenderFrameHost(&render_frame_host())); |
| if (!embedder_login_request) { |
| permission_delegate()->GrantSharingPermission( |
| origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url), |
| account_id); |
| } |
| } |
| |
| SetRequiresUserMediation(false, base::DoNothing()); |
| } |
| |
| void Request::CompleteTokenRequest(const GURL& idp_config_url, |
| FetchStatus status, |
| std::optional<base::Value> token, |
| std::optional<TokenError> token_error, |
| bool should_delay_callback) { |
| DCHECK(!start_time_.is_null()); |
| constexpr char kIdAssertionUrl[] = "id assertion endpoint"; |
| if (status.parse_status != ParseStatus::kSuccess) { |
| MaybeAddResponseCodeToConsole(render_frame_host(), kIdAssertionUrl, |
| status.response_code); |
| std::pair<FederatedRequestResult, TokenStatus> resultAndTokenStatus = |
| IdAssertionFetchStatusToRequestResultAndTokenStatus(status); |
| CompleteRequestWithError(resultAndTokenStatus.first, |
| resultAndTokenStatus.second, |
| should_delay_callback); |
| return; |
| } |
| if (token_error_) { |
| MaybeAddResponseCodeToConsole(render_frame_host(), kIdAssertionUrl, |
| status.response_code); |
| if (error_url_type_ && *error_url_type_ == ErrorUrlType::kCrossSite) { |
| CompleteRequestWithError( |
| FederatedRequestResult::kIdTokenCrossSiteIdpErrorResponse, |
| TokenStatus::kIdTokenCrossSiteIdpErrorResponse, |
| should_delay_callback); |
| return; |
| } |
| CompleteRequestWithError(FederatedRequestResult::kIdTokenIdpErrorResponse, |
| TokenStatus::kIdTokenIdpErrorResponse, |
| should_delay_callback); |
| return; |
| } |
| |
| MarkUserAsSignedIn(idp_config_url, account_id_); |
| |
| fedcm_metrics_->RecordTokenResponseAndTurnaroundTime( |
| idp_config_url, id_assertion_response_time_ - select_account_time_, |
| id_assertion_response_time_ - start_time_ - |
| (accounts_dialog_display_time_ - |
| ready_to_display_accounts_dialog_time_)); |
| |
| const IdentityProviderRequestOptionsPtr& provider = |
| idp_infos_[idp_config_url]->provider; |
| DCHECK(provider); |
| |
| if (provider->format && *provider->format == blink::mojom::Format::kSdJwt) { |
| if (token->is_string()) { |
| federated_sdjwt_handler_->ProcessSdJwt(token->GetString()); |
| return; |
| } else { |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| TokenStatus::kIdTokenInvalidResponse, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| } |
| |
| CompleteRequest(FederatedRequestResult::kSuccess, |
| TokenStatus::kSuccessUsingTokenInHttpResponse, |
| /*token_error=*/std::nullopt, idp_config_url, |
| std::move(*token), |
| /*should_delay_callback=*/false); |
| } |
| |
| void Request::CompleteRequestWithError( |
| blink::mojom::FederatedRequestResult result, |
| std::optional<RequestIdTokenStatus> token_status, |
| bool should_delay_callback) { |
| CompleteRequest(result, token_status, token_error_, |
| /*selected_idp_config_url=*/std::nullopt, |
| /*token_data=*/std::nullopt, should_delay_callback); |
| } |
| |
| void Request::CompleteRequest( |
| blink::mojom::FederatedRequestResult result, |
| std::optional<RequestIdTokenStatus> token_status, |
| std::optional<TokenError> token_error, |
| const std::optional<GURL>& selected_idp_config_url, |
| std::optional<base::Value> token_data, |
| bool should_delay_callback) { |
| DCHECK(result == FederatedRequestResult::kSuccess || !token_data.has_value()); |
| if (!request_token_callback_) { |
| return; |
| } |
| // We don't just return if `complete_request_delayed_` is true because in the |
| // case of abort() we still want to invoke the callback. |
| if (!complete_request_delayed_) { |
| // Record metrics and console errors only the first time we complete the |
| // request, even if the callback is delayed. |
| RecordMetricsAndConsoleError(result, token_status, selected_idp_config_url); |
| |
| RenderFrameHostImpl::From(&render_frame_host()) |
| ->delegate() |
| ->OnFedCmFederatedLogin( |
| FederatedRequestResultToFederatedLoginResult(result)); |
| |
| if (token_received_callback_for_autofill_) { |
| std::move(token_received_callback_for_autofill_) |
| .Run(result == FederatedRequestResult::kSuccess); |
| } |
| } |
| |
| if (!should_delay_callback || should_complete_request_immediately_) { |
| bool is_auto_selected = identity_selection_type_ != kExplicit; |
| CompleteRequestInternal(result, token_error, selected_idp_config_url, |
| std::move(token_data), is_auto_selected); |
| } else { |
| DCHECK(!complete_request_delayed_); |
| complete_request_delayed_ = true; |
| base::TimeDelta delay = GetRandomRejectionTime(); |
| TRACE_EVENT_INSTANT("content.fedcm", "Delaying FedCM rejection", |
| perfetto_track_, "delay", delay); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&Request::CompleteRequestInternal, |
| weak_ptr_factory_.GetWeakPtr(), |
| FederatedRequestResult::kError, |
| /*token_error=*/std::nullopt, |
| /*selected_idp_config_url=*/std::nullopt, |
| /*token_data=*/std::nullopt, /*is_auto_selected=*/false), |
| delay); |
| } |
| } |
| |
| void Request::RecordMetricsAndConsoleError( |
| blink::mojom::FederatedRequestResult result, |
| std::optional<RequestIdTokenStatus> token_status, |
| const std::optional<GURL>& selected_idp_config_url) { |
| if (accounts_dialog_shown_time_.has_value()) { |
| fedcm_metrics_->RecordAccountsDialogShownDuration( |
| idp_data_for_display_, |
| base::TimeTicks::Now() - accounts_dialog_shown_time_.value()); |
| } |
| |
| if (mismatch_dialog_shown_time_.has_value()) { |
| fedcm_metrics_->RecordMismatchDialogShownDuration( |
| idp_data_for_display_, |
| base::TimeTicks::Now() - mismatch_dialog_shown_time_.value()); |
| } |
| |
| if (token_status) { |
| int num_idps_mismatch = std::count_if( |
| idp_data_for_display_.begin(), idp_data_for_display_.end(), |
| [](auto& provider) { return provider->has_login_status_mismatch; }); |
| std::optional<UseOtherAccountResult> use_other_account_result; |
| // We know that use other account was used if and only if |
| // account_ids_before_login_ is not empty. |
| if (!account_ids_before_login_.empty()) { |
| use_other_account_result = |
| ComputeUseOtherAccountResult(result, selected_idp_config_url); |
| } |
| |
| if (!verifying_dialog_result_ && has_sent_token_request_) { |
| verifying_dialog_result_ = |
| identity_selection_type_ == kExplicit |
| ? VerifyingDialogResult::kDestroyExplicit |
| : VerifyingDialogResult::kDestroyAutoReauthn; |
| } |
| |
| std::optional<bool> has_signin_account; |
| // Note: accounts_ does not include the ones that got filtered out. In case |
| // that all accounts are filtered out, we'd show the mismatch UI and skip |
| // recording the account status on the mismatch UI. |
| for (const auto& account : accounts_) { |
| has_signin_account = false; |
| if (account->idp_claimed_login_state.value_or( |
| account->browser_trusted_login_state) == LoginState::kSignIn) { |
| has_signin_account = true; |
| break; |
| } |
| } |
| |
| fedcm_metrics_->RecordRequestTokenStatus( |
| *token_status, mediation_requirement_, idp_order_, num_idps_mismatch, |
| selected_idp_config_url, rp_mode_, use_other_account_result, |
| verifying_dialog_result_, |
| api_permission_delegate()->AreThirdPartyCookiesEnabledInSettings() |
| ? ThirdPartyCookiesStatus::kEnabledInSettings |
| : ThirdPartyCookiesStatus::kDisabledInSettings, |
| ComputeRequesterFrameType(render_frame_host(), origin(), |
| GetEmbeddingOrigin()), |
| has_signin_account, did_show_ui_); |
| } |
| |
| if (result == FederatedRequestResult::kSuccess) { |
| CHECK(selected_idp_config_url); |
| CHECK(fedcm_accounts_fetcher_); |
| if (IsMetricsEndpointEnabled()) { |
| fedcm_accounts_fetcher_->SendSuccessfulTokenRequestMetrics( |
| *selected_idp_config_url, |
| ready_to_display_accounts_dialog_time_ - start_time_, |
| select_account_time_ - accounts_dialog_display_time_, |
| id_assertion_response_time_ - select_account_time_, |
| id_assertion_response_time_ - start_time_ - |
| (accounts_dialog_display_time_ - |
| ready_to_display_accounts_dialog_time_), |
| did_show_ui_); |
| } |
| } else { |
| AddDevToolsIssue(result); |
| AddConsoleErrorMessage(result); |
| |
| // fedcm_accounts_fetcher_ could be null if configs were not fetched, e.g. |
| // because of cooldown. |
| if (IsMetricsEndpointEnabled() && fedcm_accounts_fetcher_) { |
| fedcm_accounts_fetcher_->SendAllFailedTokenRequestMetrics(result, |
| did_show_ui_); |
| } |
| } |
| |
| if (ShouldNotifyDevtoolsForDialogType(dialog_type_)) { |
| devtools_instrumentation::DidCloseFedCmDialog(render_frame_host()); |
| } |
| } |
| |
| void Request::CompleteRequestInternal( |
| blink::mojom::FederatedRequestResult result, |
| std::optional<TokenError> token_error, |
| const std::optional<GURL>& selected_idp_config_url, |
| std::optional<base::Value> token_data, |
| bool is_auto_selected) { |
| if (!request_token_callback_) { |
| return; |
| } |
| CleanUp(); |
| auto* page_data = GetPageData(render_frame_host().GetPage()); |
| CHECK_EQ(page_data->PendingWebIdentityRequest(), this); |
| page_data->SetPendingWebIdentityRequest(nullptr); |
| |
| blink::mojom::TokenErrorPtr error; |
| if (token_error) { |
| error = blink::mojom::TokenError::New(); |
| error->code = token_error->code; |
| error->url = token_error->url; |
| } |
| RequestTokenStatus status = |
| FederatedRequestResultToRequestTokenStatus(result); |
| std::move(request_token_callback_) |
| .Run(status, selected_idp_config_url, std::move(token_data), |
| std::move(error), is_auto_selected); |
| request_token_callback_.Reset(); |
| |
| TRACE_EVENT_END("content.fedcm", perfetto_track_); |
| } |
| |
| void Request::CleanUp() { |
| // Cancel any pending callbacks and fetches from this request. No need to |
| // reset other members since this object is not reused for a new request. |
| // `fedcm_metrics` is reset to force metrics recording right away instead of |
| // waiting for Request destruction, which might be delayed. |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| |
| permission_delegate()->RemoveIdpSigninStatusObserver(this); |
| |
| fedcm_accounts_fetcher_.reset(); |
| federated_sdjwt_handler_.reset(); |
| network_manager_.reset(); |
| fedcm_metrics_.reset(); |
| } |
| |
| void Request::AddDevToolsIssue(FederatedRequestResult result) { |
| DCHECK_NE(result, FederatedRequestResult::kSuccess); |
| |
| // It would be possible to add this inspector issue on the renderer, which |
| // will receive the callback. However, it is preferable to do so on the |
| // browser because this is closer to the source, which means adding |
| // additional metadata is easier. In addition, in the future we may only |
| // need to pass a small amount of information to the renderer in the case of |
| // an error, so it would be cleaner to do this by reporting the inspector |
| // issue from the browser. |
| auto details = blink::mojom::InspectorIssueDetails::New(); |
| auto federated_request_details = |
| blink::mojom::FederatedRequestIssueDetails::New(result); |
| details->federated_request_details = std::move(federated_request_details); |
| render_frame_host().ReportInspectorIssue( |
| blink::mojom::InspectorIssueInfo::New( |
| blink::mojom::InspectorIssueCode::kFederatedAuthRequestIssue, |
| std::move(details))); |
| } |
| |
| void Request::AddConsoleErrorMessage(FederatedRequestResult result) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| GetConsoleErrorMessageFromResult(result)); |
| } |
| |
| url::Origin Request::GetEmbeddingOrigin() const { |
| return render_frame_host().GetMainFrame()->GetLastCommittedOrigin(); |
| } |
| |
| IdentityRequestDialogController* Request::GetDialogController() { |
| return request_service_->GetOrCreateDialogController(); |
| } |
| |
| base::WeakPtr<Request> Request::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| void Request::OnClose() { |
| CHECK(request_service_->GetDialogController()); |
| request_service_->GetDialogController()->CloseModalDialog(); |
| |
| // If we have not gotten a signin status change, abort the flow. |
| // The same goes if we did get a status change but the accounts fetch |
| // failed. |
| if ((idps_user_tried_to_signin_to_.empty() || |
| (fetch_data_.pending_idps.empty() && |
| !fetch_data_.did_succeed_for_at_least_one_idp)) && |
| dialog_type_ == DialogType::kLoginToIdpPopup) { |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| TokenStatus::kLoginPopupClosedWithoutSignin, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| // When IdentityProvider.close is called in the continuation popup, we |
| // should abort the flow. |
| if (dialog_type_ == DialogType::kContinueOnPopup) { |
| fedcm_metrics_->RecordContinueOnPopupResult( |
| ContinueOnPopupResult::kClosedByIdentityProviderClose); |
| // Popups always get dismissed with reason kOther, so we never embargo. |
| CompleteRequestWithError( |
| FederatedRequestResult::kError, |
| TokenStatus::kContinuationPopupClosedByIdentityProviderClose, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| } |
| |
| bool Request::OnResolve(GURL idp_config_url, |
| const std::optional<std::string>& account_id, |
| blink::mojom::ResolveTokenParamsPtr params) { |
| // Close the pop-up window post user permission. |
| auto* controller = request_service_->GetDialogController(); |
| if (!controller) { |
| return false; |
| } |
| |
| // IdentityProvider.resolve() is only allowed for continuation API. |
| if (dialog_type_ != DialogType::kContinueOnPopup) { |
| return false; |
| } |
| |
| controller->CloseModalDialog(); |
| |
| MarkUserAsSignedIn(idp_config_url, account_id.value_or(account_id_)); |
| |
| fedcm_metrics_->RecordContinueOnResponseAndTurnaroundTime( |
| id_assertion_response_time_ - select_account_time_, |
| base::TimeTicks::Now() - start_time_ - |
| (accounts_dialog_display_time_ - |
| ready_to_display_accounts_dialog_time_)); |
| fedcm_metrics_->RecordContinueOnPopupResult( |
| ContinueOnPopupResult::kTokenReceived); |
| |
| const IdentityProviderRequestOptionsPtr& provider = |
| idp_infos_[idp_config_url]->provider; |
| DCHECK(provider); |
| |
| if (params->is_redirect_to() && can_accept_redirect_to_) { |
| const auto& redirect_to = params->get_redirect_to(); |
| if (redirect_to->is_get()) { |
| RedirectTo(idp_config_url, blink::mojom::RedirectParams::Tag::kGet, |
| redirect_to->get_get()->url, ""); |
| } else { |
| DCHECK(redirect_to->is_post()); |
| RedirectTo(idp_config_url, blink::mojom::RedirectParams::Tag::kPost, |
| redirect_to->get_post()->url, |
| redirect_to->get_post()->request_body); |
| } |
| return true; |
| } |
| |
| if (!params->is_token()) { |
| // This could happen if we get a redirect request but interception is |
| // disabled, for example when we have no active embedder initiated login. |
| return false; |
| } |
| |
| const base::Value& token = params->get_token(); |
| if (provider->format && *provider->format == blink::mojom::Format::kSdJwt) { |
| if (token.is_string()) { |
| federated_sdjwt_handler_->ProcessSdJwt(token.GetString()); |
| return true; |
| } else { |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| TokenStatus::kIdTokenInvalidResponse, |
| /*should_delay_callback=*/false); |
| return false; |
| } |
| } |
| |
| CompleteRequest(FederatedRequestResult::kSuccess, |
| TokenStatus::kSuccessUsingIdentityProviderResolve, |
| /*token_error=*/std::nullopt, idp_config_url, token.Clone(), |
| /*should_delay_callback=*/false); |
| return true; |
| } |
| |
| void Request::OnOriginMismatch(Method method, |
| const url::Origin& expected, |
| const url::Origin& actual) { |
| const char* method_string = method == Method::kClose ? "close" : "resolve"; |
| std::string error_messsage = base::StringPrintf( |
| "IdentityProvider.%s called from incorrect origin '%s'; expected '%s'", |
| method_string, actual.Serialize().c_str(), expected.Serialize().c_str()); |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, error_messsage); |
| } |
| |
| void Request::OnIntentResolved(const std::string& token) { |
| blink::mojom::ResolveTokenParamsPtr params = |
| blink::mojom::ResolveTokenParams::NewToken(base::Value(token)); |
| OnResolve(config_url_, std::nullopt, std::move(params)); |
| } |
| |
| void Request::OnNativeAppResult( |
| DialogType dialog_type, |
| const GURL& idp_config_url, |
| IdentityRequestDialogController::NativeAppResult result) { |
| if (!request_token_callback_) { |
| return; |
| } |
| if (result.type == |
| IdentityRequestDialogController::NativeAppResult::Type::kToken) { |
| if (dialog_type != DialogType::kContinueOnPopup) { |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| TokenStatus::kLoginPopupClosedWithoutSignin, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| OnIntentResolved(result.token); |
| } else if (result.type == IdentityRequestDialogController::NativeAppResult:: |
| Type::kLoginFinished) { |
| if (dialog_type != DialogType::kLoginToIdpPopup) { |
| CompleteRequestWithError(FederatedRequestResult::kError, |
| TokenStatus::kContinuationPopupClosedByUser, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| OnNativeAppLoginFinished(idp_config_url); |
| } |
| } |
| |
| void Request::OnNativeAppLoginFinished(const GURL& idp_config_url) { |
| GetDialogController()->CloseModalDialog(); |
| permission_delegate()->RemoveIdpSigninStatusObserver(this); |
| permission_delegate()->SetIdpSigninStatus( |
| url::Origin::Create(idp_config_url), /*is_signed_in=*/true, std::nullopt); |
| idps_user_tried_to_signin_to_.insert(idp_config_url); |
| FetchEndpointsForIdps({idp_config_url}); |
| } |
| |
| FederatedApiPermissionStatus Request::GetApiPermissionStatus() { |
| DCHECK(api_permission_delegate()); |
| return api_permission_delegate()->GetApiPermissionStatus( |
| GetEmbeddingOrigin()); |
| } |
| |
| bool Request::ShouldNotifyDevtoolsForDialogType(DialogType type) { |
| return type != DialogType::kNone && type != DialogType::kLoginToIdpPopup && |
| type != DialogType::kContinueOnPopup && |
| type != DialogType::kErrorUrlPopup; |
| } |
| |
| void Request::AcceptAccountsDialogForDevtools( |
| const GURL& config_url, |
| const IdentityRequestAccount& account) { |
| bool is_sign_in = account.idp_claimed_login_state.value_or( |
| account.browser_trusted_login_state) == |
| IdentityRequestAccount::LoginState::kSignIn; |
| OnAccountSelected(config_url, account.id, is_sign_in); |
| } |
| |
| void Request::DismissAccountsDialogForDevtools(bool should_embargo) { |
| // We somewhat arbitrarily pick a reason that does/does not trigger |
| // cooldown. |
| IdentityRequestDialogController::DismissReason reason = |
| should_embargo |
| ? IdentityRequestDialogController::DismissReason::kCloseButton |
| : IdentityRequestDialogController::DismissReason::kOther; |
| OnDialogDismissed(reason); |
| } |
| |
| void Request::AcceptConfirmIdpLoginDialogForDevtools() { |
| DCHECK(login_url_.is_valid()); |
| LoginToIdP(/*can_append_hints=*/true, config_url_, login_url_); |
| } |
| |
| void Request::DismissConfirmIdpLoginDialogForDevtools() { |
| // These values match what HandleAccountsFetchFailure passes. |
| OnDismissFailureDialog( |
| IdentityRequestDialogController::DismissReason::kOther); |
| } |
| |
| bool Request::UseAnotherAccountForDevtools( |
| const IdentityProviderData& provider) { |
| if (!provider.idp_metadata.supports_add_account) { |
| return false; |
| } |
| LoginToIdP(/*can_append_hints=*/true, provider.idp_metadata.config_url, |
| provider.idp_metadata.idp_login_url); |
| return true; |
| } |
| |
| bool Request::HasMoreDetailsButtonForDevtools() { |
| return token_error_ && token_error_->url.is_valid(); |
| } |
| |
| void Request::ClickErrorDialogGotItForDevtools() { |
| DCHECK(token_error_); |
| OnDismissErrorDialog( |
| config_url_, token_request_status_, |
| IdentityRequestDialogController::DismissReason::kGotItButton); |
| } |
| |
| void Request::ClickErrorDialogMoreDetailsForDevtools() { |
| DCHECK(token_error_ && token_error_->url.is_valid()); |
| ShowModalDialog(DialogType::kErrorUrlPopup, config_url_, token_error_->url); |
| OnDismissErrorDialog( |
| config_url_, token_request_status_, |
| IdentityRequestDialogController::DismissReason::kMoreDetailsButton); |
| } |
| |
| void Request::DismissErrorDialogForDevtools() { |
| OnDismissErrorDialog(config_url_, token_request_status_, |
| IdentityRequestDialogController::DismissReason::kOther); |
| } |
| |
| bool Request::GetAccountForAutoReauthn(IdentityProviderDataPtr* out_idp_data, |
| IdentityRequestAccountPtr* out_account) { |
| for (const auto& idp_info : idp_infos_) { |
| if (idp_info.second->data->has_login_status_mismatch) { |
| // If we need to show IDP login status mismatch UI, we cannot |
| // auto-reauthenticate a user even if there really is a single returning |
| // account. |
| return false; |
| } |
| } |
| for (const auto& account : accounts_) { |
| if (account->idp_claimed_login_state.value_or( |
| account->browser_trusted_login_state) == LoginState::kSignUp || |
| account->is_filtered_out) { |
| continue; |
| } |
| // account.idp_claimed_login_state will be set to kSignIn if the client is |
| // on the `approved_clients` list provided by IDP. However, in this case we |
| // have to trust the browser observed sign-in unless the IDP can be |
| // exempted. For example, they have third party cookies access on the RP |
| // site. |
| if (!HasSharingPermissionOrIdpHasThirdPartyCookiesAccess( |
| render_frame_host(), |
| /*provider_url=*/ |
| account->identity_provider->idp_metadata.config_url, |
| GetEmbeddingOrigin(), origin(), account->id, permission_delegate(), |
| api_permission_delegate())) { |
| continue; |
| } |
| |
| if (*out_account) { |
| return false; |
| } |
| *out_idp_data = account->identity_provider; |
| *out_account = account; |
| } |
| |
| if (*out_account) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool Request::ShouldFailBeforeFetchingAccounts(const GURL& config_url) { |
| if (mediation_requirement_ != MediationRequirement::kSilent) { |
| return false; |
| } |
| |
| bool is_auto_reauthn_blocked_by_embedder = |
| auto_reauthn_permission_delegate()->IsAutoReauthnDisabledByEmbedder( |
| WebContents::FromRenderFrameHost(&render_frame_host())); |
| if (is_auto_reauthn_blocked_by_embedder) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: ongoing actor task in the tab."); |
| } |
| |
| bool is_auto_reauthn_setting_enabled = |
| auto_reauthn_permission_delegate()->IsAutoReauthnSettingEnabled(); |
| if (!is_auto_reauthn_setting_enabled) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: the user has disabled auto re-authn."); |
| } |
| |
| bool is_auto_reauthn_embargoed = |
| auto_reauthn_permission_delegate()->IsAutoReauthnEmbargoed( |
| GetEmbeddingOrigin()); |
| if (is_auto_reauthn_embargoed) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: auto re-authn is in quiet period because it " |
| "was recently used on this site."); |
| } |
| |
| bool has_sharing_permission_for_any_account = |
| HasSharingPermissionOrIdpHasThirdPartyCookiesAccess( |
| render_frame_host(), config_url, GetEmbeddingOrigin(), origin(), |
| /*account_id=*/std::nullopt, permission_delegate(), |
| api_permission_delegate()); |
| |
| if (!has_sharing_permission_for_any_account) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: the user has not used FedCM on this site with " |
| "this identity provider."); |
| } |
| |
| bool requires_user_mediation = RequiresUserMediation(); |
| if (requires_user_mediation) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: preventSilentAccess() has been invoked on the " |
| "site."); |
| } |
| |
| if (requires_user_mediation || !is_auto_reauthn_setting_enabled || |
| is_auto_reauthn_embargoed || !has_sharing_permission_for_any_account || |
| is_auto_reauthn_blocked_by_embedder) { |
| // Record the relevant auto reauthn metrics before aborting the FedCM flow. |
| fedcm_metrics_->RecordAutoReauthnMetrics( |
| /*has_single_returning_account=*/std::nullopt, |
| /*auto_signin_account=*/nullptr, |
| /*auto_reauthn_success=*/false, !is_auto_reauthn_setting_enabled, |
| is_auto_reauthn_embargoed, is_auto_reauthn_blocked_by_embedder, |
| requires_user_mediation); |
| return true; |
| } |
| return false; |
| } |
| |
| bool Request::RequiresUserMediation() { |
| return auto_reauthn_permission_delegate()->RequiresUserMediation(origin()); |
| } |
| |
| void Request::SetRequiresUserMediation(bool requires_user_mediation, |
| base::OnceClosure callback) { |
| request_service_->SetRequiresUserMediation(requires_user_mediation, |
| std::move(callback)); |
| } |
| |
| void Request::LoginToIdP(bool can_append_hints, |
| const GURL& idp_config_url, |
| GURL login_url) { |
| const auto& it = idp_login_infos_.find(login_url); |
| CHECK(it != idp_login_infos_.end()); |
| login_url_ = login_url; |
| if (can_append_hints) { |
| // Before invoking UI, append the query parameters to the `idp_login_url` if |
| // needed. |
| MaybeAppendQueryParameters(it->second, &login_url); |
| } |
| |
| if (dialog_type_ == DialogType::kLoginToIdpPopup) { |
| ShowModalDialog(DialogType::kLoginToIdpPopup, idp_config_url, login_url); |
| return; |
| } |
| |
| permission_delegate()->AddIdpSigninStatusObserver(this); |
| |
| account_ids_before_login_.clear(); |
| for (const auto& account : accounts_) { |
| if (account->identity_provider->idp_metadata.idp_login_url == login_url) { |
| account_ids_before_login_.insert(account->id); |
| } |
| } |
| |
| ShowModalDialog(DialogType::kLoginToIdpPopup, idp_config_url, login_url); |
| } |
| |
| void Request::MaybeShowActiveModeModalDialog(const GURL& idp_config_url, |
| const GURL& idp_login_url) { |
| if (idp_infos_.size() > 1) { |
| // TODO(crbug.com/40283218): handle the active flow and the |
| // Multi IdP API (what should happen if you are logged in to some |
| // IdPs but not to others). |
| // TODO(crbug.com/326987150): This is temporary so we should degrade |
| // gracefully. |
| return; |
| } |
| |
| // We fail sooner before, but just to double check, we assert that |
| // we are inside a user gesture here again. |
| CHECK(had_transient_user_activation_); |
| // TODO(crbug.com/40283219): we should probably make idp_login_url |
| // optional instead of empty. |
| LoginToIdP(/*can_append_hints=*/false, idp_config_url, idp_login_url); |
| return; |
| } |
| |
| void Request::RecordErrorMetrics( |
| IdentityProviderRequestOptionsPtr idp, |
| TokenResponseType token_response_type, |
| std::optional<ErrorDialogType> error_dialog_type, |
| std::optional<ErrorUrlType> error_url_type) { |
| fedcm_metrics_->RecordErrorMetricsBeforeShowingErrorDialog( |
| token_response_type, error_dialog_type, error_url_type, |
| idp->config->config_url); |
| |
| if (error_url_type) { |
| // This is used to determine if we need to use the cross-site specific |
| // devtools issue when failing the request. |
| error_url_type_ = error_url_type; |
| } |
| } |
| |
| std::unique_ptr<Metrics> Request::CreateFedCmMetrics() { |
| // Ensure the lifecycle state as GetPageUkmSourceId doesn't support the |
| // prerendering page. As FederatedAithRequest runs behind the |
| // BrowserInterfaceBinders, the service doesn't receive any request while |
| // prerendering, and the CHECK should always meet the condition. |
| CHECK(!render_frame_host().IsInLifecycleState( |
| RenderFrameHost::LifecycleState::kPrerendering)); |
| |
| return std::make_unique<Metrics>(render_frame_host().GetPageUkmSourceId()); |
| } |
| |
| bool Request::IsNewlyLoggedIn(const IdentityRequestAccount& account) { |
| if (login_url_.is_empty() || |
| login_url_ != account.identity_provider->idp_metadata.idp_login_url) { |
| return false; |
| } |
| // Exclude filtered out accounts so they are not shown at the top. |
| return !account.is_filtered_out && |
| !account_ids_before_login_.contains(account.id); |
| } |
| |
| bool Request::IsUsingAmbient() const { |
| if (rp_mode_ != RpMode::kPassive || idp_order_.size() != 1u) { |
| return false; |
| } |
| |
| bool is_ambient_enabled = |
| IsFedCmAmbientUIEnabled() || |
| passive_dialog_volume_ == |
| IdentityRequestDialogController::PassiveDialogVolume::kAmbient; |
| if (!is_ambient_enabled) { |
| return false; |
| } |
| |
| size_t accounts_count = accounts_.size(); |
| |
| // Currently, the Ambient UI only supports single accounts, for returning |
| // users and new users. As we develop it, we'll allow more cases to be |
| // handled by the Ambient UI, such as multiple accounts, multiple IdPs and |
| // mismatch cases. |
| |
| return accounts_count == 1u; |
| } |
| |
| RelyingPartyData Request::CreateRpData(bool client_metadata_received) const { |
| // We want to show the iframe origin if any IDP requests it. |
| bool show_iframe_origin = false; |
| for (const auto& entry : idp_infos_) { |
| if (entry.second->client_is_third_party_to_top_frame_origin) { |
| show_iframe_origin = true; |
| break; |
| } |
| } |
| std::u16string iframe_origin; |
| if (show_iframe_origin) { |
| iframe_origin = base::UTF8ToUTF16(FormatOriginForDisplay(origin())); |
| } |
| bool display_strings_may_change = |
| !client_metadata_received && |
| !net::SchemefulSite::IsSameSite(origin(), GetEmbeddingOrigin()); |
| return RelyingPartyData( |
| base::UTF8ToUTF16(GetTopFrameOriginForDisplay(GetEmbeddingOrigin())), |
| iframe_origin, display_strings_may_change); |
| } |
| |
| FederatedIdentityApiPermissionContextDelegate* |
| Request::api_permission_delegate() const { |
| return request_service_->api_permission_delegate_; |
| } |
| |
| FederatedIdentityAutoReauthnPermissionContextDelegate* |
| Request::auto_reauthn_permission_delegate() const { |
| return request_service_->auto_reauthn_permission_delegate_; |
| } |
| |
| FederatedIdentityPermissionContextDelegate* Request::permission_delegate() |
| const { |
| return request_service_->permission_delegate_; |
| } |
| |
| } // namespace content::webid |