blob: 9dc01c29be7fa26ada77643a81a2ab1e9c1abc27 [file] [log] [blame]
// Copyright 2024 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/user_info_request.h"
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "base/functional/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.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 "content/browser/webid/metrics.h"
#include "content/browser/webid/test/mock_api_permission_delegate.h"
#include "content/browser/webid/test/mock_idp_network_request_manager.h"
#include "content/browser/webid/test/mock_permission_delegate.h"
#include "content/public/test/navigation_simulator.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_web_contents.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 "url/gurl.h"
#include "url/origin.h"
using ApiPermissionStatus =
content::FederatedIdentityApiPermissionContextDelegate::PermissionStatus;
using FetchStatus = content::IdpNetworkRequestManager::FetchStatus;
using LoginState = content::IdentityRequestAccount::LoginState;
using ParseStatus = content::IdpNetworkRequestManager::ParseStatus;
using blink::mojom::RequestUserInfoStatus;
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
namespace content::webid {
namespace {
constexpr char kRpUrl[] = "https://rp.example";
constexpr char kPersonalizedButtonFrameUrl[] = "https://idp.example/button";
constexpr char kProviderUrl[] = "https://idp.example/fedcm.json";
constexpr char kAccountsEndpoint[] = "https://idp.example/accounts";
constexpr char kTokenEndpoint[] = "https://idp.example/token";
constexpr char kLoginUrl[] = "https://idp.example/login";
constexpr char kClientId[] = "client_id_123";
constexpr char kAccountEmailFormat[] = "%s@foo.com";
constexpr char kAccountName[] = "The Liliputian";
constexpr char kAccountGivenName[] = "Julius";
constexpr char kAccountPicture[] = "https://image.com/yolo";
constexpr char kAccountPhone[] = "(650) 243-3243";
constexpr char kAccountUsername[] = "@julius";
struct AccountConfig {
std::string id;
std::optional<IdentityRequestAccount::LoginState> login_state;
bool was_granted_sharing_permission;
};
struct Config {
std::optional<bool> idp_signin_status;
std::vector<AccountConfig> accounts;
FetchStatus config_fetch_status;
FetchStatus accounts_fetch_status;
};
Config kValidConfig = {
/*idp_signin_status=*/true,
/*accounts=*/
{{"account1", /*idp_claimed_login_state=*/std::nullopt,
/*was_granted_sharing_permission=*/true}},
/*config_fetch_status=*/{ParseStatus::kSuccess, net::HTTP_OK},
/*accounts_fetch_status=*/{ParseStatus::kSuccess, net::HTTP_OK}};
std::string GenerateEmailForUserId(const std::string& user_id) {
return base::StringPrintf(kAccountEmailFormat, user_id.c_str());
}
// Helper class for blocking till RequestUserInfoCallback is called.
class UserInfoCallbackHelper {
public:
UserInfoCallbackHelper() = default;
~UserInfoCallbackHelper() = default;
UserInfoCallbackHelper(const UserInfoCallbackHelper&) = delete;
UserInfoCallbackHelper& operator=(const UserInfoCallbackHelper&) = delete;
// This can only be called once per lifetime of this object.
blink::mojom::FederatedAuthRequest::RequestUserInfoCallback callback() {
return base::BindOnce(&UserInfoCallbackHelper::Complete,
base::Unretained(this));
}
// Returns when callback() is called, which can be immediately if it has
// already been called.
void WaitForCallback() {
if (was_called_) {
return;
}
wait_for_callback_loop_.Run();
}
RequestUserInfoStatus user_info_status_;
std::optional<std::vector<blink::mojom::IdentityUserInfoPtr>> user_info_;
private:
void Complete(
RequestUserInfoStatus user_info_status,
std::optional<std::vector<blink::mojom::IdentityUserInfoPtr>> user_info) {
CHECK(!was_called_);
user_info_status_ = user_info_status;
user_info_ = std::move(user_info);
was_called_ = true;
wait_for_callback_loop_.Quit();
}
bool was_called_{false};
base::RunLoop wait_for_callback_loop_;
};
class TestIdpNetworkRequestManager : public MockIdpNetworkRequestManager {
public:
explicit TestIdpNetworkRequestManager(const Config& config)
: config_(config) {}
~TestIdpNetworkRequestManager() override = default;
void FetchWellKnown(const GURL& provider,
FetchWellKnownCallback callback) override {
has_fetched_well_known_ = true;
FetchStatus fetch_status = {ParseStatus::kSuccess, net::HTTP_OK};
IdpNetworkRequestManager::WellKnown well_known;
std::set<GURL> well_known_urls = {GURL(kProviderUrl)};
well_known.provider_urls = std::move(well_known_urls);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), fetch_status, 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 {
has_fetched_config_ = true;
IdpNetworkRequestManager::Endpoints endpoints;
endpoints.accounts = GURL(kAccountsEndpoint);
endpoints.token = GURL(kTokenEndpoint);
IdentityProviderMetadata idp_metadata;
idp_metadata.config_url = GURL(kProviderUrl);
idp_metadata.idp_login_url = GURL(kLoginUrl);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), config_.config_fetch_status,
endpoints, idp_metadata));
}
void SendAccountsRequest(const url::Origin& idp_origin,
const GURL& accounts_url,
const std::string& client_id,
AccountsRequestCallback callback) override {
has_fetched_accounts_endpoint_ = true;
std::vector<IdentityRequestAccountPtr> accounts;
for (const AccountConfig& account_config : config_.accounts) {
accounts.emplace_back(base::MakeRefCounted<IdentityRequestAccount>(
account_config.id, GenerateEmailForUserId(account_config.id),
kAccountName, GenerateEmailForUserId(account_config.id), kAccountName,
kAccountGivenName, GURL(kAccountPicture), kAccountPhone,
kAccountUsername,
/*login_hints=*/std::vector<std::string>(),
/*domain_hints=*/std::vector<std::string>(),
/*labels=*/std::vector<std::string>(), account_config.login_state));
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), config_.accounts_fetch_status,
std::move(accounts)));
}
bool DidFetchAnyEndpoint() {
return has_fetched_well_known_ || has_fetched_config_ ||
has_fetched_accounts_endpoint_;
}
base::WeakPtr<TestIdpNetworkRequestManager> AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
protected:
bool has_fetched_well_known_{false};
bool has_fetched_config_{false};
bool has_fetched_accounts_endpoint_{false};
private:
const Config config_;
base::WeakPtrFactory<TestIdpNetworkRequestManager> weak_ptr_factory_{this};
};
class TestApiPermissionDelegate : public MockApiPermissionDelegate {
public:
ApiPermissionStatus GetApiPermissionStatus(
const url::Origin& origin) override {
return ApiPermissionStatus::GRANTED;
}
};
class TestPermissionDelegate : public MockPermissionDelegate {
public:
bool HasSharingPermission(const url::Origin& relying_party_requester,
const url::Origin& relying_party_embedder,
const url::Origin& identity_provider) override {
url::Origin rp_origin_with_data = url::Origin::Create(GURL(kRpUrl));
url::Origin idp_origin_with_data =
url::Origin::Create(GURL(kPersonalizedButtonFrameUrl));
bool has_granted_permission_per_profile =
relying_party_requester == rp_origin_with_data &&
relying_party_embedder == rp_origin_with_data &&
identity_provider == idp_origin_with_data;
return has_granted_permission_per_profile &&
!accounts_with_sharing_permission_.empty();
}
std::optional<base::Time> GetLastUsedTimestamp(
const url::Origin& relying_party_requester,
const url::Origin& relying_party_embedder,
const url::Origin& identity_provider,
const std::string& account_id) override {
url::Origin rp_origin_with_data = url::Origin::Create(GURL(kRpUrl));
url::Origin idp_origin_with_data =
url::Origin::Create(GURL(kPersonalizedButtonFrameUrl));
bool has_granted_permission_per_profile =
relying_party_requester == rp_origin_with_data &&
relying_party_embedder == rp_origin_with_data &&
identity_provider == idp_origin_with_data;
return has_granted_permission_per_profile &&
accounts_with_sharing_permission_.count(account_id)
? std::make_optional<base::Time>()
: std::nullopt;
}
std::optional<bool> GetIdpSigninStatus(
const url::Origin& idp_origin) override {
return idp_signin_status_;
}
void SetConfig(const Config& config) {
idp_signin_status_ = config.idp_signin_status;
accounts_with_sharing_permission_.clear();
for (const AccountConfig& account_config : config.accounts) {
if (account_config.was_granted_sharing_permission) {
accounts_with_sharing_permission_.insert(account_config.id);
}
}
}
private:
std::optional<bool> idp_signin_status_;
std::set<std::string> accounts_with_sharing_permission_;
};
} // namespace
class UserInfoRequestTest : public RenderViewHostImplTestHarness {
public:
~UserInfoRequestTest() override = default;
void SetUp() override {
RenderViewHostImplTestHarness::SetUp();
api_permission_delegate_ = std::make_unique<TestApiPermissionDelegate>();
permission_delegate_ = std::make_unique<TestPermissionDelegate>();
static_cast<TestWebContents*>(web_contents())
->NavigateAndCommit(GURL(kRpUrl), ui::PAGE_TRANSITION_LINK);
// Add a subframe that navigates to kPersonalizedButtonFrameUrl.
TestRenderFrameHost* subframe = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(main_rfh())
->AppendChild("subframe"));
iframe_render_frame_host_ = static_cast<TestRenderFrameHost*>(
NavigationSimulator::NavigateAndCommitFromDocument(
GURL(kPersonalizedButtonFrameUrl), subframe));
}
void TearDown() override {
iframe_render_frame_host_ = nullptr;
RenderViewHostImplTestHarness::TearDown();
}
void RunUserInfoTest(
const Config& config,
RequestUserInfoStatus expected_user_info_status,
const std::vector<std::string>& expected_account_user_ids) {
permission_delegate_->SetConfig(config);
auto network_manager =
std::make_unique<TestIdpNetworkRequestManager>(config);
network_manager_ = network_manager->AsWeakPtr();
blink::mojom::IdentityProviderConfigPtr idp_ptr =
blink::mojom::IdentityProviderConfig::New();
idp_ptr->config_url = GURL(kProviderUrl);
idp_ptr->client_id = kClientId;
UserInfoCallbackHelper callback_helper;
request_ = UserInfoRequest::Create(
std::move(network_manager), permission_delegate_.get(),
api_permission_delegate_.get(), iframe_render_frame_host_,
std::move(idp_ptr));
request_->SetCallbackAndStart(callback_helper.callback());
callback_helper.WaitForCallback();
EXPECT_EQ(expected_user_info_status, callback_helper.user_info_status_);
CheckUserInfo(expected_account_user_ids, callback_helper.user_info_);
}
void CheckUserInfo(
const std::vector<std::string>& expected_account_ids,
const std::optional<std::vector<blink::mojom::IdentityUserInfoPtr>>&
actual_user_info) {
if (expected_account_ids.empty()) {
EXPECT_EQ(actual_user_info, std::nullopt);
return;
}
EXPECT_EQ(expected_account_ids.size(), actual_user_info->size());
for (size_t i = 0; i < expected_account_ids.size(); ++i) {
EXPECT_EQ(GenerateEmailForUserId(expected_account_ids[i]),
actual_user_info->at(i)->email);
}
}
bool DidFetchAnyEndpoint() { return network_manager_->DidFetchAnyEndpoint(); }
void ExpectConsoleMessage(const std::string& message) {
std::vector<std::string> messages =
RenderFrameHostTester::For(iframe_render_frame_host_)
->GetConsoleMessages();
ASSERT_EQ(messages.size(), 1u);
EXPECT_EQ(messages[0], message);
}
void ExpectUniqueIssue(UserInfoRequestResult result) {
EXPECT_EQ(
iframe_render_frame_host_->GetFederatedAuthUserInfoRequestIssueCount(
result),
1);
EXPECT_EQ(
iframe_render_frame_host_->GetFederatedAuthUserInfoRequestIssueCount(
std::nullopt),
1);
}
protected:
raw_ptr<TestRenderFrameHost> iframe_render_frame_host_;
base::WeakPtr<TestIdpNetworkRequestManager> network_manager_;
std::unique_ptr<TestApiPermissionDelegate> api_permission_delegate_;
std::unique_ptr<TestPermissionDelegate> permission_delegate_;
std::unique_ptr<UserInfoRequest> request_;
base::HistogramTester histogram_tester_;
};
TEST_F(UserInfoRequestTest, PreviouslySignedIn) {
const char kAccount1Id[] = "account1";
const char kAccount2Id[] = "account2";
Config config = kValidConfig;
config.accounts = {{kAccount1Id, /*idp_claimed_login_state=*/std::nullopt,
/*was_granted_sharing_permission=*/true},
{kAccount2Id, /*idp_claimed_login_state=*/std::nullopt,
/*was_granted_sharing_permission=*/false}};
RunUserInfoTest(config, RequestUserInfoStatus::kSuccess,
{kAccount1Id, kAccount2Id});
histogram_tester_.ExpectUniqueSample("Blink.FedCm.UserInfo.Status",
UserInfoRequestResult::kSuccess, 1);
histogram_tester_.ExpectUniqueSample("Blink.FedCm.UserInfo.NumAccounts",
Metrics::NumAccounts::kMultiple, 1);
histogram_tester_.ExpectTotalCount(
"Blink.FedCm.UserInfo.TimeToRequestCompleted", 1);
}
TEST_F(UserInfoRequestTest, NoSignedInAccount) {
const char kAccount1Id[] = "account1";
const char kAccount2Id[] = "account2";
Config config = kValidConfig;
config.accounts = {{kAccount1Id, /*idp_claimed_login_state=*/std::nullopt,
/*was_granted_sharing_permission=*/false},
{kAccount2Id, /*idp_claimed_login_state=*/std::nullopt,
/*was_granted_sharing_permission=*/false}};
RunUserInfoTest(config, RequestUserInfoStatus::kError, {});
EXPECT_FALSE(DidFetchAnyEndpoint());
histogram_tester_.ExpectUniqueSample(
"Blink.FedCm.UserInfo.Status",
UserInfoRequestResult::kNoAccountSharingPermission, 1);
histogram_tester_.ExpectTotalCount("Blink.FedCm.UserInfo.NumAccounts", 0);
histogram_tester_.ExpectTotalCount(
"Blink.FedCm.UserInfo.TimeToRequestCompleted", 0);
ExpectConsoleMessage(
"getUserInfo() failed because the user has not yet used FedCM on this "
"site with the provided IDP.");
ExpectUniqueIssue(UserInfoRequestResult::kNoAccountSharingPermission);
}
TEST_F(UserInfoRequestTest, NotInApprovedClientsList) {
const char kAccount1Id[] = "account1";
const char kAccount2Id[] = "account2";
Config config = kValidConfig;
config.accounts = {
{kAccount1Id, /*idp_claimed_login_state=*/LoginState::kSignUp,
/*was_granted_sharing_permission=*/true},
{kAccount2Id, /*idp_claimed_login_state=*/LoginState::kSignUp,
/*was_granted_sharing_permission=*/true}};
RunUserInfoTest(config, RequestUserInfoStatus::kError, {});
histogram_tester_.ExpectUniqueSample(
"Blink.FedCm.UserInfo.Status",
UserInfoRequestResult::kNoReturningUserFromFetchedAccounts, 1);
histogram_tester_.ExpectUniqueSample("Blink.FedCm.UserInfo.NumAccounts",
Metrics::NumAccounts::kZero, 1);
histogram_tester_.ExpectTotalCount(
"Blink.FedCm.UserInfo.TimeToRequestCompleted", 1);
ExpectConsoleMessage(
"getUserInfo() failed because no account received was a returning "
"account.");
ExpectUniqueIssue(UserInfoRequestResult::kNoReturningUserFromFetchedAccounts);
}
TEST_F(UserInfoRequestTest, InApprovedClientsList) {
const char kAccount1Id[] = "account1";
const char kAccount2Id[] = "account2";
Config config = kValidConfig;
config.accounts = {
{kAccount1Id, /*idp_claimed_login_state=*/LoginState::kSignIn,
/*was_granted_sharing_permission=*/true},
{kAccount2Id, /*idp_claimed_login_state=*/LoginState::kSignUp,
/*was_granted_sharing_permission=*/true}};
RunUserInfoTest(config, RequestUserInfoStatus::kSuccess,
{kAccount1Id, kAccount2Id});
}
TEST_F(UserInfoRequestTest,
NoSharingPermissionButIdpHasThirdPartyCookiesAccessAndClaimsSignin) {
const char kAccountId[] = "account";
Config config = kValidConfig;
config.accounts = {{kAccountId,
/*idp_claimed_login_state=*/LoginState::kSignIn,
/*was_granted_sharing_permission=*/false}};
// Pretend the IdP was given third-party cookies access.
EXPECT_CALL(*api_permission_delegate_,
HasThirdPartyCookiesAccess(_, GURL(kProviderUrl),
url::Origin::Create(GURL(kRpUrl))))
.WillRepeatedly(Return(true));
RunUserInfoTest(config, RequestUserInfoStatus::kSuccess, {kAccountId});
histogram_tester_.ExpectUniqueSample("Blink.FedCm.UserInfo.Status",
UserInfoRequestResult::kSuccess, 1);
}
TEST_F(UserInfoRequestTest,
NoSharingPermissionButIdpHasThirdPartyCookiesAccessButNotSignin) {
const char kAccountId[] = "account";
Config config = kValidConfig;
config.accounts = {{kAccountId, /*idp_claimed_login_state=*/std::nullopt,
/*was_granted_sharing_permission=*/false}};
// Pretend the IdP was given third-party cookies access.
EXPECT_CALL(*api_permission_delegate_,
HasThirdPartyCookiesAccess(_, GURL(kProviderUrl),
url::Origin::Create(GURL(kRpUrl))))
.WillRepeatedly(Return(true));
RunUserInfoTest(config, RequestUserInfoStatus::kError, {});
histogram_tester_.ExpectUniqueSample(
"Blink.FedCm.UserInfo.Status",
UserInfoRequestResult::kNoReturningUserFromFetchedAccounts, 1);
}
TEST_F(UserInfoRequestTest, ConfigFetchFailed) {
Config config = kValidConfig;
config.config_fetch_status = {ParseStatus::kHttpNotFoundError, 404};
RunUserInfoTest(config, RequestUserInfoStatus::kError, {});
histogram_tester_.ExpectUniqueSample(
"Blink.FedCm.UserInfo.Status",
UserInfoRequestResult::kInvalidConfigOrWellKnown, 1);
histogram_tester_.ExpectTotalCount("Blink.FedCm.UserInfo.NumAccounts", 0);
histogram_tester_.ExpectTotalCount(
"Blink.FedCm.UserInfo.TimeToRequestCompleted", 0);
ExpectConsoleMessage(
"getUserInfo() failed because the config and well-known files were "
"invalid.");
ExpectUniqueIssue(UserInfoRequestResult::kInvalidConfigOrWellKnown);
}
TEST_F(UserInfoRequestTest, IdpSigninStatusClearedWhenAccountsRequestFails) {
std::vector<std::optional<bool>> kTestCases = {std::nullopt, true};
for (const std::optional<bool>& test_case : kTestCases) {
EXPECT_CALL(*permission_delegate_, SetIdpSigninStatus(_, false, _));
Config config = kValidConfig;
config.idp_signin_status = test_case;
config.accounts_fetch_status = {ParseStatus::kHttpNotFoundError, 404};
RunUserInfoTest(config, RequestUserInfoStatus::kError, {});
testing::Mock::VerifyAndClearExpectations(permission_delegate_.get());
}
}
// Tests that returning accounts are returned first in the user info response.
TEST_F(UserInfoRequestTest, ReturningAccountsFirst) {
const char kAccount1Id[] = "account1";
const char kAccount2Id[] = "account2";
const char kAccount3Id[] = "account3";
const char kAccount4Id[] = "account4";
Config config = kValidConfig;
config.accounts = {
{kAccount1Id, /*idp_claimed_login_state=*/LoginState::kSignUp,
/*was_granted_sharing_permission=*/false},
{kAccount2Id, /*idp_claimed_login_state=*/LoginState::kSignIn,
/*was_granted_sharing_permission=*/true},
{kAccount3Id, /*idp_claimed_login_state=*/LoginState::kSignUp,
/*was_granted_sharing_permission=*/false},
{kAccount4Id, /*idp_claimed_login_state=*/LoginState::kSignIn,
/*was_granted_sharing_permission=*/true}};
RunUserInfoTest(config, RequestUserInfoStatus::kSuccess,
{kAccount2Id, kAccount4Id, kAccount1Id, kAccount3Id});
}
} // namespace content::webid