blob: bbee44cfad77259745fe5ee00674103531f326cc [file] [log] [blame]
// Copyright 2025 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/navigation_interceptor.h"
#include <variant>
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/mock_navigation_throttle_registry.h"
#include "content/public/test/test_renderer_host.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/structured_headers.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.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"
using ::testing::_;
namespace content::webid {
class MockFederatedAuthRequest : public blink::mojom::FederatedAuthRequest {
public:
explicit MockFederatedAuthRequest() {}
MOCK_METHOD(
void,
RequestToken,
(std::vector<blink::mojom::IdentityProviderGetParametersPtr>
idp_get_params,
password_manager::CredentialMediationRequirement mediation_requirement,
RequestTokenCallback callback),
(override));
MOCK_METHOD(void, CancelTokenRequest, (), (override));
MOCK_METHOD(void,
RequestUserInfo,
(blink::mojom::IdentityProviderConfigPtr provider,
RequestUserInfoCallback callback),
(override));
MOCK_METHOD(void,
ResolveTokenRequest,
(const std::optional<std::string>& account_id,
base::Value token,
ResolveTokenRequestCallback callback),
(override));
MOCK_METHOD(
void,
SetIdpSigninStatus,
(const ::url::Origin& origin,
blink::mojom::IdpSigninStatus status,
const std::optional<::blink::common::webid::LoginStatusOptions>& options,
blink::mojom::FederatedAuthRequest::SetIdpSigninStatusCallback callback),
(override));
MOCK_METHOD(
void,
RegisterIdP,
(const ::GURL& url,
blink::mojom::FederatedAuthRequest::RegisterIdPCallback callback),
(override));
MOCK_METHOD(
void,
UnregisterIdP,
(const ::GURL& url,
blink::mojom::FederatedAuthRequest::UnregisterIdPCallback callback),
(override));
MOCK_METHOD(void, CloseModalDialogView, (), (override));
MOCK_METHOD(void,
PreventSilentAccess,
(blink::mojom::FederatedAuthRequest::PreventSilentAccessCallback
callback),
(override));
MOCK_METHOD(void,
Disconnect,
(blink::mojom::IdentityCredentialDisconnectOptionsPtr options,
blink::mojom::FederatedAuthRequest::DisconnectCallback callback),
(override));
};
net::structured_headers::Dictionary EncodeParams(
const std::map<std::string,
std::variant<std::string, std::vector<std::string>>>&
params) {
net::structured_headers::Dictionary dictionary;
for (const auto& pair : params) {
const std::string& key = pair.first;
const auto& value_variant = pair.second;
std::vector<net::structured_headers::ParameterizedItem>
member_items_for_param_member;
if (std::holds_alternative<std::string>(value_variant)) {
const std::string& value = std::get<std::string>(value_variant);
member_items_for_param_member.emplace_back(
net::structured_headers::Item(
value, net::structured_headers::Item::kStringType),
net::structured_headers::Parameters());
} else if (std::holds_alternative<std::vector<std::string>>(
value_variant)) {
const std::vector<std::string>& values =
std::get<std::vector<std::string>>(value_variant);
for (const auto& value : values) {
member_items_for_param_member.emplace_back(
net::structured_headers::Item(
value, net::structured_headers::Item::kStringType),
net::structured_headers::Parameters());
}
}
auto member = net::structured_headers::ParameterizedMember(
std::move(member_items_for_param_member),
net::structured_headers::Parameters());
if (std::holds_alternative<std::string>(value_variant)) {
member.member_is_inner_list = false;
}
dictionary[key] = std::move(member);
}
return dictionary;
}
class NavigationStartObserver : public WebContentsObserver {
public:
explicit NavigationStartObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
~NavigationStartObserver() override = default;
void DidStartNavigation(NavigationHandle* navigation_handle) override {
started_url_ = navigation_handle->GetURL();
if (wait_loop_) {
wait_loop_->Quit();
}
}
void Wait() {
if (started_url_.is_valid()) {
return;
}
wait_loop_ = std::make_unique<base::RunLoop>();
wait_loop_->Run();
}
const GURL& started_url() const { return started_url_; }
private:
std::unique_ptr<base::RunLoop> wait_loop_;
GURL started_url_;
};
class NavigationInterceptorTest : public RenderViewHostTestHarness {
public:
NavigationInterceptorTest() = default;
~NavigationInterceptorTest() override = default;
};
TEST_F(NavigationInterceptorTest, SerializedHeaderFormat) {
auto header = webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "1234"},
{"context", "continue"},
{"login_hint", "user@email.com"},
{"domain_hint", "domain.com"},
{"params", R"({"custom_key":"custom_value"})"},
{"fields", std::vector<std::string>{"name", "email"}},
});
EXPECT_EQ(
net::structured_headers::SerializeDictionary(header).value(),
R"(client_id="1234", config_url="https://idp.example/fedcm.json", context="continue", domain_hint="domain.com", fields=("name" "email"), login_hint="user@email.com", params="{\"custom_key\":\"custom_value\"}")");
}
TEST_F(NavigationInterceptorTest, WillProcessResponse) {
// Uses an in-process data decoder service for testing.
data_decoder::test::InProcessDataDecoder in_process_data_decoder;
std::unique_ptr<MockFederatedAuthRequest> federated_auth_request =
std::make_unique<MockFederatedAuthRequest>();
NavigateAndCommit(GURL("https://rp.example/"));
content::MockNavigationHandle mock_navigation_handle(web_contents());
mock_navigation_handle.set_render_frame_host(
web_contents()->GetPrimaryMainFrame());
mock_navigation_handle.set_is_in_primary_main_frame(true);
auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers->AddHeader("FedCM-Intercept-Navigation",
net::structured_headers::SerializeDictionary(
webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "1234"},
}))
.value());
mock_navigation_handle.set_response_headers(headers);
content::MockNavigationThrottleRegistry registry(&mock_navigation_handle);
webid::NavigationInterceptor interceptor(
registry, base::BindLambdaForTesting(
[&federated_auth_request](content::RenderFrameHost* rfh)
-> blink::mojom::FederatedAuthRequest* {
return federated_auth_request.get();
}));
GURL redirect_to("https://rp.example");
EXPECT_CALL(*federated_auth_request.get(), RequestToken(_, _, _))
.WillOnce(testing::WithArgs<2>(
[&redirect_to](
blink::mojom::FederatedAuthRequest::RequestTokenCallback
callback) {
base::Value::Dict token_dict;
token_dict.Set("redirect_to", redirect_to.spec());
std::move(callback).Run(
blink::mojom::RequestTokenStatus::kSuccess,
/*selected_identity_provider_config_url=*/GURL(),
base::Value(token_dict.Clone()),
/*error=*/nullptr,
/*is_auto_selected=*/false);
}));
NavigationStartObserver observer(web_contents());
auto result = interceptor.WillProcessResponse();
EXPECT_EQ(result, content::NavigationThrottle::DEFER);
observer.Wait();
EXPECT_EQ(observer.started_url(), redirect_to);
}
TEST_F(NavigationInterceptorTest, WillProcessResponseTokenRequestFails) {
// Uses an in-process data decoder service for testing.
data_decoder::test::InProcessDataDecoder in_process_data_decoder;
auto federated_auth_request = std::make_unique<MockFederatedAuthRequest>();
NavigateAndCommit(GURL("https://rp.example/"));
content::MockNavigationHandle mock_navigation_handle(web_contents());
mock_navigation_handle.set_render_frame_host(
web_contents()->GetPrimaryMainFrame());
mock_navigation_handle.set_is_in_primary_main_frame(true);
auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers->AddHeader("FedCM-Intercept-Navigation",
net::structured_headers::SerializeDictionary(
webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "1234"},
}))
.value());
mock_navigation_handle.set_response_headers(headers);
content::MockNavigationThrottleRegistry registry(&mock_navigation_handle);
webid::NavigationInterceptor interceptor(
registry, base::BindLambdaForTesting(
[&federated_auth_request](content::RenderFrameHost* rfh)
-> blink::mojom::FederatedAuthRequest* {
return federated_auth_request.get();
}));
EXPECT_CALL(*federated_auth_request.get(), RequestToken(_, _, _))
.WillOnce(testing::WithArgs<2>(
[](blink::mojom::FederatedAuthRequest::RequestTokenCallback
callback) {
std::move(callback).Run(
blink::mojom::RequestTokenStatus::kError,
/*selected_identity_provider_config_url=*/std::nullopt,
/*token=*/std::nullopt,
/*error=*/nullptr,
/*is_auto_selected=*/false);
}));
base::RunLoop run_loop;
bool was_cancelled = false;
std::optional<NavigationThrottle::ThrottleCheckResult> cancel_result;
interceptor.set_cancel_deferred_navigation_callback_for_testing(
base::BindLambdaForTesting(
[&](NavigationThrottle::ThrottleCheckResult result) {
was_cancelled = true;
cancel_result = result;
run_loop.Quit();
}));
auto result = interceptor.WillProcessResponse();
EXPECT_EQ(result, content::NavigationThrottle::DEFER);
run_loop.Run();
EXPECT_TRUE(was_cancelled);
ASSERT_TRUE(cancel_result.has_value());
EXPECT_EQ(cancel_result->action(), content::NavigationThrottle::CANCEL);
}
TEST_F(NavigationInterceptorTest, RequestBuilderBuildsRequest) {
const char* kconfig_url = "https://idp.example/fedcm.json";
const char* kclient_id = "1234";
const char* kLoginHint = "user@example.com";
const char* kDomainHint = "example.com";
const char* kParamsJson = "{\"custom_key\":\"custom_value\"}";
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"config_url", kconfig_url},
{"client_id", kclient_id},
{"context", "continue"},
{"login_hint", kLoginHint},
{"domain_hint", kDomainHint},
{"params", kParamsJson},
{"fields", std::vector<std::string>{"name", "email"}},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_TRUE(result.has_value());
ASSERT_EQ(result->size(), 1u);
const auto& idp_get_params = (*result)[0];
ASSERT_TRUE(idp_get_params);
EXPECT_EQ(idp_get_params->context, blink::mojom::RpContext::kContinue);
EXPECT_EQ(idp_get_params->mode, blink::mojom::RpMode::kActive);
ASSERT_EQ(idp_get_params->providers.size(), 1u);
const auto& idp_options = idp_get_params->providers[0];
ASSERT_TRUE(idp_options);
EXPECT_EQ(idp_options->login_hint, kLoginHint);
EXPECT_EQ(idp_options->domain_hint, kDomainHint);
EXPECT_EQ(idp_options->params_json, kParamsJson);
EXPECT_EQ(idp_options->fields, std::vector<std::string>({"name", "email"}));
const auto& idp_config = idp_options->config;
ASSERT_TRUE(idp_config);
EXPECT_EQ(idp_config->config_url, GURL(kconfig_url));
EXPECT_EQ(idp_config->client_id, kclient_id);
}
TEST_F(NavigationInterceptorTest, RequestBuilderParsesAllFields) {
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "123"},
{"fields",
std::vector<std::string>{"name", "email", "picture", "tel", "username"}},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_TRUE(result.has_value());
const auto& idp_options = (*result)[0]->providers[0];
ASSERT_TRUE(idp_options);
EXPECT_EQ(idp_options->fields,
std::vector<std::string>(
{"name", "email", "picture", "tel", "username"}));
}
TEST_F(NavigationInterceptorTest, RequestBuilderHandlesMissingFields) {
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "123"},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_TRUE(result.has_value());
const auto& idp_options = (*result)[0]->providers[0];
ASSERT_TRUE(idp_options);
EXPECT_TRUE(!idp_options->fields.has_value() || idp_options->fields->empty());
}
TEST_F(NavigationInterceptorTest, RequestBuilderMissingParams) {
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "123"},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_TRUE(result.has_value());
const auto& idp_options = (*result)[0]->providers[0];
ASSERT_TRUE(idp_options);
EXPECT_FALSE(idp_options->params_json.has_value());
}
TEST_F(NavigationInterceptorTest,
RequestBuilderMissingContextDefaultsToSignIn) {
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "123"},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_TRUE(result.has_value());
ASSERT_EQ((*result)[0]->context, blink::mojom::RpContext::kSignIn);
}
TEST_F(NavigationInterceptorTest, RequestBuilderParsesContext) {
const struct {
std::string context_str;
blink::mojom::RpContext context_enum;
} kTestCases[] = {
{"signin", blink::mojom::RpContext::kSignIn},
{"signup", blink::mojom::RpContext::kSignUp},
{"use", blink::mojom::RpContext::kUse},
{"continue", blink::mojom::RpContext::kContinue},
};
for (const auto& test_case : kTestCases) {
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "123"},
{"context", test_case.context_str},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_TRUE(result.has_value());
ASSERT_EQ((*result)[0]->context, test_case.context_enum);
}
}
TEST_F(NavigationInterceptorTest,
RequestBuilderReturnsNulloptOnInvalidContext) {
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
{"client_id", "123"},
{"context", "invalid"},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_FALSE(result.has_value());
}
TEST_F(NavigationInterceptorTest,
RequestBuilderReturnsNulloptOnMissingconfig_url) {
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"client_id", "123"},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_FALSE(result.has_value());
}
TEST_F(NavigationInterceptorTest,
RequestBuilderReturnsNulloptOnMissingclient_id) {
webid::NavigationInterceptor::RequestBuilder builder;
auto parsed_dictionary = webid::EncodeParams({
{"config_url", "https://idp.example/fedcm.json"},
});
auto result = builder.Build(parsed_dictionary);
ASSERT_FALSE(result.has_value());
}
TEST_F(NavigationInterceptorTest, ResponseBuilderBuildsResponse) {
NavigationInterceptor::ResponseBuilder builder;
base::Value::Dict response_dict;
response_dict.Set("redirect_to", "https://example.com/redirect");
base::Value response(std::move(response_dict));
auto params = builder.Build(response);
ASSERT_TRUE(params.has_value());
EXPECT_EQ(params->url, GURL("https://example.com/redirect"));
EXPECT_EQ(static_cast<int>(params->transition_type),
static_cast<int>(ui::PAGE_TRANSITION_LINK));
}
TEST_F(NavigationInterceptorTest, ResponseBuilderFailsWithNoRedirectUrl) {
NavigationInterceptor::ResponseBuilder builder;
base::Value response(base::Value::Type::DICT);
auto params = builder.Build(response);
ASSERT_FALSE(params.has_value());
}
TEST_F(NavigationInterceptorTest, ResponseBuilderFailsWithInvalidRedirectUrl) {
NavigationInterceptor::ResponseBuilder builder;
base::Value::Dict response_dict;
response_dict.Set("redirect_to", "not a valid url");
base::Value response(std::move(response_dict));
auto params = builder.Build(response);
ASSERT_FALSE(params.has_value());
}
TEST_F(NavigationInterceptorTest, ResponseBuilderFailsWithInternalUrls) {
NavigationInterceptor::ResponseBuilder builder;
base::Value::Dict response_dict;
response_dict.Set("redirect_to", "chrome://settings");
base::Value response(std::move(response_dict));
auto params = builder.Build(response);
ASSERT_FALSE(params.has_value());
}
TEST_F(NavigationInterceptorTest, ResponseBuilderBuildsPostResponse) {
NavigationInterceptor::ResponseBuilder builder;
base::Value::Dict redirect_dict;
redirect_dict.Set("url", "https://example.com/redirect");
redirect_dict.Set("method", "POST");
redirect_dict.Set("body", "key=value");
base::Value::Dict response_dict;
response_dict.Set("redirect_to", std::move(redirect_dict));
base::Value response(std::move(response_dict));
auto params = builder.Build(response);
ASSERT_TRUE(params.has_value());
EXPECT_EQ(params->url, GURL("https://example.com/redirect"));
EXPECT_EQ(static_cast<int>(params->transition_type),
static_cast<int>(ui::PAGE_TRANSITION_FORM_SUBMIT));
ASSERT_TRUE(params->post_data);
const auto& elements = *params->post_data->elements();
ASSERT_EQ(elements.size(), 1u);
ASSERT_EQ(elements[0].type(), network::DataElement::Tag::kBytes);
EXPECT_EQ(elements[0].As<network::DataElementBytes>().AsStringPiece(),
"key=value");
}
TEST_F(NavigationInterceptorTest,
ResponseBuilderFailsWithPostResponseMissingUrl) {
NavigationInterceptor::ResponseBuilder builder;
base::Value::Dict redirect_dict;
redirect_dict.Set("method", "POST");
redirect_dict.Set("body", "key=value");
base::Value::Dict response_dict;
response_dict.Set("redirect_to", std::move(redirect_dict));
base::Value response(std::move(response_dict));
auto params = builder.Build(response);
ASSERT_FALSE(params.has_value());
}
TEST_F(NavigationInterceptorTest,
ResponseBuilderSucceedsWithPostResponseMissingMethod) {
NavigationInterceptor::ResponseBuilder builder;
base::Value::Dict redirect_dict;
redirect_dict.Set("url", "https://example.com/redirect");
redirect_dict.Set("body", "key=value");
base::Value::Dict response_dict;
response_dict.Set("redirect_to", std::move(redirect_dict));
base::Value response(std::move(response_dict));
auto params = builder.Build(response);
ASSERT_TRUE(params.has_value());
EXPECT_EQ(params->url, GURL("https://example.com/redirect"));
EXPECT_FALSE(params->post_data);
}
TEST_F(NavigationInterceptorTest,
ResponseBuilderSucceedsWithPostResponseMissingBody) {
NavigationInterceptor::ResponseBuilder builder;
base::Value::Dict redirect_dict;
redirect_dict.Set("url", "https://example.com/redirect");
redirect_dict.Set("method", "POST");
base::Value::Dict response_dict;
response_dict.Set("redirect_to", std::move(redirect_dict));
base::Value response(std::move(response_dict));
auto params = builder.Build(response);
ASSERT_TRUE(params.has_value());
EXPECT_EQ(params->url, GURL("https://example.com/redirect"));
ASSERT_TRUE(params->post_data);
const auto& elements = *params->post_data->elements();
ASSERT_EQ(elements.size(), 0u);
}
} // namespace content::webid