// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include #include "base/command_line.h" #include "base/files/file_util.h" #include "base/memory/ref_counted.h" #include "base/path_service.h" #include "base/strings/strcat.h" #include "base/strings/string16.h" #include "base/strings/stringprintf.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/threading/thread_restrictions.h" #include "build/build_config.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/web_data_service_factory.h" #include "chrome/test/payments/payment_request_platform_browsertest_base.h" #include "components/keyed_service/core/service_access_type.h" #include "components/payments/content/payment_manifest_web_data_service.h" #include "components/payments/core/features.h" #include "components/payments/core/secure_payment_confirmation_instrument.h" #include "components/webdata/common/web_data_service_consumer.h" #include "content/public/browser/authenticator_environment.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/fido/virtual_fido_device_factory.h" #include "testing/gtest/include/gtest/gtest.h" namespace payments { namespace { std::vector GetEncodedIcon(const std::string& icon_file_name) { base::FilePath base_path; CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &base_path)); std::string icon_as_string; base::FilePath icon_file_path = base_path.AppendASCII("components/test/data/payments") .AppendASCII(icon_file_name); { base::ScopedAllowBlockingForTesting allow_blocking; CHECK(base::PathExists(icon_file_path)); CHECK(base::ReadFileToString(icon_file_path, &icon_as_string)); } return std::vector(icon_as_string.begin(), icon_as_string.end()); } class SecurePaymentConfirmationTest : public PaymentRequestPlatformBrowserTestBase, public WebDataServiceConsumer { public: SecurePaymentConfirmationTest() { // Enable the browser-side feature flag as it's disabled by default on // non-origin trial platforms. feature_list_.InitAndEnableFeature(features::kSecurePaymentConfirmation); } void SetUpCommandLine(base::CommandLine* command_line) override { PaymentRequestPlatformBrowserTestBase::SetUpCommandLine(command_line); command_line->AppendSwitch( switches::kEnableExperimentalWebPlatformFeatures); } void OnWebDataServiceRequestDone( WebDataServiceBase::Handle h, std::unique_ptr result) override { ASSERT_NE(nullptr, result); ASSERT_EQ(BOOL_RESULT, result->GetType()); EXPECT_TRUE(static_cast*>(result.get())->GetValue()); database_write_responded_ = true; } void OnAppListReady() override { PaymentRequestPlatformBrowserTestBase::OnAppListReady(); if (confirm_payment_) ASSERT_TRUE(test_controller()->ConfirmPayment()); } bool database_write_responded_ = false; bool confirm_payment_ = false; base::test::ScopedFeatureList feature_list_; }; IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, NoAuthenticator) { test_controller()->SetHasAuthenticator(false); NavigateTo("a.com", "/secure_payment_confirmation.html"); // EvalJs waits for JavaScript promise to resolve. EXPECT_EQ( "The payment method \"secure-payment-confirmation\" is not supported.", content::EvalJs(GetActiveWebContents(), "getSecurePaymentConfirmationStatus()")); } #if defined(OS_ANDROID) // TODO(https://crbug.com/1110320): Implement SetHasAuthenticator() for Android, // so secure payment confirmation can be tested on Android as well. #define MAYBE_NoInstrumentInStorage DISABLED_NoInstrumentInStorage #define MAYBE_CheckInstrumentInStorageAfterCanMakePayment \ DISABLED_CheckInstrumentInStorageAfterCanMakePayment #define MAYBE_PaymentSheetShowsApp DISABLED_PaymentSheetShowsApp #define MAYBE_CanMakePayment_HasAuthenticator \ DISABLED_CanMakePayment_HasAuthenticator #else #define MAYBE_NoInstrumentInStorage NoInstrumentInStorage #define MAYBE_CheckInstrumentInStorageAfterCanMakePayment \ CheckInstrumentInStorageAfterCanMakePayment #define MAYBE_PaymentSheetShowsApp PaymentSheetShowsApp #define MAYBE_CanMakePayment_HasAuthenticator CanMakePayment_HasAuthenticator #endif // OS_ANDROID IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, MAYBE_NoInstrumentInStorage) { test_controller()->SetHasAuthenticator(true); NavigateTo("a.com", "/secure_payment_confirmation.html"); // EvalJs waits for JavaScript promise to resolve. EXPECT_EQ( "The payment method \"secure-payment-confirmation\" is not supported.", content::EvalJs(GetActiveWebContents(), "getSecurePaymentConfirmationStatus()")); } IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, MAYBE_CheckInstrumentInStorageAfterCanMakePayment) { test_controller()->SetHasAuthenticator(true); NavigateTo("a.com", "/secure_payment_confirmation.html"); // EvalJs waits for JavaScript promise to resolve. EXPECT_EQ( "The payment method \"secure-payment-confirmation\" is not supported.", content::EvalJs( GetActiveWebContents(), base::StringPrintf( "getSecurePaymentConfirmationStatusAfterCanMakePayment()"))); } IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, MAYBE_PaymentSheetShowsApp) { test_controller()->SetHasAuthenticator(true); NavigateTo("a.com", "/secure_payment_confirmation.html"); std::vector credential_id = {'c', 'r', 'e', 'd'}; std::vector icon = GetEncodedIcon("icon.png"); WebDataServiceFactory::GetPaymentManifestWebDataForProfile( Profile::FromBrowserContext(GetActiveWebContents()->GetBrowserContext()), ServiceAccessType::EXPLICIT_ACCESS) ->AddSecurePaymentConfirmationInstrument( std::make_unique( std::move(credential_id), "relying-party.example", base::ASCIIToUTF16("Stub label"), std::move(icon)), /*consumer=*/this); ResetEventWaiterForSingleEvent(TestEvent::kAppListReady); // ExecJs starts executing JavaScript and immediately returns, not waiting for // any promise to return. EXPECT_TRUE(content::ExecJs(GetActiveWebContents(), "getSecurePaymentConfirmationStatus()")); WaitForObservedEvent(); EXPECT_TRUE(database_write_responded_); ASSERT_FALSE(test_controller()->app_descriptions().empty()); EXPECT_EQ(1u, test_controller()->app_descriptions().size()); EXPECT_EQ("Stub label", test_controller()->app_descriptions().front().label); } // canMakePayment() and hasEnrolledInstrument() should return false on platforms // without a compatible authenticator. IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, CanMakePayment_NoAuthenticator) { test_controller()->SetHasAuthenticator(false); NavigateTo("a.com", "/secure_payment_confirmation.html"); EXPECT_EQ("false", content::EvalJs(GetActiveWebContents(), "securePaymentConfirmationCanMakePayment()")); EXPECT_EQ("false", content::EvalJs( GetActiveWebContents(), "securePaymentConfirmationHasEnrolledInstrument()")); } // canMakePayment() and hasEnrolledInstrument() should return true on platforms // with a compatible authenticator regardless of the presence of payment // credentials. IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, MAYBE_CanMakePayment_HasAuthenticator) { test_controller()->SetHasAuthenticator(true); NavigateTo("a.com", "/secure_payment_confirmation.html"); EXPECT_EQ("true", content::EvalJs(GetActiveWebContents(), "securePaymentConfirmationCanMakePayment()")); EXPECT_EQ("true", content::EvalJs(GetActiveWebContents(), "securePaymentConfirmationCanMakePaymentTwice()")); EXPECT_EQ("true", content::EvalJs( GetActiveWebContents(), "securePaymentConfirmationHasEnrolledInstrument()")); } // Intentionally do not enable the "SecurePaymentConfirmation" Blink runtime // feature or the browser-side Finch flag. class SecurePaymentConfirmationDisabledTest : public PaymentRequestPlatformBrowserTestBase {}; IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledTest, PaymentMethodNotSupported) { test_controller()->SetHasAuthenticator(true); NavigateTo("a.com", "/secure_payment_confirmation.html"); // EvalJs waits for JavaScript promise to resolve. EXPECT_EQ( "The payment method \"secure-payment-confirmation\" is not supported.", content::EvalJs(GetActiveWebContents(), "getSecurePaymentConfirmationStatus()")); } IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledTest, CannotMakePayment) { test_controller()->SetHasAuthenticator(true); NavigateTo("a.com", "/secure_payment_confirmation.html"); EXPECT_EQ("false", content::EvalJs(GetActiveWebContents(), "securePaymentConfirmationCanMakePayment()")); EXPECT_EQ("false", content::EvalJs( GetActiveWebContents(), "securePaymentConfirmationHasEnrolledInstrument()")); } // Test that the feature can be disabled by the browser-side Finch flag. class SecurePaymentConfirmationDisabledByFinchTest : public PaymentRequestPlatformBrowserTestBase { public: SecurePaymentConfirmationDisabledByFinchTest() { // The feature should get disabled by the feature state despite experimental // web platform features being enabled. feature_list_.InitAndDisableFeature(features::kSecurePaymentConfirmation); } void SetUpCommandLine(base::CommandLine* command_line) override { PaymentRequestPlatformBrowserTestBase::SetUpCommandLine(command_line); command_line->AppendSwitch( switches::kEnableExperimentalWebPlatformFeatures); } private: base::test::ScopedFeatureList feature_list_; }; IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledByFinchTest, PaymentMethodNotSupported) { test_controller()->SetHasAuthenticator(true); NavigateTo("a.com", "/secure_payment_confirmation.html"); // EvalJs waits for JavaScript promise to resolve. EXPECT_EQ( "The payment method \"secure-payment-confirmation\" is not supported.", content::EvalJs(GetActiveWebContents(), "getSecurePaymentConfirmationStatus()")); } IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledByFinchTest, CannotMakePayment) { test_controller()->SetHasAuthenticator(true); NavigateTo("a.com", "/secure_payment_confirmation.html"); EXPECT_EQ("false", content::EvalJs(GetActiveWebContents(), "securePaymentConfirmationCanMakePayment()")); EXPECT_EQ("false", content::EvalJs( GetActiveWebContents(), "securePaymentConfirmationHasEnrolledInstrument()")); } // Creation tests do not work on Android because there is not a way to // override authenticator creation. #if !defined(OS_ANDROID) class SecurePaymentConfirmationCreationTest : public SecurePaymentConfirmationTest { public: // PaymentCredential creation uses the normal Web Authentication code path // for creating the public key credential, rather than using // IntenralAuthenticator. This stubs out authenticator instantiation in // content. void ReplaceFidoDiscoveryFactory() { auto owned_virtual_device_factory = std::make_unique(); auto* virtual_device_factory = owned_virtual_device_factory.get(); content::AuthenticatorEnvironment::GetInstance() ->ReplaceDefaultDiscoveryFactoryForTesting( std::move(owned_virtual_device_factory)); virtual_device_factory->SetTransport( device::FidoTransportProtocol::kInternal); virtual_device_factory->SetSupportedProtocol( device::ProtocolVersion::kCtap2); virtual_device_factory->mutable_state()->fingerprints_enrolled = true; // Currently this only supports tests relying on user-verifying platform // authenticators. device::VirtualCtap2Device::Config config; config.is_platform_authenticator = true; config.internal_uv_support = true; virtual_device_factory->SetCtap2Config(config); } const std::string GetDefaultIconURL() { return https_server()->GetURL("a.com", "/icon.png").spec(); } }; IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest, CreatePaymentCredential) { base::HistogramTester histogram_tester; ReplaceFidoDiscoveryFactory(); NavigateTo("a.com", "/secure_payment_confirmation.html"); EXPECT_EQ("OK", content::EvalJs(GetActiveWebContents(), content::JsReplace("createPaymentCredential($1)", GetDefaultIconURL()))); // Verify that credential id size gets recorded. histogram_tester.ExpectTotalCount( "PaymentRequest.SecurePaymentConfirmationCredentialIdSizeInBytes", 1U); } IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest, LookupPaymentCredential) { ReplaceFidoDiscoveryFactory(); NavigateTo("a.com", "/secure_payment_confirmation.html"); std::string credentialIdentifier = content::EvalJs( GetActiveWebContents(), content::JsReplace("createCredentialAndReturnItsIdentifier($1)", GetDefaultIconURL())) .ExtractString(); // Cross the origin boundary. NavigateTo("b.com", "/secure_payment_confirmation.html"); test_controller()->SetHasAuthenticator(true); ResetEventWaiterForSingleEvent(TestEvent::kAppListReady); // ExecJs starts executing JavaScript and immediately returns, not waiting for // any promise to return. EXPECT_TRUE(content::ExecJs( GetActiveWebContents(), content::JsReplace("getSecurePaymentConfirmationStatus($1)", credentialIdentifier))); WaitForObservedEvent(); ASSERT_FALSE(test_controller()->app_descriptions().empty()); EXPECT_EQ(1u, test_controller()->app_descriptions().size()); EXPECT_EQ("display_name_for_instrument", test_controller()->app_descriptions().front().label); } IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest, ConfirmPaymentInCrossOriginIframe) { NavigateTo("a.com", "/secure_payment_confirmation.html"); ReplaceFidoDiscoveryFactory(); std::string credentialIdentifier = content::EvalJs( GetActiveWebContents(), content::JsReplace("createCredentialAndReturnItsIdentifier($1)", GetDefaultIconURL())) .ExtractString(); NavigateTo("b.com", "/iframe_poster.html"); test_controller()->SetHasAuthenticator(true); confirm_payment_ = true; // EvalJs waits for JavaScript promise to resolve. EXPECT_EQ( "success", content::EvalJs( GetActiveWebContents(), content::JsReplace( "postToIframe($1, $2);", https_server()->GetURL("c.com", "/iframe_receiver.html").spec(), credentialIdentifier))); } IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest, ChallengeIsReturned) { NavigateTo("a.com", "/secure_payment_confirmation.html"); ReplaceFidoDiscoveryFactory(); std::string credentialIdentifier = content::EvalJs( GetActiveWebContents(), content::JsReplace("createCredentialAndReturnItsIdentifier($1)", GetDefaultIconURL())) .ExtractString(); NavigateTo("b.com", "/get_challenge.html"); test_controller()->SetHasAuthenticator(true); confirm_payment_ = true; // Strip the trailing slash ("/") from the merchant origin in serialization to // match the implementation behavior. std::string merchant_origin = https_server()->GetURL("b.com", "/").spec(); ASSERT_EQ('/', merchant_origin[merchant_origin.length() - 1]); merchant_origin = merchant_origin.substr(0, merchant_origin.length() - 1); ASSERT_NE('/', merchant_origin[merchant_origin.length() - 1]); // EvalJs waits for JavaScript promise to resolve. // The `networkData` field is the base64 encoding of 'hello world', which is // set in `get_challenge.js`. EXPECT_EQ("{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin + "\",\"total\":{\"currency\":\"USD\"," + "\"value\":\"0.01\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}", content::EvalJs(GetActiveWebContents(), content::JsReplace("getChallenge($1, $2);", credentialIdentifier, "0.01"))); // Verify that passing a promise into PaymentRequest.show() that updates the // `total` price will result in the challenge price being set only after the // promise resolves with the finalized price. EXPECT_EQ("{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin + "\",\"total\":{\"currency\":\"USD\"," + "\"value\":\"0.02\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}", content::EvalJs( GetActiveWebContents(), content::JsReplace("getChallengeWithShowPromise($1, $2, $3);", credentialIdentifier, "0.01", "0.02"))); // Verify that the returned challenge correctly reflects the modified amount. EXPECT_EQ( "{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin + "\",\"total\":{\"currency\":\"USD\"," + "\"value\":\"0.03\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}", content::EvalJs(GetActiveWebContents(), content::JsReplace("getChallengeWithModifier($1, $2);", credentialIdentifier, "0.03"))); // Verify that the returned challenge correctly reflects the modified amount // that is set when the promised passed into PaymentRequest.show() resolves. EXPECT_EQ( "{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin + "\",\"total\":{\"currency\":\"USD\"," + "\"value\":\"0.04\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}", content::EvalJs( GetActiveWebContents(), content::JsReplace("getChallengeWithModifierAndShowPromise($1, $2);", credentialIdentifier, "0.04"))); } #endif // !defined(OS_ANDROID) } // namespace } // namespace payments