// 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 #include #include #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/strings/stringprintf.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"; // Use localhost for IDP so that the manifest list can be fetched from the test // server's custom port. // IdpNetworkRequestManager::ComputeManifestListUrl() does not enforce a // specific port if the IDP is localhost. constexpr char kIdpOrigin[] = "https://127.0.0.1"; constexpr char kExpectedManifestPath[] = "/fedcm.json"; constexpr char kExpectedManifestListPath[] = "/.well-known/web-identity"; constexpr char kTestContentType[] = "application/json"; constexpr char kIdpForbiddenHeader[] = "Sec-FedCM-CSRF"; // Token value in //content/test/data/id_assertion_endpoint.json constexpr char kToken[] = "[not a real token]"; bool IsGetRequestWithPath(const HttpRequest& request, const std::string& expected_path) { return request.method == HttpMethod::METHOD_GET && request.relative_url == expected_path; } // 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_assertion_endpoint_url; }; IdpTestServer() = default; ~IdpTestServer() = default; IdpTestServer(const IdpTestServer&) = delete; IdpTestServer& operator=(const IdpTestServer&) = delete; std::unique_ptr 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(); if (IsGetRequestWithPath(request, kExpectedManifestPath)) { BuildManifestResponseFromDetails(*response.get(), manifest_details_); return response; } if (IsGetRequestWithPath(request, kExpectedManifestListPath)) { BuildManifestListResponse(*response.get()); return response; } return nullptr; } void SetManifestResponseDetails(ManifestDetails details) { manifest_details_ = details; } private: 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_assertion_endpoint", details.id_assertion_endpoint_url}}); response.set_code(details.status_code); response.set_content(content); response.set_content_type(details.content_type); } void BuildManifestListResponse(BasicHttpResponse& response) { std::string content = base::StringPrintf("{\"provider_urls\": [\"%s\"]}", kExpectedManifestPath); response.set_code(net::HTTP_OK); response.set_content(content); response.set_content_type("application/json"); } std::string ConvertToJsonDictionary( const std::map& 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(); 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(); 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 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); scoped_feature_list_.InitWithFeatures(features, {}); 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_assertion_endpoint_url = "/fedcm/id_assertion_endpoint.json"; return {net::HTTP_OK, kTestContentType, accounts_endpoint_url, client_metadata_endpoint_url, id_assertion_endpoint_url}; } IdpTestServer* idp_server() { return idp_server_.get(); } void SetTestIdentityRequestDialogController( const std::string& dialog_selected_account) { auto controller = std::make_unique( 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 idp_server_; std::unique_ptr test_browser_client_; raw_ptr 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_assertion_endpoint_url = "/fedcm/id_assertion_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