Commit ee6ca197 authored by Hayato Ito's avatar Hayato Ito Committed by Commit Bot

Support basic access authentication through serviceworkers

Authentication dialog is not shown when ServiceWorker returns 401 response.

This CL addresses that in a similar manner as https://crrev.com/c/1290435 did,
adding |window_id| on OnAuthRequired requests, however, unlike the client
certificate case, we'll need even more plumbing for this bit because
LoginHandlerDelegate needs to know whether a request is for the main frame or a
subframe.

This CL is effective only when Network Service is enabled. No behavior change
on platforms where Network Service is disabled.

Regarding the behavior of basic access auth login prompt, the prompt is always
shown, regardless of any combination of mainframe/subframe and mainresource
/subresource.

In summary, the behavior of login prompt became as follows:

|                        | TOT    | TOT (via SW) | With CL | With CL (via SW) |
| mainframe-mainresource | prompt | no prompt    | prompt  | prompt           |
| mainframe-subresource  | prompt | no prompt    | prompt  | prompt           |
| subframe-mainresource  | prompt | no prompt    | prompt  | prompt           |
| subframe-subresource   | prompt | no prompt    | prompt  | prompt           |

Regarding tests, I confirmed that basic access authentication dialog is surely
shown for main resources (and sub resources), using the repro case reported in
the bug, manually, as well as adding a new browsertest for basic auth.

This CL added new browsertest to service_worker_basic_tls_browsertest.cc,
however, which is not renamed yet for easier reviewing. We'll rename in a
follow-up CL.

Bug: 623464,963748

