blob: c734d48fc351824dd2e59036750a9fe066ff8afd [file] [log] [blame]
// Copyright 2021 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 <string>
#include <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/browser/webid/fake_identity_request_dialog_controller.h"
#include "content/browser/webid/test/webid_test_content_browser_client.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/identity_request_dialog_controller.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "net/base/features.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.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 "url/gurl.h"
#include "url/origin.h"
using net::EmbeddedTestServer;
using net::HttpStatusCode;
using net::test_server::BasicHttpResponse;
using net::test_server::HttpMethod;
using net::test_server::HttpRequest;
using net::test_server::HttpResponse;
namespace content {
namespace {
constexpr char kRpHostName[] = "rp.example";
constexpr char kIdpOrigin[] = "https://idp.example.org";
constexpr char kExpectedManifestPath[] = "/fedcm.json";
constexpr char kTestContentType[] = "application/json";
constexpr char kIdpForbiddenHeader[] = "Sec-FedCM-CSRF";
// Token value in //content/test/data/id_token_endpoint.json
constexpr char kToken[] = "[not a real token]";
// This class implements the IdP logic, and responds to requests sent to the
// test HTTP server.
class IdpTestServer {
public:
struct ManifestDetails {
HttpStatusCode status_code;
std::string content_type;
std::string accounts_endpoint_url;
std::string client_metadata_endpoint_url;
std::string id_token_endpoint_url;
};
IdpTestServer() = default;
~IdpTestServer() = default;
IdpTestServer(const IdpTestServer&) = delete;
IdpTestServer& operator=(const IdpTestServer&) = delete;
std::unique_ptr<HttpResponse> HandleRequest(const HttpRequest& request) {
// RP files are fetched from the /test base directory. Assume anything
// to other paths is directed to the IdP.
if (request.relative_url.rfind("/test", 0) == 0)
return nullptr;
if (request.all_headers.find(kIdpForbiddenHeader) != std::string::npos) {
EXPECT_EQ(request.headers.at(kIdpForbiddenHeader), "?1");
}
auto response = std::make_unique<BasicHttpResponse>();
if (IsManifestRequest(request)) {
BuildManifestResponseFromDetails(*response.get(), manifest_details_);
return response;
}
return nullptr;
}
void SetManifestResponseDetails(ManifestDetails details) {
manifest_details_ = details;
}
private:
bool IsManifestRequest(const HttpRequest& request) {
if (request.method == HttpMethod::METHOD_GET &&
request.relative_url == kExpectedManifestPath) {
return true;
}
return false;
}
void BuildManifestResponseFromDetails(BasicHttpResponse& response,
const ManifestDetails& details) {
std::string content = ConvertToJsonDictionary(
{{"accounts_endpoint", details.accounts_endpoint_url},
{"client_metadata_endpoint", details.client_metadata_endpoint_url},
{"id_token_endpoint", details.id_token_endpoint_url}});
response.set_code(details.status_code);
response.set_content(content);
response.set_content_type(details.content_type);
}
std::string ConvertToJsonDictionary(
const std::map<std::string, std::string>& data) {
std::string out = "{";
for (auto it : data) {
out += "\"" + it.first + "\":\"" + it.second + "\",";
}
if (!out.empty()) {
out[out.length() - 1] = '}';
}
return out;
}
ManifestDetails manifest_details_;
};
} // namespace
class WebIdBrowserTest : public ContentBrowserTest {
public:
WebIdBrowserTest() = default;
~WebIdBrowserTest() override = default;
WebIdBrowserTest(const WebIdBrowserTest&) = delete;
WebIdBrowserTest& operator=(const WebIdBrowserTest&) = delete;
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
idp_server_ = std::make_unique<IdpTestServer>();
https_server().SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server().ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server().RegisterRequestHandler(base::BindRepeating(
&IdpTestServer::HandleRequest, base::Unretained(idp_server_.get())));
ASSERT_TRUE(https_server().Start());
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/title1.html")));
test_browser_client_ = std::make_unique<WebIdTestContentBrowserClient>();
SetTestIdentityRequestDialogController("not_real_account");
old_client_ = SetBrowserClientForTesting(test_browser_client_.get());
}
void TearDown() override {
CHECK_EQ(SetBrowserClientForTesting(old_client_),
test_browser_client_.get());
ContentBrowserTest::TearDown();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
std::vector<base::Feature> features;
// kSplitCacheByNetworkIsolationKey feature is needed to verify
// that the network shard for fetching the fedcm manifest file is different
// from that used for other IdP transactions, to prevent data leakage.
features.push_back(net::features::kSplitCacheByNetworkIsolationKey);
features.push_back(features::kFedCm);
// TODO(https://1314987): Test manifest validation.
scoped_feature_list_.InitWithFeatures(features,
{features::kFedCmManifestValidation});
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
net::EmbeddedTestServer& https_server() { return https_server_; }
std::string BaseIdpUrl() {
return std::string(kIdpOrigin) + ":" +
base::NumberToString(https_server().port()) + "/fedcm.json";
}
std::string GetBasicRequestString() {
return R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345',
}]
}
}));
return x.token;
}) ()
)";
}
IdpTestServer::ManifestDetails BuildValidManifestDetails() {
std::string accounts_endpoint_url = "/fedcm/accounts_endpoint.json";
std::string client_metadata_endpoint_url =
"/fedcm/client_metadata_endpoint.json";
std::string id_token_endpoint_url = "/fedcm/id_token_endpoint.json";
return {net::HTTP_OK, kTestContentType, accounts_endpoint_url,
client_metadata_endpoint_url, id_token_endpoint_url};
}
IdpTestServer* idp_server() { return idp_server_.get(); }
void SetTestIdentityRequestDialogController(
const std::string& dialog_selected_account) {
auto controller = std::make_unique<FakeIdentityRequestDialogController>(
dialog_selected_account);
test_browser_client_->SetIdentityRequestDialogController(
std::move(controller));
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
std::unique_ptr<IdpTestServer> idp_server_;
std::unique_ptr<WebIdTestContentBrowserClient> test_browser_client_;
raw_ptr<ContentBrowserClient> old_client_ = nullptr;
};
// Verify a standard login flow with IdP sign-in page.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, FullLoginFlow) {
idp_server()->SetManifestResponseDetails(BuildValidManifestDetails());
EXPECT_EQ(std::string(kToken), EvalJs(shell(), GetBasicRequestString()));
}
// Verify full login flow where the IdP uses absolute rather than relative
// URLs.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, AbsoluteURLs) {
IdpTestServer::ManifestDetails manifest_details = BuildValidManifestDetails();
manifest_details.accounts_endpoint_url = "/fedcm/accounts_endpoint.json";
manifest_details.client_metadata_endpoint_url =
"/fedcm/client_metadata_endpoint.json";
manifest_details.id_token_endpoint_url = "/fedcm/id_token_endpoint.json";
idp_server()->SetManifestResponseDetails(manifest_details);
EXPECT_EQ(std::string(kToken), EvalJs(shell(), GetBasicRequestString()));
}
// Verify an attempt to invoke FedCM with an insecure IDP path fails.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, FailsOnHTTP) {
idp_server()->SetManifestResponseDetails(BuildValidManifestDetails());
std::string script = R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
configURL: 'http://idp.example:)" +
base::NumberToString(https_server().port()) +
R"(/fedcm.json',
clientId: 'client_id_1',
nonce: '12345',
}]
}
}));
return x.token;
}) ()
)";
std::string expected_error =
"a JavaScript error: \"NetworkError: Error "
"retrieving a token.\"\n";
EXPECT_EQ(expected_error, EvalJs(shell(), script).error);
}
} // namespace content