1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <stdint.h>
6 
7 #include <memory>
8 #include <utility>
9 #include <vector>
10 
11 #include "base/command_line.h"
12 #include "base/files/file_util.h"
13 #include "base/memory/ref_counted.h"
14 #include "base/path_service.h"
15 #include "base/strings/strcat.h"
16 #include "base/strings/string16.h"
17 #include "base/strings/stringprintf.h"
18 #include "base/test/metrics/histogram_tester.h"
19 #include "base/test/scoped_feature_list.h"
20 #include "base/threading/thread_restrictions.h"
21 #include "build/build_config.h"
22 #include "chrome/browser/profiles/profile.h"
23 #include "chrome/browser/web_data_service_factory.h"
24 #include "chrome/test/payments/payment_request_platform_browsertest_base.h"
25 #include "components/keyed_service/core/service_access_type.h"
26 #include "components/payments/content/payment_manifest_web_data_service.h"
27 #include "components/payments/core/features.h"
28 #include "components/payments/core/secure_payment_confirmation_instrument.h"
29 #include "components/webdata/common/web_data_service_consumer.h"
30 #include "content/public/browser/authenticator_environment.h"
31 #include "content/public/common/content_switches.h"
32 #include "content/public/test/browser_test.h"
33 #include "content/public/test/browser_test_utils.h"
34 #include "device/fido/virtual_fido_device_factory.h"
35 #include "testing/gtest/include/gtest/gtest.h"
36 
37 namespace payments {
38 namespace {
39 
GetEncodedIcon(const std::string & icon_file_name)40 std::vector<uint8_t> GetEncodedIcon(const std::string& icon_file_name) {
41   base::FilePath base_path;
42   CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &base_path));
43   std::string icon_as_string;
44   base::FilePath icon_file_path =
45       base_path.AppendASCII("components/test/data/payments")
46           .AppendASCII(icon_file_name);
47   {
48     base::ScopedAllowBlockingForTesting allow_blocking;
49     CHECK(base::PathExists(icon_file_path));
50     CHECK(base::ReadFileToString(icon_file_path, &icon_as_string));
51   }
52 
53   return std::vector<uint8_t>(icon_as_string.begin(), icon_as_string.end());
54 }
55 
56 class SecurePaymentConfirmationTest
57     : public PaymentRequestPlatformBrowserTestBase,
58       public WebDataServiceConsumer {
59  public:
SecurePaymentConfirmationTest()60   SecurePaymentConfirmationTest() {
61     // Enable the browser-side feature flag as it's disabled by default on
62     // non-origin trial platforms.
63     feature_list_.InitAndEnableFeature(features::kSecurePaymentConfirmation);
64   }
65 
SetUpCommandLine(base::CommandLine * command_line)66   void SetUpCommandLine(base::CommandLine* command_line) override {
67     PaymentRequestPlatformBrowserTestBase::SetUpCommandLine(command_line);
68     command_line->AppendSwitch(
69         switches::kEnableExperimentalWebPlatformFeatures);
70   }
71 
OnWebDataServiceRequestDone(WebDataServiceBase::Handle h,std::unique_ptr<WDTypedResult> result)72   void OnWebDataServiceRequestDone(
73       WebDataServiceBase::Handle h,
74       std::unique_ptr<WDTypedResult> result) override {
75     ASSERT_NE(nullptr, result);
76     ASSERT_EQ(BOOL_RESULT, result->GetType());
77     EXPECT_TRUE(static_cast<WDResult<bool>*>(result.get())->GetValue());
78     database_write_responded_ = true;
79   }
80 
OnAppListReady()81   void OnAppListReady() override {
82     PaymentRequestPlatformBrowserTestBase::OnAppListReady();
83     if (confirm_payment_)
84       ASSERT_TRUE(test_controller()->ConfirmPayment());
85   }
86 
87   bool database_write_responded_ = false;
88   bool confirm_payment_ = false;
89   base::test::ScopedFeatureList feature_list_;
90 };
91 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,NoAuthenticator)92 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, NoAuthenticator) {
93   test_controller()->SetHasAuthenticator(false);
94   NavigateTo("a.com", "/secure_payment_confirmation.html");
95 
96   // EvalJs waits for JavaScript promise to resolve.
97   EXPECT_EQ(
98       "The payment method \"secure-payment-confirmation\" is not supported.",
99       content::EvalJs(GetActiveWebContents(),
100                       "getSecurePaymentConfirmationStatus()"));
101 }
102 
103 #if defined(OS_ANDROID)
104 // TODO(https://crbug.com/1110320): Implement SetHasAuthenticator() for Android,
105 // so secure payment confirmation can be tested on Android as well.
106 #define MAYBE_NoInstrumentInStorage DISABLED_NoInstrumentInStorage
107 #define MAYBE_CheckInstrumentInStorageAfterCanMakePayment \
108   DISABLED_CheckInstrumentInStorageAfterCanMakePayment
109 #define MAYBE_PaymentSheetShowsApp DISABLED_PaymentSheetShowsApp
110 #define MAYBE_CanMakePayment_HasAuthenticator \
111   DISABLED_CanMakePayment_HasAuthenticator
112 #else
113 #define MAYBE_NoInstrumentInStorage NoInstrumentInStorage
114 #define MAYBE_CheckInstrumentInStorageAfterCanMakePayment \
115   CheckInstrumentInStorageAfterCanMakePayment
116 #define MAYBE_PaymentSheetShowsApp PaymentSheetShowsApp
117 #define MAYBE_CanMakePayment_HasAuthenticator CanMakePayment_HasAuthenticator
118 #endif  // OS_ANDROID
119 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,MAYBE_NoInstrumentInStorage)120 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
121                        MAYBE_NoInstrumentInStorage) {
122   test_controller()->SetHasAuthenticator(true);
123   NavigateTo("a.com", "/secure_payment_confirmation.html");
124 
125   // EvalJs waits for JavaScript promise to resolve.
126   EXPECT_EQ(
127       "The payment method \"secure-payment-confirmation\" is not supported.",
128       content::EvalJs(GetActiveWebContents(),
129                       "getSecurePaymentConfirmationStatus()"));
130 }
131 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,MAYBE_CheckInstrumentInStorageAfterCanMakePayment)132 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
133                        MAYBE_CheckInstrumentInStorageAfterCanMakePayment) {
134   test_controller()->SetHasAuthenticator(true);
135   NavigateTo("a.com", "/secure_payment_confirmation.html");
136 
137   // EvalJs waits for JavaScript promise to resolve.
138   EXPECT_EQ(
139       "The payment method \"secure-payment-confirmation\" is not supported.",
140       content::EvalJs(
141           GetActiveWebContents(),
142           base::StringPrintf(
143               "getSecurePaymentConfirmationStatusAfterCanMakePayment()")));
144 }
145 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,MAYBE_PaymentSheetShowsApp)146 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
147                        MAYBE_PaymentSheetShowsApp) {
148   test_controller()->SetHasAuthenticator(true);
149   NavigateTo("a.com", "/secure_payment_confirmation.html");
150   std::vector<uint8_t> credential_id = {'c', 'r', 'e', 'd'};
151   std::vector<uint8_t> icon = GetEncodedIcon("icon.png");
152   WebDataServiceFactory::GetPaymentManifestWebDataForProfile(
153       Profile::FromBrowserContext(GetActiveWebContents()->GetBrowserContext()),
154       ServiceAccessType::EXPLICIT_ACCESS)
155       ->AddSecurePaymentConfirmationInstrument(
156           std::make_unique<SecurePaymentConfirmationInstrument>(
157               std::move(credential_id), "relying-party.example",
158               base::ASCIIToUTF16("Stub label"), std::move(icon)),
159           /*consumer=*/this);
160   ResetEventWaiterForSingleEvent(TestEvent::kAppListReady);
161 
162   // ExecJs starts executing JavaScript and immediately returns, not waiting for
163   // any promise to return.
164   EXPECT_TRUE(content::ExecJs(GetActiveWebContents(),
165                               "getSecurePaymentConfirmationStatus()"));
166 
167   WaitForObservedEvent();
168   EXPECT_TRUE(database_write_responded_);
169   ASSERT_FALSE(test_controller()->app_descriptions().empty());
170   EXPECT_EQ(1u, test_controller()->app_descriptions().size());
171   EXPECT_EQ("Stub label", test_controller()->app_descriptions().front().label);
172 }
173 
174 // canMakePayment() and hasEnrolledInstrument() should return false on platforms
175 // without a compatible authenticator.
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,CanMakePayment_NoAuthenticator)176 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
177                        CanMakePayment_NoAuthenticator) {
178   test_controller()->SetHasAuthenticator(false);
179   NavigateTo("a.com", "/secure_payment_confirmation.html");
180 
181   EXPECT_EQ("false",
182             content::EvalJs(GetActiveWebContents(),
183                             "securePaymentConfirmationCanMakePayment()"));
184   EXPECT_EQ("false", content::EvalJs(
185                          GetActiveWebContents(),
186                          "securePaymentConfirmationHasEnrolledInstrument()"));
187 }
188 
189 // canMakePayment() and hasEnrolledInstrument() should return true on platforms
190 // with a compatible authenticator regardless of the presence of payment
191 // credentials.
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,MAYBE_CanMakePayment_HasAuthenticator)192 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
193                        MAYBE_CanMakePayment_HasAuthenticator) {
194   test_controller()->SetHasAuthenticator(true);
195   NavigateTo("a.com", "/secure_payment_confirmation.html");
196 
197   EXPECT_EQ("true",
198             content::EvalJs(GetActiveWebContents(),
199                             "securePaymentConfirmationCanMakePayment()"));
200   EXPECT_EQ("true",
201             content::EvalJs(GetActiveWebContents(),
202                             "securePaymentConfirmationCanMakePaymentTwice()"));
203   EXPECT_EQ("true", content::EvalJs(
204                         GetActiveWebContents(),
205                         "securePaymentConfirmationHasEnrolledInstrument()"));
206 }
207 
208 // Intentionally do not enable the "SecurePaymentConfirmation" Blink runtime
209 // feature or the browser-side Finch flag.
210 class SecurePaymentConfirmationDisabledTest
211     : public PaymentRequestPlatformBrowserTestBase {};
212 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledTest,PaymentMethodNotSupported)213 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledTest,
214                        PaymentMethodNotSupported) {
215   test_controller()->SetHasAuthenticator(true);
216   NavigateTo("a.com", "/secure_payment_confirmation.html");
217 
218   // EvalJs waits for JavaScript promise to resolve.
219   EXPECT_EQ(
220       "The payment method \"secure-payment-confirmation\" is not supported.",
221       content::EvalJs(GetActiveWebContents(),
222                       "getSecurePaymentConfirmationStatus()"));
223 }
224 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledTest,CannotMakePayment)225 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledTest,
226                        CannotMakePayment) {
227   test_controller()->SetHasAuthenticator(true);
228   NavigateTo("a.com", "/secure_payment_confirmation.html");
229 
230   EXPECT_EQ("false",
231             content::EvalJs(GetActiveWebContents(),
232                             "securePaymentConfirmationCanMakePayment()"));
233   EXPECT_EQ("false", content::EvalJs(
234                          GetActiveWebContents(),
235                          "securePaymentConfirmationHasEnrolledInstrument()"));
236 }
237 
238 // Test that the feature can be disabled by the browser-side Finch flag.
239 class SecurePaymentConfirmationDisabledByFinchTest
240     : public PaymentRequestPlatformBrowserTestBase {
241  public:
SecurePaymentConfirmationDisabledByFinchTest()242   SecurePaymentConfirmationDisabledByFinchTest() {
243     // The feature should get disabled by the feature state despite experimental
244     // web platform features being enabled.
245     feature_list_.InitAndDisableFeature(features::kSecurePaymentConfirmation);
246   }
247 
SetUpCommandLine(base::CommandLine * command_line)248   void SetUpCommandLine(base::CommandLine* command_line) override {
249     PaymentRequestPlatformBrowserTestBase::SetUpCommandLine(command_line);
250     command_line->AppendSwitch(
251         switches::kEnableExperimentalWebPlatformFeatures);
252   }
253 
254  private:
255   base::test::ScopedFeatureList feature_list_;
256 };
257 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledByFinchTest,PaymentMethodNotSupported)258 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledByFinchTest,
259                        PaymentMethodNotSupported) {
260   test_controller()->SetHasAuthenticator(true);
261   NavigateTo("a.com", "/secure_payment_confirmation.html");
262 
263   // EvalJs waits for JavaScript promise to resolve.
264   EXPECT_EQ(
265       "The payment method \"secure-payment-confirmation\" is not supported.",
266       content::EvalJs(GetActiveWebContents(),
267                       "getSecurePaymentConfirmationStatus()"));
268 }
269 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledByFinchTest,CannotMakePayment)270 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledByFinchTest,
271                        CannotMakePayment) {
272   test_controller()->SetHasAuthenticator(true);
273   NavigateTo("a.com", "/secure_payment_confirmation.html");
274 
275   EXPECT_EQ("false",
276             content::EvalJs(GetActiveWebContents(),
277                             "securePaymentConfirmationCanMakePayment()"));
278   EXPECT_EQ("false", content::EvalJs(
279                          GetActiveWebContents(),
280                          "securePaymentConfirmationHasEnrolledInstrument()"));
281 }
282 
283 // Creation tests do not work on Android because there is not a way to
284 // override authenticator creation.
285 #if !defined(OS_ANDROID)
286 class SecurePaymentConfirmationCreationTest
287     : public SecurePaymentConfirmationTest {
288  public:
289   // PaymentCredential creation uses the normal Web Authentication code path
290   // for creating the public key credential, rather than using
291   // IntenralAuthenticator. This stubs out authenticator instantiation in
292   // content.
ReplaceFidoDiscoveryFactory()293   void ReplaceFidoDiscoveryFactory() {
294     auto owned_virtual_device_factory =
295         std::make_unique<device::test::VirtualFidoDeviceFactory>();
296     auto* virtual_device_factory = owned_virtual_device_factory.get();
297     content::AuthenticatorEnvironment::GetInstance()
298         ->ReplaceDefaultDiscoveryFactoryForTesting(
299             std::move(owned_virtual_device_factory));
300     virtual_device_factory->SetTransport(
301         device::FidoTransportProtocol::kInternal);
302     virtual_device_factory->SetSupportedProtocol(
303         device::ProtocolVersion::kCtap2);
304     virtual_device_factory->mutable_state()->fingerprints_enrolled = true;
305 
306     // Currently this only supports tests relying on user-verifying platform
307     // authenticators.
308     device::VirtualCtap2Device::Config config;
309     config.is_platform_authenticator = true;
310     config.internal_uv_support = true;
311     virtual_device_factory->SetCtap2Config(config);
312   }
313 
GetDefaultIconURL()314   const std::string GetDefaultIconURL() {
315     return https_server()->GetURL("a.com", "/icon.png").spec();
316   }
317 };
318 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,CreatePaymentCredential)319 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
320                        CreatePaymentCredential) {
321   base::HistogramTester histogram_tester;
322   ReplaceFidoDiscoveryFactory();
323   NavigateTo("a.com", "/secure_payment_confirmation.html");
324 
325   EXPECT_EQ("OK",
326             content::EvalJs(GetActiveWebContents(),
327                             content::JsReplace("createPaymentCredential($1)",
328                                                GetDefaultIconURL())));
329 
330   // Verify that credential id size gets recorded.
331   histogram_tester.ExpectTotalCount(
332       "PaymentRequest.SecurePaymentConfirmationCredentialIdSizeInBytes", 1U);
333 }
334 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,LookupPaymentCredential)335 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
336                        LookupPaymentCredential) {
337   ReplaceFidoDiscoveryFactory();
338   NavigateTo("a.com", "/secure_payment_confirmation.html");
339   std::string credentialIdentifier =
340       content::EvalJs(
341           GetActiveWebContents(),
342           content::JsReplace("createCredentialAndReturnItsIdentifier($1)",
343                              GetDefaultIconURL()))
344           .ExtractString();
345 
346   // Cross the origin boundary.
347   NavigateTo("b.com", "/secure_payment_confirmation.html");
348   test_controller()->SetHasAuthenticator(true);
349   ResetEventWaiterForSingleEvent(TestEvent::kAppListReady);
350 
351   // ExecJs starts executing JavaScript and immediately returns, not waiting for
352   // any promise to return.
353   EXPECT_TRUE(content::ExecJs(
354       GetActiveWebContents(),
355       content::JsReplace("getSecurePaymentConfirmationStatus($1)",
356                          credentialIdentifier)));
357 
358   WaitForObservedEvent();
359   ASSERT_FALSE(test_controller()->app_descriptions().empty());
360   EXPECT_EQ(1u, test_controller()->app_descriptions().size());
361   EXPECT_EQ("display_name_for_instrument",
362             test_controller()->app_descriptions().front().label);
363 }
364 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,ConfirmPaymentInCrossOriginIframe)365 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
366                        ConfirmPaymentInCrossOriginIframe) {
367   NavigateTo("a.com", "/secure_payment_confirmation.html");
368   ReplaceFidoDiscoveryFactory();
369   std::string credentialIdentifier =
370       content::EvalJs(
371           GetActiveWebContents(),
372           content::JsReplace("createCredentialAndReturnItsIdentifier($1)",
373                              GetDefaultIconURL()))
374           .ExtractString();
375 
376   NavigateTo("b.com", "/iframe_poster.html");
377   test_controller()->SetHasAuthenticator(true);
378   confirm_payment_ = true;
379   // EvalJs waits for JavaScript promise to resolve.
380   EXPECT_EQ(
381       "success",
382       content::EvalJs(
383           GetActiveWebContents(),
384           content::JsReplace(
385               "postToIframe($1, $2);",
386               https_server()->GetURL("c.com", "/iframe_receiver.html").spec(),
387               credentialIdentifier)));
388 }
389 
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,ChallengeIsReturned)390 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
391                        ChallengeIsReturned) {
392   NavigateTo("a.com", "/secure_payment_confirmation.html");
393   ReplaceFidoDiscoveryFactory();
394   std::string credentialIdentifier =
395       content::EvalJs(
396           GetActiveWebContents(),
397           content::JsReplace("createCredentialAndReturnItsIdentifier($1)",
398                              GetDefaultIconURL()))
399           .ExtractString();
400 
401   NavigateTo("b.com", "/get_challenge.html");
402   test_controller()->SetHasAuthenticator(true);
403   confirm_payment_ = true;
404 
405   // Strip the trailing slash ("/") from the merchant origin in serialization to
406   // match the implementation behavior.
407   std::string merchant_origin = https_server()->GetURL("b.com", "/").spec();
408   ASSERT_EQ('/', merchant_origin[merchant_origin.length() - 1]);
409   merchant_origin = merchant_origin.substr(0, merchant_origin.length() - 1);
410   ASSERT_NE('/', merchant_origin[merchant_origin.length() - 1]);
411 
412   // EvalJs waits for JavaScript promise to resolve.
413   // The `networkData` field is the base64 encoding of 'hello world', which is
414   // set in `get_challenge.js`.
415   EXPECT_EQ("{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin +
416                 "\",\"total\":{\"currency\":\"USD\"," +
417                 "\"value\":\"0.01\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}",
418             content::EvalJs(GetActiveWebContents(),
419                             content::JsReplace("getChallenge($1, $2);",
420                                                credentialIdentifier, "0.01")));
421 
422   // Verify that passing a promise into PaymentRequest.show() that updates the
423   // `total` price will result in the challenge price being set only after the
424   // promise resolves with the finalized price.
425   EXPECT_EQ("{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin +
426                 "\",\"total\":{\"currency\":\"USD\"," +
427                 "\"value\":\"0.02\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}",
428             content::EvalJs(
429                 GetActiveWebContents(),
430                 content::JsReplace("getChallengeWithShowPromise($1, $2, $3);",
431                                    credentialIdentifier, "0.01", "0.02")));
432 
433   // Verify that the returned challenge correctly reflects the modified amount.
434   EXPECT_EQ(
435       "{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin +
436           "\",\"total\":{\"currency\":\"USD\"," +
437           "\"value\":\"0.03\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}",
438       content::EvalJs(GetActiveWebContents(),
439                       content::JsReplace("getChallengeWithModifier($1, $2);",
440                                          credentialIdentifier, "0.03")));
441 
442   // Verify that the returned challenge correctly reflects the modified amount
443   // that is set when the promised passed into PaymentRequest.show() resolves.
444   EXPECT_EQ(
445       "{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin +
446           "\",\"total\":{\"currency\":\"USD\"," +
447           "\"value\":\"0.04\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}",
448       content::EvalJs(
449           GetActiveWebContents(),
450           content::JsReplace("getChallengeWithModifierAndShowPromise($1, $2);",
451                              credentialIdentifier, "0.04")));
452 }
453 #endif  // !defined(OS_ANDROID)
454 
455 }  // namespace
456 }  // namespace payments
457