blob: a5f90928e3fd7e3e5bdf6754f9cd7e6640016b60 [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/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/pattern.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/browser/in_memory_federated_permission_context.h"
#include "content/browser/webid/delegation/jwt_signer.h"
#include "content/browser/webid/delegation/sd_jwt.h"
#include "content/browser/webid/fake_identity_request_dialog_controller.h"
#include "content/browser/webid/identity_registry.h"
#include "content/browser/webid/test/mock_digital_identity_provider.h"
#include "content/browser/webid/test/mock_identity_request_dialog_controller.h"
#include "content/browser/webid/test/mock_modal_dialog_view_delegate.h"
#include "content/browser/webid/test/webid_test_content_browser_client.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/webid/autofill_source.h"
#include "content/public/browser/webid/identity_request_account.h"
#include "content/public/browser/webid/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 "content/shell/browser/shell.h"
#include "crypto/sha2.h"
#include "net/base/features.h"
#include "net/base/url_util.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 "services/network/public/cpp/cors/cors.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/webid/login_status_account.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.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;
using DigitalCredential = content::DigitalIdentityProvider::DigitalCredential;
using ::testing::_;
using ::testing::Eq;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::WithArg;
using ::testing::WithArgs;
namespace content {
namespace {
constexpr char kRpHostName[] = "rp.example";
// Use localhost for IDP so that the well-known file can be fetched from the
// test server's custom port. IdpNetworkRequestManager::ComputeWellKnownUrl()
// does not enforce a specific port if the IDP is localhost.
constexpr char kIdpOrigin[] = "https://127.0.0.1";
constexpr char kExpectedConfigPath[] = "/fedcm.json";
constexpr char kExpectedWellKnownPath[] = "/.well-known/web-identity";
constexpr char kTestContentType[] = "application/json";
constexpr char kIdpForbiddenHeader[] = "Sec-FedCM-CSRF";
static constexpr char kSetLoginHeader[] = "Set-Login";
static constexpr char kLoggedInHeaderValue[] = "logged-in";
static constexpr char kLoggedOutHeaderValue[] = "logged-out";
// Token value in //content/test/data/id_assertion_endpoint.json
constexpr char kToken[] = "[not a real token]";
constexpr char kJsErrorPrefix[] = "a JavaScript error:";
// Extracts error from `result` removing `kJsErrorPrefix` and removing leading
// and trailing whitespace and quotes.
std::string ExtractJsError(const EvalJsResult& result) {
if (result.is_ok()) {
return "";
}
if (!base::StartsWith(result.ExtractError(), kJsErrorPrefix)) {
return result.ExtractError();
}
std::string error_message =
result.ExtractError().substr(strlen(kJsErrorPrefix));
base::TrimString(error_message, "\n \"", &error_message);
return error_message;
}
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 ConfigDetails {
HttpStatusCode status_code;
std::string content_type;
std::string accounts_endpoint_url;
std::string client_metadata_endpoint_url;
std::string id_assertion_endpoint_url;
std::string vc_issuance_endpoint_url;
std::string metrics_endpoint_url;
std::string login_url;
std::vector<std::string> types;
std::map<std::string,
base::RepeatingCallback<std::unique_ptr<HttpResponse>(
const HttpRequest&)>>
servlets;
};
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.relative_url.rfind("/header/", 0) == 0) {
return BuildIdpHeaderResponse(request);
}
if (request.all_headers.find(kIdpForbiddenHeader) != std::string::npos) {
EXPECT_EQ(request.headers.at(kIdpForbiddenHeader), "?1");
}
auto response = std::make_unique<BasicHttpResponse>();
if (IsGetRequestWithPath(request, kExpectedConfigPath)) {
BuildConfigResponseFromDetails(*response.get(), config_details_);
return response;
}
if (IsGetRequestWithPath(request, kExpectedWellKnownPath)) {
BuildWellKnownResponse(*response.get());
return response;
}
if (config_details_.servlets[request.relative_url]) {
return config_details_.servlets[request.relative_url].Run(request);
}
return nullptr;
}
std::unique_ptr<HttpResponse> BuildIdpHeaderResponse(
const HttpRequest& request) {
auto response = std::make_unique<BasicHttpResponse>();
if (request.relative_url.find("/header/signin") != std::string::npos) {
response->AddCustomHeader(kSetLoginHeader, kLoggedInHeaderValue);
} else if (request.relative_url.find("/header/signout") !=
std::string::npos) {
response->AddCustomHeader(kSetLoginHeader, kLoggedOutHeaderValue);
} else {
return nullptr;
}
response->set_code(net::HTTP_OK);
response->set_content_type("text/plain");
response->set_content("Header sent.");
return response;
}
void SetConfigResponseDetails(ConfigDetails details) {
config_details_ = details;
}
private:
void BuildConfigResponseFromDetails(BasicHttpResponse& response,
const ConfigDetails& details) {
std::map<std::string, std::string> map = {
{"accounts_endpoint", "\"" + details.accounts_endpoint_url + "\""},
{"client_metadata_endpoint",
"\"" + details.client_metadata_endpoint_url + "\""},
{"id_assertion_endpoint",
"\"" + details.id_assertion_endpoint_url + "\""},
{"vc_issuance_endpoint",
"\"" + details.vc_issuance_endpoint_url + "\""},
{"login_url", "\"" + details.login_url + "\""},
{"metrics_endpoint", "\"" + details.metrics_endpoint_url + "\""},
{"formats", "[\"vc+sd-jwt\"]"},
};
std::string content = ConvertToJsonDictionary(map, details.types);
response.set_code(details.status_code);
response.set_content(content);
response.set_content_type(details.content_type);
}
void BuildWellKnownResponse(BasicHttpResponse& response) {
std::string content = base::StringPrintf("{\"provider_urls\": [\"%s\"]}",
kExpectedConfigPath);
response.set_code(net::HTTP_OK);
response.set_content(content);
response.set_content_type("application/json");
}
std::string ConvertToJsonDictionary(
const std::map<std::string, std::string>& data,
const std::vector<std::string>& types) {
std::string out = "{";
for (auto it : data) {
out += "\"" + it.first + "\":" + it.second + ",";
}
if (!types.empty()) {
out += "\"types\":[";
for (const auto& type : types) {
out += "\"" + type + "\",";
}
out[out.length() - 1] = ']';
// Adding comma which will be replaced when setting '}'.
out += ",";
}
out[out.length() - 1] = '}';
return out;
}
ConfigDetails config_details_;
};
class TestFederatedIdentityModalDialogViewDelegate
: public NiceMock<MockModalDialogViewDelegate> {
public:
base::OnceClosure closure_;
bool closed_{false};
void SetClosure(base::OnceClosure closure) { closure_ = std::move(closure); }
void OnClose() override {
DCHECK(closure_);
std::move(closure_).Run();
closed_ = true;
}
base::WeakPtr<TestFederatedIdentityModalDialogViewDelegate> GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
private:
base::WeakPtrFactory<TestFederatedIdentityModalDialogViewDelegate>
weak_ptr_factory_{this};
};
} // 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");
SetTestModalDialogViewDelegate();
}
void TearDown() override { ContentBrowserTest::TearDown(); }
void SetUpCommandLine(base::CommandLine* command_line) override {
std::vector<base::test::FeatureRef> features;
// kSplitCacheByNetworkIsolationKey feature is needed to verify
// that the network shard for fetching the config 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 IdpRootUrl() {
return std::string(kIdpOrigin) + ":" +
base::NumberToString(https_server().port());
}
std::string BaseIdpUrl() { return IdpRootUrl() + "/fedcm.json"; }
std::string BaseRpUrl() {
return https_server().GetOrigin(kRpHostName).Serialize();
}
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::ConfigDetails BuildValidConfigDetails() {
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";
std::string login_url = "/fedcm/login.html";
std::map<std::string, base::RepeatingCallback<std::unique_ptr<HttpResponse>(
const HttpRequest&)>>
servlets;
servlets[id_assertion_endpoint_url] = base::BindRepeating(
[](const HttpRequest& request) -> std::unique_ptr<HttpResponse> {
EXPECT_EQ(request.method, HttpMethod::METHOD_POST);
EXPECT_EQ(request.has_content, true);
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/json");
CHECK(request.headers.contains("Origin"));
response->AddCustomHeader(
network::cors::header_names::kAccessControlAllowOrigin,
request.headers.at("Origin"));
response->AddCustomHeader(
network::cors::header_names::kAccessControlAllowCredentials,
"true");
// Standard scopes were used, so no extra permission needed.
// Return a token immediately.
response->set_content(R"({"token": ")" + std::string(kToken) +
R"("})");
return response;
});
return {net::HTTP_OK,
kTestContentType,
accounts_endpoint_url,
client_metadata_endpoint_url,
id_assertion_endpoint_url,
/*vc_issuance_endpoint_url=*/std::string(),
/*metrics_endpoint_url=*/std::string(),
login_url,
/*types=*/{},
servlets};
}
IdpTestServer* idp_server() { return idp_server_.get(); }
void SetTestIdentityRequestDialogController(
std::optional<std::string> dialog_selected_account) {
auto controller = std::make_unique<FakeIdentityRequestDialogController>(
std::move(dialog_selected_account), /*web_contents=*/nullptr);
test_browser_client_->SetIdentityRequestDialogController(
std::move(controller));
}
void SetTestDigitalIdentityProvider() {
auto provider = std::make_unique<MockDigitalIdentityProvider>();
test_browser_client_->SetDigitalIdentityProvider(std::move(provider));
}
void SetTestModalDialogViewDelegate() {
test_modal_dialog_view_delegate_ =
std::make_unique<TestFederatedIdentityModalDialogViewDelegate>();
test_browser_client_->SetIdentityRegistry(
shell()->web_contents(), test_modal_dialog_view_delegate_->GetWeakPtr(),
GURL(BaseIdpUrl()));
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<WebIdTestContentBrowserClient> test_browser_client_;
std::unique_ptr<TestFederatedIdentityModalDialogViewDelegate>
test_modal_dialog_view_delegate_;
private:
EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
std::unique_ptr<IdpTestServer> idp_server_;
};
class WebIdIdpSigninStatusBrowserTest : public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
InMemoryFederatedPermissionContext* sharing_context() {
BrowserContext* context = shell()->web_contents()->GetBrowserContext();
return static_cast<InMemoryFederatedPermissionContext*>(
context->GetFederatedIdentityPermissionContext());
}
};
class WebIdLightweightFedcmBrowserTest : public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
std::vector<base::test::FeatureRef> features;
features.push_back(features::kFedCmLightweightMode);
scoped_feature_list_.InitWithFeatures(features, {});
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
InMemoryFederatedPermissionContext* sharing_context() {
BrowserContext* context = shell()->web_contents()->GetBrowserContext();
return static_cast<InMemoryFederatedPermissionContext*>(
context->GetFederatedIdentityPermissionContext());
}
};
class WebIdIdpSigninStatusForFetchKeepAliveBrowserTest
: public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
scoped_feature_list_.InitWithFeatures(
{blink::features::kKeepAliveInBrowserMigration}, {});
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
InMemoryFederatedPermissionContext* sharing_context() {
BrowserContext* context = shell()->web_contents()->GetBrowserContext();
return static_cast<InMemoryFederatedPermissionContext*>(
context->GetFederatedIdentityPermissionContext());
}
};
class WebIdIdPRegistryBrowserTest : public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
std::vector<base::test::FeatureRef> features;
features.push_back(net::features::kSplitCacheByNetworkIsolationKey);
features.push_back(features::kFedCm);
features.push_back(features::kFedCmIdPRegistration);
features.push_back(features::kFedCmLightweightMode);
scoped_feature_list_.InitWithFeatures(features, {});
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
InMemoryFederatedPermissionContext* sharing_context() {
BrowserContext* context = shell()->web_contents()->GetBrowserContext();
return static_cast<InMemoryFederatedPermissionContext*>(
context->GetFederatedIdentityPermissionContext());
}
void NavigateToIdpToRegisterAndSetLoginStatus(const GURL& configURL) {
EXPECT_TRUE(NavigateToURL(shell(), configURL));
std::string script = R"(
(async () => {
await IdentityProvider.register(')" +
configURL.spec() + R"(');
// The permission was accepted if the promise resolves.
return true;
}) ()
)";
EXPECT_EQ(true, EvalJs(shell(), script));
// Assert that the IdP was added to the Registry.
const std::vector<GURL>& registeredIdPs =
sharing_context()->GetRegisteredIdPs();
EXPECT_NE(
std::find(registeredIdPs.begin(), registeredIdPs.end(), configURL),
registeredIdPs.end());
// Set the login status and push accounts since we only support pushed
// accounts for registered IDPs.
static constexpr char set_status_script[] = R"(
(async () => {
await navigator.login.setStatus("logged-in", {accounts: [
{id: "not_real_account", name: "Test Name", email: "test@idp.example"}
]});
return true;
})()
)";
EXPECT_EQ(true, EvalJs(shell(), set_status_script));
}
base::HistogramTester histogram_tester_;
};
class WebIdAuthzBrowserTest : public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
};
// Verify a standard login flow with IdP sign-in page.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, FullLoginFlow) {
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
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::ConfigDetails config_details = BuildValidConfigDetails();
config_details.accounts_endpoint_url = "/fedcm/accounts_endpoint.json";
config_details.client_metadata_endpoint_url =
"/fedcm/client_metadata_endpoint.json";
config_details.id_assertion_endpoint_url =
"/fedcm/id_assertion_endpoint.json";
idp_server()->SetConfigResponseDetails(config_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) {
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"The IdP is not potentially trustworthy \\(are you using HTTP\\?\\)");
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
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 = "NetworkError: Error retrieving a token.";
EXPECT_EQ(expected_error, ExtractJsError(EvalJs(shell(), script)));
ASSERT_TRUE(console_observer.Wait());
}
// Verify that passing a non-string token in the ID assertion response results
// in an error when the flexible token formats feature is disabled.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, NonStringTokenRejected) {
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
config_details.id_assertion_endpoint_url = "/non_string_token_endpoint.php";
// Add a servlet to serve a response with a non-string token.
config_details.servlets["/non_string_token_endpoint.php"] =
base::BindRepeating(
[](const HttpRequest& request) -> std::unique_ptr<HttpResponse> {
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type(kTestContentType);
response->AddCustomHeader("Access-Control-Allow-Origin", "*");
response->AddCustomHeader("Access-Control-Allow-Credentials",
"true");
// Return a JSON response with a non-string token (object instead of
// string)
response->set_content(R"({
"token": {
"type": "jwt",
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
})");
return std::move(response);
});
idp_server()->SetConfigResponseDetails(config_details);
std::string script = R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
}]
}
}));
return x.token;
}) ()
)";
// Expect an error when non-string token is returned without flexible formats
// enabled
std::string expected_error =
"IdentityCredentialError: Error retrieving a token.";
EXPECT_EQ(expected_error, ExtractJsError(EvalJs(shell(), script)));
}
// Verify that an IdP can register itself.
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest, RegisterIdP) {
GURL configURL = GURL(BaseIdpUrl());
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
// Expects the account chooser to be opened. Selects the first account.
EXPECT_CALL(*controller, RequestIdPRegistrationPermision)
.WillOnce(::testing::WithArg<1>(
[](base::OnceCallback<void(bool accepted)> callback) {
std::move(callback).Run(true);
}));
// We navigate to the IdP's configURL so that we can run
// the script below with the IdP's origin as the top level
// first party context.
EXPECT_TRUE(NavigateToURL(shell(), configURL));
std::string script = R"(
(async () => {
await IdentityProvider.register(')" +
configURL.spec() + R"(');
// The permission was accepted if the promise resolves.
return true;
}) ()
)";
EXPECT_EQ(true, EvalJs(shell(), script));
EXPECT_EQ(std::vector<GURL>{configURL},
sharing_context()->GetRegisteredIdPs());
histogram_tester_.ExpectTotalCount("Blink.FedCm.AccountsRequestSent", 0);
}
// Verify that the RP cannot register the IdP across origins.
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest, RpCantRegisterIdP) {
std::string script = R"(
(async () => {
return await IdentityProvider.register(')" +
BaseIdpUrl() + R"(');
}) ()
)";
std::string expected_error =
"NotAllowedError: Attempting to register a cross-origin config.";
EXPECT_EQ(expected_error, ExtractJsError(EvalJs(shell(), script)));
}
// Verify that an IdP can unregister itself.
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest, UnregisterIdP) {
GURL configURL = GURL(BaseIdpUrl());
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
// Expects the account chooser to be opened. Selects the first account.
EXPECT_CALL(*controller, RequestIdPRegistrationPermision)
.WillOnce(::testing::WithArg<1>(
[](base::OnceCallback<void(bool accepted)> callback) {
std::move(callback).Run(true);
}));
// We navigate to the IdP's configURL so that we can run
// the script below with the IdP's origin as the top level
// first party context.
EXPECT_TRUE(NavigateToURL(shell(), configURL));
std::string script = R"(
(async () => {
await IdentityProvider.register(')" +
configURL.spec() + R"(');
await IdentityProvider.unregister(')" +
configURL.spec() + R"(');
return true;
}) ()
)";
EXPECT_EQ(true, EvalJs(shell(), script));
EXPECT_TRUE(sharing_context()->GetRegisteredIdPs().empty());
histogram_tester_.ExpectTotalCount("Blink.FedCm.AccountsRequestSent", 0);
}
// Verify that an RP can request from registered IdPs.
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest, UseRegistry) {
GURL configURL = GURL(BaseIdpUrl());
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
// Expects the account chooser to be opened. Selects the first account.
EXPECT_CALL(*controller, RequestIdPRegistrationPermision)
.WillOnce(::testing::WithArg<1>(
[](base::OnceCallback<void(bool accepted)> callback) {
std::move(callback).Run(true);
}));
NavigateToIdpToRegisterAndSetLoginStatus(configURL);
// Navigate to the RP.
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/title1.html")));
std::string get_script = R"(
(async () => {
var {token} = await navigator.credentials.get({
identity: {
providers: [{
nonce: "1234",
configURL: "any",
clientId: "https://rp.example",
}]
}
});
return token;
}) ()
)";
SetTestIdentityRequestDialogController("not_real_account");
EXPECT_EQ(std::string(kToken), EvalJs(shell(), get_script));
histogram_tester_.ExpectTotalCount("Blink.FedCm.AccountsRequestSent", 0);
}
// Verify that when type is requested, an IDP not matching it will not show
// up.
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest, RegistryWithTypeNoMatch) {
GURL configURL = GURL(BaseIdpUrl());
auto details = BuildValidConfigDetails();
details.types = {"idp_type"};
idp_server()->SetConfigResponseDetails(details);
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
EXPECT_CALL(*controller, RequestIdPRegistrationPermision)
.WillOnce(::testing::WithArg<1>(
[](base::OnceCallback<void(bool accepted)> callback) {
std::move(callback).Run(true);
}));
// We navigate to the IdP's configURL so that we can run
// the script below with the IdP's origin as the top level
// first party context.
EXPECT_TRUE(NavigateToURL(shell(), configURL));
std::string script = R"(
(async () => {
await IdentityProvider.register(')" +
configURL.spec() + R"(');
// The permission was accepted if the promise resolves.
return true;
}) ()
)";
EXPECT_EQ(true, EvalJs(shell(), script));
// Navigate to the RP.
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/title1.html")));
std::string get_script = R"(
(async () => {
var {token} = await navigator.credentials.get({
identity: {
providers: [{
nonce: "1234",
configURL: "any",
clientId: "https://rp.example",
type: "no_match"
}]
}
});
return token;
}) ()
)";
SetTestIdentityRequestDialogController("not_real_account");
std::string expected_error = "NetworkError: Error retrieving a token.";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"The requested IdP type did not match the registered IdP.");
// If the IdP does not have type set, it should not show up.
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
EXPECT_EQ(expected_error, ExtractJsError(EvalJs(shell(), get_script)));
ASSERT_TRUE(console_observer.Wait());
histogram_tester_.ExpectTotalCount("Blink.FedCm.AccountsRequestSent", 0);
}
// Verify that when the type of the registered IdP matches the requested one,
// the FedCM flow is successful.
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest, RegistryWithTypeMatch) {
GURL configURL = GURL(BaseIdpUrl());
auto details = BuildValidConfigDetails();
details.types = {"type_no_match", "idp_type"};
idp_server()->SetConfigResponseDetails(details);
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
EXPECT_CALL(*controller, RequestIdPRegistrationPermision)
.WillOnce(::testing::WithArg<1>(
[](base::OnceCallback<void(bool accepted)> callback) {
std::move(callback).Run(true);
}));
NavigateToIdpToRegisterAndSetLoginStatus(configURL);
// Navigate to the RP.
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/title1.html")));
std::string get_script = R"(
(async () => {
var {token} = await navigator.credentials.get({
identity: {
providers: [{
nonce: "1234",
configURL: "any",
clientId: "https://rp.example",
type: "idp_type"
}]
}
});
return token;
}) ()
)";
SetTestIdentityRequestDialogController("not_real_account");
EXPECT_EQ(std::string(kToken), EvalJs(shell(), get_script));
histogram_tester_.ExpectTotalCount("Blink.FedCm.AccountsRequestSent", 0);
}
// Test that multiple IdPs can be registered and that the FedCM flow is
// successful when the two registered IdPs are shown.
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest, MultipleRegisteredIdps) {
GURL configURL = GURL(BaseIdpUrl());
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
EXPECT_CALL(*controller, RequestIdPRegistrationPermision)
.WillRepeatedly(::testing::WithArg<1>(
[](base::OnceCallback<void(bool accepted)> callback) {
std::move(callback).Run(true);
}));
// Register the first IdP and push accounts.
NavigateToIdpToRegisterAndSetLoginStatus(configURL);
// Register the second IdP.
mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
controller = static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
EXPECT_CALL(*controller, RequestIdPRegistrationPermision)
.WillOnce(::testing::WithArg<1>(
[](base::OnceCallback<void(bool accepted)> callback) {
std::move(callback).Run(true);
}));
GURL otherConfigURL = https_server().GetURL("idp2.example", "/fedcm.json");
NavigateToIdpToRegisterAndSetLoginStatus(otherConfigURL);
// Navigate to the RP.
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/title1.html")));
std::string get_script = R"(
(async () => {
var {token} = await navigator.credentials.get({
identity: {
providers: [{
nonce: "1234",
configURL: "any",
clientId: "https://rp.example",
}]
}
});
return token;
}) ()
)";
SetTestIdentityRequestDialogController("not_real_account");
EXPECT_EQ(std::string(kToken), EvalJs(shell(), get_script));
histogram_tester_.ExpectTotalCount("Blink.FedCm.AccountsRequestSent", 0);
}
// Verify that if an IDP is registered but has no pushed accounts, the FedCM
// call fails.
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest, RegistryNoPushedAccounts) {
GURL configURL = GURL(BaseIdpUrl());
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
// Expects the account chooser to be opened. Selects the first account.
EXPECT_CALL(*controller, RequestIdPRegistrationPermision)
.WillOnce(::testing::WithArg<1>(
[](base::OnceCallback<void(bool accepted)> callback) {
std::move(callback).Run(true);
}));
// We navigate to the IdP's configURL so that we can run
// the script below with the IdP's origin as the top level
// first party context.
EXPECT_TRUE(NavigateToURL(shell(), configURL));
std::string script = R"(
(async () => {
await IdentityProvider.register(')" +
configURL.spec() + R"(');
// The permission was accepted if the promise resolves.
return true;
}) ()
)";
EXPECT_EQ(true, EvalJs(shell(), script));
// Assert that the IdP was added to the Registry.
const std::vector<GURL>& registeredIdPs =
sharing_context()->GetRegisteredIdPs();
EXPECT_NE(std::find(registeredIdPs.begin(), registeredIdPs.end(), configURL),
registeredIdPs.end());
// Do not push accounts.
// Navigate to the RP.
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/title1.html")));
std::string get_script = R"(
(async () => {
var {token} = await navigator.credentials.get({
identity: {
providers: [{
nonce: "1234",
configURL: "any",
clientId: "https://rp.example",
}]
}
});
return token;
}) ()
)";
SetTestIdentityRequestDialogController("not_real_account");
std::string expected_error = "NetworkError: Error retrieving a token.";
EXPECT_EQ(expected_error, ExtractJsError(EvalJs(shell(), get_script)));
histogram_tester_.ExpectTotalCount("Blink.FedCm.AccountsRequestSent", 0);
}
IN_PROC_BROWSER_TEST_F(WebIdIdPRegistryBrowserTest,
RegistrationFailsWithInvalidLoginUrl) {
GURL configURL = GURL(BaseIdpUrl());
auto details = BuildValidConfigDetails();
// Set this as empty so that the login URL is invalid.
details.login_url = "";
idp_server()->SetConfigResponseDetails(details);
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
// We navigate to the IdP's configURL so that we can run
// the script below with the IdP's origin as the top level
// first party context.
EXPECT_TRUE(NavigateToURL(shell(), configURL));
std::string script = R"(
(async () => {
await IdentityProvider.register(')" +
configURL.spec() + R"(');
// The permission was accepted if the promise resolves.
return true;
}) ()
)";
EXPECT_EQ("NotAllowedError: Invalid identity provider registration config.",
ExtractJsError(EvalJs(shell(), script)));
EXPECT_TRUE(sharing_context()->GetRegisteredIdPs().empty());
histogram_tester_.ExpectTotalCount("Blink.FedCm.AccountsRequestSent", 0);
}
// Verify that IDP sign-in headers work.
IN_PROC_BROWSER_TEST_F(WebIdIdpSigninStatusBrowserTest, IdpSigninToplevel) {
GURL url = https_server().GetURL(kRpHostName, "/header/signin");
EXPECT_FALSE(sharing_context()
->GetIdpSigninStatus(url::Origin::Create(url))
.has_value());
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url));
auto value = sharing_context()->GetIdpSigninStatus(url::Origin::Create(url));
ASSERT_TRUE(value.has_value());
EXPECT_TRUE(*value);
}
// Verify that IDP sign-out headers work.
IN_PROC_BROWSER_TEST_F(WebIdIdpSigninStatusBrowserTest, IdpSignoutToplevel) {
GURL url = https_server().GetURL(kRpHostName, "/header/signout");
EXPECT_FALSE(sharing_context()
->GetIdpSigninStatus(url::Origin::Create(url))
.has_value());
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url));
auto value = sharing_context()->GetIdpSigninStatus(url::Origin::Create(url));
ASSERT_TRUE(value.has_value());
EXPECT_FALSE(*value);
}
// Verify that IDP sign-in/out headers work in subresources.
IN_PROC_BROWSER_TEST_F(WebIdIdpSigninStatusBrowserTest,
IdpSigninAndOutSubresource) {
static constexpr char script[] = R"(
(async () => {
var resp = await fetch('/header/sign%s');
return resp.status;
}) ();
)";
GURL url_for_origin = https_server().GetURL(kRpHostName, "/header/");
url::Origin origin = url::Origin::Create(url_for_origin);
EXPECT_FALSE(sharing_context()->GetIdpSigninStatus(origin).has_value());
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "in")));
run_loop.Run();
}
auto value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_TRUE(*value);
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "out")));
run_loop.Run();
}
value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_FALSE(*value);
}
// Verify that IDP sign-in/out headers work in fetch keepalive subresources when
// proxied via browser.
IN_PROC_BROWSER_TEST_F(WebIdIdpSigninStatusForFetchKeepAliveBrowserTest,
IdpSigninAndOutSubresourceFetchKeepAliveInBrowser) {
static constexpr char script[] = R"(
(async () => {
var resp = await fetch('/header/sign%s', {keepalive: true});
return resp.status;
}) ();
)";
GURL url_for_origin = https_server().GetURL(kRpHostName, "/header/");
url::Origin origin = url::Origin::Create(url_for_origin);
EXPECT_FALSE(sharing_context()->GetIdpSigninStatus(origin).has_value());
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "in")));
run_loop.Run();
}
auto value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_TRUE(*value);
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "out")));
run_loop.Run();
}
value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_FALSE(*value);
}
// Verify that IDP sign-in/out headers work in sync XHR.
IN_PROC_BROWSER_TEST_F(WebIdIdpSigninStatusBrowserTest,
IdpSigninAndOutSyncXhr) {
static constexpr char script[] = R"(
(async () => {
const request = new XMLHttpRequest();
request.open('GET', '/header/sign%s', false);
request.send(null);
return request.status;
}) ();
)";
GURL url_for_origin = https_server().GetURL(kRpHostName, "/header/");
url::Origin origin = url::Origin::Create(url_for_origin);
EXPECT_FALSE(sharing_context()->GetIdpSigninStatus(origin).has_value());
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "in")));
run_loop.Run();
}
auto value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_TRUE(*value);
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "out")));
run_loop.Run();
}
value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_FALSE(*value);
}
// Verify that IDP sign-in/out headers work in fetch from worker.
IN_PROC_BROWSER_TEST_F(WebIdIdpSigninStatusBrowserTest,
IdpSigninAndOutFetchFromWorker) {
static constexpr char script[] = R"(
(async () => {
const script =
'(async () => { return (await fetch("/header/sign%s")).status; })()'
return new Promise(resolve => {
const channel = new MessageChannel();
channel.port1.addEventListener('message', (e) => {
resolve(e.data);
});
channel.port1.start();
const worker = new Worker('/fedcm/eval_worker.js');
worker.postMessage(
{
nested: false,
script: script,
},
[channel.port2]
);
});
}) ();
)";
GURL url_for_origin = https_server().GetURL(kRpHostName, "/header/");
url::Origin origin = url::Origin::Create(url_for_origin);
EXPECT_FALSE(sharing_context()->GetIdpSigninStatus(origin).has_value());
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "in")));
run_loop.Run();
}
auto value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_TRUE(*value);
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "out")));
run_loop.Run();
}
value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_FALSE(*value);
}
// Verify that IDP sign-in/out headers work in fetch from nested worker.
IN_PROC_BROWSER_TEST_F(WebIdIdpSigninStatusBrowserTest,
IdpSigninAndOutFetchFromNestedWorker) {
static constexpr char script[] = R"(
(async () => {
const script =
'(async () => { return (await fetch("/header/sign%s")).status; })()'
return new Promise(resolve => {
const channel = new MessageChannel();
channel.port1.addEventListener('message', (e) => {
resolve(e.data);
});
channel.port1.start();
const worker = new Worker('/fedcm/eval_worker.js');
worker.postMessage(
{
nested: true,
script: script,
},
[channel.port2]
);
});
}) ();
)";
GURL url_for_origin = https_server().GetURL(kRpHostName, "/header/");
url::Origin origin = url::Origin::Create(url_for_origin);
EXPECT_FALSE(sharing_context()->GetIdpSigninStatus(origin).has_value());
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "in")));
run_loop.Run();
}
auto value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_TRUE(*value);
{
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
EXPECT_EQ(200, EvalJs(shell(), base::StringPrintf(script, "out")));
run_loop.Run();
}
value = sharing_context()->GetIdpSigninStatus(origin);
ASSERT_TRUE(value.has_value());
EXPECT_FALSE(*value);
}
// Verify that an IdP can call close to close modal dialog views.
IN_PROC_BROWSER_TEST_F(WebIdIdpSigninStatusBrowserTest, IdPClose) {
GURL configURL = GURL(BaseIdpUrl());
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
// We navigate to the IdP's configURL so that we can run
// the script below with the IdP's origin as the top level
// first party context.
EXPECT_TRUE(NavigateToURL(shell(), configURL));
std::string script = R"(
(async () => {
await IdentityProvider.close();
return true;
}) ()
)";
// IdentityProvider.close() should invoke NotifyClose() on the delegate set
// on the identity registry. Check that modal dialog is not closed.
EXPECT_FALSE(test_modal_dialog_view_delegate_->closed_);
// Run the script.
{
base::RunLoop run_loop;
test_modal_dialog_view_delegate_->SetClosure(run_loop.QuitClosure());
EXPECT_EQ(true, EvalJs(shell(), script));
run_loop.Run();
}
// Check that modal dialog is closed.
EXPECT_TRUE(test_modal_dialog_view_delegate_->closed_);
}
class WebIdDigitalCredentialsBrowserTest : public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
std::vector<base::test::FeatureRef> features;
features.push_back(net::features::kSplitCacheByNetworkIsolationKey);
features.push_back(features::kWebIdentityDigitalCredentials);
scoped_feature_list_.InitWithFeatures(features, {});
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
InMemoryFederatedPermissionContext* sharing_context() {
BrowserContext* context = shell()->web_contents()->GetBrowserContext();
return static_cast<InMemoryFederatedPermissionContext*>(
context->GetFederatedIdentityPermissionContext());
}
void SetUpOnMainThread() override {
WebIdBrowserTest::SetUpOnMainThread();
SetTestDigitalIdentityProvider();
MockDigitalIdentityProvider* digital_identity_provider =
static_cast<MockDigitalIdentityProvider*>(
test_browser_client_->GetDigitalIdentityProviderForTests());
ON_CALL(*digital_identity_provider,
ShowDigitalIdentityInterstitial(_, _, _, _))
.WillByDefault(WithArg<3>(
[](DigitalIdentityProvider::DigitalIdentityInterstitialCallback
callback) {
std::move(callback).Run(
DigitalIdentityProvider::RequestStatusForMetrics::kSuccess);
return base::OnceClosure();
}));
}
};
std::string BuildDigitalIdentityValidJsRequestDictionary() {
return R"({
digital: {
requests: [{
protocol: "openid4vp",
data: {
// Based on https://github.com/openid/OpenID4VP/issues/125
client_id: "client.example.org",
client_id_scheme: "web-origin",
nonce: "n-0S6_WzA2Mj",
presentation_definition: {
// Presentation Exchange request, omitted for brevity
}
}
}],
},
})";
}
EvalJsResult EvalJsAndReturnToken(const ToRenderFrameHost& execution_target,
std::string_view script_setting_token) {
std::string script = base::StringPrintf(R"(
(async () => {
%s
return token;
}) ()
)",
script_setting_token.data());
return EvalJs(execution_target, script);
}
EvalJsResult RunDigitalIdentityValidRequest(
const ToRenderFrameHost& execution_target) {
std::string script = base::StringPrintf(
"const {data} = await navigator.credentials.get(%s);return data;",
BuildDigitalIdentityValidJsRequestDictionary().c_str());
return EvalJsAndReturnToken(execution_target, script);
}
// Leniently parses the input string as JSON and compares it to already-parsed
// JSON.
MATCHER_P(JsonMatches, ref, "") {
int json_parsing_options =
base::JSONParserOptions::JSON_PARSE_CHROMIUM_EXTENSIONS |
base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS;
auto ref_json =
base::JSONReader::ReadAndReturnValueWithError(ref, json_parsing_options);
return ref_json.has_value() && (ref_json.value() == arg.ToValue());
}
// Test that a Verifiable Credential can be requested via the
// navigator.credentials JS API.
IN_PROC_BROWSER_TEST_F(WebIdDigitalCredentialsBrowserTest,
NavigatorCredentialsApi) {
base::Value kIdentityProviderResponse =
base::JSONReader::Read(
R"({"vp_token": "token data" , "presentation_submission":"bar"})",
base::JSON_PARSE_CHROMIUM_EXTENSIONS)
.value();
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
MockDigitalIdentityProvider* digital_identity_provider =
static_cast<MockDigitalIdentityProvider*>(
test_browser_client_->GetDigitalIdentityProviderForTests());
std::string_view request = R"(
{
"requests": [ {
"protocol": "openid4vp",
"data": {
"client_id": "client.example.org",
"client_id_scheme": "web-origin",
"nonce": "n-0S6_WzA2Mj",
"presentation_definition": {
}
},
} ]
}
)";
std::string json;
// Invalid whitespace and newlines are added to the request string to make it
// easier to read in this test, so we remove them before actually making the
// JSON comparison in IsJson below.
base::RemoveChars(request, "\n ", &json);
EXPECT_CALL(*digital_identity_provider, Get(_, _, JsonMatches(json), _))
.WillOnce(WithArg<3>(
[&kIdentityProviderResponse](
DigitalIdentityProvider::DigitalIdentityCallback callback) {
std::move(callback).Run(DigitalCredential(
"openid4vp", kIdentityProviderResponse.Clone()));
}));
EXPECT_EQ(kIdentityProviderResponse, RunDigitalIdentityValidRequest(shell()));
}
// Test that when there's a pending mdoc request, a second `get` call should be
// rejected.
IN_PROC_BROWSER_TEST_F(WebIdDigitalCredentialsBrowserTest,
OnlyOneInFlightDigitalCredentialRequestIsAllowed) {
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
MockDigitalIdentityProvider* digital_identity_provider =
static_cast<MockDigitalIdentityProvider*>(
test_browser_client_->GetDigitalIdentityProviderForTests());
base::Value kResponse =
base::JSONReader::Read(R"({"token":"test-mdoc"})",
base::JSON_PARSE_CHROMIUM_EXTENSIONS)
.value();
EXPECT_CALL(*digital_identity_provider, Get)
.WillOnce(WithArg<3>(
[&](DigitalIdentityProvider::DigitalIdentityCallback callback) {
EXPECT_EQ(
"NotAllowedError: Only one navigator.credentials.get/create "
"request may be outstanding at one time.",
ExtractJsError(RunDigitalIdentityValidRequest(shell())));
std::move(callback).Run(
DigitalCredential("openid4vp", kResponse.Clone()));
}));
EXPECT_EQ(kResponse, RunDigitalIdentityValidRequest(shell()));
}
// Test that when the user declines a digital identity request, the error
// message returned to JavaScript does not indicate that the user declined the
// request.
IN_PROC_BROWSER_TEST_F(WebIdDigitalCredentialsBrowserTest,
UserDeclinesRequest) {
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
MockDigitalIdentityProvider* digital_identity_provider =
static_cast<MockDigitalIdentityProvider*>(
test_browser_client_->GetDigitalIdentityProviderForTests());
EXPECT_CALL(*digital_identity_provider, Get)
.WillOnce(WithArg<3>(
[&](DigitalIdentityProvider::DigitalIdentityCallback callback) {
std::move(callback).Run(base::unexpected(
DigitalIdentityProvider::RequestStatusForMetrics::
kErrorUserDeclined));
}));
EXPECT_EQ("NetworkError: Error retrieving a token.",
ExtractJsError(RunDigitalIdentityValidRequest(shell())));
}
// Test that Blink.DigitalIdentityRequest.Status UMA metric is recorded when
// digital identity request completes.
IN_PROC_BROWSER_TEST_F(WebIdDigitalCredentialsBrowserTest,
RecordRequestStatusHistogramAfterRequestCompletes) {
base::HistogramTester histogram_tester;
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
MockDigitalIdentityProvider* digital_identity_provider =
static_cast<MockDigitalIdentityProvider*>(
test_browser_client_->GetDigitalIdentityProviderForTests());
EXPECT_CALL(*digital_identity_provider, Get)
.WillOnce(WithArg<3>(
[](DigitalIdentityProvider::DigitalIdentityCallback callback) {
std::move(callback).Run(DigitalCredential(
"openid4vp",
base::JSONReader::Read(R"({"token":"test-mdoc"})",
base::JSON_PARSE_CHROMIUM_EXTENSIONS)
.value()));
}));
RunDigitalIdentityValidRequest(shell());
histogram_tester.ExpectUniqueSample(
"Blink.DigitalIdentityRequest.Status",
DigitalIdentityProvider::RequestStatusForMetrics::kSuccess, 1);
}
// Verify that the Authz parameters are passed to the id assertion endpoint.
IN_PROC_BROWSER_TEST_F(WebIdAuthzBrowserTest, Authz_noPopUpWindow) {
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
// Points the id assertion endpoint to a servlet.
config_details.id_assertion_endpoint_url = "/authz/id_assertion_endpoint.php";
// Add a servlet to serve a response for the id assertoin endpoint.
config_details.servlets["/authz/id_assertion_endpoint.php"] =
base::BindRepeating(
[](const HttpRequest& request) -> std::unique_ptr<HttpResponse> {
EXPECT_EQ(request.method, HttpMethod::METHOD_POST);
EXPECT_EQ(request.has_content, true);
std::string content;
content += "client_id=client_id_1&";
content += "nonce=12345&";
content += "account_id=not_real_account&";
content += "disclosure_text_shown=false&";
content += "is_auto_selected=false&";
content += "mode=passive&";
// Asserts that the fields and params parameters
// were passed correctly to the id assertion endpoint.
content += "fields=name,email,picture&";
content +=
"params=%7B%22foo%22:%22bar%22,%22hello%22"
":%22world%22,%22%3F+gets+://%22:%22%26+escaped+!%22%7D";
EXPECT_EQ(request.content, content);
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/json");
DCHECK(request.headers.contains("Origin"));
response->AddCustomHeader(
network::cors::header_names::kAccessControlAllowOrigin,
request.headers.at("Origin"));
response->AddCustomHeader(
network::cors::header_names::kAccessControlAllowCredentials,
"true");
// Standard scopes were used, so no extra permission needed.
// Return a token immediately.
response->set_content(R"({"token": "[request lgtm!]"})");
return response;
});
idp_server()->SetConfigResponseDetails(config_details);
std::string script = R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345',
fields: [
'name',
'email',
'picture',
],
params: {
'foo': 'bar',
'hello': 'world',
'? gets ://': '& escaped !',
}
}]
}
}));
return x.token;
}) ()
)";
EXPECT_EQ(std::string("[request lgtm!]"), EvalJs(shell(), script));
}
// Verify that subsets of the default fields work.
IN_PROC_BROWSER_TEST_F(WebIdAuthzBrowserTest, Authz_invalidFields) {
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
// Points the id assertion endpoint to a servlet.
config_details.id_assertion_endpoint_url = "/authz/id_assertion_endpoint.php";
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
std::string script = R"(
(async () => {
var result = await navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345',
fields: [
'name'
],
}]
}
});
return result.token;
}) ()
)";
EXPECT_EQ(std::string("[not a real token]"), EvalJs(shell(), script));
}
// Verify that the id assertion endpoint can request a pop-up window.
IN_PROC_BROWSER_TEST_F(WebIdAuthzBrowserTest, Authz_openPopUpWindow) {
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
// Points the id assertion endpoint to a servlet.
config_details.id_assertion_endpoint_url = "/authz/id_assertion_endpoint.php";
// Points to the relative url of the authorization servlet.
std::string continue_on = "/authz.html";
// Add a servlet to serve a response for the id assertoin endpoint.
config_details.servlets["/authz/id_assertion_endpoint.php"] =
base::BindRepeating(
[](std::string url,
const HttpRequest& request) -> std::unique_ptr<HttpResponse> {
std::string content;
content += "client_id=client_id_1&";
content += "nonce=12345&";
content += "account_id=not_real_account&";
content += "disclosure_text_shown=false&";
content += "is_auto_selected=false&";
content += "mode=passive&";
content += "fields=locale";
EXPECT_EQ(request.content, content);
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/json");
// fields=locale was requested, so need to
// return a continuation url instead of a token.
auto body = R"({"continue_on": ")" + url + R"("})";
response->set_content(body);
DCHECK(request.headers.contains("Origin"));
response->AddCustomHeader(
network::cors::header_names::kAccessControlAllowOrigin,
request.headers.at("Origin"));
response->AddCustomHeader(
network::cors::header_names::kAccessControlAllowCredentials,
"true");
return response;
},
continue_on);
idp_server()->SetConfigResponseDetails(config_details);
// Create a WebContents that represents the modal dialog, specifically
// the structure that the Identity Registry hangs to.
Shell* modal = CreateBrowser();
auto config_url = GURL(BaseIdpUrl());
modal->LoadURL(config_url);
EXPECT_TRUE(WaitForLoadStop(modal->web_contents()));
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
// Expects the account chooser to be opened. Selects the first account.
EXPECT_CALL(*controller, ShowAccountsDialog)
.WillOnce(::testing::WithArg<5>([&config_url](auto on_selected) {
std::move(on_selected)
.Run(config_url,
/* account_id=*/"not_real_account",
/* is_sign_in= */ true);
return true;
}));
base::RunLoop run_loop;
EXPECT_CALL(*controller, ShowModalDialog)
.WillOnce(::testing::WithArg<0>(
[&config_url, continue_on, &modal, &run_loop](const GURL& url) {
// Expect that the relative continue_on url will be resolved
// before opening the dialog.
EXPECT_EQ(url.spec(), config_url.Resolve(continue_on));
// When the pop-up window is opened, resolve it immediately by
// returning a test web contents, which can then later be used
// to refer to the identity registry.
run_loop.Quit();
return modal->web_contents();
}));
std::string script = R"(
var result = navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345',
fields: [
'locale'
],
}]
}
}).then(({token}) => token);
)";
// Kick off the identity credential request and deliberately
// leave the promise hanging, since it requires UX permission
// prompts to be accepted later.
EXPECT_TRUE(content::ExecJs(shell(), script,
content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// Wait for the modal dialog to be resolved.
run_loop.Run();
std::string token = "--fake-token-from-pop-up-window--";
base::RunLoop run_loop2;
EXPECT_CALL(*controller, CloseModalDialog).WillOnce([&run_loop2]() {
run_loop2.Quit();
});
// Resolve the hanging token request by notifying the registry.
EXPECT_TRUE(content::ExecJs(
modal, R"(IdentityProvider.resolve(')" + token + R"('))"));
run_loop2.Run();
// Finally, wait for the promise to resolve and compare its result
// to the expected token that was provided in the modal dialog.
EXPECT_EQ(token, EvalJs(shell(), "result"));
}
IN_PROC_BROWSER_TEST_F(WebIdAuthzBrowserTest, IdpLoginCallsResolve) {
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
// Mark us as signed out from this IdP.
GURL url{IdpRootUrl() + "/header/signout"};
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url));
// This will be used for the login dialog.
Shell* modal = CreateBrowser();
auto config_url = GURL(BaseIdpUrl());
modal->LoadURL(config_url);
EXPECT_TRUE(WaitForLoadStop(modal->web_contents()));
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
MockIdentityRequestDialogController* controller =
static_cast<MockIdentityRequestDialogController*>(
test_browser_client_->GetIdentityRequestDialogControllerForTests());
base::RunLoop modal_dialog_loop;
EXPECT_CALL(*controller, ShowModalDialog)
.WillOnce(WithArgs<0, 2>(
[&modal, &modal_dialog_loop](
const GURL& url,
content::IdentityRequestDialogController::DismissCallback cb) {
modal_dialog_loop.Quit();
return modal->web_contents();
}));
EXPECT_CALL(*controller, ShowLoadingDialog).WillOnce(Return(true));
// Now run the actual test.
std::string script = R"(
promise = navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345',
}],
mode: 'active'
}
});
)";
// Initiate the FedCM call
EXPECT_TRUE(ExecJs(shell(), script, EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// Wait for the popup window to open.
modal_dialog_loop.Run();
// IdentityProvider.resolve() should be ignored and not crash.
std::string expected_error =
"NotAllowedError: Not allowed to provide a token.";
EXPECT_EQ(
expected_error,
ExtractJsError(EvalJs(modal, R"(IdentityProvider.resolve('token'))")));
}
// Verify that an IdentityCredentialError exception is returned.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, IdentityCredentialError) {
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
// Points the id assertion endpoint to a servlet.
config_details.id_assertion_endpoint_url = "/error/id_assertion_endpoint.php";
// Add a servlet to serve a response for the id assertion endpoint.
config_details.servlets["/error/id_assertion_endpoint.php"] =
base::BindRepeating([](const HttpRequest& request)
-> std::unique_ptr<HttpResponse> {
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/json");
response->set_content(
R"({"error": {"code": "invalid_request", "url": "https://idp.com/error"}})");
DCHECK(request.headers.contains("Origin"));
response->AddCustomHeader(
network::cors::header_names::kAccessControlAllowOrigin,
request.headers.at("Origin"));
response->AddCustomHeader(
network::cors::header_names::kAccessControlAllowCredentials,
"true");
return response;
});
idp_server()->SetConfigResponseDetails(config_details);
std::string expected_error =
"IdentityCredentialError: Error retrieving a token.";
EXPECT_EQ(expected_error,
ExtractJsError(EvalJs(shell(), GetBasicRequestString())));
}
// Verify that an CORSError is returned.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest, CorsError) {
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
// Points the id assertion endpoint to a servlet.
config_details.id_assertion_endpoint_url = "/error/id_assertion_endpoint.php";
// Add a servlet to serve a response for the id assertion endpoint.
config_details.servlets["/error/id_assertion_endpoint.php"] =
base::BindRepeating(
[](const HttpRequest& request) -> std::unique_ptr<HttpResponse> {
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_FORBIDDEN);
response->set_content_type("text/json");
return response;
});
idp_server()->SetConfigResponseDetails(config_details);
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("Server did not send the correct CORS headers.");
std::string expected_error =
"IdentityCredentialError: Error retrieving a token.";
EXPECT_EQ(expected_error,
ExtractJsError(EvalJs(shell(), GetBasicRequestString())));
ASSERT_TRUE(console_observer.Wait());
}
// Verify that auto re-authn can be triggered if the Rp is on the
// approved_clients list and the IdP has third party cookies access.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest,
IdpHas3PCAccessAndAddsRPInApprovedClients) {
// Does not manually select any account. If auto re-authn is not triggered,
// the test will time out.
SetTestIdentityRequestDialogController(
/*dialog_selected_account=*/std::nullopt);
// The client id `client_id_1` is on the `approved_clients` list defined in
// content/test/data/fedcm/accounts_endpoint.json so by exempting the IdP from
// the check, auto re-authn can be triggered and a token can be returned.
static_cast<InMemoryFederatedPermissionContext*>(
shell()
->web_contents()
->GetBrowserContext()
->GetFederatedIdentityPermissionContext())
->SetHasThirdPartyCookiesAccessForTesting(BaseIdpUrl(), BaseRpUrl());
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
EXPECT_EQ(std::string(kToken), EvalJs(shell(), GetBasicRequestString()));
}
// Verify that using mediation in the wrong place adds log to console.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest,
MediationInIdentityCredentialRequestOptions) {
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
std::string script = R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345',
}],
mediation: 'required'
}
}));
return x.token;
}) ()
)";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"The 'mediation' parameter should be used outside of 'identity' in the "
"FedCM API call.");
EXPECT_EQ(std::string(kToken), EvalJs(shell(), script));
ASSERT_TRUE(console_observer.Wait());
}
// Verify that using mediation in the right place does not add log to console.
IN_PROC_BROWSER_TEST_F(WebIdBrowserTest,
NoConsoleWarningWithProperMediationCall) {
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
std::string script = R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345',
}],
},
mediation: 'required'
}));
return x.token;
}) ()
)";
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ(std::string(kToken), EvalJs(shell(), script));
// TODO(crbug.com/451219310): Remove when FedCM deprecation warnings removed.
// EXPECT_TRUE(console_observer.messages().empty());
}
class WebIdModeBrowserTest : public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
};
std::vector<uint8_t> TestSha256(std::string_view data) {
std::string str = crypto::SHA256HashString(data);
std::vector<uint8_t> result(str.begin(), str.end());
return result;
}
class WebIdDelegationBrowserTest : public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
std::vector<base::test::FeatureRef> features;
features.push_back(features::kFedCm);
features.push_back(features::kFedCmDelegation);
scoped_feature_list_.InitWithFeatures(features, {});
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
protected:
void SetVcIssuanceConfigDetails(base::RunLoop* run_loop) {
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
config_details.vc_issuance_endpoint_url = "/vc_issuance_endpoint.json";
config_details.servlets["/vc_issuance_endpoint.json"] =
base::BindLambdaForTesting([&](const HttpRequest& request)
-> std::unique_ptr<HttpResponse> {
EXPECT_EQ(request.method, HttpMethod::METHOD_POST);
EXPECT_EQ(request.has_content, true);
// Assert that the Verifier's origin isn't passed to the issuer.
EXPECT_TRUE(request.headers.contains("Origin"));
EXPECT_EQ("null", request.headers.at("Origin"));
GURL query_url("http://localhost/?" + request.content);
// Assert that the format type is a supported one by the issuer.
std::string format;
EXPECT_TRUE(net::GetValueForKeyInQuery(query_url, "format", &format));
EXPECT_EQ("vc+sd-jwt", format);
// Expects a holder_key JWK as a parameter.
std::string holder_key_json;
EXPECT_TRUE(net::GetValueForKeyInQuery(query_url, "holder_key",
&holder_key_json));
auto holder_key_value = base::JSONReader::ReadDict(
holder_key_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
EXPECT_TRUE(holder_key_value);
auto holder_key = sdjwt::Jwk::From(*holder_key_value);
EXPECT_TRUE(holder_key);
std::string account_id;
EXPECT_TRUE(
net::GetValueForKeyInQuery(query_url, "account_id", &account_id));
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/json");
// Issues a real signed SD-JWT with a "sub" and "name" disclosures.
sdjwt::Disclosure sub;
sub.name = "sub";
sub.value = account_id;
sub.salt = sdjwt::Disclosure::CreateSalt();
sdjwt::Disclosure name;
name.name = "name";
name.value = "Sam";
name.salt = sdjwt::Disclosure::CreateSalt();
sdjwt::Header header;
sdjwt::Payload payload;
sdjwt::ConfirmationKey confirmation;
// Binds the holder's public key to the issued JWT.
confirmation.jwk = *holder_key;
payload.cnf = confirmation;
payload._sd = {
*sub.Digest(base::BindRepeating(TestSha256)),
*name.Digest(base::BindRepeating(TestSha256)),
};
sdjwt::Jwt jwt;
jwt.header = *header.ToJson();
jwt.payload = *payload.ToJson();
jwt.Sign(sdjwt::CreateJwtSigner(private_key_));
sdjwt::SdJwt sd_jwt;
sd_jwt.jwt = jwt;
sd_jwt.disclosures = {*sub.ToJson(), *name.ToJson()};
response->set_content(R"({"token": ")" + sd_jwt.Serialize() +
R"("})");
return response;
});
idp_server()->SetConfigResponseDetails(config_details);
}
crypto::keypair::PrivateKey private_key_{
crypto::keypair::PrivateKey::GenerateEcP256()};
};
IN_PROC_BROWSER_TEST_F(WebIdDelegationBrowserTest, IssueVCs) {
base::RunLoop run_loop;
SetVcIssuanceConfigDetails(&run_loop);
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/fedcm/sd_jwt.html")));
std::string script = R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
format: 'vc+sd-jwt',
fields: ['name'],
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345',
}],
mode: 'active'
},
}));
return x.token;
}) ()
)";
auto token = EvalJs(shell(), script).ExtractString();
auto public_key = sdjwt::ExportPublicKey(private_key_);
EXPECT_TRUE(public_key);
// Load the token into a string
ASSERT_TRUE(ExecJs(shell(), "var token = '" + token + "';"));
// Load the key into an object
ASSERT_TRUE(ExecJs(shell(), "var key = " + *public_key->Serialize() + ";"));
// Load the audience into a string
ASSERT_TRUE(ExecJs(shell(), "var aud = '" + BaseRpUrl() + "';"));
// Verify the SD-JWT+KB.
EXPECT_THAT(
EvalJs(shell(), "main(token, key, aud, '12345')").TakeValue().TakeList(),
testing::UnorderedElementsAre("Sam"));
}
IN_PROC_BROWSER_TEST_F(WebIdDelegationBrowserTest, ConditionalMediation) {
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
// Keep a copy of the pointer before the std::move.
MockIdentityRequestDialogController* controller = mock.get();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
base::RunLoop modal_loop;
auto configURL = BaseIdpUrl();
EXPECT_CALL(*controller, ShowAccountsDialog)
.WillOnce(
::testing::WithArg<5>([&modal_loop, &configURL](auto on_selected) {
std::move(on_selected)
.Run(GURL(configURL),
/*account_id=*/"not_real_account",
/*is_sign_in=*/true);
modal_loop.Quit();
return true;
}));
EXPECT_CALL(*controller, ShowLoadingDialog).WillOnce(Return(true));
base::RunLoop run_loop;
SetVcIssuanceConfigDetails(&run_loop);
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/fedcm/sd_jwt.html")));
std::string script = R"(
var token = navigator.credentials.get({
mediation: 'conditional',
identity: {
providers: [{
format: 'vc+sd-jwt',
fields: ['name'],
configURL: ')" +
configURL + R"(',
clientId: 'client_id_1',
nonce: '12345',
}],
},
}).then(({token}) => token)
)";
// Await until the accounts are available for autofill.
EXPECT_CALL(*controller, NotifyAutofillSourceReadyForTesting)
.WillOnce([&run_loop]() { run_loop.Quit(); });
auto promise = EvalJs(shell(), script, EXECUTE_SCRIPT_NO_RESOLVE_PROMISES);
run_loop.Run();
// Gets the pending conditional request.
auto* source = webid::AutofillSource::FromPage(
shell()->web_contents()->GetPrimaryPage());
EXPECT_TRUE(source != nullptr);
// Gets all the autofill suggestion and selects the first one.
auto suggestions = source->GetAutofillSuggestions();
EXPECT_TRUE(suggestions);
EXPECT_EQ(suggestions->size(), 1ul);
auto account = (*suggestions)[0];
EXPECT_EQ(account->identity_provider->format, blink::mojom::Format::kSdJwt);
source->NotifyAutofillSuggestionAccepted(
account->identity_provider->idp_metadata.config_url, account->id,
/*show_modal=*/true, base::NullCallback());
// Wait for the user to accept the prompt.
modal_loop.Run();
// Verify that the token is correct.
auto public_key = sdjwt::ExportPublicKey(private_key_);
EXPECT_TRUE(public_key);
// Load the key into an object
ASSERT_TRUE(ExecJs(shell(), "var key = " + *public_key->Serialize() + ";"));
// Load the audience into a string
ASSERT_TRUE(ExecJs(shell(), "var aud = '" + BaseRpUrl() + "';"));
// Verify the SD-JWT+KB.
EXPECT_THAT(
EvalJs(shell(), "(async () => main(await token, key, aud, '12345'))()")
.TakeValue()
.TakeList(),
testing::UnorderedElementsAre("Sam"));
}
// Flaky on mac, https://crbug.com/415953689
#if BUILDFLAG(IS_MAC)
#define MAYBE_ConditionalMediationForMediatedRequest \
DISABLED_ConditionalMediationForMediatedRequest
#else
#define MAYBE_ConditionalMediationForMediatedRequest \
ConditionalMediationForMediatedRequest
#endif
IN_PROC_BROWSER_TEST_F(WebIdDelegationBrowserTest,
MAYBE_ConditionalMediationForMediatedRequest) {
idp_server()->SetConfigResponseDetails(BuildValidConfigDetails());
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
// Keep a copy of the pointer before the std::move.
MockIdentityRequestDialogController* controller = mock.get();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
auto configURL = BaseIdpUrl();
base::RunLoop run_loop;
EXPECT_TRUE(NavigateToURL(shell(), GURL(configURL)));
std::string script = R"(
var token = navigator.credentials.get({
mediation: 'conditional',
identity: {
providers: [{
fields: ['name'],
configURL: ')" +
configURL + R"(',
clientId: 'client_id_1',
nonce: '12345',
}],
},
}).then(({token}) => token)
)";
// Await until the accounts are available for autofill.
EXPECT_CALL(*controller, NotifyAutofillSourceReadyForTesting)
.WillOnce([&run_loop]() { run_loop.Quit(); });
auto promise = EvalJs(shell(), script, EXECUTE_SCRIPT_NO_RESOLVE_PROMISES);
run_loop.Run();
// Gets the pending conditional request.
auto* source = webid::AutofillSource::FromPage(
shell()->web_contents()->GetPrimaryPage());
EXPECT_TRUE(source != nullptr);
// Gets all the autofill suggestion and selects the first one.
auto suggestions = source->GetAutofillSuggestions();
EXPECT_TRUE(suggestions);
EXPECT_EQ(suggestions->size(), 1ul);
auto account = (*suggestions)[0];
// Mediated FedCM has an empty format.
EXPECT_EQ(account->identity_provider->format, std::nullopt);
base::RunLoop callback;
source->NotifyAutofillSuggestionAccepted(
account->identity_provider->idp_metadata.config_url, account->id,
/*show_modal=*/false,
base::BindLambdaForTesting([&callback](bool accepted) {
EXPECT_TRUE(accepted);
callback.Quit();
}));
// Wait for the identity provider to return a token.
callback.Run();
// Assert that the conditional mediation request resolved and that
// the right token was provided.
EXPECT_EQ(std::string(kToken), EvalJs(shell(), "token"));
}
class WebIdMetricsBrowserTest : public WebIdBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
scoped_feature_list_.InitAndEnableFeature(features::kFedCmMetricsEndpoint);
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
protected:
enum TestType { kSuccess, kAccountsFailure, kLoginFailure };
void SetMetricsConfigDetails(base::RunLoop* run_loop, TestType type) {
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
if (type == kAccountsFailure) {
config_details.accounts_endpoint_url = "/404";
}
if (type == kLoginFailure) {
// Just load a file that does not call IdentityProvider.close.
config_details.login_url = "/blue.html";
}
config_details.metrics_endpoint_url = "/metrics";
config_details.servlets["/metrics"] = base::BindRepeating(
[](WebIdMetricsBrowserTest* test, base::RunLoop* run_loop,
const HttpRequest& request) -> std::unique_ptr<HttpResponse> {
EXPECT_EQ(request.method, HttpMethod::METHOD_POST);
EXPECT_EQ(request.has_content, true);
if (request.headers.contains("Origin")) {
test->metrics_request_origin_ = request.headers.at("Origin");
} else {
test->metrics_request_origin_.reset();
}
auto parameters = base::SplitStringPiece(request.content, "&",
base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
for (const auto& param : parameters) {
auto pair = base::SplitStringOnce(param, '=');
if (pair) {
test->metrics_parameters_.emplace(*pair);
}
}
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/json");
response->set_content("{}");
run_loop->Quit();
return response;
},
this, base::Unretained(run_loop));
idp_server()->SetConfigResponseDetails(config_details);
}
std::map<std::string, std::string> metrics_parameters_;
std::optional<std::string> metrics_request_origin_;
};
IN_PROC_BROWSER_TEST_F(WebIdMetricsBrowserTest, Success) {
base::RunLoop run_loop;
SetMetricsConfigDetails(&run_loop, kSuccess);
std::string script = R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345'
}]
}
}));
return x.token;
}) ()
)";
EXPECT_EQ(std::string("[not a real token]"), EvalJs(shell(), script));
run_loop.Run();
ASSERT_TRUE(metrics_request_origin_);
EXPECT_TRUE(metrics_request_origin_->starts_with("https://rp.example:"));
EXPECT_EQ("success", metrics_parameters_["outcome"]);
EXPECT_EQ(1ul, metrics_parameters_.count("time_to_show_ui"));
EXPECT_EQ(1ul, metrics_parameters_.count("time_to_continue"));
EXPECT_EQ(1ul, metrics_parameters_.count("time_to_receive_token"));
EXPECT_EQ(1ul, metrics_parameters_.count("turnaround_time"));
EXPECT_EQ(0ul, metrics_parameters_.count("error_code"));
EXPECT_EQ(0ul, metrics_parameters_.count("did_show_ui"));
}
IN_PROC_BROWSER_TEST_F(WebIdMetricsBrowserTest, IdpLoginClosed) {
// This will be used for the login dialog.
Shell* modal = CreateBrowser();
// Mark us as signed out from this IdP.
GURL url{IdpRootUrl() + "/header/signout"};
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url));
auto mock = std::make_unique<
::testing::NiceMock<MockIdentityRequestDialogController>>();
// Keep a copy of the pointer before the std::move.
MockIdentityRequestDialogController* controller = mock.get();
test_browser_client_->SetIdentityRequestDialogController(std::move(mock));
base::RunLoop modal_dialog_loop;
content::IdentityRequestDialogController::DismissCallback saved_cb;
EXPECT_CALL(*controller, ShowModalDialog)
.WillOnce(WithArgs<0, 2>(
[&modal, &modal_dialog_loop, &saved_cb](
const GURL& url,
content::IdentityRequestDialogController::DismissCallback cb) {
modal->LoadURL(url);
saved_cb = std::move(cb);
modal_dialog_loop.Quit();
return modal->web_contents();
}));
EXPECT_CALL(*controller, ShowLoadingDialog).WillOnce(Return(true));
EXPECT_CALL(*controller, DidShowUi).WillRepeatedly(Return(true));
// Now run the actual test.
base::RunLoop run_loop;
SetMetricsConfigDetails(&run_loop, kLoginFailure);
std::string script = R"(
promise = navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345'
}],
mode: 'active'
}
});
)";
// Initiate the FedCM call
EXPECT_TRUE(ExecJs(shell(), script, EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// Wait for the popup window to open.
modal_dialog_loop.Run();
// Close the dialog and notify the callback.
modal->Close();
std::move(saved_cb).Run(
IdentityRequestDialogController::DismissReason::kOther);
std::string expected_error = "NetworkError: Error retrieving a token.";
EXPECT_EQ(expected_error,
ExtractJsError(EvalJs(shell(), "(async () => await promise)()")));
// Wait for the metrics endpoint result
run_loop.Run();
EXPECT_EQ("null", metrics_request_origin_);
EXPECT_EQ("failure", metrics_parameters_["outcome"]);
// In the failure case we should not send timing data.
EXPECT_EQ(0ul, metrics_parameters_.count("time_to_show_ui"));
EXPECT_EQ(0ul, metrics_parameters_.count("time_to_continue"));
EXPECT_EQ(0ul, metrics_parameters_.count("time_to_receive_token"));
EXPECT_EQ(0ul, metrics_parameters_.count("turnaround_time"));
EXPECT_EQ("200", metrics_parameters_["error_code"]);
EXPECT_EQ("true", metrics_parameters_["did_show_ui"]);
}
IN_PROC_BROWSER_TEST_F(WebIdMetricsBrowserTest, Failure) {
base::RunLoop run_loop;
SetMetricsConfigDetails(&run_loop, kAccountsFailure);
std::string script = R"(
(async () => {
var x = (await navigator.credentials.get({
identity: {
providers: [{
configURL: ')" +
BaseIdpUrl() + R"(',
clientId: 'client_id_1',
nonce: '12345'
}]
}
}));
return x.token;
}) ()
)";
std::string expected_error = "NetworkError: Error retrieving a token.";
EXPECT_EQ(expected_error, ExtractJsError(EvalJs(shell(), script)));
run_loop.Run();
EXPECT_EQ("null", metrics_request_origin_);
EXPECT_EQ("failure", metrics_parameters_["outcome"]);
// In the failure case we should not send timing data.
EXPECT_EQ(0ul, metrics_parameters_.count("time_to_show_ui"));
EXPECT_EQ(0ul, metrics_parameters_.count("time_to_continue"));
EXPECT_EQ(0ul, metrics_parameters_.count("time_to_receive_token"));
EXPECT_EQ(0ul, metrics_parameters_.count("turnaround_time"));
EXPECT_EQ("301", metrics_parameters_["error_code"]);
EXPECT_EQ("false", metrics_parameters_["did_show_ui"]);
}
// Verify that stored accounts via login.setStatus can be used to complete
// a signin flow with an empty accounts endpoint.
IN_PROC_BROWSER_TEST_F(WebIdLightweightFedcmBrowserTest,
IdpSigninTopLevelSetViaJs) {
GURL configURL = GURL(BaseIdpUrl());
IdpTestServer::ConfigDetails config_details = BuildValidConfigDetails();
config_details.accounts_endpoint_url = "";
idp_server()->SetConfigResponseDetails(config_details);
EXPECT_TRUE(NavigateToURL(shell(), configURL));
EXPECT_FALSE(sharing_context()
->GetIdpSigninStatus(url::Origin::Create(configURL))
.has_value());
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), configURL));
base::RunLoop run_loop;
sharing_context()->SetIdpStatusClosureForTesting(run_loop.QuitClosure());
static constexpr char script[] = R"(
(async () => {
await navigator.login.setStatus("logged-in", {accounts: [
{id: "12345", name: "User", email: "user@idp.example"}
]});
return true;
})()
)";
EXPECT_EQ(true, EvalJs(shell(), script));
run_loop.Run();
std::optional<bool> value =
sharing_context()->GetIdpSigninStatus(url::Origin::Create(configURL));
ASSERT_TRUE(value.has_value());
EXPECT_TRUE(*value);
base::Value::List accounts =
sharing_context()->GetAccounts(url::Origin::Create(configURL));
ASSERT_EQ(1U, accounts.size());
EXPECT_EQ("12345", *accounts[0].GetDict().FindString("id"));
EXPECT_EQ("User", *accounts[0].GetDict().FindString("name"));
EXPECT_EQ("user@idp.example", *accounts[0].GetDict().FindString("email"));
EXPECT_TRUE(NavigateToURL(
shell(), https_server().GetURL(kRpHostName, "/title1.html")));
SetTestIdentityRequestDialogController("12345");
EXPECT_EQ(std::string(kToken), EvalJs(shell(), GetBasicRequestString()));
}
} // namespace content