blob: dafeabf3e8fc44a9f6c0e89cf9ff5caca04a62f9 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// 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/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/id_token_request_callback_data.h"
#include "content/browser/webid/test/webid_test_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 kExpectedWellKnownPath[] = "/.well-known/webid";
constexpr char kIdpEndpointRelativeValue[] = "/webid/sign-in";
constexpr char kTestWellKnownResponseBody[] =
"{\"idp_endpoint\": \"/webid/sign-in\"}";
constexpr char kTestIdpEndpointBody[] = "{\"signin_url\": \"/webid/\"}";
constexpr char kTestContentType[] = "application/json";
constexpr char kIdpForbiddenHeader[] = "Sec-WebID-CSRF";
// Value will be added here as token introspection is implemented.
constexpr char kIdToken[] = "[not a real token]";
constexpr char kIdpEndpointTokenResponse[] =
"{\"id_token\": \"[not a real token]\"}";
// This fakes the request dialogs to always provide user consent.
// Tests that need to vary the responses or set test expectations should use
// MockIdentityRequestDialogController.
// This also fakes an IdP sign-in page until tests can be set up to
// verify the FederatedAuthResponse mechanics.
class FakeIdentityRequestDialogController
: public content::IdentityRequestDialogController {
public:
FakeIdentityRequestDialogController(
UserApproval initial_permission_response,
UserApproval token_exchange_permission_response)
: initial_permission_response_(initial_permission_response),
token_exchange_permission_response_(
token_exchange_permission_response) {}
~FakeIdentityRequestDialogController() override = default;
FakeIdentityRequestDialogController(
const FakeIdentityRequestDialogController&) = delete;
FakeIdentityRequestDialogController& operator=(
const FakeIdentityRequestDialogController&) = delete;
void ShowInitialPermissionDialog(WebContents*,
const GURL&,
InitialApprovalCallback callback) override {
std::move(callback).Run(initial_permission_response_);
}
void ShowIdProviderWindow(WebContents*,
WebContents* idp_web_contents,
const GURL&,
IdProviderWindowClosedCallback callback) override {
close_idp_window_callback_ = std::move(callback);
auto* request_callback_data =
IdTokenRequestCallbackData::Get(idp_web_contents);
EXPECT_TRUE(request_callback_data);
// TODO(kenrb, majidvp): This is faking the IdP response which in reality
// comes from the navigator.id.provide() API call. We should instead load
// the IdP page in the new WebContents and that API's behavior.
auto rp_done_callback = request_callback_data->TakeDoneCallback();
IdTokenRequestCallbackData::Remove(idp_web_contents);
EXPECT_TRUE(rp_done_callback);
std::move(rp_done_callback).Run(kIdToken);
}
void CloseIdProviderWindow() override {
std::move(close_idp_window_callback_).Run();
}
void ShowTokenExchangePermissionDialog(
content::WebContents*,
const GURL&,
TokenExchangeApprovalCallback callback) override {
std::move(callback).Run(token_exchange_permission_response_);
}
private:
// User action on the initial IdP tracking permission prompt.
UserApproval initial_permission_response_ = UserApproval::kApproved;
// User action on the token exchange permission prompt.
UserApproval token_exchange_permission_response_ = UserApproval::kApproved;
base::OnceClosure close_idp_window_callback_;
};
// This class implements the IdP logic, and responds to requests sent to the
// test HTTP server.
class IdpTestServer {
public:
struct ResponseDetails {
HttpStatusCode status_code;
std::string body;
std::string content_type;
};
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;
auto response = std::make_unique<BasicHttpResponse>();
if (IsWellKnownRequest(request)) {
BuildResponseFromDetails(*response.get(), well_known_details_);
return response;
}
if (IsIdpEndpointRequest(request)) {
BuildResponseFromDetails(*response.get(), idp_endpoint_details_);
return response;
}
return nullptr;
}
void SetWellKnownResponseDetails(ResponseDetails details) {
well_known_details_ = details;
}
void SetIdpEndpointResponseDetails(ResponseDetails details) {
idp_endpoint_details_ = details;
}
private:
bool IsWellKnownRequest(const HttpRequest& request) {
if (request.method == HttpMethod::METHOD_GET &&
request.relative_url == kExpectedWellKnownPath) {
return true;
}
return false;
}
bool IsIdpEndpointRequest(const HttpRequest& request) {
if (request.method == HttpMethod::METHOD_GET &&
request.relative_url.rfind(kIdpEndpointRelativeValue, 0) == 0 &&
request.all_headers.find(kIdpForbiddenHeader) != std::string::npos) {
return true;
}
return false;
}
void BuildResponseFromDetails(BasicHttpResponse& response,
const ResponseDetails& details) {
response.set_code(details.status_code);
response.set_content(details.body);
response.set_content_type(details.content_type);
}
// Response values for the types of requests that are sent to the IdP.
// These have default values that can be overridden for specific tests.
ResponseDetails well_known_details_ = {
net::HTTP_OK, kTestWellKnownResponseBody, kTestContentType};
ResponseDetails idp_endpoint_details_ = {net::HTTP_OK, kTestIdpEndpointBody,
kTestContentType};
};
} // 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(
IdentityRequestDialogController::UserApproval::kApproved,
IdentityRequestDialogController::UserApproval::kApproved);
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 .well-known file is different
// from that used for other IdP transactions, to prevent data leakage.
features.push_back(net::features::kSplitCacheByNetworkIsolationKey);
features.push_back(features::kWebID);
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());
}
std::string GetBasicRequestString() {
return R"(
(async () => {
var x = (await navigator.id.get({
provider: ')" +
BaseIdpUrl() + R"(',
request: '[not a real request]',
}));
return x;
}) ()
)";
}
IdpTestServer* idp_server() { return idp_server_.get(); }
void SetTestIdentityRequestDialogController(
IdentityRequestDialogController::UserApproval initial_permission_response,
IdentityRequestDialogController::UserApproval token_exchange_response) {
auto controller = std::make_unique<FakeIdentityRequestDialogController>(
initial_permission_response, token_exchange_response);
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_;
ContentBrowserClient* old_client_ = nullptr;
};
// Verify a standard login flow with IdP sign-in page.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, FullLoginFlow) {
EXPECT_EQ(std::string(kIdToken), EvalJs(shell(), GetBasicRequestString()));
}
// Verify abbreviated login flow where IdP returns a token from the
// |idp_endpoint|.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, FastLoginFlow) {
idp_server()->SetIdpEndpointResponseDetails(
{net::HTTP_OK, kIdpEndpointTokenResponse, kTestContentType});
EXPECT_EQ(std::string(kIdToken), EvalJs(shell(), GetBasicRequestString()));
}
// Verify full login flow where the IdP uses absolute rather than relative
// URLs.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, AbsoluteURLs) {
std::string idp_endpoint_absolute_url =
BaseIdpUrl() + kIdpEndpointRelativeValue;
std::string well_known_response_body =
"{\"idp_endpoint\": \"" + idp_endpoint_absolute_url + "\"}";
idp_server()->SetWellKnownResponseDetails(
{net::HTTP_OK, well_known_response_body, kTestContentType});
std::string signin_url_absolute_url = BaseIdpUrl() + "/webid";
std::string idp_endpoint_response_body =
"{\"signin_url\": \"" + signin_url_absolute_url + "\"}";
idp_server()->SetIdpEndpointResponseDetails(
{net::HTTP_OK, idp_endpoint_response_body, kTestContentType});
EXPECT_EQ(std::string(kIdToken), EvalJs(shell(), GetBasicRequestString()));
}
// Simulate the user declining the permission dialog to allow the request to
// proceed.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, InitialPermissionDeclined) {
SetTestIdentityRequestDialogController(
IdentityRequestDialogController::UserApproval::kDenied,
IdentityRequestDialogController::UserApproval::kApproved);
std::string expected_error =
"a JavaScript error: \"AbortError: User "
"declined the sign-in attempt.\"\n";
EXPECT_EQ(expected_error, EvalJs(shell(), GetBasicRequestString()).error);
}
// Simulate the user declining tot share the ID token after it has been
// provided.
// TODO(kenrb): Add a variant of this test that denies approval when the token
// has been provided from the idp_endpoint. Currently the permission prompt does
// not get displayed in that case.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, TokenExchangePermissionDeclined) {
SetTestIdentityRequestDialogController(
IdentityRequestDialogController::UserApproval::kApproved,
IdentityRequestDialogController::UserApproval::kDenied);
std::string expected_error =
"a JavaScript error: \"AbortError: User "
"declined the sign-in attempt.\"\n";
EXPECT_EQ(expected_error, EvalJs(shell(), GetBasicRequestString()).error);
}
// Verify an error is returned when WebID is not supported by the provided IdP.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, WebIdNotSupported) {
idp_server()->SetWellKnownResponseDetails({net::HTTP_NOT_FOUND, "", ""});
std::string expected_error =
"a JavaScript error: \"NetworkError: The "
"indicated provider does not support WebID.\"\n";
EXPECT_EQ(expected_error, EvalJs(shell(), GetBasicRequestString()).error);
}
// Verify an attempt to invoke WebID with an insecure IDP path fails.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, FailsOnHTTP) {
std::string script = R"(
(async () => {
var x = (await navigator.id.get({
provider: 'http://idp.example)" +
base::NumberToString(https_server().port()) + R"(',
request: '[not a real request]',
}));
return x;
}) ()
)";
std::string expected_error =
"a JavaScript error: \"NetworkError: Error "
"retrieving an id token.\"\n";
EXPECT_EQ(expected_error, EvalJs(shell(), script).error);
}
} // namespace content