blob: fcf013cd6500ee449fa1b622f768d2d8533158b8 [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 "base/functional/callback.h"
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/values.h"
#include "content/browser/webid/flags.h"
#include "content/browser/webid/request_service.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "mojo/public/cpp/bindings/remote.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/data_decoder.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
#include "url/gurl.h"
namespace content::webid {
// static
void NavigationInterceptor::MaybeCreateAndAdd(
NavigationThrottleRegistry& registry) {
if (!IsNavigationInterceptionEnabled()) {
return;
}
registry.AddThrottle(std::make_unique<NavigationInterceptor>(registry));
}
NavigationInterceptor::NavigationInterceptor(
NavigationThrottleRegistry& registry)
: NavigationInterceptor(
registry,
base::BindRepeating([](content::RenderFrameHost* rfh)
-> blink::mojom::FederatedAuthRequest* {
return webid::RequestService::GetOrCreateForCurrentDocument(rfh);
})) {}
NavigationInterceptor::NavigationInterceptor(
NavigationThrottleRegistry& registry,
RequestServiceBuilder service_builder)
: content::NavigationThrottle(registry),
service_builder_(std::move(service_builder)) {}
NavigationInterceptor::~NavigationInterceptor() = default;
content::NavigationThrottle::ThrottleCheckResult
NavigationInterceptor::WillProcessResponse() {
if (!navigation_handle()->IsInPrimaryMainFrame()) {
// Only top level navigations can be intercepted.
return PROCEED;
}
auto* headers = navigation_handle()->GetResponseHeaders();
if (!headers) {
// IdPs need to opt-in for the interception via response header.
return PROCEED;
}
std::optional<std::string> header =
headers->GetNormalizedHeader("FedCM-Intercept-Navigation");
if (!header) {
return PROCEED;
}
content::RenderFrameHost* rfh = navigation_handle()->GetRenderFrameHost();
data_decoder::DataDecoder::ParseStructuredHeaderDictionaryIsolated(
*header, base::BindOnce(&NavigationInterceptor::OnHeaderParsed,
weak_ptr_factory_.GetWeakPtr(),
rfh->GetWeakDocumentPtr()));
// TODO(http://crbug.com/455614294): Ideally, we'd like to cancel the
// navigation early on so that the spinner stops. However, we need to
// defer here because cancelling will destroy this object before the
// async header parsing and token request complete.
return DEFER;
}
void NavigationInterceptor::OnHeaderParsed(
content::WeakDocumentPtr doc_ptr,
base::expected<net::structured_headers::Dictionary, std::string> result) {
content::RenderFrameHost* rfh = doc_ptr.AsRenderFrameHostIfValid();
if (!rfh) {
// The document is no longer valid, likely because the page initiated a new
// navigation in the meantime.
// Resume the deferred navigation without cancelling.
Resume();
return;
}
if (!result.has_value()) {
// The header was available, but malformed.
// Cancel the navigation because it is a developer error.
CancelDeferredNavigation(CANCEL);
return;
}
RequestBuilder request_builder;
auto idp_get_params_vector = request_builder.Build(*result);
if (!idp_get_params_vector) {
// The header was available, parsed, but contained an invalid set of
// parameters.
// Cancel the navigation because it is a developer error.
CancelDeferredNavigation(CANCEL);
return;
}
service_builder_.Run(rfh)->RequestToken(
std::move(*idp_get_params_vector),
password_manager::CredentialMediationRequirement::kOptional,
base::BindOnce(&NavigationInterceptor::OnTokenResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void NavigationInterceptor::OnTokenResponse(
blink::mojom::RequestTokenStatus status,
const std::optional<GURL>& selected_identity_provider_config_url,
std::optional<base::Value> token,
blink::mojom::TokenErrorPtr error,
bool is_auto_selected) {
// TODO(http://crbug.com/455614294): expose the redirect_to URL outside
// of the token so that either one or the other can be used.
// TODO(http://crbug.com/455614294): consider supporting the redirect_to
// response for non-interception use cases too.
if (status != blink::mojom::RequestTokenStatus::kSuccess) {
// The FedCM request failed.
// Cancel the navigation because it is a developer error.
CancelDeferredNavigation(CANCEL);
return;
}
ResponseBuilder response_builder;
auto params = response_builder.Build(std::move(*token));
if (!params) {
// The FedCM request succeeded, but the IdP returned an
// invalid response.
// Cancel the navigation because it is a developer error.
CancelDeferredNavigation(CANCEL);
return;
}
auto frame_tree_node_id = navigation_handle()->GetFrameTreeNodeId();
params->frame_tree_node_id = frame_tree_node_id;
content::WebContents* web_contents = navigation_handle()->GetWebContents();
if (!web_contents) {
return;
}
// Redirect the navigation to the URL specified in the token (which also
// cancels the current one).
// TODO(http://crbug.com/455614294): re-consider the security properties that
// the redirection need to have (e.g. CSP, who is the initiator, should
// SameSite cookies be passed, how does it relate to the history, the Referer
// header, navigating to internal schemes, like chrome://settings, etc).
web_contents->GetController().LoadURLWithParams(*params);
}
const char* NavigationInterceptor::GetNameForLogging() {
return "FedCMNavigationInterceptor";
}
std::optional<std::vector<blink::mojom::IdentityProviderGetParametersPtr>>
NavigationInterceptor::RequestBuilder::Build(
const net::structured_headers::Dictionary& dictionary) {
auto get_string =
[&dictionary](const std::string& key) -> std::optional<std::string> {
auto it = dictionary.find(key);
if (it == dictionary.end() || it->second.member.size() != 1 ||
!it->second.member[0].item.is_string()) {
return std::nullopt;
}
return it->second.member[0].item.GetString();
};
auto config_url = get_string("config_url");
if (!config_url) {
return std::nullopt;
}
auto client_id = get_string("client_id");
if (!client_id) {
return std::nullopt;
}
auto idp_options = blink::mojom::IdentityProviderRequestOptions::New();
idp_options->login_hint = get_string("login_hint").value_or("");
idp_options->domain_hint = get_string("domain_hint").value_or("");
idp_options->params_json = get_string("params");
auto fields_it = dictionary.find("fields");
if (fields_it != dictionary.end()) {
std::vector<std::string> fields;
for (const auto& member_item : fields_it->second.member) {
if (!member_item.item.is_string()) {
return std::nullopt;
}
const std::string& field_str = member_item.item.GetString();
fields.push_back(field_str);
}
idp_options->fields = fields;
}
blink::mojom::RpContext context = blink::mojom::RpContext::kSignIn;
auto context_string = get_string("context");
if (context_string) {
if (*context_string == "signin") {
context = blink::mojom::RpContext::kSignIn;
} else if (*context_string == "signup") {
context = blink::mojom::RpContext::kSignUp;
} else if (*context_string == "use") {
context = blink::mojom::RpContext::kUse;
} else if (*context_string == "continue") {
context = blink::mojom::RpContext::kContinue;
} else {
// Unknown context.
return std::nullopt;
}
}
auto idp_config = blink::mojom::IdentityProviderConfig::New();
idp_config->config_url = GURL(*config_url);
idp_config->client_id = *client_id;
idp_options->config = std::move(idp_config);
std::vector<blink::mojom::IdentityProviderRequestOptionsPtr>
idp_options_vector;
idp_options_vector.push_back(std::move(idp_options));
auto idp_get_params = blink::mojom::IdentityProviderGetParameters::New();
idp_get_params->providers = std::move(idp_options_vector);
idp_get_params->context = context;
// IdPs are not allowed to trigger passive modes via navigation interceptors.
idp_get_params->mode = blink::mojom::RpMode::kActive;
std::vector<blink::mojom::IdentityProviderGetParametersPtr>
idp_get_params_vector;
idp_get_params_vector.push_back(std::move(idp_get_params));
return idp_get_params_vector;
}
std::optional<content::NavigationController::LoadURLParams>
NavigationInterceptor::ResponseBuilder::Build(const base::Value& response) {
if (!response.is_dict()) {
return std::nullopt;
}
const base::Value* redirect_to = response.GetDict().Find("redirect_to");
if (!redirect_to) {
return std::nullopt;
}
if (redirect_to->is_string()) {
GURL url(redirect_to->GetString());
if (!url.is_valid()) {
return std::nullopt;
}
// TODO(http://crbug.com/455614294): does OAuth allow redirections
// to non-secure origins, e.g. http?
if (!url.SchemeIsHTTPOrHTTPS()) {
return std::nullopt;
}
content::NavigationController::LoadURLParams navigation(url);
navigation.transition_type = ui::PAGE_TRANSITION_LINK;
return navigation;
}
// If it's not a string, it must be a dictionary to be valid.
if (!redirect_to->is_dict()) {
return std::nullopt;
}
GURL url;
std::string method = "GET";
const std::string* url_str = redirect_to->GetDict().FindString("url");
if (!url_str) {
return std::nullopt;
}
url = GURL(*url_str);
if (!url.SchemeIsHTTPOrHTTPS()) {
return std::nullopt;
}
const std::string* method_str = redirect_to->GetDict().FindString("method");
if (method_str) {
method = *method_str;
}
if (!url.is_valid()) {
return std::nullopt;
}
content::NavigationController::LoadURLParams submission(url);
submission.transition_type = ui::PAGE_TRANSITION_FORM_SUBMIT;
if (method == "POST") {
std::string body;
const std::string* body_str = redirect_to->GetDict().FindString("body");
if (body_str) {
body = *body_str;
}
submission.post_data = network::ResourceRequestBody::CreateFromBytes(
std::vector<uint8_t>(body.begin(), body.end()));
submission.extra_headers =
"Content-Type: application/x-www-form-urlencoded\r\n";
}
return submission;
}
} // namespace content::webid