Change-Id: I01284b73b7af5de1e8a25112f2b06d50efe8dcc8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1614723Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarMatt Falkenhagen <falken@chromium.org>
Commit-Queue: Hayato Ito <hayato@chromium.org>
Cr-Commit-Position: refs/heads/master@{#665791}
parent 3a7a7354
......@@ -179,7 +179,8 @@ class BrowsingDataRemoverImplBrowserTest : public ContentBrowserTest {
// on all platforms.
bool login_requested = false;
ShellContentBrowserClient::Get()->set_login_request_callback(
base::BindLambdaForTesting([&]() { login_requested = true; }));
base::BindLambdaForTesting(
[&](bool is_main_frame /* unused */) { login_requested = true; }));
GURL url = ssl_server_.GetURL(kHttpAuthPath);
bool navigation_suceeded = NavigateToURL(shell(), url);
......
......@@ -288,6 +288,12 @@ void HandleFileUploadRequest(
std::move(files)));
}
FrameTreeNodeIdRegistry::IsMainFrameGetter GetIsMainFrameFromRegistry(
const base::UnguessableToken& window_id) {
return FrameTreeNodeIdRegistry::GetInstance()->GetIsMainFrameGetter(
window_id);
}
base::RepeatingCallback<WebContents*(void)> GetWebContentsFromRegistry(
const base::UnguessableToken& window_id) {
return FrameTreeNodeIdRegistry::GetInstance()->GetWebContentsGetter(
......@@ -309,6 +315,60 @@ bool IsMainFrameRequest(int process_id, int routing_id) {
return frame_tree_node && frame_tree_node->IsMainFrame();
}
void OnAuthRequiredContinuation(
uint32_t process_id,
uint32_t routing_id,
uint32_t request_id,
const GURL& url,
bool is_request_for_main_frame,
bool first_auth_attempt,
const net::AuthChallengeInfo& auth_info,
const base::Optional<network::ResourceResponseHead>& head,
network::mojom::AuthChallengeResponderPtr auth_challenge_responder,
base::RepeatingCallback<WebContents*(void)> web_contents_getter) {
if (!web_contents_getter) {
web_contents_getter =
base::BindRepeating(GetWebContents, process_id, routing_id);
}
if (!web_contents_getter.Run()) {
std::move(auth_challenge_responder)->OnAuthCredentials(base::nullopt);
return;
}
new LoginHandlerDelegate(std::move(auth_challenge_responder),
std::move(web_contents_getter), auth_info,
is_request_for_main_frame, process_id, routing_id,
request_id, url, head ? head->headers : nullptr,
first_auth_attempt); // deletes self
}
void OnAuthRequiredContinuationForWindowId(
const base::UnguessableToken& window_id,
uint32_t process_id,
uint32_t routing_id,
uint32_t request_id,
const GURL& url,
bool first_auth_attempt,
const net::AuthChallengeInfo& auth_info,
const base::Optional<network::ResourceResponseHead>& head,
network::mojom::AuthChallengeResponderPtr auth_challenge_responder,
FrameTreeNodeIdRegistry::IsMainFrameGetter is_main_frame_getter) {
// |is_main_frame_getter| should not be a null callback because the
// FrameTreeNodeIdRegistry should have a corresponding FrameTreeNode id.
CHECK(is_main_frame_getter);
base::Optional<bool> is_main_frame_opt = is_main_frame_getter.Run();
// The frame may already be gone due to thread hopping.
if (!is_main_frame_opt) {
std::move(auth_challenge_responder)->OnAuthCredentials(base::nullopt);
return;
}
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {BrowserThread::IO},
base::BindOnce(&GetWebContentsFromRegistry, window_id),
base::BindOnce(&OnAuthRequiredContinuation, process_id, routing_id,
request_id, url, *is_main_frame_opt, first_auth_attempt,
auth_info, head, std::move(auth_challenge_responder)));
}
void CreateSSLClientAuthDelegateOnIO(
network::mojom::ClientCertificateResponderPtrInfo
client_cert_responder_info,
......@@ -489,6 +549,7 @@ NetworkServiceClient::~NetworkServiceClient() {
}
void NetworkServiceClient::OnAuthRequired(
const base::Optional<base::UnguessableToken>& window_id,
uint32_t process_id,
uint32_t routing_id,
uint32_t request_id,
......@@ -497,20 +558,20 @@ void NetworkServiceClient::OnAuthRequired(
const net::AuthChallengeInfo& auth_info,
const base::Optional<network::ResourceResponseHead>& head,
network::mojom::AuthChallengeResponderPtr auth_challenge_responder) {
base::RepeatingCallback<WebContents*(void)> web_contents_getter =
base::BindRepeating(GetWebContents, process_id, routing_id);
if (!web_contents_getter.Run()) {
std::move(auth_challenge_responder)->OnAuthCredentials(base::nullopt);
if (window_id) {
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {BrowserThread::IO},
base::BindOnce(&GetIsMainFrameFromRegistry, *window_id),
base::BindOnce(&OnAuthRequiredContinuationForWindowId, *window_id,
process_id, routing_id, request_id, url,
first_auth_attempt, auth_info, head,
std::move(auth_challenge_responder)));
return;
}
bool is_request_for_main_frame = IsMainFrameRequest(process_id, routing_id);
new LoginHandlerDelegate(std::move(auth_challenge_responder),
std::move(web_contents_getter), auth_info,
is_request_for_main_frame, process_id, routing_id,
request_id, url, head ? head->headers : nullptr,
first_auth_attempt); // deletes self
OnAuthRequiredContinuation(process_id, routing_id, request_id, url,
IsMainFrameRequest(process_id, routing_id),
first_auth_attempt, auth_info, head,
std::move(auth_challenge_responder), {});
}
void NetworkServiceClient::OnCertificateRequested(
......
......@@ -11,6 +11,7 @@
#include "base/macros.h"
#include "base/memory/memory_pressure_listener.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "content/common/content_export.h"
#include "mojo/public/cpp/bindings/binding.h"
......@@ -39,7 +40,8 @@ class CONTENT_EXPORT NetworkServiceClient
~NetworkServiceClient() override;
// network::mojom::NetworkServiceClient implementation:
void OnAuthRequired(uint32_t process_id,
void OnAuthRequired(const base::Optional<base::UnguessableToken>& window_id,
uint32_t process_id,
uint32_t routing_id,
uint32_t request_id,
const GURL& url,
......
......@@ -3,7 +3,9 @@
// found in the LICENSE file.
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
......@@ -11,12 +13,31 @@
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "net/base/net_errors.h"
#include "net/ssl/ssl_server_config.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/network/public/cpp/features.h"
#include "url/gurl.h"
namespace content {
const char kWorkerHttpBasicAuthPath[] = "/workers/http_basic_auth?intercept";
// Serves a Basic Auth challenge.
std::unique_ptr<net::test_server::HttpResponse> HandleHttpAuthRequest(
const net::test_server::HttpRequest& request) {
if (request.relative_url != kWorkerHttpBasicAuthPath)
return nullptr;
auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_UNAUTHORIZED);
http_response->AddCustomHeader("WWW-Authenticate",
"Basic realm=\"test realm\"");
return http_response;
}
// Tests TLS + service workers. Inspired by
// content/browser/worker_host/worker_browsertest.cc.
class ServiceWorkerTlsTest : public ContentBrowserTest {
......@@ -103,4 +124,99 @@ IN_PROC_BROWSER_TEST_F(ServiceWorkerTlsTest, ClientAuthFetchSubResource) {
EXPECT_EQ(1, select_certificate_count());
}
// Tests basic authentication + service workers. Inspired by
// content/browser/browsing_data/browsing_data_remover_impl_browsertest.cc.
class ServiceWorkerBasicAuthTest : public ContentBrowserTest {
public:
ServiceWorkerBasicAuthTest()
: ssl_server_(net::test_server::EmbeddedTestServer::TYPE_HTTPS) {
ssl_server_.AddDefaultHandlers(GetTestDataFilePath());
ssl_server_.RegisterRequestHandler(
base::BindRepeating(&HandleHttpAuthRequest));
EXPECT_TRUE(ssl_server_.Start());
}
void SetUpOnMainThread() override {
login_requested_ = LoginRequested::kNone;
// Set a login request callback to be used instead of a login dialog since
// such a dialog is difficult to control programmatically and doesn't work
// on all platforms.
ShellContentBrowserClient::Get()->set_login_request_callback(
base::BindLambdaForTesting([&](bool is_main_frame) {
login_requested_ = is_main_frame ? LoginRequested::kMainFrame
: LoginRequested::kSubFrame;
}));
}
protected:
enum class LoginRequested { kNone, kMainFrame, kSubFrame };
LoginRequested login_requested_ = LoginRequested::kNone;
net::test_server::EmbeddedTestServer ssl_server_;
};
// Tests that basic auth prompts for a page controlled by a service
// worker, when the service worker calls fetch() for the main resource.
IN_PROC_BROWSER_TEST_F(ServiceWorkerBasicAuthTest,
BasicAuthPromptFetchMainResourceMainFrame) {
// The test should run only when Network Service is enabled.
if (!base::FeatureList::IsEnabled(network::features::kNetworkService))
return;
// Load a page that installs the service worker.
EXPECT_TRUE(NavigateToURL(
shell(), ssl_server_.GetURL("/workers/service_worker_setup.html")));
EXPECT_EQ("ok", EvalJs(shell(), "setup();"));
EXPECT_EQ(LoginRequested::kNone, login_requested_);
// Because our login request callback does nothing, navigation should
// fail.
EXPECT_FALSE(
NavigateToURL(shell(), ssl_server_.GetURL(kWorkerHttpBasicAuthPath)));
EXPECT_EQ(LoginRequested::kMainFrame, login_requested_);
}
// Tests that basic auth prompts for a page controlled by a service
// worker, when the service worker calls fetch() for the main resource for
// subframe.
IN_PROC_BROWSER_TEST_F(ServiceWorkerBasicAuthTest,
BasicAuthPromptFetchMainResourceSubframe) {
// The test should run only when Network Service is enabled.
if (!base::FeatureList::IsEnabled(network::features::kNetworkService))
return;
// Load a page that installs the service worker.
EXPECT_TRUE(NavigateToURL(
shell(), ssl_server_.GetURL("/workers/service_worker_setup.html")));
EXPECT_EQ("ok", EvalJs(shell(), "setup();"));
EXPECT_EQ(LoginRequested::kNone, login_requested_);
EXPECT_TRUE(NavigateToURL(
shell(), ssl_server_.GetURL("/workers/iframe_basic_auth.html")));
// Login request callback should be called for a iframe's main resource.
EXPECT_EQ(LoginRequested::kSubFrame, login_requested_);
}
// Tests that basic auth prompts for a page controlled by a service
// worker, when the service worker calls fetch() for a subresource.
IN_PROC_BROWSER_TEST_F(ServiceWorkerBasicAuthTest,
BasicAuthPromptFetchSubResource) {
// The test should run only when Network Service is enabled.
if (!base::FeatureList::IsEnabled(network::features::kNetworkService))
return;
// Load a page that installs the service worker.
EXPECT_TRUE(NavigateToURL(
shell(), ssl_server_.GetURL("/workers/service_worker_setup.html")));
EXPECT_EQ("ok", EvalJs(shell(), "setup();"));
// Load a page controlled by the service worker.
EXPECT_TRUE(
NavigateToURL(shell(), ssl_server_.GetURL("/workers/simple.html")));
EXPECT_EQ(LoginRequested::kNone, login_requested_);
// Perform a fetch from the controlled page to the page which needs basic
// auth (The fetch should return status code 401.)
std::string url = ssl_server_.GetURL(kWorkerHttpBasicAuthPath).spec();
EXPECT_EQ(401, EvalJs(shell(), "try_fetch_status('" + url + "');"));
EXPECT_EQ(LoginRequested::kMainFrame, login_requested_);
}
} // namespace content
......@@ -5,11 +5,25 @@
#include "content/browser/web_contents/frame_tree_node_id_registry.h"
#include "base/bind_helpers.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/public/browser/web_contents.h"
namespace content {
using WebContentsGetter = FrameTreeNodeIdRegistry::WebContentsGetter;
using IsMainFrameGetter = FrameTreeNodeIdRegistry::IsMainFrameGetter;
namespace {
base::Optional<bool> IsMainFrameFromFrameTreeNodeId(int frame_tree_node_id) {
if (FrameTreeNode* frame_tree_node =
FrameTreeNode::GloballyFindByID(frame_tree_node_id)) {
return frame_tree_node->IsMainFrame();
}
return base::nullopt;
}
} // namespace
// static
FrameTreeNodeIdRegistry* FrameTreeNodeIdRegistry::GetInstance() {
......@@ -38,6 +52,16 @@ WebContentsGetter FrameTreeNodeIdRegistry::GetWebContentsGetter(
return base::BindRepeating(&WebContents::FromFrameTreeNodeId, iter->second);
}
IsMainFrameGetter FrameTreeNodeIdRegistry::GetIsMainFrameGetter(
const base::UnguessableToken& id) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto iter = map_.find(id);
if (iter == map_.end()) {
return base::NullCallback();
}
return base::BindRepeating(&IsMainFrameFromFrameTreeNodeId, iter->second);
}
FrameTreeNodeIdRegistry::FrameTreeNodeIdRegistry() = default;
FrameTreeNodeIdRegistry::~FrameTreeNodeIdRegistry() = default;
......
......@@ -9,6 +9,7 @@
#include "base/callback.h"
#include "base/no_destructor.h"
#include "base/optional.h"
#include "base/sequence_checker.h"
#include "base/unguessable_token.h"
......@@ -37,14 +38,19 @@ class WebContents;
class FrameTreeNodeIdRegistry {
public:
using WebContentsGetter = base::RepeatingCallback<WebContents*()>;
using IsMainFrameGetter = base::RepeatingCallback<base::Optional<bool>()>;
static FrameTreeNodeIdRegistry* GetInstance();
void Add(const base::UnguessableToken& id, const int frame_tree_node_id);
void Remove(const base::UnguessableToken&);
// Returns null callback if not found.
// Returns a null callback if not found.
WebContentsGetter GetWebContentsGetter(
const base::UnguessableToken& id) const;
// Returns a null callback if not found. The returned callback will return
// nullopt if a corresponding FrameTreeNode is not found.
IsMainFrameGetter GetIsMainFrameGetter(
const base::UnguessableToken& id) const;
private:
friend class base::NoDestructor<FrameTreeNodeIdRegistry>;
......
......@@ -98,7 +98,8 @@ class WorkerTest : public ContentBrowserTest {
RunTest(shell(), url, expect_failure);
}
static void QuitUIMessageLoop(base::OnceClosure callback) {
static void QuitUIMessageLoop(base::OnceClosure callback,
bool is_main_frame /* unused */) {
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
std::move(callback));
}
......
......@@ -466,9 +466,8 @@ std::unique_ptr<LoginDelegate> ShellContentBrowserClient::CreateLoginDelegate(
bool first_auth_attempt,
LoginAuthRequiredCallback auth_required_callback) {
if (!login_request_callback_.is_null()) {
std::move(login_request_callback_).Run();
std::move(login_request_callback_).Run(is_main_frame);
}
return nullptr;
}
......
......@@ -127,7 +127,8 @@ class ShellContentBrowserClient : public ContentBrowserClient {
base::OnceCallback<bool(const service_manager::Identity&)> callback) {
should_terminate_on_service_quit_callback_ = std::move(callback);
}
void set_login_request_callback(base::OnceClosure login_request_callback) {
void set_login_request_callback(
base::OnceCallback<void(bool is_main_frame)> login_request_callback) {
login_request_callback_ = std::move(login_request_callback);
}
......@@ -147,7 +148,7 @@ class ShellContentBrowserClient : public ContentBrowserClient {
base::OnceClosure select_client_certificate_callback_;
base::OnceCallback<bool(const service_manager::Identity&)>
should_terminate_on_service_quit_callback_;
base::OnceClosure login_request_callback_;
base::OnceCallback<void(bool is_main_frame)> login_request_callback_;
std::unique_ptr<
service_manager::BinderRegistryWithArgs<content::RenderFrameHost*>>
......
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Assumes that browsertest set up a handler for "./http_basic_auth" and returns 401 -->
<iframe src="./http_basic_auth?intercept"></iframe>
......@@ -12,4 +12,14 @@ function try_fetch(url) {
}
);
}
function try_fetch_status(url) {
return fetch(url).then(
response => {
return response.status;
},
err => {
return err.name;
}
);
}
</script>
......@@ -96,7 +96,12 @@ interface NetworkServiceClient {
// The |auth_challenge_responder| will respond to auth challenge with
// credentials. |head| can provide response headers for the response
// which has elicited this auth request, if applicable.
OnAuthRequired(uint32 process_id,
//
// |window_id| or else |process_id| and |routing_id| indicates
// the frame making the request, see
// network::ResourceRequest::fetch_window_id.
OnAuthRequired(mojo_base.mojom.UnguessableToken? window_id,
uint32 process_id,
uint32 routing_id,
uint32 request_id,
url.mojom.Url url,
......
......@@ -21,6 +21,7 @@ TestNetworkServiceClient::TestNetworkServiceClient(
TestNetworkServiceClient::~TestNetworkServiceClient() {}
void TestNetworkServiceClient::OnAuthRequired(
const base::Optional<base::UnguessableToken>& window_id,
uint32_t process_id,
uint32_t routing_id,
uint32_t request_id,
......
......@@ -33,6 +33,7 @@ class TestNetworkServiceClient : public network::mojom::NetworkServiceClient {
// network::mojom::NetworkServiceClient implementation:
void OnAuthRequired(
const base::Optional<base::UnguessableToken>& window_id,
uint32_t process_id,
uint32_t routing_id,
uint32_t request_id,
......
......@@ -769,8 +769,8 @@ void URLLoader::OnAuthRequired(net::URLRequest* url_request,
head.headers = url_request->response_headers();
head.auth_challenge_info = auth_info;
network_service_client_->OnAuthRequired(
factory_params_->process_id, render_frame_id_, request_id_,
url_request_->url(), first_auth_attempt_, auth_info, head,
fetch_window_id_, factory_params_->process_id, render_frame_id_,
request_id_, url_request_->url(), first_auth_attempt_, auth_info, head,
std::move(auth_challenge_responder));
first_auth_attempt_ = false;
......
......@@ -2137,6 +2137,7 @@ class MockNetworkServiceClient : public TestNetworkServiceClient {
// mojom::NetworkServiceClient:
void OnAuthRequired(
const base::Optional<base::UnguessableToken>& window_id,
uint32_t process_id,
uint32_t routing_id,
uint32_t request_id,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment