blob: d93cdea8e97a63f1e763783693b2159336da3af0 [file] [log] [blame]
// Copyright 2019 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 <sstream>
#include <vector>
#include "base/base64url.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/install_verifier.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/cert_verifier_browser_test.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_controller.h"
#include "chrome/browser/webauthn/authenticator_transport.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h"
#include "chrome/browser/webauthn/passkey_model_factory.h"
#include "chrome/browser/webauthn/test_util.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "components/password_manager/core/common/password_manager_ui.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "components/webauthn/core/browser/passkey_change_quota_tracker.h"
#include "components/webauthn/core/browser/test_passkey_model.h"
#include "content/public/browser/authenticator_request_client_delegate.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/scoped_authenticator_environment_for_testing.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/features.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/fido_types.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device.h"
#include "device/fido/virtual_fido_device_factory.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/common/extension_builder.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_WIN)
#include "device/fido/win/authenticator.h"
#include "device/fido/win/fake_webauthn_api.h"
#include "device/fido/win/webauthn_api.h"
#endif // BUILDFLAG(IS_WIN)
namespace {
static constexpr uint8_t kCredentialID[] = {1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16};
static constexpr uint8_t kCredentialID2[] = {16, 15, 14, 13, 12, 11, 10, 9,
8, 7, 6, 5, 4, 3, 2, 1};
constexpr uint8_t kUserId1[] = {1, 2, 3, 4};
constexpr uint8_t kUserId2[] = {5, 6, 7, 8};
constexpr char kUsername1[] = "flandre";
constexpr char kDisplayName1[] = "Flandre Scarlet";
constexpr char kUsername2[] = "sakuya";
constexpr char kDisplayName2[] = "Sakuya Izayoi";
static constexpr char kSignalUnknownCredentialId[] = R"(
PublicKeyCredential.signalUnknownCredential({
rpId: "www.example.com",
credentialId: "$1",
}).then(c => 'webauthn: OK', e => 'error ' + e);
)";
std::string GetSignalUnknownCredentialScript(
base::span<const uint8_t> credential_id) {
std::string b64_credential_id;
base::Base64UrlEncode(credential_id,
base::Base64UrlEncodePolicy::OMIT_PADDING,
&b64_credential_id);
return base::ReplaceStringPlaceholders(kSignalUnknownCredentialId,
{b64_credential_id},
/*offsets=*/nullptr);
}
std::string GetSignalAllAcceptedCredentials(
base::span<const uint8_t> credential_id,
base::span<const uint8_t> user_id) {
static constexpr char kSignalAllAcceptedCredentials[] = R"(
PublicKeyCredential.signalAllAcceptedCredentials({
rpId: "www.example.com",
allAcceptedCredentialIds: ["$1"],
userId: "$2",
}).then(c => 'webauthn: OK', e => 'error ' + e);
)";
std::string b64_credential_id, b64_user_id;
base::Base64UrlEncode(credential_id,
base::Base64UrlEncodePolicy::OMIT_PADDING,
&b64_credential_id);
base::Base64UrlEncode(user_id, base::Base64UrlEncodePolicy::OMIT_PADDING,
&b64_user_id);
return base::ReplaceStringPlaceholders(kSignalAllAcceptedCredentials,
{b64_credential_id, b64_user_id},
/*offsets=*/nullptr);
}
sync_pb::WebauthnCredentialSpecifics CreateWebAuthnCredentialSpecifics(
base::span<const uint8_t> credential_id,
base::span<const uint8_t> user_id,
const char* username,
const char* display_name) {
sync_pb::WebauthnCredentialSpecifics passkey;
passkey.set_sync_id(base::RandBytesAsString(16));
passkey.set_credential_id(credential_id.data(), credential_id.size());
passkey.set_rp_id("www.example.com");
passkey.set_user_id(user_id.data(), user_id.size());
passkey.set_user_name(username);
passkey.set_user_display_name(display_name);
return passkey;
}
// This file tests WebAuthn features that depend on specific //chrome behaviour.
// Tests that don't depend on that should go into
// content/browser/webauth/webauth_browsertest.cc.
class WebAuthnBrowserTest : public CertVerifierBrowserTest {
public:
WebAuthnBrowserTest() = default;
WebAuthnBrowserTest(const WebAuthnBrowserTest&) = delete;
WebAuthnBrowserTest& operator=(const WebAuthnBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
CertVerifierBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void SetUp() override {
ASSERT_TRUE(https_server_.InitializeAndListen());
CertVerifierBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
CertVerifierBrowserTest::SetUpOnMainThread();
https_server_.ServeFilesFromSourceDirectory(GetChromeTestDataDir());
https_server_.StartAcceptingConnections();
host_resolver()->AddRule("*", "127.0.0.1");
// Allowlist all certs for the HTTPS server.
mock_cert_verifier()->set_default_result(net::OK);
// Mock bluetooth support to allow discovery of fake hybrid devices.
mock_bluetooth_adapter_ =
base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
ON_CALL(*mock_bluetooth_adapter_, IsPresent)
.WillByDefault(testing::Return(true));
ON_CALL(*mock_bluetooth_adapter_, IsPowered)
.WillByDefault(testing::Return(true));
device::BluetoothAdapterFactory::SetAdapterForTesting(
mock_bluetooth_adapter_);
// Other parts of Chrome may keep a reference to the bluetooth adapter.
// Since we do not verify any expectations, it is okay to leak this mock.
testing::Mock::AllowLeak(mock_bluetooth_adapter_.get());
}
void SetUpInProcessBrowserTestFixture() override {
subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterCreateServicesCallbackForTesting(base::BindRepeating(
&WebAuthnBrowserTest::OnWillCreateBrowserContextServices,
base::Unretained(this)));
}
protected:
void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
PasskeyModelFactory::GetInstance()->SetTestingFactory(
context,
base::BindRepeating(
[](content::BrowserContext*) -> std::unique_ptr<KeyedService> {
return std::make_unique<webauthn::TestPasskeyModel>();
}));
}
base::CallbackListSubscription subscription_;
scoped_refptr<device::MockBluetoothAdapter> mock_bluetooth_adapter_ = nullptr;
device::FidoRequestHandlerBase::ScopedAlwaysAllowBLECalls always_allow_ble_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};
static constexpr char kGetAssertionCredID1234[] = R"((() => {
let cred_id = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);
return navigator.credentials.get({ publicKey: {
challenge: cred_id,
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [{type: 'public-key', id: cred_id}],
}}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest, ChromeExtensions) {
// Test that WebAuthn works inside of Chrome extensions. WebAuthn is based on
// Relying Party IDs, which are domain names. But Chrome extensions don't have
// domain names therefore the origin is used in their case.
//
// This test creates and installs an extension and then loads an HTML page
// from inside that extension. A WebAuthn call is injected into that context
// and it should get an assertion from a credential that's injected into the
// virtual authenticator, scoped to the origin string.
base::ScopedAllowBlockingForTesting allow_blocking;
extensions::ScopedInstallVerifierBypassForTest install_verifier_bypass;
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
static constexpr char kPageFile[] = "page.html";
static constexpr char kContents[] = R"(
<html>
<head>
<title>WebAuthn in extensions test</title>
</head>
<body>
</body>
</html>
)";
WriteFile(temp_dir.GetPath().AppendASCII(kPageFile), kContents);
static constexpr char kExtensionSite[] = "https://extension-site.com/";
static constexpr char kWebAccessibleResources[] =
R"([{
"resources": ["page.html"],
"matches": ["*://*/*"]
}])";
extensions::ExtensionBuilder builder("test");
builder.SetPath(temp_dir.GetPath())
.SetVersion("1.0")
.AddHostPermission(kExtensionSite)
.SetLocation(extensions::mojom::ManifestLocation::kExternalPolicyDownload)
.SetManifestKey("web_accessible_resources",
base::test::ParseJson(kWebAccessibleResources));
scoped_refptr<const extensions::Extension> extension = builder.Build();
extensions::ExtensionRegistrar::Get(browser()->profile())
->OnExtensionInstalled(extension.get(), syncer::StringOrdinal(), 0);
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
const GURL url = extension->GetResourceURL(kPageFile);
auto extension_id = url.host();
virtual_device_factory->mutable_state()->InjectRegistration(
kCredentialID, "chrome-extension://" + extension_id);
content::ScopedAuthenticatorEnvironmentForTesting auth_env(
std::move(virtual_device_factory));
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kGetAssertionCredID1234));
static constexpr char kMakeCredentialCrossDomainWithHostPerms[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { id: "extension-site.com", name: "" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
}}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
// This should work as the extension has host permissions over the site.
EXPECT_EQ(
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kMakeCredentialCrossDomainWithHostPerms)
.ExtractString(),
"webauthn: OK");
static constexpr char kMakeCredentialCrossDomainNoHostPerms[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { id: "example.com", name: "" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
}}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
// This should fail with INVALID_PROTOCOL and never one of the errors from
// related-origin processing because extensions don't participate in that
// system.
EXPECT_THAT(
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kMakeCredentialCrossDomainNoHostPerms)
.ExtractString(),
testing::HasSubstr("Public-key credentials are only available to"));
}
#if BUILDFLAG(IS_WIN)
class WinWebAuthnBrowserTest
: public WebAuthnBrowserTest,
device::WinWebAuthnApiAuthenticator::TestObserver {
public:
static constexpr char kMakeDiscoverableCredential[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
authenticatorSelection: {
requireResidentKey: true,
},
}}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
void SetUpOnMainThread() override {
WebAuthnBrowserTest::SetUpOnMainThread();
signal_unknown_credential_run_loop_ = std::make_unique<base::RunLoop>();
signal_all_accepted_credentials_run_loop_ =
std::make_unique<base::RunLoop>();
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory->set_discover_win_webauthn_api_authenticator(true);
auth_env_ =
std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
device::WinWebAuthnApiAuthenticator::SetGlobalObserverForTesting(this);
}
void TearDownOnMainThread() override {
device::WinWebAuthnApiAuthenticator::SetGlobalObserverForTesting(nullptr);
WebAuthnBrowserTest::TearDownOnMainThread();
}
void WaitForSignalUnknownCredential() {
signal_unknown_credential_run_loop_->Run();
signal_unknown_credential_run_loop_ = std::make_unique<base::RunLoop>();
}
void WaitForSignalAllAcceptedCredentials() {
signal_all_accepted_credentials_run_loop_->Run();
signal_all_accepted_credentials_run_loop_ =
std::make_unique<base::RunLoop>();
}
// device::WinWebAuthnApiAuthenticator::TestObserver:
void OnSignalUnknownCredential() override {
signal_unknown_credential_run_loop_->Quit();
}
void OnSignalAllAcceptedCredentials() override {
signal_all_accepted_credentials_run_loop_->Quit();
}
protected:
std::unique_ptr<base::RunLoop> signal_unknown_credential_run_loop_;
std::unique_ptr<base::RunLoop> signal_all_accepted_credentials_run_loop_;
device::FakeWinWebAuthnApi win_api_;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override_{&win_api_};
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting> auth_env_;
base::test::ScopedFeatureList scoped_feature_list_{
device::kWebAuthnHelloSignal};
};
// Integration test for Large Blob on Windows.
IN_PROC_BROWSER_TEST_F(WinWebAuthnBrowserTest, WinLargeBlob) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
win_api_.set_version(WEBAUTHN_API_VERSION_3);
constexpr char kMakeCredentialLargeBlob[] = R"(
let cred_id;
const blob = "blobby volley";
navigator.credentials.create({ publicKey: {
challenge: new TextEncoder().encode('climb a mountain'),
rp: { name: 'Acme' },
user: {
id: new TextEncoder().encode('1098237235409872'),
name: 'avery.a.jones@example.com',
displayName: 'Avery A. Jones'},
pubKeyCredParams: [{ type: 'public-key', alg: '-257'}],
authenticatorSelection: {
requireResidentKey: true,
},
extensions: { largeBlob: { support: 'required' } },
}}).then(cred => {
cred_id = cred.rawId;
if (!cred.getClientExtensionResults().largeBlob ||
!cred.getClientExtensionResults().largeBlob.supported) {
throw new Error('large blob not supported');
}
return navigator.credentials.get({ publicKey: {
challenge: new TextEncoder().encode('run a marathon'),
allowCredentials: [{type: 'public-key', id: cred_id}],
extensions: {
largeBlob: {
write: new TextEncoder().encode(blob),
},
},
}});
}).then(assertion => {
if (!assertion.getClientExtensionResults().largeBlob.written) {
throw new Error('large blob not written to');
}
return navigator.credentials.get({ publicKey: {
challenge: new TextEncoder().encode('solve p=np'),
allowCredentials: [{type: 'public-key', id: cred_id}],
extensions: {
largeBlob: {
read: true,
},
},
}});
}).then(assertion => {
if (new TextDecoder().decode(
assertion.getClientExtensionResults().largeBlob.blob) != blob) {
throw new Error('blob does not match');
}
return 'webauthn: OK';
}).catch(error => 'webauthn: ' + error.toString());)";
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kMakeCredentialLargeBlob));
}
// Integration test for signalUnknownCredentialId on Windows.
IN_PROC_BROWSER_TEST_F(WinWebAuthnBrowserTest, WinSignalUnknownCredential) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
win_api_.set_version(WEBAUTHN_API_VERSION_4);
win_api_.set_supports_silent_discovery(true);
// Set up a Windows Hello passkey.
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kMakeDiscoverableCredential));
ASSERT_EQ(win_api_.registrations().size(), 1u);
const std::vector<uint8_t> credential_id =
win_api_.registrations().begin()->first;
// Signal the passkey as unknown, which should delete it.
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
GetSignalUnknownCredentialScript(credential_id)));
WaitForSignalUnknownCredential();
EXPECT_TRUE(win_api_.registrations().empty());
}
// Integration test for signalAllAcceptedCredentials on Windows.
IN_PROC_BROWSER_TEST_F(WinWebAuthnBrowserTest,
WinSignalAllAcceptedCredentials) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
win_api_.set_version(WEBAUTHN_API_VERSION_4);
win_api_.set_supports_silent_discovery(true);
// Set up a Windows Hello passkey.
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kMakeDiscoverableCredential));
ASSERT_EQ(win_api_.registrations().size(), 1u);
const std::vector<uint8_t>& credential_id =
win_api_.registrations().begin()->first;
const std::vector<uint8_t>& user_id =
win_api_.registrations().begin()->second.user->id;
// Signal the passkey as known, which should keep it.
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
GetSignalAllAcceptedCredentials(credential_id, user_id)));
WaitForSignalAllAcceptedCredentials();
EXPECT_EQ(win_api_.registrations().size(), 1u);
// Signal a different passkey as known, which should delete the existing one.
EXPECT_EQ("webauthn: OK",
content::EvalJs(
browser()->tab_strip_model()->GetActiveWebContents(),
GetSignalAllAcceptedCredentials(kCredentialID2, user_id)));
WaitForSignalAllAcceptedCredentials();
EXPECT_TRUE(win_api_.registrations().empty());
}
#endif // BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest,
SignalUnknownCredentialGPMPasskeys) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
// Set up GPM Passkey.
auto* passkey_model = static_cast<webauthn::TestPasskeyModel*>(
PasskeyModelFactory::GetForProfile(browser()->profile()));
passkey_model->AddNewPasskeyForTesting(CreateWebAuthnCredentialSpecifics(
kCredentialID, kUserId1, kUsername1, kDisplayName1));
// Reports the credential ID matching the passkey created.
EXPECT_TRUE(ExecJs(browser()->tab_strip_model()->GetActiveWebContents(),
GetSignalUnknownCredentialScript(kCredentialID)));
// After reporting the passkey, it should be deleted from the credentials
// list, so the vector of passkeys matching the relying party should be empty.
EXPECT_TRUE(
passkey_model->GetPasskeysForRelyingPartyId("www.example.com").empty());
}
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest,
SignalAllAcceptedCredsNoPasskeyDeletion) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
// Set up GPM Passkey.
auto* passkey_model = static_cast<webauthn::TestPasskeyModel*>(
PasskeyModelFactory::GetForProfile(browser()->profile()));
passkey_model->AddNewPasskeyForTesting(CreateWebAuthnCredentialSpecifics(
kCredentialID, kUserId1, kUsername1, kDisplayName1));
// Reports the user ID and credential ID matching the created passkey.
// The passkey will not be deleted.
EXPECT_EQ("webauthn: OK",
content::EvalJs(
browser()->tab_strip_model()->GetActiveWebContents(),
GetSignalAllAcceptedCredentials(kCredentialID, kUserId1)));
// Check that the passkey with kCredentialID was not deleted.
EXPECT_TRUE(passkey_model->GetPasskeyByCredentialId(
"www.example.com",
std::string(reinterpret_cast<const char*>(kCredentialID), 16)));
password_manager::ui::State model_state =
PasswordsModelDelegateFromWebContents(
browser()->tab_strip_model()->GetActiveWebContents())
->GetState();
// If the model_state is INACTIVE_STATE, it means that DeletePasskey didn't
// run, and hence the Passkey Not Accepted bubble did not show up.
EXPECT_EQ(model_state, password_manager::ui::INACTIVE_STATE);
}
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest,
SignalAllAcceptedCredsPasskeyDeletion) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
// Set up GPM Passkey.
auto* passkey_model = static_cast<webauthn::TestPasskeyModel*>(
PasskeyModelFactory::GetForProfile(browser()->profile()));
passkey_model->AddNewPasskeyForTesting(CreateWebAuthnCredentialSpecifics(
kCredentialID2, kUserId2, kUsername2, kDisplayName2));
// Reports the user ID that matches the passkey created with an empty
// allCurrentCredentialIds. The passkey will be deleted.
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
R"(
PublicKeyCredential.signalAllAcceptedCredentials({
rpId: "www.example.com",
userId: "BQYHCA",
allAcceptedCredentialIds: [],
}).then(c => 'webauthn: OK', e => 'error ' + e);
)"));
// Check that the passkey with kCredentialID2 was deleted.
EXPECT_FALSE(passkey_model->GetPasskeyByCredentialId(
"www.example.com",
std::string(reinterpret_cast<const char*>(kCredentialID2), 16)));
password_manager::ui::State model_state =
PasswordsModelDelegateFromWebContents(
browser()->tab_strip_model()->GetActiveWebContents())
->GetState();
// Check if the Passkey Not Accepted bubble showed up.
EXPECT_EQ(model_state, password_manager::ui::PASSKEY_NOT_ACCEPTED_STATE);
}
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest, ReportInvalidStrings) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
// This should fail with a TypeError due to an invalid base64url
// string in the userId.
EXPECT_THAT(
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(), R"(
PublicKeyCredential.signalAllAcceptedCredentials({
rpId: "www.example.com",
userId: "a/+c+/c",
allAcceptedCredentialIds: ["AQIDBAUGBwgJCgsMDQ4PEA"],
}).then(c => 'webauthn: OK', e => 'error ' + e);
)")
.ExtractString(),
testing::HasSubstr("Invalid base64url string for userId."));
// This should fail with a TypeError due to an invalid base64url
// string in the allAcceptedCredentialIds list.
EXPECT_THAT(
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(), R"(
PublicKeyCredential.signalAllAcceptedCredentials({
rpId: "www.example.com",
userId: "BQYHCA",
allAcceptedCredentialIds: ["/a+/b+/c"],
}).then(c => 'webauthn: OK', e => 'error ' + e);
)")
.ExtractString(),
testing::HasSubstr(
"Invalid base64url string for allAcceptedCredentialIds."));
// This should fail with a TypeError due to an invalid base64url
// string in the userId of signalCurrentUserDetails;
EXPECT_THAT(
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
R"(
PublicKeyCredential.signalCurrentUserDetails({
rpId: "www.example.com",
userId: "A+/+A",
name: "Pepito",
displayName: "Pepito The Cat",
}).then(c => 'webauthn: OK', e => 'error ' + e);
)")
.ExtractString(),
testing::HasSubstr("Invalid base64url string for userId."));
// This should fail with a TypeError due to an invalid base64url
// string in the credentialId report.
EXPECT_THAT(
content::EvalJs(
browser()->tab_strip_model()->GetActiveWebContents(),
base::ReplaceStringPlaceholders(kSignalUnknownCredentialId, {"A+/+A"},
/*offsets=*/nullptr))
.ExtractString(),
testing::HasSubstr("Invalid base64url string for credentialId."));
}
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest,
SignalCurrentUserDetailsGPMPasskeys) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
// Set up GPM Passkey.
auto* passkey_model = static_cast<webauthn::TestPasskeyModel*>(
PasskeyModelFactory::GetForProfile(browser()->profile()));
passkey_model->AddNewPasskeyForTesting(CreateWebAuthnCredentialSpecifics(
kCredentialID, kUserId1, kUsername1, kDisplayName1));
// Reports the user ID that matches the passkey created with the
// current user details.
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
R"(
PublicKeyCredential.signalCurrentUserDetails({
rpId: "www.example.com",
userId: "AQIDBA",
name: "Pepito",
displayName: "Pepito The Cat",
}).then(c => 'webauthn: OK', e => 'error ' + e);
)"));
auto passkey = passkey_model->GetPasskeyByCredentialId(
"www.example.com",
std::string(reinterpret_cast<const char*>(kCredentialID), 16));
// Check if the name and displayName of the passkey reported was updated.
EXPECT_EQ(passkey->user_name(), "Pepito");
EXPECT_EQ(passkey->user_display_name(), "Pepito The Cat");
password_manager::ui::State model_state =
PasswordsModelDelegateFromWebContents(
browser()->tab_strip_model()->GetActiveWebContents())
->GetState();
// Check if the Passkey Updated bubble showed up.
EXPECT_EQ(model_state,
password_manager::ui::PASSKEY_UPDATED_CONFIRMATION_STATE);
}
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest, SignalCurrentUserDetailsQuota) {
constexpr char kRequest[] = R"(
PublicKeyCredential.signalCurrentUserDetails({
rpId: "www.example.com",
userId: "AQIDBA",
name: "$1",
displayName: "Pepito The Cat",
}).then(c => 'webauthn: OK', e => 'error ' + e);
)";
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
// Set up GPM Passkey.
auto* passkey_model = static_cast<webauthn::TestPasskeyModel*>(
PasskeyModelFactory::GetForProfile(browser()->profile()));
passkey_model->AddNewPasskeyForTesting(CreateWebAuthnCredentialSpecifics(
kCredentialID, kUserId1, kUsername1, kDisplayName1));
// Call the signal methods enough that we run into the quota.
for (int i = 0; i < webauthn::PasskeyChangeQuotaTracker::kMaxTokensPerRP;
++i) {
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
base::ReplaceStringPlaceholders(
kRequest, {base::NumberToString(i)}, nullptr)));
}
// This request should be silently dropped now.
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(
browser()->tab_strip_model()->GetActiveWebContents(),
base::ReplaceStringPlaceholders(kRequest, {kUsername1}, nullptr)));
// Check that the name hasn't been updated.
auto passkey = passkey_model->GetPasskeyByCredentialId(
"www.example.com",
std::string(reinterpret_cast<const char*>(kCredentialID), 16));
EXPECT_NE(passkey->user_name(), kUsername1);
}
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest,
SignalCurrentUserDetailsWithNoChanges) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
// Set up GPM Passkey.
auto* passkey_model = static_cast<webauthn::TestPasskeyModel*>(
PasskeyModelFactory::GetForProfile(browser()->profile()));
passkey_model->AddNewPasskeyForTesting(CreateWebAuthnCredentialSpecifics(
kCredentialID, kUserId1, kUsername1, kDisplayName1));
// Reports the user ID that matches the passkey created with the
// current user details.
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
R"(
PublicKeyCredential.signalCurrentUserDetails({
rpId: "www.example.com",
userId: "AQIDBA",
name: "flandre",
displayName: "Flandre Scarlet",
}).then(c => 'webauthn: OK', e => 'error ' + e);
)"));
auto passkey = passkey_model->GetPasskeyByCredentialId(
"www.example.com",
std::string(reinterpret_cast<const char*>(kCredentialID), 16));
// Check if the name and displayName of the passkey reported did not change.
EXPECT_EQ(passkey->user_name(), kUsername1);
EXPECT_EQ(passkey->user_display_name(), kDisplayName1);
password_manager::ui::State model_state =
PasswordsModelDelegateFromWebContents(
browser()->tab_strip_model()->GetActiveWebContents())
->GetState();
// If the model_state is INACTIVE_STATE, it means that UpdatePasskey didn't
// run, and hence the Passkey Updated bubble did not show up.
EXPECT_EQ(model_state, password_manager::ui::INACTIVE_STATE);
}
class WebAuthnHintsTest : public WebAuthnBrowserTest {
class Observer : public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
virtual ~Observer() = default;
void WaitForHints() { run_loop_.Run(); }
const content::AuthenticatorRequestClientDelegate::Hints& hints() const {
return hints_;
}
void HintsSet(const content::AuthenticatorRequestClientDelegate::Hints&
hints) override {
hints_ = hints;
run_loop_.Quit();
}
private:
content::AuthenticatorRequestClientDelegate::Hints hints_;
base::RunLoop run_loop_;
};
void SetUpOnMainThread() override {
WebAuthnBrowserTest::SetUpOnMainThread();
observer_ = std::make_unique<Observer>();
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory_ = virtual_device_factory.get();
auth_env_ =
std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(
observer_.get());
}
void PostRunTestOnMainThread() override {
// To avoid dangling raw_ptr's these values need to be destroyed before
// this test class.
virtual_device_factory_ = nullptr;
auth_env_.reset();
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
WebAuthnBrowserTest::PostRunTestOnMainThread();
}
protected:
std::unique_ptr<Observer> observer_;
raw_ptr<device::test::VirtualFidoDeviceFactory> virtual_device_factory_;
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting> auth_env_;
};
// The hints parameter here contains nonsense values (which should be ignored)
// and lists `security-key` and `hybrid` (more than once). This is contradictory
// but Chromium will prioritize in the order of the enum values, so
// `security-key` will win out.
static constexpr char kMakeCredentialWithHints[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
hints: ["nonsense", "hybrid", "security-key", "hybrid", "nonsense"],
userVerification: 'discouraged',
}}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
IN_PROC_BROWSER_TEST_F(WebAuthnHintsTest, HintsArePassedThrough) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialWithHints);
observer_->WaitForHints();
ASSERT_TRUE(observer_->hints().transport.has_value());
EXPECT_EQ(observer_->hints().transport.value(),
AuthenticatorTransport::kUsbHumanInterfaceDevice);
}
class WebAuthnConditionalUITest : public WebAuthnBrowserTest {
class Observer : public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
enum State {
kHasNotShowedUI,
kWaitingForUI,
kShowedUI,
};
virtual ~Observer() = default;
void WaitForUI() {
if (state_ != kHasNotShowedUI) {
return;
}
state_ = kWaitingForUI;
run_loop_.Run();
}
// ChromeAuthenticatorRequestDelegate::TestObserver:
void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
delegate_ = delegate;
}
void OnTransportAvailabilityEnumerated(
ChromeAuthenticatorRequestDelegate* delegate,
device::FidoRequestHandlerBase::TransportAvailabilityInfo* tai)
override {}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
if (state_ == kWaitingForUI) {
// When the content layer controls authenticator dispatch, dispatching
// happens on tasks posted right before the UI is shown. We need to
// QuitWhenIdle to make sure that, if an authenticator is dispatched to,
// that task has a chance to finish before the test continues. That way
// we can catch any potentially unexpected authenticator dispatches.
run_loop_.QuitWhenIdle();
}
state_ = kShowedUI;
}
void CableV2ExtensionSeen(
base::span<const uint8_t> server_link_data) override {}
void AccountSelectorShown(
const std::vector<device::AuthenticatorGetAssertionResponse>& responses)
override {
for (const auto& response : responses) {
accounts_.emplace_back(base::HexEncode(response.credential->id));
}
}
raw_ptr<ChromeAuthenticatorRequestDelegate, AcrossTasksDanglingUntriaged>
delegate_ = nullptr;
std::vector<std::string> accounts_;
private:
State state_ = kHasNotShowedUI;
base::RunLoop run_loop_;
};
void SetUpOnMainThread() override {
WebAuthnBrowserTest::SetUpOnMainThread();
observer_ = std::make_unique<Observer>();
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory_ = virtual_device_factory.get();
virtual_device_factory->mutable_state()->InjectResidentKey(
kCredentialID, "www.example.com", std::vector<uint8_t>{5, 6, 7, 8},
"flandre", "Flandre Scarlet");
virtual_device_factory->mutable_state()->fingerprints_enrolled = true;
device::VirtualCtap2Device::Config config;
config.resident_key_support = true;
config.internal_uv_support = true;
virtual_device_factory->SetCtap2Config(std::move(config));
auth_env_ =
std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(
observer_.get());
}
void PostRunTestOnMainThread() override {
// To avoid dangling raw_ptr's these values need to be destroyed before
// this test class.
virtual_device_factory_ = nullptr;
auth_env_.reset();
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
WebAuthnBrowserTest::PostRunTestOnMainThread();
}
protected:
std::unique_ptr<Observer> observer_;
raw_ptr<device::test::VirtualFidoDeviceFactory> virtual_device_factory_;
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting> auth_env_;
};
static constexpr char kConditionalUIRequest[] = R"((() => {
window.requestAbortController = new AbortController();
navigator.credentials.get({
signal: window.requestAbortController.signal,
mediation: 'conditional',
publicKey: {
challenge: new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]),
timeout: 10000,
allowCredentials: [],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
// Tests that the "Sign in with another device…" button dispatches requests to
// plugged in authenticators.
IN_PROC_BROWSER_TEST_F(WebAuthnConditionalUITest,
ConditionalUIOtherDeviceButton) {
// Make a Conditional UI request. The authenticator should not be dispatched
// to before the user clicks the "Sign in with another device…" button.
virtual_device_factory_->mutable_state()->simulate_press_callback =
base::BindLambdaForTesting([](device::VirtualFidoDevice* device) -> bool {
NOTREACHED() << "Virtual device should not have been dispatched to";
});
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kConditionalUIRequest);
observer_->WaitForUI();
// Allow the virtual device to respond to requests, then simulate clicking the
// "Sign in with another device…" button and wait for a result.
virtual_device_factory_->mutable_state()->simulate_press_callback =
base::BindLambdaForTesting(
[&](device::VirtualFidoDevice* device) { return true; });
ChromeWebAuthnCredentialsDelegate delegate(web_contents);
delegate.LaunchSecurityKeyOrHybridFlow();
std::string result;
ASSERT_TRUE(message_queue.WaitForMessage(&result));
EXPECT_EQ(result, "\"webauthn: OK\"");
EXPECT_EQ(observer_->accounts_.size(), 1u);
EXPECT_EQ(observer_->accounts_.at(0), "0102030405060708090A0B0C0D0E0F10");
}
// WebAuthnCableExtension exercises code paths where a server sends a caBLEv2
// extension in a get() request.
class WebAuthnCableExtension : public WebAuthnBrowserTest {
public:
WebAuthnCableExtension() {
scoped_feature_list_.InitWithFeatures(
{device::kWebAuthCableExtensionAnywhere}, {});
}
void PostRunTestOnMainThread() override {
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
WebAuthnBrowserTest::PostRunTestOnMainThread();
}
protected:
static constexpr char kRequest[] = R"((() => {
return navigator.credentials.get({
publicKey: {
timeout: 1000,
challenge: new Uint8Array([
0x79, 0x50, 0x68, 0x71, 0xDA, 0xEE, 0xEE, 0xB9,
0x94, 0xC3, 0xC2, 0x15, 0x67, 0x65, 0x26, 0x22,
0xE3, 0xF3, 0xAB, 0x3B, 0x78, 0x2E, 0xD5, 0x6F,
0x81, 0x26, 0xE2, 0xA6, 0x01, 0x7D, 0x74, 0x50
]).buffer,
allowCredentials: [{
type: 'public-key',
id: new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]).buffer,
}],
userVerification: 'discouraged',
extensions: {
"cableAuthentication": [{
version: 2,
sessionPreKey: new Uint8Array([$1]).buffer,
clientEid: new Uint8Array(),
authenticatorEid: new Uint8Array(),
}],
},
},
}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
void DoRequest(std::string server_link_data) {
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory->mutable_state()->InjectRegistration(
kCredentialID, "www.example.com");
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting>
auth_env =
std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(&observer_);
const std::string request =
base::ReplaceStringPlaceholders(kRequest, {server_link_data}, nullptr);
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
request));
}
class ExtensionObserver
: public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
void Created(ChromeAuthenticatorRequestDelegate* delegate) override {}
void OnTransportAvailabilityEnumerated(
ChromeAuthenticatorRequestDelegate* delegate,
device::FidoRequestHandlerBase::TransportAvailabilityInfo* tai)
override {}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {}
void CableV2ExtensionSeen(
base::span<const uint8_t> server_link_data) override {
extensions_.emplace_back(base::HexEncode(server_link_data));
}
std::vector<std::string> extensions_;
};
ExtensionObserver observer_;
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(WebAuthnCableExtension, ServerLink) {
DoRequest("1,2,3,4");
ASSERT_EQ(observer_.extensions_.size(), 1u);
EXPECT_EQ(observer_.extensions_[0], "01020304");
}
class ChallengeUrlBrowserTest : public WebAuthnBrowserTest {
public:
static constexpr char kValidChallenge[] = "1234567890123456";
class DelegateObserver
: public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
explicit DelegateObserver(ChallengeUrlBrowserTest* test_instance)
: test_instance_(test_instance) {}
virtual ~DelegateObserver() = default;
void WaitForUI() {
ui_shown_run_loop_->Run();
ui_shown_run_loop_ = std::make_unique<base::RunLoop>();
}
// ChromeAuthenticatorRequestDelegate::TestObserver:
void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
test_instance_->UpdateRequestDelegate(delegate);
}
void OnDestroy(ChromeAuthenticatorRequestDelegate* delegate) override {
test_instance_->UpdateRequestDelegate(nullptr);
}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
ui_shown_run_loop_->QuitWhenIdle();
}
private:
raw_ptr<ChallengeUrlBrowserTest> test_instance_;
std::unique_ptr<base::RunLoop> ui_shown_run_loop_ =
std::make_unique<base::RunLoop>();
};
class ModelObserver : public AuthenticatorRequestDialogModel::Observer {
public:
explicit ModelObserver(AuthenticatorRequestDialogModel* model)
: model_(model) {
model_->observers.AddObserver(this);
}
~ModelObserver() override {
if (model_) {
model_->observers.RemoveObserver(this);
model_ = nullptr;
}
}
// Call this before the state transition you are looking to observe.
void SetStepToObserve(AuthenticatorRequestDialogModel::Step step) {
ASSERT_FALSE(run_loop_);
step_ = step;
run_loop_ = std::make_unique<base::RunLoop>();
}
// Call this to observer the next step change, whatever it might be.
void ObserveNextStep() {
ASSERT_FALSE(run_loop_);
run_loop_ = std::make_unique<base::RunLoop>();
}
// This will return after a transition to the state previously specified by
// `SetStepToObserve`. Returns immediately if the current step matches.
void WaitForStep() {
if (model_->step() == step_) {
run_loop_.reset();
return;
}
ASSERT_TRUE(run_loop_);
run_loop_->Run();
// When waiting for `kClosed` the model is deleted at this point.
if (step_ != AuthenticatorRequestDialogModel::Step::kClosed) {
CHECK_EQ(step_, model_->step());
}
Reset();
}
// AuthenticatorRequestDialogModel::Observer:
void OnStepTransition() override {
if (run_loop_ && step_ == model_->step()) {
run_loop_->QuitWhenIdle();
}
}
void OnModelDestroyed(AuthenticatorRequestDialogModel* model) override {
model_ = nullptr;
}
void Reset() {
step_ = AuthenticatorRequestDialogModel::Step::kNotStarted;
run_loop_.reset();
}
private:
raw_ptr<AuthenticatorRequestDialogModel> model_;
AuthenticatorRequestDialogModel::Step step_ =
AuthenticatorRequestDialogModel::Step::kNotStarted;
std::unique_ptr<base::RunLoop> run_loop_;
};
void SetUpOnMainThread() override {
// Handlers have to be registered before the server is started.
https_server_.RegisterRequestHandler(
base::BindRepeating(&ChallengeUrlBrowserTest::HandleChallengeRequest,
base::Unretained(this)));
WebAuthnBrowserTest::SetUpOnMainThread();
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory_ = virtual_device_factory.get();
virtual_device_factory->mutable_state()->InjectResidentKey(
kCredentialID, "www.example.com", std::vector<uint8_t>{5, 6, 7, 8},
"flandre", "Flandre Scarlet");
virtual_device_factory->mutable_state()->fingerprints_enrolled = true;
device::VirtualCtap2Device::Config config;
config.resident_key_support = true;
config.internal_uv_support = true;
virtual_device_factory->SetCtap2Config(std::move(config));
auth_env_ =
std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
delegate_observer_ = std::make_unique<DelegateObserver>(this);
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(
delegate_observer_.get());
}
void PostRunTestOnMainThread() override {
// To avoid dangling raw_ptr's these values need to be destroyed before
// this test class.
virtual_device_factory_ = nullptr;
auth_env_.reset();
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
WebAuthnBrowserTest::PostRunTestOnMainThread();
}
void SetRequestHandlerOverride(
net::EmbeddedTestServer::HandleRequestCallback override) {
request_handler_override_ = std::move(override);
}
void UpdateRequestDelegate(ChromeAuthenticatorRequestDelegate* delegate) {
request_delegate_ = delegate;
if (request_delegate_) {
model_observer_ =
std::make_unique<ModelObserver>(delegate->dialog_model());
}
}
ChromeAuthenticatorRequestDelegate* request_delegate() {
return request_delegate_;
}
DelegateObserver* delegate_observer() { return delegate_observer_.get(); }
ModelObserver* model_observer() { return model_observer_.get(); }
protected:
static constexpr std::string kChallengePath = "/challenge";
static constexpr char kGetAssertionWithChallengeUrl[] = R"((() => {
let cred_id = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);
return navigator.credentials.get({ publicKey: {
challengeUrl: '/challenge',
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [{type: 'public-key', id: cred_id}],
}}).then(c => { var decoder = new TextDecoder("utf-8");
window.domAutomationController.send(
decoder.decode(new Uint8Array(
c.response.clientDataJSON))); },
e => window.domAutomationController.send('error ' + e));
})())";
private:
std::unique_ptr<net::test_server::HttpResponse> HandleChallengeRequest(
const net::test_server::HttpRequest& request) {
if (request.relative_url != kChallengePath) {
return nullptr;
}
if (request_handler_override_) {
return std::move(request_handler_override_).Run(request);
}
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content_type("application/x-webauthn-challenge");
http_response->set_content(kValidChallenge);
return http_response;
}
net::EmbeddedTestServer::HandleRequestCallback request_handler_override_;
std::unique_ptr<DelegateObserver> delegate_observer_;
std::unique_ptr<ModelObserver> model_observer_;
raw_ptr<ChromeAuthenticatorRequestDelegate> request_delegate_;
raw_ptr<device::test::VirtualFidoDeviceFactory> virtual_device_factory_;
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting> auth_env_;
};
IN_PROC_BROWSER_TEST_F(ChallengeUrlBrowserTest, ChallengeUrlGetAssertion) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionWithChallengeUrl);
std::string encoded_challenge;
base::Base64UrlEncode(kValidChallenge,
base::Base64UrlEncodePolicy::OMIT_PADDING,
&encoded_challenge);
std::string result;
ASSERT_TRUE(message_queue.WaitForMessage(&result));
EXPECT_THAT(result, testing::HasSubstr(encoded_challenge));
}
// TODO(https://crbug.com/389255414): Fix and re-enable.
IN_PROC_BROWSER_TEST_F(ChallengeUrlBrowserTest,
DISABLED_ChallengeUrlEmptyChallenge) {
SetRequestHandlerOverride(base::BindLambdaForTesting(
[](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content_type("application/x-webauthn-challenge");
http_response->set_content("");
return http_response;
}));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionWithChallengeUrl);
delegate_observer()->WaitForUI();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kErrorFetchingChallenge);
model_observer()->WaitForStep();
request_delegate()->dialog_model()->CancelAuthenticatorRequest();
std::string result;
ASSERT_TRUE(message_queue.WaitForMessage(&result));
EXPECT_THAT(result, testing::HasSubstr("NotAllowedError"));
}
// TODO(https://crbug.com/389255414): Fix and re-enable.
IN_PROC_BROWSER_TEST_F(ChallengeUrlBrowserTest,
DISABLED_ChallengeUrlWrongContentType) {
SetRequestHandlerOverride(base::BindLambdaForTesting(
[](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content_type("text/plain");
http_response->set_content(kValidChallenge);
return http_response;
}));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionWithChallengeUrl);
delegate_observer()->WaitForUI();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kErrorFetchingChallenge);
model_observer()->WaitForStep();
request_delegate()->dialog_model()->CancelAuthenticatorRequest();
std::string result;
ASSERT_TRUE(message_queue.WaitForMessage(&result));
EXPECT_THAT(result, testing::HasSubstr("NotAllowedError"));
}
#if BUILDFLAG(IS_LINUX)
// TODO(crbug.com/393055190): There is segfault in Linux during the move of the
// newly constructed `request_handler` to the `RequestState` in
// `AuthenticatorCommonImpl::StartGetAssertionRequest`. Fix and re-enable.
#define WebAuthnImmediateGetTest DISABLED_WebAuthnImmediateGetTest
#else
#define WebAuthnImmediateGetTest WebAuthnImmediateGetTest
#endif
class WebAuthnImmediateGetTest : public WebAuthnBrowserTest {
protected:
static constexpr std::string_view kRequestWithPasswordTemplate = R"(
navigator.credentials.get({
mediation: 'immediate',
password: $1,
publicKey: {
challenge: new Uint8Array([1,3,2,7,1,3,2,7]),
timeout: 10000,
userVerification: 'discouraged',
}}).then(c => 'webauthn: OK', e => 'error ' + e);
)";
static constexpr std::string_view kRequestWithAllowlistTemplate = R"(
navigator.credentials.get({
mediation: 'immediate',
publicKey: {
challenge: new Uint8Array([1,3,2,7,1,3,2,7]),
allowCredentials: [$1],
timeout: 10000,
userVerification: 'discouraged',
}}).then(c => 'webauthn: OK', e => 'error ' + e);
)";
private:
base::test::ScopedFeatureList scoped_feature_list_{
device::kWebAuthnImmediateGet};
};
IN_PROC_BROWSER_TEST_F(WebAuthnImmediateGetTest, NoCreds_NotFoundError) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
for (const auto& request_password : {"false", "true"}) {
const auto& script = base::ReplaceStringPlaceholders(
kRequestWithPasswordTemplate, {request_password},
/*offsets=*/nullptr);
const auto& result = content::EvalJs(web_contents, script);
EXPECT_THAT(result.ExtractString(), testing::HasSubstr("NotAllowedError"));
}
}
IN_PROC_BROWSER_TEST_F(WebAuthnImmediateGetTest,
Incognito_NoCreds_NotFoundError) {
auto* otr_browser = OpenURLOffTheRecord(
browser()->profile(),
https_server_.GetURL("www.example.com", "/title1.html"));
content::WebContents* web_contents =
otr_browser->tab_strip_model()->GetActiveWebContents();
for (const auto& request_password : {"false", "true"}) {
const auto& script = base::ReplaceStringPlaceholders(
kRequestWithPasswordTemplate, {request_password},
/*offsets=*/nullptr);
const auto& result = content::EvalJs(web_contents, script);
EXPECT_THAT(result.ExtractString(), testing::HasSubstr("NotAllowedError"));
}
}
IN_PROC_BROWSER_TEST_F(WebAuthnImmediateGetTest, Allowlist_NotAllowedError) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
const auto& script = base::ReplaceStringPlaceholders(
kRequestWithAllowlistTemplate,
{"{type: 'public-key', id: new Uint8Array([1,3,2,7])}"},
/*offsets=*/nullptr);
const auto& result = content::EvalJs(web_contents, script);
EXPECT_THAT(result.ExtractString(), testing::HasSubstr("NotAllowedError"));
}
} // namespace