blob: bd77b07bb889d9bb74bfe5eb792d7d9ecb238f04 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <optional>
#include <ostream>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/webid/request_service.h"
#include "content/browser/webid/test/federated_auth_request_request_token_callback_helper.h"
#include "content/browser/webid/test/mock_api_permission_delegate.h"
#include "content/browser/webid/test/mock_auto_reauthn_permission_delegate.h"
#include "content/browser/webid/test/mock_identity_registry.h"
#include "content/browser/webid/test/mock_identity_request_dialog_controller.h"
#include "content/browser/webid/test/mock_idp_network_request_manager.h"
#include "content/browser/webid/test/mock_modal_dialog_view_delegate.h"
#include "content/browser/webid/test/mock_permission_delegate.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/webid/identity_request_dialog_controller.h"
#include "content/public/common/content_features.h"
#include "content/public/test/navigation_simulator.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/http/http_status_code.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/origin.h"
using ApiPermissionStatus =
content::FederatedIdentityApiPermissionContextDelegate::PermissionStatus;
using AuthRequestCallbackHelper =
content::FederatedAuthRequestRequestTokenCallbackHelper;
using FedCmEntry = ukm::builders::Blink_FedCm;
using FedCmIdpEntry = ukm::builders::Blink_FedCmIdp;
using RequesterFrameType = content::webid::RequesterFrameType;
using FetchStatus = content::IdpNetworkRequestManager::FetchStatus;
using RequestTokenCallback =
content::webid::RequestService::RequestTokenCallback;
using blink::mojom::RequestTokenStatus;
using ::testing::NiceMock;
namespace content::webid {
namespace {
constexpr char kIdpUrl[] = "https://idp.example/";
constexpr char kProviderUrlFull[] = "https://idp.example/fedcm.json";
constexpr char kProviderUrlTwoFull[] = "https://idp-2.example/fedcm.json";
constexpr char kTopFrameUrl[] = "https://top-frame.example/";
constexpr char kAccountsEndpoint[] = "https://idp.example/accounts";
constexpr char kClientMetadataEndpoint[] = "https://idp.example/clientmd";
constexpr char kTokenEndpoint[] = "https://idp.example/token";
constexpr char kLoginUrl[] = "https://idp.example/login";
constexpr char kClientId[] = "client_id_123";
constexpr char kNonce[] = "nonce123";
constexpr char kAccountId[] = "1234";
constexpr char kToken[] = "[not a real token]";
// If true, will send `client_matches_top_frame_origin: false` in the client
// metadata request.
static bool sSendClientMatchesTopFrameOrigin = false;
static std::vector<IdentityRequestAccountPtr> kAccounts;
// IdpNetworkRequestManager which returns valid data from IdP.
class TestIdpNetworkRequestManager : public MockIdpNetworkRequestManager {
public:
void FetchWellKnown(const GURL& provider,
FetchWellKnownCallback callback) override {
IdpNetworkRequestManager::WellKnown well_known;
std::set<GURL> well_known_configs;
well_known_configs.insert(GURL(kProviderUrlFull));
well_known.provider_urls = std::move(well_known_configs);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), kFetchStatusSuccess, well_known));
}
void FetchConfig(const GURL& provider,
blink::mojom::RpMode rp_mode,
int idp_brand_icon_ideal_size,
int idp_brand_icon_minimum_size,
FetchConfigCallback callback) override {
IdpNetworkRequestManager::Endpoints endpoints;
endpoints.token = GURL(kTokenEndpoint);
endpoints.accounts = GURL(kAccountsEndpoint);
if (sSendClientMatchesTopFrameOrigin) {
endpoints.client_metadata = GURL(kClientMetadataEndpoint);
}
IdentityProviderMetadata idp_metadata;
idp_metadata.idp_login_url = GURL(kLoginUrl);
idp_metadata.config_url = provider;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), kFetchStatusSuccess,
endpoints, idp_metadata));
}
void SendAccountsRequest(const url::Origin& idp_origin,
const GURL& accounts_url,
const std::string& client_id,
AccountsRequestCallback callback) override {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), kFetchStatusSuccess, kAccounts));
}
void SendTokenRequest(
const GURL& token_url,
const std::string& account,
const std::string& url_encoded_post_data,
bool idp_blindness,
TokenRequestCallback callback,
ContinueOnCallback continue_on,
RecordErrorMetricsCallback record_error_metrics_callback) override {
TokenResult result;
result.token = kToken;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), kFetchStatusSuccess, result));
}
void FetchClientMetadata(const GURL& endpoint,
const std::string& client_id,
int rp_brand_icon_ideal_size,
int rp_brand_icon_minimum_size,
FetchClientMetadataCallback callback) override {
ClientMetadata client_metadata;
if (sSendClientMatchesTopFrameOrigin) {
client_metadata.client_matches_top_frame_origin = false;
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), kFetchStatusSuccess,
client_metadata));
}
private:
FetchStatus kFetchStatusSuccess{
IdpNetworkRequestManager::ParseStatus::kSuccess, net::HTTP_OK};
};
class TestDialogController
: public NiceMock<MockIdentityRequestDialogController> {
public:
struct State {
bool did_show_accounts_dialog{false};
std::string rp_for_display;
std::string iframe_for_display;
};
enum class AccountsDialogAction {
kNone,
kSelectAccount,
};
// `state` is a pointer parameter so that it can outlive TestDialogController.
TestDialogController(AccountsDialogAction accounts_dialog_action,
State* state)
: accounts_dialog_action_(accounts_dialog_action), state_(state) {}
~TestDialogController() override = default;
TestDialogController(TestDialogController&) = delete;
TestDialogController& operator=(TestDialogController&) = delete;
bool ShowAccountsDialog(
content::RelyingPartyData rp_data,
const std::vector<IdentityProviderDataPtr>& idp_list,
const std::vector<IdentityRequestAccountPtr>& accounts,
blink::mojom::RpMode rp_mode,
const std::vector<IdentityRequestAccountPtr>& new_accounts,
IdentityRequestDialogController::AccountSelectionCallback on_selected,
IdentityRequestDialogController::LoginToIdPCallback on_add_account,
IdentityRequestDialogController::DismissCallback dismiss_callback,
IdentityRequestDialogController::AccountsDisplayedCallback
accounts_displayed_callback) override {
state_->did_show_accounts_dialog = true;
state_->rp_for_display = base::UTF16ToUTF8(rp_data.rp_for_display);
state_->iframe_for_display = base::UTF16ToUTF8(rp_data.iframe_for_display);
if (accounts_dialog_action_ == AccountsDialogAction::kSelectAccount) {
std::move(on_selected)
.Run(GURL(kProviderUrlFull), kAccountId, /*is_sign_in=*/true);
}
return true;
}
private:
AccountsDialogAction accounts_dialog_action_{AccountsDialogAction::kNone};
raw_ptr<State> state_;
};
class TestApiPermissionDelegate : public MockApiPermissionDelegate {
public:
ApiPermissionStatus GetApiPermissionStatus(
const url::Origin& origin) override {
return ApiPermissionStatus::GRANTED;
}
};
class TestFederatedIdentityModalDialogViewDelegate
: public NiceMock<MockModalDialogViewDelegate> {
public:
base::WeakPtr<TestFederatedIdentityModalDialogViewDelegate> GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
private:
base::WeakPtrFactory<TestFederatedIdentityModalDialogViewDelegate>
weak_ptr_factory_{this};
};
} // namespace
class RequestServiceMultipleFramesTest : public RenderViewHostImplTestHarness {
protected:
RequestServiceMultipleFramesTest() = default;
~RequestServiceMultipleFramesTest() override = default;
void SetUp() override {
RenderViewHostImplTestHarness::SetUp();
// Initialize `kAccounts` on SetUp() to ensure it is initialized correctly
// in every test.
kAccounts = {base::MakeRefCounted<IdentityRequestAccount>(
kAccountId, // id
"ken@idp.example", // display_identifier
"Ken R. Example", // display_name
"ken@idp.example", // email
"Ken R. Example", // name
"Ken", // given_name
GURL(), // picture
"(403) 293-3421", // phone
"@kenr", // username
std::vector<std::string>(), // login_hints
std::vector<std::string>(), // domain_hints
std::vector<std::string>() // labels
)};
sSendClientMatchesTopFrameOrigin = false;
test_api_permission_delegate_ =
std::make_unique<TestApiPermissionDelegate>();
mock_auto_reauthn_permission_delegate_ =
std::make_unique<NiceMock<MockAutoReauthnPermissionDelegate>>();
mock_permission_delegate_ =
std::make_unique<NiceMock<MockPermissionDelegate>>();
test_modal_dialog_view_delegate_ =
std::make_unique<TestFederatedIdentityModalDialogViewDelegate>();
mock_identity_registry_ = std::make_unique<NiceMock<MockIdentityRegistry>>(
web_contents(), test_modal_dialog_view_delegate_->GetWeakPtr(),
GURL(kIdpUrl));
static_cast<TestWebContents*>(web_contents())
->NavigateAndCommit(GURL(kTopFrameUrl), ui::PAGE_TRANSITION_LINK);
ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
}
// Does token request and waits for result.
void DoRequestTokenAndWait(
mojo::Remote<blink::mojom::FederatedAuthRequest>& request_remote,
AuthRequestCallbackHelper& callback_helper) {
DoRequestToken(request_remote, callback_helper.callback());
request_remote.set_disconnect_handler(callback_helper.quit_closure());
// Ensure that the request makes its way to RequestService.
base::RunLoop().RunUntilIdle();
// Fast forward clock so that the pending
// RequestService::OnRejectRequest() task, if any, gets a
// chance to run.
task_environment()->FastForwardBy(base::Minutes(10));
callback_helper.WaitForCallback();
request_remote.set_disconnect_handler(base::OnceClosure());
}
RequestService* CreateRequestService(
RenderFrameHost& render_frame_host,
mojo::Remote<blink::mojom::FederatedAuthRequest>& request_remote,
TestDialogController::AccountsDialogAction accounts_dialog_action,
TestDialogController::State* dialog_controller_state) {
RequestService* federated_auth_request_impl =
&RequestService::CreateForTesting(
render_frame_host, test_api_permission_delegate_.get(),
mock_auto_reauthn_permission_delegate_.get(),
mock_permission_delegate_.get(), mock_identity_registry_.get(),
request_remote.BindNewPipeAndPassReceiver());
federated_auth_request_impl->SetDialogControllerForTests(
std::make_unique<TestDialogController>(accounts_dialog_action,
dialog_controller_state));
federated_auth_request_impl->SetNetworkManagerForTests(
std::make_unique<TestIdpNetworkRequestManager>());
return federated_auth_request_impl;
}
void DoRequestToken(
mojo::Remote<blink::mojom::FederatedAuthRequest>& request_remote,
RequestTokenCallback callback,
const char* provider = kProviderUrlFull) {
auto config_ptr = blink::mojom::IdentityProviderConfig::New();
config_ptr->config_url = GURL(provider);
config_ptr->client_id = kClientId;
auto federated = blink::mojom::IdentityProviderRequestOptions::New();
federated->config = std::move(config_ptr);
federated->nonce = kNonce;
std::vector<blink::mojom::IdentityProviderRequestOptionsPtr> idp_ptrs;
idp_ptrs.push_back(std::move(federated));
auto get_params = blink::mojom::IdentityProviderGetParameters::New(
std::move(idp_ptrs),
/*rp_context=*/blink::mojom::RpContext::kSignIn,
/*rp_mode=*/blink::mojom::RpMode::kPassive);
std::vector<blink::mojom::IdentityProviderGetParametersPtr> idp_get_params;
idp_get_params.push_back(std::move(get_params));
request_remote->RequestToken(std::move(idp_get_params),
MediationRequirement::kOptional,
std::move(callback));
request_remote.FlushForTesting();
}
void ExpectUkmValueInEntry(const std::string& metric_name,
const char* entry_name,
int expected_value) {
auto entries = ukm_recorder_->GetEntriesByName(entry_name);
int count = 0;
for (const ukm::mojom::UkmEntry* const entry : entries) {
const int64_t* value = ukm_recorder_->GetEntryMetric(entry, metric_name);
if (!value) {
continue;
}
++count;
}
EXPECT_GT(count, 0) << "Did not find " << metric_name << " in "
<< entry_name;
}
ukm::TestAutoSetUkmRecorder* ukm_recorder() { return ukm_recorder_.get(); }
protected:
std::unique_ptr<TestApiPermissionDelegate> test_api_permission_delegate_;
std::unique_ptr<NiceMock<MockAutoReauthnPermissionDelegate>>
mock_auto_reauthn_permission_delegate_;
std::unique_ptr<NiceMock<MockPermissionDelegate>> mock_permission_delegate_;
std::unique_ptr<TestFederatedIdentityModalDialogViewDelegate>
test_modal_dialog_view_delegate_;
std::unique_ptr<NiceMock<MockIdentityRegistry>> mock_identity_registry_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
};
// Test that test harness can execute successful FedCM flow for iframe.
TEST_F(RequestServiceMultipleFramesTest, TestHarness) {
RenderFrameHost* iframe_rfh = content::RenderFrameHostTester::For(main_rfh())
->AppendChild(/*frame_name=*/"");
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
CreateRequestService(
*iframe_rfh, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
AuthRequestCallbackHelper iframe_callback_helper;
DoRequestTokenAndWait(iframe_request_remote, iframe_callback_helper);
EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
}
// Test that FedCM request fails on iframe if there is an in-progress FedCM
// request for a different frame on the page.
TEST_F(RequestServiceMultipleFramesTest, IframeTooManyRequests) {
base::HistogramTester histogram_tester;
mojo::Remote<blink::mojom::FederatedAuthRequest> main_frame_request_remote;
TestDialogController::State main_frame_dialog_state;
CreateRequestService(*main_rfh(), main_frame_request_remote,
TestDialogController::AccountsDialogAction::kNone,
&main_frame_dialog_state);
DoRequestToken(main_frame_request_remote, RequestTokenCallback());
EXPECT_TRUE(main_frame_dialog_state.did_show_accounts_dialog);
RenderFrameHost* iframe_rfh = content::RenderFrameHostTester::For(main_rfh())
->AppendChild(/*frame_name=*/"");
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
CreateRequestService(
*iframe_rfh, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
AuthRequestCallbackHelper iframe_callback_helper;
DoRequestTokenAndWait(iframe_request_remote, iframe_callback_helper);
EXPECT_EQ(RequestTokenStatus::kErrorTooManyRequests,
iframe_callback_helper.status());
EXPECT_FALSE(iframe_dialog_state.did_show_accounts_dialog);
histogram_tester.ExpectUniqueSample(
"Blink.FedCm.MultipleRequestsFromDifferentIdPs", 0, 1);
}
// Test that when requests from different IdPs get rejected, a proper histogram
// can be recorded.
TEST_F(RequestServiceMultipleFramesTest, IframeTooManyRequestsDifferentIdP) {
base::HistogramTester histogram_tester;
mojo::Remote<blink::mojom::FederatedAuthRequest> main_frame_request_remote;
TestDialogController::State main_frame_dialog_state;
CreateRequestService(*main_rfh(), main_frame_request_remote,
TestDialogController::AccountsDialogAction::kNone,
&main_frame_dialog_state);
DoRequestToken(main_frame_request_remote, RequestTokenCallback());
EXPECT_TRUE(main_frame_dialog_state.did_show_accounts_dialog);
RenderFrameHost* iframe_rfh = content::RenderFrameHostTester::For(main_rfh())
->AppendChild(/*frame_name=*/"");
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
CreateRequestService(
*iframe_rfh, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
// Initiates a new API call with a different IdP.
DoRequestToken(iframe_request_remote, RequestTokenCallback(),
kProviderUrlTwoFull);
EXPECT_FALSE(iframe_dialog_state.did_show_accounts_dialog);
histogram_tester.ExpectUniqueSample(
"Blink.FedCm.MultipleRequestsFromDifferentIdPs", 1, 1);
}
// Test that only top frame URL is available for display when FedCM is called
// within iframes which are same-origin with the top frame.
TEST_F(RequestServiceMultipleFramesTest, SameOriginIframe) {
base::HistogramTester histogram_tester;
const char kSameOriginIframeUrl[] = "https://top-frame.example/iframe.html";
RenderFrameHost* same_origin_iframe =
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kSameOriginIframeUrl),
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
->AppendChild("same_origin_iframe"));
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
CreateRequestService(
*same_origin_iframe, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
base::RunLoop ukm_loop;
ukm_recorder()->SetOnAddEntryCallback(FedCmEntry::kEntryName,
ukm_loop.QuitClosure());
AuthRequestCallbackHelper iframe_callback_helper;
DoRequestTokenAndWait(iframe_request_remote, iframe_callback_helper);
ukm_loop.Run();
EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
EXPECT_EQ("top-frame.example", iframe_dialog_state.rp_for_display);
// Same-origin iframe is treated the same as same-site frame.
histogram_tester.ExpectUniqueSample(
"Blink.FedCm.FrameType",
static_cast<int>(RequesterFrameType::kSameSiteIframe), 1);
ExpectUkmValueInEntry("FrameType", FedCmEntry::kEntryName,
static_cast<int>(RequesterFrameType::kSameSiteIframe));
ExpectUkmValueInEntry("FrameType", FedCmIdpEntry::kEntryName,
static_cast<int>(RequesterFrameType::kSameSiteIframe));
}
// Test that only top frame URL is available for display when FedCM is called
// within iframes which are same-site with the top frame.
TEST_F(RequestServiceMultipleFramesTest, SameSiteIframe) {
base::HistogramTester histogram_tester;
const char kSameSiteIframeUrl[] =
"https://subdomain.top-frame.example/iframe.html";
RenderFrameHost* same_site_iframe =
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kSameSiteIframeUrl),
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
->AppendChild("same_site_iframe"));
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
CreateRequestService(
*same_site_iframe, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
base::RunLoop ukm_loop;
ukm_recorder()->SetOnAddEntryCallback(FedCmEntry::kEntryName,
ukm_loop.QuitClosure());
AuthRequestCallbackHelper iframe_callback_helper;
DoRequestTokenAndWait(iframe_request_remote, iframe_callback_helper);
ukm_loop.Run();
EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
EXPECT_EQ("top-frame.example", iframe_dialog_state.rp_for_display);
histogram_tester.ExpectUniqueSample(
"Blink.FedCm.FrameType",
static_cast<int>(RequesterFrameType::kSameSiteIframe), 1);
ExpectUkmValueInEntry("FrameType", FedCmEntry::kEntryName,
static_cast<int>(RequesterFrameType::kSameSiteIframe));
ExpectUkmValueInEntry("FrameType", FedCmIdpEntry::kEntryName,
static_cast<int>(RequesterFrameType::kSameSiteIframe));
}
// Test that both top frame and iframe URLs are available for display when FedCM
// is called within iframes which are cross-site with the top frame.
TEST_F(RequestServiceMultipleFramesTest, CrossSiteIframe) {
base::HistogramTester histogram_tester;
const char kCrossSiteIframeUrl[] = "https://cross-site.example/iframe.html";
RenderFrameHost* cross_site_iframe =
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kCrossSiteIframeUrl),
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
->AppendChild("cross_site_iframe"));
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
CreateRequestService(
*cross_site_iframe, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
base::RunLoop ukm_loop;
ukm_recorder()->SetOnAddEntryCallback(FedCmEntry::kEntryName,
ukm_loop.QuitClosure());
AuthRequestCallbackHelper iframe_callback_helper;
DoRequestTokenAndWait(iframe_request_remote, iframe_callback_helper);
ukm_loop.Run();
EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
EXPECT_EQ("top-frame.example", iframe_dialog_state.rp_for_display);
histogram_tester.ExpectUniqueSample(
"Blink.FedCm.FrameType",
static_cast<int>(RequesterFrameType::kCrossSiteIframe), 1);
ExpectUkmValueInEntry("FrameType", FedCmEntry::kEntryName,
static_cast<int>(RequesterFrameType::kCrossSiteIframe));
ExpectUkmValueInEntry("FrameType", FedCmIdpEntry::kEntryName,
static_cast<int>(RequesterFrameType::kCrossSiteIframe));
}
// Tests that preventSilentAccess UKM is not recorded if the embedder does not
// have sharing permissions.
TEST_F(RequestServiceMultipleFramesTest,
IframePreventSilentAccessNoSharingPermission) {
const char kSameSiteIframeUrl[] =
"https://subdomain.top-frame.example/iframe.html";
RenderFrameHost* same_site_iframe =
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kSameSiteIframeUrl),
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
->AppendChild("same_site_iframe"));
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
auto* federated_auth_request_impl = CreateRequestService(
*same_site_iframe, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
// Assume that the embeddder does not have a sharing permission, and hence UKM
// should not be recorded.
EXPECT_CALL(*mock_permission_delegate_,
HasSharingPermission(url::Origin::Create(GURL(kTopFrameUrl))))
.WillOnce(testing::Return(false));
base::RunLoop ukm_loop;
ukm_recorder_->SetOnAddEntryCallback(FedCmEntry::kEntryName,
ukm_loop.QuitClosure());
federated_auth_request_impl->PreventSilentAccess(base::DoNothing());
// Perform an actual FedCM request to log some metrics and flush the ukm
// recorder.
AuthRequestCallbackHelper iframe_callback_helper;
DoRequestTokenAndWait(iframe_request_remote, iframe_callback_helper);
ukm_loop.Run();
auto entries = ukm_recorder_->GetEntriesByName(FedCmEntry::kEntryName);
ASSERT_FALSE(entries.empty());
for (const ukm::mojom::UkmEntry* entry : entries) {
const int64_t* metric =
ukm_recorder_->GetEntryMetric(entry, "PreventSilentAccessFrameType");
EXPECT_FALSE(metric);
}
}
// Tests the preventSilentAccess UKM recorded when invoked from the main frame.
TEST_F(RequestServiceMultipleFramesTest, MainFramePreventSilentAccess) {
// We add an iframe but it should not affect the UKM recording since we will
// call preventSilentAccess() from the main frame.
const char kSameSiteIframeUrl[] =
"https://subdomain.top-frame.example/iframe.html";
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kSameSiteIframeUrl),
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
->AppendChild("same_site_iframe"));
mojo::Remote<blink::mojom::FederatedAuthRequest> main_frame_request_remote;
TestDialogController::State main_frame_dialog_state;
auto* federated_auth_request_impl =
CreateRequestService(*main_rfh(), main_frame_request_remote,
TestDialogController::AccountsDialogAction::kNone,
&main_frame_dialog_state);
// Assume that the embeddder does has a sharing permission so that UKM is
// recorded.
EXPECT_CALL(*mock_permission_delegate_,
HasSharingPermission(url::Origin::Create(GURL(kTopFrameUrl))))
.WillOnce(testing::Return(true));
base::RunLoop ukm_loop;
ukm_recorder_->SetOnAddEntryCallback(FedCmEntry::kEntryName,
ukm_loop.QuitClosure());
federated_auth_request_impl->PreventSilentAccess(base::DoNothing());
ukm_loop.Run();
auto entries = ukm_recorder_->GetEntriesByName(FedCmEntry::kEntryName);
ASSERT_FALSE(entries.empty());
bool metric_found = false;
for (const ukm::mojom::UkmEntry* entry : entries) {
const int64_t* metric =
ukm_recorder_->GetEntryMetric(entry, "PreventSilentAccessFrameType");
if (metric) {
metric_found = true;
EXPECT_EQ(*metric, static_cast<int>(RequesterFrameType::kMainFrame));
}
}
EXPECT_TRUE(metric_found);
}
// Tests the preventSilentAccess UKM recorded when invoked from a same site
// iframe.
TEST_F(RequestServiceMultipleFramesTest, SameSiteIframePreventSilentAccess) {
const char kSameSiteIframeUrl[] =
"https://subdomain.top-frame.example/iframe.html";
RenderFrameHost* same_site_iframe =
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kSameSiteIframeUrl),
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
->AppendChild("same_site_iframe"));
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
auto* federated_auth_request_impl = CreateRequestService(
*same_site_iframe, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
// Assume that the embeddder does has a sharing permission so that UKM is
// recorded.
EXPECT_CALL(*mock_permission_delegate_,
HasSharingPermission(url::Origin::Create(GURL(kTopFrameUrl))))
.WillOnce(testing::Return(true));
base::RunLoop ukm_loop;
ukm_recorder_->SetOnAddEntryCallback(FedCmEntry::kEntryName,
ukm_loop.QuitClosure());
federated_auth_request_impl->PreventSilentAccess(base::DoNothing());
ukm_loop.Run();
auto entries = ukm_recorder_->GetEntriesByName(FedCmEntry::kEntryName);
ASSERT_FALSE(entries.empty());
bool metric_found = false;
for (const ukm::mojom::UkmEntry* entry : entries) {
const int64_t* metric =
ukm_recorder_->GetEntryMetric(entry, "PreventSilentAccessFrameType");
if (metric) {
metric_found = true;
EXPECT_EQ(*metric, static_cast<int>(RequesterFrameType::kSameSiteIframe));
}
}
EXPECT_TRUE(metric_found);
}
// Tests the preventSilentAccess UKM recorded when invoked from a cross site
// iframe.
TEST_F(RequestServiceMultipleFramesTest, CrossSiteIframePreventSilentAccess) {
const char kCrossSiteIframeUrl[] = "https://cross-site.example/iframe.html";
RenderFrameHost* cross_site_iframe =
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kCrossSiteIframeUrl),
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
->AppendChild("cross_site_iframe"));
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
auto* federated_auth_request_impl = CreateRequestService(
*cross_site_iframe, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
// Assume that the embeddder does has a sharing permission so that UKM is
// recorded.
EXPECT_CALL(*mock_permission_delegate_,
HasSharingPermission(url::Origin::Create(GURL(kTopFrameUrl))))
.WillOnce(testing::Return(true));
base::RunLoop ukm_loop;
ukm_recorder_->SetOnAddEntryCallback(FedCmEntry::kEntryName,
ukm_loop.QuitClosure());
federated_auth_request_impl->PreventSilentAccess(base::DoNothing());
ukm_loop.Run();
auto entries = ukm_recorder_->GetEntriesByName(FedCmEntry::kEntryName);
ASSERT_FALSE(entries.empty());
bool metric_found = false;
for (const ukm::mojom::UkmEntry* entry : entries) {
const int64_t* metric =
ukm_recorder_->GetEntryMetric(entry, "PreventSilentAccessFrameType");
if (metric) {
metric_found = true;
EXPECT_EQ(*metric,
static_cast<int>(RequesterFrameType::kCrossSiteIframe));
}
}
EXPECT_TRUE(metric_found);
}
// Tests that we send a client metadata request for cross-site iframes even if
// all accounts are returning.
TEST_F(RequestServiceMultipleFramesTest, CrossSiteIframeSendClientMetadata) {
base::test::ScopedFeatureList list;
list.InitAndEnableFeature(features::kFedCmIframeOrigin);
const char kCrossSiteIframeUrl[] = "https://cross-site.example/iframe.html";
RenderFrameHost* cross_site_iframe =
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kCrossSiteIframeUrl),
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
->AppendChild("cross_site_iframe"));
mojo::Remote<blink::mojom::FederatedAuthRequest> iframe_request_remote;
TestDialogController::State iframe_dialog_state;
CreateRequestService(
*cross_site_iframe, iframe_request_remote,
TestDialogController::AccountsDialogAction::kSelectAccount,
&iframe_dialog_state);
sSendClientMatchesTopFrameOrigin = true;
AuthRequestCallbackHelper iframe_callback_helper;
DoRequestTokenAndWait(iframe_request_remote, iframe_callback_helper);
EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
EXPECT_EQ("cross-site.example", iframe_dialog_state.iframe_for_display);
}
} // namespace content::webid