blob: 4ffe04668c82e32356247f3c529367e32d25f853 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_WEBAUTH_AUTHENTICATOR_TEST_BASE_H_
#define CONTENT_BROWSER_WEBAUTH_AUTHENTICATOR_TEST_BASE_H_
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "content/public/browser/authenticator_request_client_delegate.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/web_authentication_delegate.h"
#include "content/public/browser/web_authentication_request_proxy.h"
#include "content/public/test/test_renderer_host.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "url/origin.h"
#if BUILDFLAG(IS_WIN)
#include "device/fido/win/fake_webauthn_api.h"
#include "device/fido/win/util.h"
#endif
namespace device {
class FidoDiscoveryFactory;
namespace test {
class VirtualFidoDeviceFactory;
}
} // namespace device
namespace content {
using blink::mojom::AuthenticationExtensionsClientInputs;
using blink::mojom::AuthenticatorStatus;
using blink::mojom::GetCredentialOptions;
using blink::mojom::GetCredentialOptionsPtr;
using blink::mojom::PublicKeyCredentialCreationOptions;
using blink::mojom::PublicKeyCredentialCreationOptionsPtr;
using blink::mojom::PublicKeyCredentialRequestOptions;
using blink::mojom::PublicKeyCredentialRequestOptionsPtr;
typedef struct {
std::string_view origin;
// Either a relying party ID or a U2F AppID.
std::string_view claimed_authority;
AuthenticatorStatus expected_status;
} OriginClaimedAuthorityPair;
inline constexpr auto kValidRpTestCases =
std::to_array<OriginClaimedAuthorityPair>({
{"http://localhost", "localhost", AuthenticatorStatus::SUCCESS},
{"https://myawesomedomain", "myawesomedomain",
AuthenticatorStatus::SUCCESS},
{"https://foo.bar.google.com", "foo.bar.google.com",
AuthenticatorStatus::SUCCESS},
{"https://foo.bar.google.com", "bar.google.com",
AuthenticatorStatus::SUCCESS},
{"https://foo.bar.google.com", "google.com",
AuthenticatorStatus::SUCCESS},
{"https://earth.login.awesomecompany", "login.awesomecompany",
AuthenticatorStatus::SUCCESS},
{"https://google.com:1337", "google.com", AuthenticatorStatus::SUCCESS},
// Hosts with trailing dot valid for rpIds with or without trailing dot.
// Hosts without trailing dots only matches rpIDs without trailing dot.
// Two trailing dots only matches rpIDs with two trailing dots.
{"https://google.com.", "google.com", AuthenticatorStatus::SUCCESS},
{"https://google.com.", "google.com.", AuthenticatorStatus::SUCCESS},
{"https://google.com..", "google.com..", AuthenticatorStatus::SUCCESS},
// Leading dots are ignored in canonicalized hosts.
{"https://.google.com", "google.com", AuthenticatorStatus::SUCCESS},
{"https://..google.com", "google.com", AuthenticatorStatus::SUCCESS},
{"https://.google.com", ".google.com", AuthenticatorStatus::SUCCESS},
{"https://..google.com", ".google.com", AuthenticatorStatus::SUCCESS},
{"https://accounts.google.com", ".google.com",
AuthenticatorStatus::SUCCESS},
});
inline constexpr auto kInvalidRpTestCases = std::to_array<
OriginClaimedAuthorityPair>({
{"https://google.com", "com", AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"http://google.com", "google.com", AuthenticatorStatus::INVALID_DOMAIN},
{"http://myawesomedomain", "myawesomedomain",
AuthenticatorStatus::INVALID_DOMAIN},
{"https://google.com", "foo.bar.google.com",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"http://myawesomedomain", "randomdomain",
AuthenticatorStatus::INVALID_DOMAIN},
{"https://myawesomedomain", "randomdomain",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://notgoogle.com", "google.com)",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://not-google.com", "google.com)",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://evil.appspot.com", "appspot.com",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://evil.co.uk", "co.uk", AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://google.com", "google.com.",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://google.com", "google.com..",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://google.com", ".google.com",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://google.com..", "google.com",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://.com", "com.", AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://.co.uk", "co.uk.", AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://1.2.3", "1.2.3", AuthenticatorStatus::INVALID_DOMAIN},
{"https://1.2.3", "2.3", AuthenticatorStatus::INVALID_DOMAIN},
{"https://127.0.0.1", "127.0.0.1", AuthenticatorStatus::INVALID_DOMAIN},
{"https://127.0.0.1", "27.0.0.1", AuthenticatorStatus::INVALID_DOMAIN},
{"https://127.0.0.1", ".0.0.1", AuthenticatorStatus::INVALID_DOMAIN},
{"https://127.0.0.1", "0.0.1", AuthenticatorStatus::INVALID_DOMAIN},
{"https://[::127.0.0.1]", "127.0.0.1", AuthenticatorStatus::INVALID_DOMAIN},
{"https://[::127.0.0.1]", "[127.0.0.1]",
AuthenticatorStatus::INVALID_DOMAIN},
{"https://[::1]", "1", AuthenticatorStatus::INVALID_DOMAIN},
{"https://[::1]", "1]", AuthenticatorStatus::INVALID_DOMAIN},
{"https://[::1]", "::1", AuthenticatorStatus::INVALID_DOMAIN},
{"https://[::1]", "[::1]", AuthenticatorStatus::INVALID_DOMAIN},
{"https://[1::1]", "::1", AuthenticatorStatus::INVALID_DOMAIN},
{"https://[1::1]", "::1]", AuthenticatorStatus::INVALID_DOMAIN},
{"https://[1::1]", "[::1]", AuthenticatorStatus::INVALID_DOMAIN},
{"http://google.com:443", "google.com",
AuthenticatorStatus::INVALID_DOMAIN},
{"data:google.com", "google.com", AuthenticatorStatus::OPAQUE_DOMAIN},
{"data:text/html,google.com", "google.com",
AuthenticatorStatus::OPAQUE_DOMAIN},
{"ws://google.com", "google.com", AuthenticatorStatus::INVALID_PROTOCOL},
{"gopher://google.com", "google.com", AuthenticatorStatus::OPAQUE_DOMAIN},
{"ftp://google.com", "google.com", AuthenticatorStatus::INVALID_PROTOCOL},
{"file:///google.com", "google.com", AuthenticatorStatus::INVALID_PROTOCOL},
// Use of webauthn from a WSS origin may be technically valid, but we
// prohibit use on non-HTTPS origins. (At least for now.)
{"wss://google.com", "google.com", AuthenticatorStatus::INVALID_PROTOCOL},
{"data:,", "", AuthenticatorStatus::OPAQUE_DOMAIN},
{"https://google.com", "", AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"ws:///google.com", "", AuthenticatorStatus::INVALID_PROTOCOL},
{"wss:///google.com", "", AuthenticatorStatus::INVALID_PROTOCOL},
{"gopher://google.com", "", AuthenticatorStatus::OPAQUE_DOMAIN},
{"ftp://google.com", "", AuthenticatorStatus::INVALID_PROTOCOL},
{"file:///google.com", "", AuthenticatorStatus::INVALID_PROTOCOL},
// This case is acceptable according to spec, but both renderer
// and browser handling currently do not permit it.
{"https://login.awesomecompany", "awesomecompany",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
// These are AppID test cases, but should also be invalid relying party
// examples too.
{"https://example.com", "https://com/",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://example.com", "https://com/foo",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://example.com", "https://foo.com/",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://example.com", "http://example.com",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"http://example.com", "https://example.com",
AuthenticatorStatus::INVALID_DOMAIN},
{"https://127.0.0.1", "https://127.0.0.1",
AuthenticatorStatus::INVALID_DOMAIN},
{"https://www.notgoogle.com",
"https://www.gstatic.com/securitykey/origins.json",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://www.google.com",
"https://www.gstatic.com/securitykey/origins.json#x",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://www.google.com",
"https://www.gstatic.com/securitykey/origins.json2",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://www.google.com", "https://gstatic.com/securitykey/origins.json",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://ggoogle.com", "https://www.gstatic.com/securitykey/origi",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
{"https://com", "https://www.gstatic.com/securitykey/origins.json",
AuthenticatorStatus::BAD_RELYING_PARTY_ID},
});
inline constexpr char kTestRelyingPartyId[] = "google.com";
// The size of credential IDs returned by GetTestCredentials().
inline constexpr size_t kTestCredentialIdLength = 32u;
device::PublicKeyCredentialUserEntity GetTestPublicKeyCredentialUserEntity();
device::AuthenticatorSelectionCriteria GetTestAuthenticatorSelectionCriteria();
std::vector<device::PublicKeyCredentialDescriptor> GetTestCredentials(
size_t num_credentials = 1);
std::vector<device::PublicKeyCredentialParams::CredentialInfo>
GetTestPublicKeyCredentialParameters(int32_t algorithm_identifier);
device::PublicKeyCredentialRpEntity GetTestPublicKeyCredentialRPEntity();
PublicKeyCredentialCreationOptionsPtr
GetTestPublicKeyCredentialCreationOptions();
PublicKeyCredentialRequestOptionsPtr GetTestPublicKeyCredentialRequestOptions();
GetCredentialOptionsPtr GetTestGetCredentialOptions();
// TestWebAuthenticationRequestProxy is a test fake implementation of the
// WebAuthenticationRequestProxy embedder interface.
class TestWebAuthenticationRequestProxy : public WebAuthenticationRequestProxy {
public:
struct Config {
Config();
~Config();
// If true, resolves all request event callbacks instantly.
bool resolve_callbacks = true;
// The return value of IsActive().
bool is_active = true;
// The fake response to SignalIsUVPAARequest().
bool is_uvpaa = true;
// Whether the request to SignalCreateRequest() should succeed.
bool request_success = true;
// If `request_success` is false, the name of the DOMError to be
// returned.
std::string request_error_name = "NotAllowedError";
// If `request_success` is true, the fake response to be returned for an
// onCreateRequest event.
blink::mojom::MakeCredentialAuthenticatorResponsePtr
make_credential_response = nullptr;
// If `request_success` is true, the fake response to be returned for an
// onGetRequest event.
blink::mojom::GetAssertionAuthenticatorResponsePtr get_assertion_response =
nullptr;
};
struct Observations {
Observations();
~Observations();
std::vector<PublicKeyCredentialCreationOptionsPtr> create_requests;
std::vector<PublicKeyCredentialRequestOptionsPtr> get_requests;
size_t num_isuvpaa = 0;
size_t num_cancel = 0;
};
TestWebAuthenticationRequestProxy();
~TestWebAuthenticationRequestProxy() override;
Config& config() { return config_; }
Observations& observations() { return observations_; }
bool IsActive(const url::Origin& caller_origin) override;
RequestId SignalCreateRequest(
const PublicKeyCredentialCreationOptionsPtr& options,
CreateCallback callback) override;
RequestId SignalGetRequest(
const blink::mojom::PublicKeyCredentialRequestOptionsPtr& options,
GetCallback callback) override;
RequestId SignalIsUvpaaRequest(IsUvpaaCallback callback) override;
void CancelRequest(RequestId request_id) override;
void RunPendingCreateCallback();
void RunPendingGetCallback();
void RunPendingIsUvpaaCallback();
bool HasPendingRequest();
private:
Config config_;
Observations observations_;
RequestId current_request_id_ = 0;
CreateCallback pending_create_callback_;
GetCallback pending_get_callback_;
IsUvpaaCallback pending_is_uvpaa_callback_;
};
// TestWebAuthenticationDelegate is a test fake implementation of the
// WebAuthenticationDelegate embedder interface.
class TestWebAuthenticationDelegate : public WebAuthenticationDelegate {
public:
TestWebAuthenticationDelegate();
~TestWebAuthenticationDelegate() override;
void IsUserVerifyingPlatformAuthenticatorAvailableOverride(
RenderFrameHost*,
base::OnceCallback<void(std::optional<bool>)> callback) override;
bool OverrideCallerOriginAndRelyingPartyIdValidation(
content::BrowserContext* browser_context,
const url::Origin& origin,
const std::string& rp_id) override;
std::optional<std::string> MaybeGetRelyingPartyIdOverride(
const std::string& claimed_rp_id,
const url::Origin& caller_origin) override;
bool ShouldPermitIndividualAttestation(
content::BrowserContext* browser_context,
const url::Origin& caller_origin,
const std::string& relying_party_id) override;
bool SupportsResidentKeys(RenderFrameHost*) override;
bool IsFocused(WebContents* web_contents) override;
#if BUILDFLAG(IS_MAC)
std::optional<TouchIdAuthenticatorConfig> GetTouchIdAuthenticatorConfig(
BrowserContext* browser_context) override;
#endif
WebAuthenticationRequestProxy* MaybeGetRequestProxy(
content::BrowserContext* browser_context,
const url::Origin& caller_origin) override;
bool OriginMayUseRemoteDesktopClientOverride(
content::BrowserContext* browser_context,
const url::Origin& caller_origin) override;
void BrowserProvidedPasskeysAvailable(
BrowserContext* browser_context,
base::OnceCallback<void(bool)> callback) override;
// If set, the return value of IsUVPAA() will be overridden with this value.
// Platform-specific implementations will not be invoked.
std::optional<bool> is_uvpaa_override;
// If set, the delegate will permit WebAuthn requests from chrome-extension
// origins.
bool permit_extensions = false;
// Indicates whether individual attestation should be permitted by the
// delegate.
bool permit_individual_attestation = false;
// A specific RP ID for which individual attestation will be permitted.
std::optional<std::string> permit_individual_attestation_for_rp_id;
// Indicates whether resident key operations should be permitted by the
// delegate.
bool supports_resident_keys = false;
// The return value of the focus check issued at the end of a request.
bool is_focused = true;
#if BUILDFLAG(IS_MAC)
// Configuration data for the macOS platform authenticator.
std::optional<TouchIdAuthenticatorConfig> touch_id_authenticator_config;
#endif
// The WebAuthenticationRequestProxy returned by |MaybeGetRequestProxy|.
std::unique_ptr<TestWebAuthenticationRequestProxy> request_proxy = nullptr;
// The origin permitted to use the RemoteDesktopClientOverride extension.
std::optional<url::Origin> remote_desktop_client_override_origin;
// Return value of `BrowserProvidedPasskeysAvailable()`.
bool browser_provided_passkeys_available = false;
};
// TestAuthenticatorContentBrowserClient is a test fake implementation of the
// ContentBrowserClient interface that injects |TestWebAuthenticationDelegate|
// and |TestAuthenticatorRequestDelegate| instances into |AuthenticatorImpl|.
class TestAuthenticatorContentBrowserClient : public ContentBrowserClient {
public:
TestAuthenticatorContentBrowserClient();
~TestAuthenticatorContentBrowserClient() override;
TestWebAuthenticationDelegate* GetTestWebAuthenticationDelegate();
// ContentBrowserClient:
WebAuthenticationDelegate* GetWebAuthenticationDelegate() override;
bool IsSecurityLevelAcceptableForWebAuthn(content::RenderFrameHost* rfh,
const url::Origin& origin) override;
std::unique_ptr<AuthenticatorRequestClientDelegate>
GetWebAuthenticationRequestDelegate(
RenderFrameHost* render_frame_host) override;
TestWebAuthenticationDelegate web_authentication_delegate;
// If set, this closure will be called when the subsequently constructed
// delegate is informed that the request has started.
base::OnceClosure action_callbacks_registered_callback;
// This emulates scenarios where a nullptr RequestClientDelegate is returned
// because a request is already in progress.
bool return_null_delegate = false;
// If started_over_callback_ is set to a non-null callback, the request will
// be restarted after action callbacks are registered, and
// |started_over_callback| will replace
// |action_callbacks_registered_callback|. This should then be called the
// second time action callbacks are registered. It also causes
// DoesBlockRequestOnFailure to return true, once.
base::OnceClosure started_over_callback_;
// This simulates the user immediately cancelling the request after transport
// availability info is enumerated.
bool simulate_user_cancelled_ = false;
// The return value of IsSecurityLevelAcceptableForWebAuthn.
bool is_webauthn_security_level_acceptable = true;
// Whether discovery of the enclave authenticator was requested or not.
std::optional<bool> enclave_authenticator_should_be_discovered_;
// The set of transports allowed for a request.
base::flat_set<device::FidoTransportProtocol> discovered_transports_;
};
class AuthenticatorTestBase : public RenderViewHostTestHarness {
protected:
AuthenticatorTestBase();
~AuthenticatorTestBase() override;
static void SetUpTestSuite();
void SetUp() override;
void TearDown() override;
virtual void ResetVirtualDevice();
virtual void ReplaceDiscoveryFactory(
std::unique_ptr<device::FidoDiscoveryFactory> device_factory);
void SetMojoErrorHandler(
base::RepeatingCallback<void(const std::string&)> callback);
raw_ptr<device::test::VirtualFidoDeviceFactory> virtual_device_factory_ =
nullptr;
#if BUILDFLAG(IS_WIN)
device::FakeWinWebAuthnApi fake_win_webauthn_api_;
device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override_{
&fake_win_webauthn_api_};
std::unique_ptr<device::fido::win::ScopedBiometricsOverride>
biometrics_override_;
#endif
private:
void OnMojoError(const std::string& error);
base::RepeatingCallback<void(const std::string&)> mojo_error_handler_;
};
} // namespace content
#endif // CONTENT_BROWSER_WEBAUTH_AUTHENTICATOR_TEST_BASE_H_