blob: 7c45f96a6f111a455c7a13ed9f96963f02d0015e [file] [log] [blame]
// 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/federated_auth_request_impl.h"
#include <algorithm>
#include <random>
#include <vector>
#include "base/barrier_closure.h"
#include "base/base64url.h"
#include "base/command_line.h"
#include "base/containers/contains.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/federated_auth_disconnect_request.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/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/render_frame_host.h"
#include "content/public/browser/web_contents.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 "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_auth_request.mojom.h"
#include "url/gurl.h"
using base::Value;
using blink::mojom::DisconnectStatus;
using blink::mojom::FederatedAuthRequestResult;
using blink::mojom::IdentityProviderConfig;
using blink::mojom::IdentityProviderRequestOptionsPtr;
using blink::mojom::RegisterIdpStatus;
using blink::mojom::RequestTokenStatus;
using blink::mojom::RequestUserInfoStatus;
using FederatedApiPermissionStatus =
content::FederatedIdentityApiPermissionContextDelegate::PermissionStatus;
using DisconnectStatusForMetrics = content::FedCmDisconnectStatus;
using TokenStatus = content::FedCmRequestIdTokenStatus;
using SignInStateMatchStatus = content::FedCmSignInStateMatchStatus;
using TokenResponseType =
content::IdpNetworkRequestManager::FedCmTokenResponseType;
using ErrorDialogType = content::IdpNetworkRequestManager::FedCmErrorDialogType;
using ErrorUrlType = content::IdpNetworkRequestManager::FedCmErrorUrlType;
using LoginState = content::IdentityRequestAccount::LoginState;
using SignInMode = content::IdentityRequestAccount::SignInMode;
using ErrorDialogResult = content::FedCmErrorDialogResult;
using CompleteRequestWithErrorCallback =
base::OnceCallback<void(blink::mojom::FederatedAuthRequestResult,
std::optional<content::FedCmRequestIdTokenStatus>,
bool)>;
namespace content {
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 webid::FormatUrlForDisplay(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() == content::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 ||
(webid::IsAutofillEnabled() &&
mediation_requirement == MediationRequirement::kConditional);
}
} // namespace
FederatedAuthRequestImpl::FetchData::FetchData() = default;
FederatedAuthRequestImpl::FetchData::~FetchData() = default;
FederatedAuthRequestImpl::FederatedAuthRequestImpl(
RenderFrameHost& host,
FederatedIdentityApiPermissionContextDelegate* api_permission_context,
FederatedIdentityAutoReauthnPermissionContextDelegate*
auto_reauthn_permission_context,
FederatedIdentityPermissionContextDelegate* permission_context,
IdentityRegistry* identity_registry,
mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver)
: DocumentService(host, std::move(receiver)),
api_permission_delegate_(api_permission_context),
auto_reauthn_permission_delegate_(auto_reauthn_permission_context),
permission_delegate_(permission_context),
identity_registry_(identity_registry),
perfetto_track_(webid::CreatePerfettoTrackForFedCM(this)) {}
FederatedAuthRequestImpl::~FederatedAuthRequestImpl() {
// Ensures key data members are destructed in proper order and resolves any
// pending promise.
if (auth_request_token_callback_) {
CompleteRequestWithError(FederatedAuthRequestResult::kError,
TokenStatus::kUnhandledRequest,
/*should_delay_callback=*/false);
}
// Calls |UserInfoRequest|'s destructor to complete the user
// info request. This is needed because otherwise some resources like
// `fedcm_metrics_` may no longer be usable when the destructor get invoked
// naturally.
user_info_requests_.clear();
// Calls |FederatedAuthDisconnectRequest|'s destructor to complete the
// revocation request. This is needed because otherwise some resources like
// `fedcm_metrics_` may no longer be usable when the destructor get invoked
// naturally.
disconnect_request_.reset();
// Since FederatedAuthRequestImpl is a subclass of
// DocumentService<blink::mojom::FederatedAuthRequest>, it only lives as long
// as the current document.
if (num_requests_ > 0) {
FedCmMetrics::RecordNumRequestsPerDocument(
render_frame_host().GetPageUkmSourceId(), num_requests_);
}
}
// static
void FederatedAuthRequestImpl::Create(
RenderFrameHost* host,
mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver) {
CHECK(host);
BrowserContext* browser_context = host->GetBrowserContext();
raw_ptr<FederatedIdentityApiPermissionContextDelegate>
api_permission_context =
browser_context->GetFederatedIdentityApiPermissionContext();
raw_ptr<FederatedIdentityAutoReauthnPermissionContextDelegate>
auto_reauthn_permission_context =
browser_context->GetFederatedIdentityAutoReauthnPermissionContext();
raw_ptr<FederatedIdentityPermissionContextDelegate> permission_context =
browser_context->GetFederatedIdentityPermissionContext();
raw_ptr<IdentityRegistry> identity_registry =
IdentityRegistry::FromWebContents(WebContents::FromRenderFrameHost(host));
if (!api_permission_context || !auto_reauthn_permission_context ||
!permission_context) {
return;
}
// FederatedAuthRequestImpl owns itself. It will self-destruct when a mojo
// interface error occurs, the RenderFrameHost is deleted, or the
// RenderFrameHost navigates to a new document.
new FederatedAuthRequestImpl(
*host, api_permission_context, auto_reauthn_permission_context,
permission_context, identity_registry, std::move(receiver));
}
FederatedAuthRequestImpl& FederatedAuthRequestImpl::CreateForTesting(
RenderFrameHost& host,
FederatedIdentityApiPermissionContextDelegate* api_permission_context,
FederatedIdentityAutoReauthnPermissionContextDelegate*
auto_reauthn_permission_context,
FederatedIdentityPermissionContextDelegate* permission_context,
IdentityRegistry* identity_registry,
mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver) {
return *new FederatedAuthRequestImpl(
host, api_permission_context, auto_reauthn_permission_context,
permission_context, identity_registry, std::move(receiver));
}
std::vector<IdentityProviderRequestOptionsPtr>
FederatedAuthRequestImpl::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::reverse(registered_config_urls.begin(), registered_config_urls.end());
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;
}
void FederatedAuthRequestImpl::RequestToken(
std::vector<IdentityProviderGetParametersPtr> idp_get_params_ptrs,
MediationRequirement requirement,
RequestTokenCallback callback) {
if (ShouldTerminateRequest(idp_get_params_ptrs, requirement)) {
return;
}
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 (webid::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, "",
/*error=*/nullptr,
/*is_auto_selected=*/false),
delay);
return;
}
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(
webid::
LifecycleStateImplLifecycleStateImplToFedCmLifecycleStateFailureReason(
host_impl->lifecycle_state()));
std::move(callback).Run(RequestTokenStatus::kError, std::nullopt, "",
/*error=*/nullptr,
/*is_auto_selected=*/false);
return;
}
had_transient_user_activation_ =
render_frame_host().HasTransientUserActivation();
// Store the previous `idp_order_` value from this class. Note that this is {}
// unless there is a pending request from the same RFH. In particular, this is
// still {} if there is a pending request but from a different RFH.
std::vector<GURL> old_idp_order = std::move(idp_order_);
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);
}
}
if (HasPendingRequest() &&
HandlePendingRequestAndCancelNewRequest(
old_idp_order, idp_get_params_ptrs, requirement)) {
std::move(callback).Run(RequestTokenStatus::kErrorTooManyRequests,
std::nullopt, "", /*error=*/nullptr,
/*is_auto_selected=*/false);
return;
}
// 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;
auth_request_token_callback_ = std::move(callback);
webid::GetPageData(render_frame_host().GetPage())
->SetPendingWebIdentityRequest(this);
network_manager_ = CreateNetworkManager();
request_dialog_controller_ = CreateDialogController();
start_time_ = base::TimeTicks::Now();
if (!fedcm_metrics_) {
fedcm_metrics_ = CreateFedCmMetrics();
}
// TODO(crbug.com/40218857): handle active mode with multiple IdP.
if (idp_get_params_ptrs[0]->mode == blink::mojom::RpMode::kActive) {
rp_mode_ = RpMode::kActive;
std::optional<base::TimeTicks> user_info_accounts_response_time =
webid::GetPageData(render_frame_host().GetPage())
->ConsumeUserInfoAccountsResponseTime(
idp_get_params_ptrs[0]->providers[0]->config->config_url);
if (user_info_accounts_response_time) {
fedcm_metrics_->RecordTimeBetweenUserInfoAndActiveModeAPI(
start_time_ - user_info_accounts_response_time.value());
}
if (!had_transient_user_activation_) {
CompleteRequestWithError(
FederatedAuthRequestResult::kMissingTransientUserActivation,
TokenStatus::kMissingTransientUserActivation,
/*should_delay_callback=*/false);
return;
}
} else {
rp_mode_ = RpMode::kPassive;
}
if (origin().opaque()) {
CompleteRequestWithError(
FederatedAuthRequestResult::kRelyingPartyOriginIsOpaque,
TokenStatus::kRpOriginIsOpaque,
/*should_delay_callback=*/false);
return;
}
FederatedApiPermissionStatus permission_status = GetApiPermissionStatus();
if (!CanBypassPermissionStatusCheck(rp_mode_, mediation_requirement_)) {
if (permission_status != FederatedApiPermissionStatus::GRANTED) {
std::pair<FederatedAuthRequestResult, TokenStatus> resultAndTokenStatus =
webid::PermissionStatusToRequestResultAndTokenStatus(
permission_status);
CompleteRequestWithError(resultAndTokenStatus.first,
resultAndTokenStatus.second,
/*should_delay_callback=*/true);
return;
}
}
++num_requests_;
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(FederatedAuthRequestResult::kError,
/*token_status=*/std::nullopt,
/*should_delay_callback=*/false);
return;
}
url::Origin idp_origin = url::Origin::Create(idp_ptr->config->config_url);
if (!network::IsOriginPotentiallyTrustworthy(idp_origin)) {
CompleteRequestWithError(
FederatedAuthRequestResult::kIdpNotPotentiallyTrustworthy,
TokenStatus::kIdpNotPotentiallyTrustworthy,
/*should_delay_callback=*/false);
return;
}
}
}
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 =
webid::ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp(
render_frame_host(), 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 ||
webid::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 =
webid::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) {
FedCmRpParameters parameters;
if (any_idp_has_custom_scopes && any_idp_has_parameters) {
parameters = FedCmRpParameters::kHasParametersAndNonDefaultScope;
} else if (any_idp_has_parameters) {
parameters = FedCmRpParameters::kHasParameters;
} else {
DCHECK(any_idp_has_custom_scopes);
parameters = FedCmRpParameters::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
? FederatedAuthRequestResult::kSilentMediationFailure
: FederatedAuthRequestResult::kNotSignedInWithIdp;
auto token_status = mediation_requirement_ == MediationRequirement::kSilent
? TokenStatus::kSilentMediationFailure
: TokenStatus::kNotSignedInWithIdp;
CompleteRequestWithError(result, token_status,
/*should_delay_callback=*/true);
return;
}
// 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);
// TODO(crbug.com/40218857): Handle active mode with multiple IdP.
const GURL& idp_config_url = idp_order_[0];
auto get_info_it = token_request_get_infos_.find(idp_config_url);
CHECK(get_info_it != token_request_get_infos_.end());
if (!request_dialog_controller_->ShowLoadingDialog(
CreateRpData(),
FormatOriginForDisplay(url::Origin::Create(idp_config_url)),
get_info_it->second.rp_context, rp_mode_,
base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed,
weak_ptr_factory_.GetWeakPtr()))) {
return;
}
}
fedcm_metrics_->RecordIdentityProvidersCount(idp_order_.size());
CHECK(!unique_idps.empty());
FetchEndpointsForIdps(std::move(unique_idps));
}
void FederatedAuthRequestImpl::RequestUserInfo(
blink::mojom::IdentityProviderConfigPtr provider,
RequestUserInfoCallback callback) {
if (!render_frame_host().GetPage().IsPrimary()) {
ReportBadMessageAndDeleteThis(
"FedCM should not be allowed in nested frame trees.");
return;
}
// FedCmMetrics class is currently not used for UserInfo API. If we log UKM
// metrics later on, we should call CreateFedCmMetrics() here.
auto network_manager = IdpNetworkRequestManager::Create(
static_cast<RenderFrameHostImpl*>(&render_frame_host()));
auto user_info_request = webid::UserInfoRequest::Create(
std::move(network_manager), permission_delegate_,
api_permission_delegate_, &render_frame_host(), std::move(provider));
webid::UserInfoRequest* user_info_request_ptr = user_info_request.get();
user_info_requests_.insert(std::move(user_info_request));
user_info_request_ptr->SetCallbackAndStart(
base::BindOnce(&FederatedAuthRequestImpl::CompleteUserInfoRequest,
weak_ptr_factory_.GetWeakPtr(), user_info_request_ptr,
std::move(callback)));
}
void FederatedAuthRequestImpl::CancelTokenRequest() {
if (!auth_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 for request_dialog_controller_,
// triggered by CompleteRequest.
CompleteRequestWithError(FederatedAuthRequestResult::kCanceled,
TokenStatus::kAborted,
/*should_delay_callback=*/false);
}
void FederatedAuthRequestImpl::ResolveTokenRequest(
const std::optional<std::string>& account_id,
const std::string& token,
ResolveTokenRequestCallback callback) {
if (!identity_registry_ && !SetupIdentityRegistryFromPopup()) {
std::move(callback).Run(false);
return;
}
bool accepted =
identity_registry_->NotifyResolve(origin(), account_id, token);
std::move(callback).Run(accepted);
}
void FederatedAuthRequestImpl::SetIdpSigninStatus(
const url::Origin& idp_origin,
blink::mojom::IdpSigninStatus status,
const std::optional<blink::common::webid::LoginStatusOptions>& options,
SetIdpSigninStatusCallback callback) {
auto scoped_closure = base::ScopedClosureRunner(std::move(callback));
if (render_frame_host().IsNestedWithinFencedFrame()) {
RecordSetLoginStatusIgnoredReason(
FedCmSetLoginStatusIgnoredReason::kInFencedFrame);
return;
}
// We only allow setting the IDP signin status when the subresource is loaded
// from the same site as the document, and the document is same site with
// all ancestors. This is to protect from an RP embedding a tracker resource
// that would set this signin status for the tracker, enabling the FedCM
// request.
if (!webid::IsSameSiteWithAncestors(idp_origin, &render_frame_host())) {
RecordSetLoginStatusIgnoredReason(
FedCmSetLoginStatusIgnoredReason::kCrossOrigin);
return;
}
if (!webid::IsLightweightModeEnabled()) {
permission_delegate_->SetIdpSigninStatus(
idp_origin, status == blink::mojom::IdpSigninStatus::kSignedIn,
std::nullopt);
} else {
if (options.has_value()) {
std::vector<GURL> picture_urls;
for (const blink::common::webid::LoginStatusAccount& account :
options->accounts) {
if (account.picture.has_value() && account.picture->is_valid()) {
picture_urls.emplace_back(account.picture.value());
}
}
if (!network_manager_) {
network_manager_ = CreateNetworkManager();
}
network_manager_->CacheAccountPictures(idp_origin, picture_urls);
}
permission_delegate_->SetIdpSigninStatus(
idp_origin, status == blink::mojom::IdpSigninStatus::kSignedIn,
options);
}
}
void FederatedAuthRequestImpl::RegisterIdP(const GURL& idp,
RegisterIdPCallback callback) {
if (!webid::IsIdPRegistrationEnabled()) {
std::move(callback).Run(RegisterIdpStatus::kErrorFeatureDisabled);
return;
}
if (!origin().IsSameOriginWith(url::Origin::Create(idp))) {
std::move(callback).Run(RegisterIdpStatus::kErrorCrossOriginConfig);
return;
}
if (!render_frame_host().HasTransientUserActivation()) {
std::move(callback).Run(RegisterIdpStatus::kErrorNoTransientActivation);
return;
}
if (!network_manager_) {
network_manager_ = CreateNetworkManager();
}
fedcm_idp_registration_handler_ =
std::make_unique<webid::IdpRegistrationHandler>(
render_frame_host(), network_manager_.get(), idp);
fedcm_idp_registration_handler_->FetchConfig(
base::BindOnce(&FederatedAuthRequestImpl::OnIdpRegistrationConfigFetched,
weak_ptr_factory_.GetWeakPtr(), std::move(callback), idp));
}
void FederatedAuthRequestImpl::OnIdpRegistrationConfigFetched(
RegisterIdPCallback callback,
const GURL& idp,
std::vector<webid::ConfigFetcher::FetchResult> fetch_results) {
CHECK_EQ(fetch_results.size(), 1u);
fedcm_idp_registration_handler_.reset();
if (fetch_results[0].error) {
std::move(callback).Run(RegisterIdpStatus::kErrorInvalidConfig);
return;
}
if (!request_dialog_controller_) {
request_dialog_controller_ = CreateDialogController();
}
request_dialog_controller_->RequestIdPRegistrationPermision(
origin(),
base::BindOnce(&FederatedAuthRequestImpl::OnRegisterIdPPermissionResponse,
weak_ptr_factory_.GetWeakPtr(), std::move(callback), idp));
}
void FederatedAuthRequestImpl::OnRegisterIdPPermissionResponse(
RegisterIdPCallback callback,
const GURL& idp,
bool accepted) {
if (accepted) {
permission_delegate_->RegisterIdP(idp);
}
std::move(callback).Run(accepted ? RegisterIdpStatus::kSuccess
: RegisterIdpStatus::kErrorDeclined);
}
void FederatedAuthRequestImpl::UnregisterIdP(const GURL& idp,
UnregisterIdPCallback callback) {
if (!webid::IsIdPRegistrationEnabled()) {
std::move(callback).Run(false);
return;
}
if (!origin().IsSameOriginWith(url::Origin::Create(idp))) {
std::move(callback).Run(false);
return;
}
permission_delegate_->UnregisterIdP(idp);
std::move(callback).Run(true);
}
void FederatedAuthRequestImpl::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 FederatedAuthRequestImpl::HasPendingRequest() const {
webid::RequestPageData* page_data =
webid::GetPageData(render_frame_host().GetPage());
bool has_pending_request = page_data->PendingWebIdentityRequest() != nullptr;
DCHECK(has_pending_request || !auth_request_token_callback_);
return has_pending_request;
}
void FederatedAuthRequestImpl::FetchEndpointsForIdps(
const std::set<GURL>& idp_config_urls) {
int icon_ideal_size =
request_dialog_controller_->GetBrandIconIdealSize(rp_mode_);
int icon_minimum_size =
request_dialog_controller_->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);
fedcm_accounts_fetcher_ = std::make_unique<webid::AccountsFetcher>(
render_frame_host(), network_manager_.get(), api_permission_delegate_,
permission_delegate_,
webid::AccountsFetcher::FedCmFetchingParams(
rp_mode_, icon_ideal_size, icon_minimum_size, mediation_requirement_),
this);
fedcm_accounts_fetcher_->FetchEndpointsForIdps(idp_config_urls);
}
void FederatedAuthRequestImpl::CompleteDisconnectRequest(
DisconnectCallback callback,
blink::mojom::DisconnectStatus status) {
// `disconnect_request_` may be null here if the completion is invoked from
// the FederatedAuthRequestImpl destructor, which destroys
// `disconnect_request_`. The FederatedAuthDisconnectRequest destructor would
// trigger the callback.
if (!disconnect_request_ &&
status == blink::mojom::DisconnectStatus::kSuccess) {
NOTREACHED() << "The successful disconnect request is nowhere to be found";
}
std::move(callback).Run(status);
disconnect_request_.reset();
}
bool FederatedAuthRequestImpl::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;
}
DCHECK_EQ(identity_selection_type_, kAutoPassive);
return had_transient_user_activation_;
}
FedCmUseOtherAccountResult
FederatedAuthRequestImpl::ComputeUseOtherAccountResult(
blink::mojom::FederatedAuthRequestResult result,
const std::optional<GURL>& selected_idp_config_url) {
if (result != FederatedAuthRequestResult::kSuccess) {
return FedCmUseOtherAccountResult::kUserDoesNotSignIn;
}
CHECK(selected_idp_config_url);
if (webid::IsEndpointSameOrigin(*selected_idp_config_url, login_url_) &&
!account_ids_before_login_.contains(account_id_)) {
return FedCmUseOtherAccountResult::kUserSignsInWithNewAccount;
}
return FedCmUseOtherAccountResult::kUserSignsInWithExistingAccount;
}
void FederatedAuthRequestImpl::OnFetchDataForIdpSucceeded(
std::vector<IdentityRequestAccountPtr> 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;
});
}
idp_infos_[idp_config_url] = std::move(idp_info);
idp_accounts_[idp_config_url] = std::move(accounts);
fetch_data_.pending_idps.erase(idp_config_url);
MaybeShowAccountsDialog();
}
void FederatedAuthRequestImpl::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 FederatedAuthRequestImpl::SetWellKnownAndConfigFetchedTime(
base::TimeTicks time) {
well_known_and_config_fetched_time_ = time;
fedcm_metrics_->RecordWellKnownAndConfigFetchTime(
well_known_and_config_fetched_time_ - start_time_);
}
void FederatedAuthRequestImpl::OnFetchDataForIdpFailed(
const std::unique_ptr<IdentityProviderInfo> idp_info,
blink::mojom::FederatedAuthRequestResult result,
std::optional<TokenStatus> 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>>
FederatedAuthRequestImpl::GetAutofillSuggestions() const {
// Requires conditional FedCM to be enabled.
if (!webid::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 FederatedAuthRequestImpl::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(FederatedAuthRequestResult::kDisabledInSettings,
TokenStatus::kDisabledInSettings,
/*should_delay_callback=*/true);
return;
}
// This map may have contents already if we came here through the "Add
// Account" flow or the IDP login mismatch in multiple IDP case.
idp_data_for_display_.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()));
}
}
idp_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;
});
// Copy the newly logged in accounts into `new_accounts_`, if there are any.
new_accounts_.clear();
for (const auto& account : accounts_) {
if (IsNewlyLoggedIn(*account)) {
new_accounts_.push_back(account);
}
if (account->is_filtered_out) {
account->identity_provider->idp_metadata.has_filtered_out_account = true;
}
}
// 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) {
request_dialog_controller_->NotifyAutofillSourceReadyForTesting();
return;
}
// 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;
dialog_type_ = auto_reauthn_enabled ? kAutoReauth : kSelectAccount;
bool is_auto_reauthn_setting_enabled = false;
bool is_auto_reauthn_embargoed = false;
std::optional<base::TimeDelta> time_from_embargo;
bool requires_user_mediation = false;
IdentityProviderDataPtr auto_reauthn_idp = nullptr;
IdentityRequestAccountPtr auto_reauthn_account = nullptr;
bool has_single_returning_account = false;
if (auto_reauthn_enabled) {
is_auto_reauthn_setting_enabled =
auto_reauthn_permission_delegate_->IsAutoReauthnSettingEnabled();
is_auto_reauthn_embargoed =
auto_reauthn_permission_delegate_->IsAutoReauthnEmbargoed(
GetEmbeddingOrigin());
if (is_auto_reauthn_embargoed) {
time_from_embargo =
base::Time::Now() -
auto_reauthn_permission_delegate_->GetAutoReauthnEmbargoStartTime(
GetEmbeddingOrigin());
// 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.");
}
requires_user_mediation = RequiresUserMediation();
// Auto signs in returning users if they have a single returning account and
// are signing in.
has_single_returning_account =
GetAccountForAutoReauthn(&auto_reauthn_idp, &auto_reauthn_account);
if (dialog_type_ == kAutoReauth &&
(requires_user_mediation || !is_auto_reauthn_setting_enabled ||
is_auto_reauthn_embargoed || !has_single_returning_account)) {
dialog_type_ = kSelectAccount;
}
if (!has_single_returning_account &&
mediation_requirement_ == MediationRequirement::kSilent) {
fedcm_metrics_->RecordAutoReauthnMetrics(
has_single_returning_account, auto_reauthn_account.get(),
dialog_type_ == kAutoReauth, !is_auto_reauthn_setting_enabled,
is_auto_reauthn_embargoed, time_from_embargo,
requires_user_mediation);
// 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(
FederatedAuthRequestResult::kSilentMediationFailure,
TokenStatus::kSilentMediationFailure,
/*should_delay_callback=*/true);
return;
}
if (dialog_type_ == kAutoReauth) {
accounts_ = {auto_reauthn_account};
idp_data_for_display_ = {auto_reauthn_idp};
new_accounts_.clear();
accounts_[0]->identity_provider = idp_data_for_display_[0];
}
}
if (dialog_type_ != kAutoReauth) {
identity_selection_type_ = kExplicit;
} else if (rp_mode_ == blink::mojom::RpMode::kPassive) {
identity_selection_type_ = kAutoPassive;
} else {
identity_selection_type_ = kAutoActive;
}
if (auto_reauthn_enabled) {
fedcm_metrics_->RecordAutoReauthnMetrics(
has_single_returning_account, auto_reauthn_account.get(),
dialog_type_ == kAutoReauth, !is_auto_reauthn_setting_enabled,
is_auto_reauthn_embargoed, time_from_embargo, requires_user_mediation);
}
// 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(FederatedAuthRequestResult::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) {
request_dialog_controller_->SetIsInterceptionEnabled(intercept);
}
if (identity_selection_type_ != kExplicit) {
OnAccountSelected(accounts_[0]->identity_provider->idp_metadata.config_url,
accounts_[0]->id, /*is_sign_in=*/true);
if (!request_dialog_controller_->ShowVerifyingDialog(
CreateRpData(), auto_reauthn_idp, accounts_[0], SignInMode::kAuto,
rp_mode_,
base::BindOnce(&FederatedAuthRequestImpl::OnAccountsDisplayed,
weak_ptr_factory_.GetWeakPtr()))) {
return;
}
} else {
if (rp_mode_ == RpMode::kPassive) {
request_dialog_controller_->ShouldShowAccountsPassiveDialog(
base::BindOnce(&FederatedAuthRequestImpl::
OnShouldShowAccountsPassiveDialogResult,
weak_ptr_factory_.GetWeakPtr(),
did_succeed_for_at_least_one_idp));
return;
}
if (!request_dialog_controller_->ShowAccountsDialog(
CreateRpData(), idp_data_for_display_, accounts_, rp_mode_,
new_accounts_,
base::BindOnce(&FederatedAuthRequestImpl::OnAccountSelected,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&FederatedAuthRequestImpl::LoginToIdP,
weak_ptr_factory_.GetWeakPtr(),
/*can_append_hints=*/false),
base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&FederatedAuthRequestImpl::OnAccountsDisplayed,
weak_ptr_factory_.GetWeakPtr()))) {
return;
}
}
AfterAccountsDialogShown(did_succeed_for_at_least_one_idp);
}
void FederatedAuthRequestImpl::OnShouldShowAccountsPassiveDialogResult(
bool did_succeed_for_at_least_one_idp,
bool should_show) {
if (!should_show) {
OnDialogDismissed(
IdentityRequestDialogController::DismissReason::kSuppressed);
return;
}
if (!request_dialog_controller_->ShowAccountsDialog(
CreateRpData(), idp_data_for_display_, accounts_, rp_mode_,
new_accounts_,
base::BindOnce(&FederatedAuthRequestImpl::OnAccountSelected,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&FederatedAuthRequestImpl::LoginToIdP,
weak_ptr_factory_.GetWeakPtr(),
/*can_append_hints=*/false),
base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&FederatedAuthRequestImpl::OnAccountsDisplayed,
weak_ptr_factory_.GetWeakPtr()))) {
return;
}
AfterAccountsDialogShown(did_succeed_for_at_least_one_idp);
}
void FederatedAuthRequestImpl::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().path() != "/");
}
void FederatedAuthRequestImpl::NotifyAutofillSuggestionAccepted(
const GURL& idp,
const std::string& account_id,
bool show_modal,
OnFederatedTokenReceivedCallback callback) {
token_received_callback_for_autofill_ = std::move(callback);
// 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.
auto get_info_it = token_request_get_infos_.find(idp);
// 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 (!request_dialog_controller_->ShowLoadingDialog(
CreateRpData(), FormatOriginForDisplay(url::Origin::Create(idp)),
get_info_it->second.rp_context, blink::mojom::RpMode::kActive,
base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed,
weak_ptr_factory_.GetWeakPtr()))) {
return;
}
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
// overload the use of "new_accounts" in the ShowAccountsDialog. We should
// probably refactor the API to support this use case, rather than overload
// an unintended use.
if (!request_dialog_controller_->ShowAccountsDialog(
CreateRpData(), idp_data_for_display_, {},
blink::mojom::RpMode::kActive, selected,
base::BindOnce(&FederatedAuthRequestImpl::OnAccountSelected,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&FederatedAuthRequestImpl::LoginToIdP,
weak_ptr_factory_.GetWeakPtr(),
/*can_append_hints=*/false),
base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&FederatedAuthRequestImpl::OnAccountsDisplayed,
weak_ptr_factory_.GetWeakPtr()))) {
return;
}
// TODO(crbug.com/435216589): Should we call AfterAccountsDialogShown here?
}
void FederatedAuthRequestImpl::OnAccountsDisplayed() {
accounts_dialog_display_time_ = base::TimeTicks::Now();
}
void FederatedAuthRequestImpl::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 FederatedAuthRequestImpl::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_ = 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 (!request_dialog_controller_->ShowFailureDialog(
CreateRpData(), FormatOriginForDisplay(idp_origin),
idp_info->rp_context, rp_mode_, idp_info->metadata,
base::BindOnce(&FederatedAuthRequestImpl::OnDismissFailureDialog,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&FederatedAuthRequestImpl::LoginToIdP,
weak_ptr_factory_.GetWeakPtr(),
/*can_append_hints=*/true))) {
return;
}
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 FederatedAuthRequestImpl::CloseModalDialogView() {
#if BUILDFLAG(IS_ANDROID)
SetupIdentityRegistryFromPopup();
#endif
// Invoke OnClose on the opener.
if (identity_registry_) {
identity_registry_->NotifyClose(origin());
}
}
void FederatedAuthRequestImpl::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(FederatedAuthRequestResult::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(
&FederatedAuthRequestImpl::OnContinueOnResponseReceived,
weak_ptr_factory_.GetWeakPtr(), idp_info.provider->Clone());
std::vector<std::string> disclosure_shown_for;
if (!is_sign_in) {
disclosure_shown_for =
webid::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(webid::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 = webid::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(&FederatedAuthRequestImpl::OnTokenResponseReceived,
weak_ptr_factory_.GetWeakPtr(),
idp_info.provider->Clone()),
std::move(continue_on),
base::BindOnce(&FederatedAuthRequestImpl::RecordErrorMetrics,
weak_ptr_factory_.GetWeakPtr(),
idp_info.provider->Clone()));
}
void FederatedAuthRequestImpl::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;
if (should_embargo) {
api_permission_delegate_->RecordDismissAndEmbargo(GetEmbeddingOrigin());
}
CompleteRequestWithError(
should_embargo ? FederatedAuthRequestResult::kShouldEmbargo
: FederatedAuthRequestResult::kUiDismissedNoEmbargo,
should_embargo ? TokenStatus::kShouldEmbargo
: TokenStatus::kNotSignedInWithIdp,
/*should_delay_callback=*/false);
}
void FederatedAuthRequestImpl::OnDismissErrorDialog(
const GURL& idp_config_url,
IdpNetworkRequestManager::FetchStatus status,
IdentityRequestDialogController::DismissReason dismiss_reason) {
bool has_url = token_error_ && !token_error_->url.is_empty();
ErrorDialogResult result =
webid::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 FederatedAuthRequestImpl::OnDialogDismissed(
IdentityRequestDialogController::DismissReason dismiss_reason) {
if (has_sent_token_request_) {
verifying_dialog_result_ =
identity_selection_type_ == kExplicit
? FedCmVerifyingDialogResult::kCancelExplicit
: FedCmVerifyingDialogResult::kCancelAutoReauthn;
}
if (dialog_type_ == kContinueOnPopup) {
fedcm_metrics_->RecordContinueOnPopupResult(
FedCmContinueOnPopupResult::kWindowClosed);
// Popups always get dismissed with reason kOther, so we never embargo.
CompleteRequestWithError(FederatedAuthRequestResult::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;
if (should_embargo) {
api_permission_delegate_->RecordDismissAndEmbargo(GetEmbeddingOrigin());
}
TokenStatus token_status;
FederatedAuthRequestResult result;
if (should_embargo) {
token_status = TokenStatus::kShouldEmbargo;
result = FederatedAuthRequestResult::kShouldEmbargo;
} else if (dismiss_reason ==
IdentityRequestDialogController::DismissReason::kSuppressed) {
token_status = TokenStatus::kNotSelectAccount;
result = FederatedAuthRequestResult::kSuppressedBySegmentationPlatform;
} else {
token_status = TokenStatus::kNotSelectAccount;
result = FederatedAuthRequestResult::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 FederatedAuthRequestImpl::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_ != kNone) {
// 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?
dialog_type_ = dialog_type;
config_url_ = idp_config_url;
UMA_HISTOGRAM_ENUMERATION("Blink.FedCm.Popup.DialogType", dialog_type_);
WebContents* web_contents = request_dialog_controller_->ShowModalDialog(
url_to_show, rp_mode_,
base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed,
weak_ptr_factory_.GetWeakPtr()));
// This may be null on Android, as the method cannot return the WebContents of
// the CCT that will be created.
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 FederatedAuthRequestImpl::OnContinueOnResponseReceived(
IdentityProviderRequestOptionsPtr idp,
IdpNetworkRequestManager::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(
FedCmContinueOnPopupStatus::kUrlNotSameOriginAndPopupNotAllowed);
} else if (!is_same_origin) {
fedcm_metrics_->RecordContinueOnPopupStatus(
FedCmContinueOnPopupStatus::kUrlNotSameOrigin);
} else if (!can_show_popup) {
fedcm_metrics_->RecordContinueOnPopupStatus(
FedCmContinueOnPopupStatus::kPopupNotAllowed);
}
CompleteRequestWithError(
FederatedAuthRequestResult::kIdTokenInvalidResponse,
TokenStatus::kIdTokenInvalidResponse,
/*should_delay_callback=*/false);
return;
}
fedcm_metrics_->RecordContinueOnPopupStatus(
FedCmContinueOnPopupStatus::kPopupOpened);
ShowModalDialog(kContinueOnPopup, idp->config->config_url, continue_on);
}
void FederatedAuthRequestImpl::ShowErrorDialog(
const GURL& idp_config_url,
IdpNetworkRequestManager::FetchStatus status,
std::optional<TokenError> token_error) {
CHECK(idp_infos_.find(idp_config_url) != idp_infos_.end());
dialog_type_ = kError;
config_url_ = idp_config_url;
token_request_status_ = status;
token_error_ = token_error;
// TODO(crbug.com/40282657): Refactor IdentityCredentialTokenError
if (!request_dialog_controller_->ShowErrorDialog(
CreateRpData(),
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(&FederatedAuthRequestImpl::OnDismissErrorDialog,
weak_ptr_factory_.GetWeakPtr(), idp_config_url,
status),
token_error && !token_error->url.is_empty()
? base::BindOnce(&FederatedAuthRequestImpl::ShowModalDialog,
weak_ptr_factory_.GetWeakPtr(), kErrorUrlPopup,
config_url_, token_error->url)
: base::NullCallback())) {
return;
}
devtools_instrumentation::DidShowFedCmDialog(render_frame_host());
}
void FederatedAuthRequestImpl::OnTokenResponseReceived(
IdentityProviderRequestOptionsPtr idp,
IdpNetworkRequestManager::FetchStatus status,
IdpNetworkRequestManager::TokenResult result) {
CHECK(result.token.empty() || !result.error);
verifying_dialog_result_ =
identity_selection_type_ == kExplicit
? FedCmVerifyingDialogResult::kSuccessExplicit
: FedCmVerifyingDialogResult::kSuccessAutoReauthn;
bool should_show_error_ui =
result.error ||
status.parse_status != IdpNetworkRequestManager::ParseStatus::kSuccess;
auto complete_request_callback =
should_show_error_ui
? base::BindOnce(&FederatedAuthRequestImpl::ShowErrorDialog,
weak_ptr_factory_.GetWeakPtr(),
idp->config->config_url, status, result.error)
: base::BindOnce(&FederatedAuthRequestImpl::CompleteTokenRequest,
weak_ptr_factory_.GetWeakPtr(),
idp->config->config_url, status, 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 or conditional flow we can complete without delay
// because there is no contextual UI displayed to users.
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 ||
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 FederatedAuthRequestImpl::MarkUserAsSignedIn(
const GURL& idp_config_url,
const std::string& account_id) {
// 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.
CHECK(!account_id_.empty());
if (identity_selection_type_ == kExplicit) {
permission_delegate_->GrantSharingPermission(
origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url),
account_id);
} else {
permission_delegate_->RefreshExistingSharingPermission(
origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url),
account_id);
}
SetRequiresUserMediation(false, base::DoNothing());
}
void FederatedAuthRequestImpl::CompleteTokenRequest(
const GURL& idp_config_url,
IdpNetworkRequestManager::FetchStatus status,
std::optional<std::string> 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 != IdpNetworkRequestManager::ParseStatus::kSuccess) {
webid::MaybeAddResponseCodeToConsole(render_frame_host(), kIdAssertionUrl,
status.response_code);
std::pair<FederatedAuthRequestResult, TokenStatus> resultAndTokenStatus =
webid::IdAssertionFetchStatusToRequestResultAndTokenStatus(status);
CompleteRequestWithError(resultAndTokenStatus.first,
resultAndTokenStatus.second,
should_delay_callback);
return;
}
if (token_error_) {
webid::MaybeAddResponseCodeToConsole(render_frame_host(), kIdAssertionUrl,
status.response_code);
if (error_url_type_ && *error_url_type_ == ErrorUrlType::kCrossSite) {
CompleteRequestWithError(
FederatedAuthRequestResult::kIdTokenCrossSiteIdpErrorResponse,
TokenStatus::kIdTokenCrossSiteIdpErrorResponse,
should_delay_callback);
return;
}
CompleteRequestWithError(
FederatedAuthRequestResult::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) {
federated_sdjwt_handler_->ProcessSdJwt(token.value());
return;
}
CompleteRequest(FederatedAuthRequestResult::kSuccess,
TokenStatus::kSuccessUsingTokenInHttpResponse,
/*token_error=*/std::nullopt, idp_config_url, token.value(),
/*should_delay_callback=*/false);
}
void FederatedAuthRequestImpl::CompleteRequestWithError(
blink::mojom::FederatedAuthRequestResult result,
std::optional<TokenStatus> token_status,
bool should_delay_callback) {
CompleteRequest(result, token_status, token_error_,
/*selected_idp_config_url=*/std::nullopt,
/*token=*/"", should_delay_callback);
}
void FederatedAuthRequestImpl::CompleteRequest(
blink::mojom::FederatedAuthRequestResult result,
std::optional<TokenStatus> token_status,
std::optional<TokenError> token_error,
const std::optional<GURL>& selected_idp_config_url,
const std::string& id_token,
bool should_delay_callback) {
DCHECK(result == FederatedAuthRequestResult::kSuccess || id_token.empty());
bool should_trigger_cooldown_on_ignore =
webid::IsCooldownOnIgnoreEnabled() &&
token_status == TokenStatus::kUnhandledRequest &&
rp_mode_ == RpMode::kPassive;
if (accounts_dialog_shown_time_.has_value()) {
fedcm_metrics_->RecordAccountsDialogShownDuration(
idp_data_for_display_,
base::TimeTicks::Now() - accounts_dialog_shown_time_.value());
accounts_dialog_shown_time_ = std::nullopt;
if (should_trigger_cooldown_on_ignore && dialog_type_ == kSelectAccount) {
api_permission_delegate_->RecordIgnoreAndEmbargo(GetEmbeddingOrigin());
}
}
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;
if (should_trigger_cooldown_on_ignore && dialog_type_ == kConfirmIdpLogin) {
api_permission_delegate_->RecordIgnoreAndEmbargo(GetEmbeddingOrigin());
}
}
if (!auth_request_token_callback_) {
return;
}
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<FedCmUseOtherAccountResult> 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
? FedCmVerifyingDialogResult::kDestroyExplicit
: FedCmVerifyingDialogResult::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()
? FedCmThirdPartyCookiesStatus::kEnabledInSettings
: FedCmThirdPartyCookiesStatus::kDisabledInSettings,
webid::ComputeRequesterFrameType(render_frame_host(), origin(),
GetEmbeddingOrigin()),
has_signin_account, request_dialog_controller_->DidShowUi());
}
if (result == FederatedAuthRequestResult::kSuccess) {
CHECK(selected_idp_config_url);
CHECK(fedcm_accounts_fetcher_);
if (webid::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_),
request_dialog_controller_->DidShowUi());
}
} else if (!errors_logged_to_console_) {
errors_logged_to_console_ = true;
AddDevToolsIssue(result);
AddConsoleErrorMessage(result);
// fedcm_accounts_fetcher_ could be null if configs were not fetched, e.g.
// because of cooldown.
if (webid::IsMetricsEndpointEnabled() && fedcm_accounts_fetcher_) {
fedcm_accounts_fetcher_->SendAllFailedTokenRequestMetrics(
result, request_dialog_controller_->DidShowUi());
}
}
bool is_auto_selected = identity_selection_type_ != kExplicit;
if (ShouldNotifyDevtoolsForDialogType(dialog_type_)) {
devtools_instrumentation::DidCloseFedCmDialog(render_frame_host());
}
if (token_received_callback_for_autofill_) {
std::move(token_received_callback_for_autofill_)
.Run(result == FederatedAuthRequestResult::kSuccess);
}
if (!should_delay_callback || should_complete_request_immediately_) {
CleanUp();
webid::GetPageData(render_frame_host().GetPage())
->SetPendingWebIdentityRequest(nullptr);
errors_logged_to_console_ = false;
blink::mojom::TokenErrorPtr error;
if (token_error) {
error = blink::mojom::TokenError::New();
error->code = token_error->code;
error->url = token_error->url.spec();
}
RequestTokenStatus status =
webid::FederatedAuthRequestResultToRequestTokenStatus(result);
std::move(auth_request_token_callback_)
.Run(status, selected_idp_config_url, id_token, std::move(error),
is_auto_selected);
auth_request_token_callback_.Reset();
TRACE_EVENT_END("content.fedcm", perfetto_track_);
} else {
base::TimeDelta delay = GetRandomRejectionTime();
TRACE_EVENT_INSTANT("content.fedcm", "Delaying FedCM rejection",
perfetto_track_, "delay", delay);
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&FederatedAuthRequestImpl::OnRejectRequest,
weak_ptr_factory_.GetWeakPtr()),
delay);
}
}
void FederatedAuthRequestImpl::CleanUp() {
weak_ptr_factory_.InvalidateWeakPtrs();
permission_delegate_->RemoveIdpSigninStatusObserver(this);
// Given that |request_dialog_controller_| has reference to this web content
// instance we destroy that first.
request_dialog_controller_.reset();
fedcm_accounts_fetcher_.reset();
federated_sdjwt_handler_.reset();
network_manager_.reset();
fedcm_metrics_.reset();
account_id_ = std::string();
start_time_ = base::TimeTicks();
well_known_and_config_fetched_time_ = base::TimeTicks();
accounts_fetched_time_ = base::TimeTicks();
client_metadata_fetched_time_ = base::TimeTicks();
ready_to_display_accounts_dialog_time_ = base::TimeTicks();
accounts_dialog_display_time_ = base::TimeTicks();
select_account_time_ = base::TimeTicks();
id_assertion_response_time_ = base::TimeTicks();
accounts_dialog_shown_time_ = std::nullopt;
mismatch_dialog_shown_time_ = std::nullopt;
has_shown_mismatch_ = false;
idp_accounts_.clear();
new_accounts_.clear();
accounts_.clear();
idp_login_infos_.clear();
idp_infos_.clear();
idp_data_for_display_.clear();
account_ids_before_login_.clear();
fetch_data_ = FetchData();
idps_user_tried_to_signin_to_.clear();
idp_order_.clear();
token_request_get_infos_.clear();
login_url_ = GURL();
config_url_ = GURL();
token_error_ = std::nullopt;
dialog_type_ = kNone;
identity_selection_type_ = kExplicit;
had_transient_user_activation_ = false;
rp_mode_ = RpMode::kPassive;
}
void FederatedAuthRequestImpl::AddDevToolsIssue(
FederatedAuthRequestResult result) {
DCHECK_NE(result, FederatedAuthRequestResult::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_auth_request_details =
blink::mojom::FederatedAuthRequestIssueDetails::New(result);
details->federated_auth_request_details =
std::move(federated_auth_request_details);
render_frame_host().ReportInspectorIssue(
blink::mojom::InspectorIssueInfo::New(
blink::mojom::InspectorIssueCode::kFederatedAuthRequestIssue,
std::move(details)));
}
void FederatedAuthRequestImpl::AddConsoleErrorMessage(
FederatedAuthRequestResult result) {
render_frame_host().AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
webid::GetConsoleErrorMessageFromResult(result));
}
url::Origin FederatedAuthRequestImpl::GetEmbeddingOrigin() const {
return render_frame_host().GetMainFrame()->GetLastCommittedOrigin();
}
void FederatedAuthRequestImpl::CompleteUserInfoRequest(
webid::UserInfoRequest* request,
RequestUserInfoCallback callback,
blink::mojom::RequestUserInfoStatus status,
std::optional<std::vector<blink::mojom::IdentityUserInfoPtr>> user_info) {
auto it = std::find_if(
user_info_requests_.begin(), user_info_requests_.end(),
[request](const std::unique_ptr<webid::UserInfoRequest>& ptr) {
return ptr.get() == request;
});
// The request may not be found if the completion is invoked from
// FederatedAuthRequestImpl destructor. The destructor clears
// `user_info_requests_`, which destroys the FederatedAuthUserInfoRequests it
// contains. The FederatedAuthUserInfoRequest destructor invokes this
// callback.
if (it == user_info_requests_.end() &&
status == blink::mojom::RequestUserInfoStatus::kSuccess) {
NOTREACHED() << "The successful user info request is nowhere to be found";
}
std::move(callback).Run(status, std::move(user_info));
if (it != user_info_requests_.end()) {
user_info_requests_.erase(it);
}
}
std::unique_ptr<IdpNetworkRequestManager>
FederatedAuthRequestImpl::CreateNetworkManager() {
if (mock_network_manager_) {
return std::move(mock_network_manager_);
}
return IdpNetworkRequestManager::Create(
static_cast<RenderFrameHostImpl*>(&render_frame_host()));
}
std::unique_ptr<IdentityRequestDialogController>
FederatedAuthRequestImpl::CreateDialogController() {
if (mock_dialog_controller_) {
return std::move(mock_dialog_controller_);
}
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kUseFakeUIForFedCM)) {
std::string selected_account =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kUseFakeUIForFedCM);
return std::make_unique<FakeIdentityRequestDialogController>(
selected_account.empty() ? std::nullopt
: std::optional<std::string>(selected_account),
web_contents);
}
return GetContentClient()->browser()->CreateIdentityRequestDialogController(
web_contents);
}
void FederatedAuthRequestImpl::SetNetworkManagerForTests(
std::unique_ptr<IdpNetworkRequestManager> manager) {
mock_network_manager_ = std::move(manager);
}
void FederatedAuthRequestImpl::SetDialogControllerForTests(
std::unique_ptr<IdentityRequestDialogController> controller) {
mock_dialog_controller_ = std::move(controller);
}
void FederatedAuthRequestImpl::OnClose() {
CHECK(request_dialog_controller_);
request_dialog_controller_->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_ == kLoginToIdpPopup) {
CompleteRequestWithError(FederatedAuthRequestResult::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_ == kContinueOnPopup) {
fedcm_metrics_->RecordContinueOnPopupResult(
FedCmContinueOnPopupResult::kClosedByIdentityProviderClose);
// Popups always get dismissed with reason kOther, so we never embargo.
CompleteRequestWithError(
FederatedAuthRequestResult::kError,
TokenStatus::kContinuationPopupClosedByIdentityProviderClose,
/*should_delay_callback=*/false);
return;
}
}
bool FederatedAuthRequestImpl::OnResolve(
GURL idp_config_url,
const std::optional<std::string>& account_id,
const std::string& token) {
// Close the pop-up window post user permission.
if (!request_dialog_controller_) {
return false;
}
// IdentityProvider.resolve() is only allowed for continuation API.
if (dialog_type_ != kContinueOnPopup) {
return false;
}
request_dialog_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(
FedCmContinueOnPopupResult::kTokenReceived);
const IdentityProviderRequestOptionsPtr& provider =
idp_infos_[idp_config_url]->provider;
DCHECK(provider);
if (provider->format && *provider->format == blink::mojom::Format::kSdJwt) {
federated_sdjwt_handler_->ProcessSdJwt(token);
return true;
}
CompleteRequest(FederatedAuthRequestResult::kSuccess,
TokenStatus::kSuccessUsingIdentityProviderResolve,
/*token_error=*/std::nullopt, idp_config_url, token,
/*should_delay_callback=*/false);
// TODO(crbug.com/40262526): handle the corner cases where CompleteRequest
// can't actually fulfill the request.
return true;
}
void FederatedAuthRequestImpl::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);
}
bool FederatedAuthRequestImpl::SetupIdentityRegistryFromPopup() {
#if BUILDFLAG(IS_ANDROID)
if (identity_registry_) {
return true;
}
if (!request_dialog_controller_) {
request_dialog_controller_ = CreateDialogController();
CHECK(request_dialog_controller_);
}
// Because ShowModalDialog does not return the web contents on Android, we
// need to set up the IdentityRegistry now.
WebContents* rp_web_contents = request_dialog_controller_->GetRpWebContents();
// This can be null if resolve was called in a regular tab (as opposed to
// a CCT opened from ShowModalDialog).
if (!rp_web_contents) {
return false;
}
FederatedAuthRequestImpl* rp_auth_request =
webid::GetPageData(rp_web_contents->GetPrimaryPage())
->PendingWebIdentityRequest();
if (!rp_auth_request) {
return false;
}
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
IdentityRegistry::CreateForWebContents(
web_contents, rp_auth_request->weak_ptr_factory_.GetWeakPtr(),
rp_auth_request->config_url_);
identity_registry_ = IdentityRegistry::FromWebContents(web_contents);
return true;
#else
return false;
#endif
}
void FederatedAuthRequestImpl::OnRejectRequest() {
if (!auth_request_token_callback_) {
return;
}
DCHECK(errors_logged_to_console_);
CompleteRequestWithError(FederatedAuthRequestResult::kError,
/*token_status=*/std::nullopt,
/*should_delay_callback=*/false);
}
FederatedApiPermissionStatus
FederatedAuthRequestImpl::GetApiPermissionStatus() {
DCHECK(api_permission_delegate_);
return api_permission_delegate_->GetApiPermissionStatus(GetEmbeddingOrigin());
}
bool FederatedAuthRequestImpl::ShouldNotifyDevtoolsForDialogType(
DialogType type) {
return type != kNone && type != kLoginToIdpPopup &&
type != kContinueOnPopup && type != kErrorUrlPopup;
}
void FederatedAuthRequestImpl::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 FederatedAuthRequestImpl::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 FederatedAuthRequestImpl::AcceptConfirmIdpLoginDialogForDevtools() {
DCHECK(login_url_.is_valid());
LoginToIdP(/*can_append_hints=*/true, config_url_, login_url_);
}
void FederatedAuthRequestImpl::DismissConfirmIdpLoginDialogForDevtools() {
// These values match what HandleAccountsFetchFailure passes.
OnDismissFailureDialog(
IdentityRequestDialogController::DismissReason::kOther);
}
bool FederatedAuthRequestImpl::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 FederatedAuthRequestImpl::HasMoreDetailsButtonForDevtools() {
return token_error_ && token_error_->url.is_valid();
}
void FederatedAuthRequestImpl::ClickErrorDialogGotItForDevtools() {
DCHECK(token_error_);
OnDismissErrorDialog(
config_url_, token_request_status_,
IdentityRequestDialogController::DismissReason::kGotItButton);
}
void FederatedAuthRequestImpl::ClickErrorDialogMoreDetailsForDevtools() {
DCHECK(token_error_ && token_error_->url.is_valid());
ShowModalDialog(kErrorUrlPopup, config_url_, token_error_->url);
OnDismissErrorDialog(
config_url_, token_request_status_,
IdentityRequestDialogController::DismissReason::kMoreDetailsButton);
}
void FederatedAuthRequestImpl::DismissErrorDialogForDevtools() {
OnDismissErrorDialog(config_url_, token_request_status_,
IdentityRequestDialogController::DismissReason::kOther);
}
bool FederatedAuthRequestImpl::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 (!webid::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 FederatedAuthRequestImpl::ShouldFailBeforeFetchingAccounts(
const GURL& config_url) {
if (mediation_requirement_ != MediationRequirement::kSilent) {
return false;
}
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());
std::optional<base::TimeDelta> time_from_embargo;
if (is_auto_reauthn_embargoed) {
time_from_embargo =
base::Time::Now() -
auto_reauthn_permission_delegate_->GetAutoReauthnEmbargoStartTime(
GetEmbeddingOrigin());
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 =
webid::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) {
// 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, time_from_embargo, requires_user_mediation);
return true;
}
return false;
}
bool FederatedAuthRequestImpl::RequiresUserMediation() {
return auto_reauthn_permission_delegate_->RequiresUserMediation(origin());
}
void FederatedAuthRequestImpl::SetRequiresUserMediation(
bool requires_user_mediation,
base::OnceClosure callback) {
auto_reauthn_permission_delegate_->SetRequiresUserMediation(
origin(), requires_user_mediation);
if (permission_delegate_) {
permission_delegate_->OnSetRequiresUserMediation(origin(),
std::move(callback));
} else {
std::move(callback).Run();
}
}
void FederatedAuthRequestImpl::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.
webid::MaybeAppendQueryParameters(it->second, &login_url);
}
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(kLoginToIdpPopup, idp_config_url, login_url);
}
void FederatedAuthRequestImpl::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 FederatedAuthRequestImpl::PreventSilentAccess(
PreventSilentAccessCallback callback) {
SetRequiresUserMediation(true, std::move(callback));
if (permission_delegate_->HasSharingPermission(GetEmbeddingOrigin())) {
// Ensure the lifecycle state as GetPageUkmSourceId doesn't support the
// prerendering page. As FederatedAuthRequest 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));
RecordPreventSilentAccess(
webid::ComputeRequesterFrameType(render_frame_host(), origin(),
GetEmbeddingOrigin()),
render_frame_host().GetPageUkmSourceId());
}
}
void FederatedAuthRequestImpl::Disconnect(
blink::mojom::IdentityCredentialDisconnectOptionsPtr options,
DisconnectCallback callback) {
std::unique_ptr<FedCmMetrics> disconnect_metrics = CreateFedCmMetrics();
if (disconnect_request_) {
// Since we do not send any fetches in this case, consider the request to be
// instant, e.g. duration is 0.
render_frame_host().AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
webid::GetDisconnectConsoleErrorMessage(
FedCmDisconnectStatus::kTooManyRequests));
disconnect_metrics->RecordDisconnectMetrics(
FedCmDisconnectStatus::kTooManyRequests, std::nullopt,
webid::ComputeRequesterFrameType(render_frame_host(), origin(),
GetEmbeddingOrigin()),
options->config->config_url);
std::move(callback).Run(DisconnectStatus::kErrorTooManyRequests);
return;
}
bool intercept = false;
bool should_complete_request_immediately = false;
devtools_instrumentation::WillSendFedCmRequest(
render_frame_host(), &intercept, &should_complete_request_immediately);
auto network_manager = CreateNetworkManager();
disconnect_request_ = FederatedAuthDisconnectRequest::Create(
std::move(network_manager), permission_delegate_, &render_frame_host(),
std::move(disconnect_metrics), std::move(options));
FederatedAuthDisconnectRequest* disconnect_request_ptr =
disconnect_request_.get();
disconnect_request_ptr->SetCallbackAndStart(
base::BindOnce(&FederatedAuthRequestImpl::CompleteDisconnectRequest,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
api_permission_delegate_);
}
void FederatedAuthRequestImpl::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<FedCmMetrics> FederatedAuthRequestImpl::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<FedCmMetrics>(
render_frame_host().GetPageUkmSourceId());
}
bool FederatedAuthRequestImpl::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 FederatedAuthRequestImpl::ShouldTerminateRequest(
const std::vector<IdentityProviderGetParametersPtr>& idp_get_params_ptrs,
const MediationRequirement& requirement) {
// idp_get_params_ptrs sent from the renderer should be of size 1.
if (idp_get_params_ptrs.size() != 1u) {
ReportBadMessageAndDeleteThis("idp_get_params_ptrs should be of size 1.");
return true;
}
// This could only happen with a compromised renderer process. We ensure that
// the provider list size is > 0 on the renderer side at the beginning of
// parsing |IdentityCredentialRequestOptions|.
for (const auto& idp_get_params_ptr : idp_get_params_ptrs) {
if (idp_get_params_ptr->providers.size() == 0) {
ReportBadMessageAndDeleteThis("The provider list should not be empty.");
return true;
}
if (idp_get_params_ptr->providers.size() > 10u) {
ReportBadMessageAndDeleteThis(
"The provider list should not be greater than 10.");
return true;
}
if (idp_get_params_ptr->mode == RpMode::kActive &&
requirement == MediationRequirement::kSilent) {
ReportBadMessageAndDeleteThis(
"mediation: silent is not supported in active mode.");
return true;
}
}
if (requirement == MediationRequirement::kConditional &&
!webid::IsAutofillEnabled()) {
// The conditional mediation parameter can only be used when delegation
// is enabled while it is under development.
//
// TODO(crbug.com/380367784): handle all of the many cases in which a
// conditional mediation may interact with other features.
ReportBadMessageAndDeleteThis(
"Conditional mediation is not supported when both autofill and "
"delegation are disabled.");
return true;
}
if (render_frame_host().IsNestedWithinFencedFrame()) {
ReportBadMessageAndDeleteThis(
"FedCM should not be allowed in fenced frame trees.");
return true;
}
return false;
}
bool FederatedAuthRequestImpl::HandlePendingRequestAndCancelNewRequest(
const std::vector<GURL>& old_idp_order,
const std::vector<IdentityProviderGetParametersPtr>& idp_get_params_ptrs,
const MediationRequirement& requirement) {
FederatedAuthRequestImpl* pending_request =
webid::GetPageData(render_frame_host().GetPage())
->PendingWebIdentityRequest();
std::unique_ptr<FedCmMetrics> new_request_metrics = CreateFedCmMetrics();
RpMode pending_request_rp_mode = pending_request->GetRpMode();
RpMode new_request_rp_mode = idp_get_params_ptrs[0]->mode;
new_request_metrics->RecordMultipleRequestsRpMode(
pending_request_rp_mode, new_request_rp_mode, idp_order_);
bool can_replace_pending_request = had_transient_user_activation_ &&
new_request_rp_mode == RpMode::kActive &&
pending_request_rp_mode != RpMode::kActive;
if (!can_replace_pending_request) {
// Cancel this new request.
new_request_metrics->RecordRequestTokenStatus(
TokenStatus::kTooManyRequests, requirement, idp_order_,
/*num_idps_mismatch=*/0,
/*selected_idp_config_url=*/std::nullopt,
(idp_get_params_ptrs[0]->mode == blink::mojom::RpMode::kActive)
? RpMode::kActive
: RpMode::kPassive,
/*use_other_account_result=*/std::nullopt,
/*verifying_dialog_result=*/std::nullopt,
api_permission_delegate_->AreThirdPartyCookiesEnabledInSettings()
? FedCmThirdPartyCookiesStatus::kEnabledInSettings
: FedCmThirdPartyCookiesStatus::kDisabledInSettings,
webid::ComputeRequesterFrameType(render_frame_host(), origin(),
GetEmbeddingOrigin()),
/*has_signin_account=*/std::nullopt, /*did_show_ui=*/false);
AddDevToolsIssue(
blink::mojom::FederatedAuthRequestResult::kTooManyRequests);
AddConsoleErrorMessage(
blink::mojom::FederatedAuthRequestResult::kTooManyRequests);
// Since multiple `get` calls is not yet supported, if one IdP invokes the
// API while another request from different IdPs is in-flight, the new API
// call will be rejected. The two requests may be from different RFHs so
// we should calculate properly.
if (old_idp_order.empty()) {
new_request_metrics->RecordMultipleRequestsFromDifferentIdPs(
idp_order_ != pending_request->idp_order_);
} else {
new_request_metrics->RecordMultipleRequestsFromDifferentIdPs(
idp_order_ != old_idp_order);
}
idp_order_ = std::move(old_idp_order);
return true;
}
// Cancel the pending request before starting the new active flow request.
// Set the old values before completing in case the pending request
// corresponds to one in this object.
std::vector<GURL> new_idp_order = std::move(idp_order_);
idp_order_ = std::move(old_idp_order);
pending_request->CompleteRequestWithError(
FederatedAuthRequestResult::kReplacedByActiveMode,
TokenStatus::kReplacedByActiveMode,
/*should_delay_callback=*/false);
CHECK(!auth_request_token_callback_);
// Some members were reset to false during CleanUp when replacing a passive
// flow from the same frame so we need to set them again.
had_transient_user_activation_ = true;
fedcm_metrics_ = std::move(new_request_metrics);
idp_order_ = std::move(new_idp_order);
return false;
}
RelyingPartyData FederatedAuthRequestImpl::CreateRpData() 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_matches_top_frame_origin.value_or(true)) {
show_iframe_origin = true;
break;
}
}
std::u16string iframe_origin;
if (show_iframe_origin) {
iframe_origin = base::UTF8ToUTF16(FormatOriginForDisplay(origin()));
}
return RelyingPartyData(
base::UTF8ToUTF16(GetTopFrameOriginForDisplay(GetEmbeddingOrigin())),
iframe_origin);
}
} // namespace content