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