Commit cbdfcd12 authored by Liquan (Max) Gu's avatar Liquan (Max) Gu Committed by Commit Bot

[Payment] Enforce full delegation (behind a flag)

Context:
When the merchant send a PaymentRequest to the browser, the merchant
specifies what additional information (shipping, payer name/email/phone)
it requests. On the payment apps side, payment apps specify which
delegations they support. The browser as a middleman compares the
requested information from the merchant and the supported delegations
from the payment apps and decides which payment app can supports all
requested delegations (aka full delegation), which app can support only
part of the delegations (aka partial delegation).

Before:
For those partial delegations, the browser used to prompt users to
input shipping addresses or contact info.

After:
* When the feature EnforceFullDelegation is enabled, for those
apps with partial delegations, the browser will treat them as invalid
apps - hiding them from the payment app list in the payment sheet. This
applies for both Android payment apps and sw payment apps. For Android
payment apps, an error will be logged in logcat; for SW payment apps,
no error message would be logged yet (crbug.com/1100656).
* Although the flag is default to disabled, the Android Apps that
support the play billing method will be treated like the full
delegation is enforced.
* When disabled, the old behaviour remains.

Affected: Android Payment Apps, Service Worker Payment Apps

Change:
* Add a flag: EnforceFullDelegation (default to disabled)
* payment apps with the app store billing methods bypasses the flag
with the enforcement behaviours.
* Not to show a Android or SW Payment Apps in the payment sheet when
the app does not support full delegation.

Bug: 1095821, 1103695

Change-Id: I99023ed3383b53b5e3c006925b3f4cd032808e0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2260956Reviewed-by: default avatarSahel Sharify <sahel@chromium.org>
Reviewed-by: default avatarAvi Drissman <avi@chromium.org>
Commit-Queue: Liquan (Max) Gu <maxlg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#790066}
parent d948af6b
......@@ -16,6 +16,7 @@ import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.payments.PaymentManifestVerifier.ManifestVerifyCallback;
import org.chromium.components.payments.ErrorStrings;
import org.chromium.components.payments.MethodStrings;
import org.chromium.components.payments.PackageManagerDelegate;
import org.chromium.components.payments.PaymentFeatureList;
......@@ -629,6 +630,20 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
*/
private void onValidPaymentAppForPaymentMethodName(ResolveInfo resolveInfo, String methodName) {
String packageName = resolveInfo.activityInfo.packageName;
SupportedDelegations appSupportedDelegations =
getAppsSupportedDelegations(resolveInfo.activityInfo);
// Allow-lists the Play Billing method for this feature in order for the Play Billing case
// to skip the sheet in this case.
if (PaymentFeatureList.isEnabled(PaymentFeatureList.ENFORCE_FULL_DELEGATION)
|| methodName.equals(MethodStrings.GOOGLE_PLAY_BILLING)) {
if (!appSupportedDelegations.providesAll(
mFactoryDelegate.getParams().getPaymentOptions())) {
Log.e(TAG, ErrorStrings.SKIP_APP_FOR_PARTIAL_DELEGATION.replace("$1", packageName));
return;
}
}
AndroidPaymentApp app = mValidApps.get(packageName);
if (app == null) {
CharSequence label = mPackageManagerDelegate.getAppLabel(resolveInfo);
......@@ -648,7 +663,7 @@ public class AndroidPaymentAppFinder implements ManifestVerifyCallback {
packageName, resolveInfo.activityInfo.name,
mIsReadyToPayServices.get(packageName), label.toString(),
mPackageManagerDelegate.getAppIcon(resolveInfo), mIsIncognito,
webAppIdCanDeduped, getAppsSupportedDelegations(resolveInfo.activityInfo));
webAppIdCanDeduped, appSupportedDelegations);
mValidApps.put(packageName, app);
}
......
......@@ -12,6 +12,7 @@ import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.payments.mojom.PaymentDetailsModifier;
import org.chromium.payments.mojom.PaymentMethodData;
import org.chromium.payments.mojom.PaymentOptions;
import org.chromium.url.Origin;
import java.util.Map;
......@@ -92,8 +93,10 @@ public interface PaymentAppFactoryParams {
return null;
}
/** @return The currency of the total amount. Should not be null. */
default String getTotalAmountCurrency() {
/**
* @return The PaymentOptions of the payment request.
*/
default PaymentOptions getPaymentOptions() {
return null;
}
......
......@@ -2619,14 +2619,13 @@ public class PaymentRequestImpl
// PaymentAppFactoryParams implementation.
@Override
public String getTotalAmountCurrency() {
return mRawTotal.amount.currency;
public boolean requestShippingOrPayerContact() {
return mRequestShipping || mRequestPayerName || mRequestPayerPhone || mRequestPayerEmail;
}
// PaymentAppFactoryParams implementation.
@Override
public boolean requestShippingOrPayerContact() {
return mRequestShipping || mRequestPayerName || mRequestPayerPhone || mRequestPayerEmail;
public PaymentOptions getPaymentOptions() {
return mPaymentOptions;
}
// PaymentAppFactoryParams implementation.
......
......@@ -32,6 +32,15 @@ public class SupportedDelegations {
mPayerEmail = false;
}
public boolean providesAll(org.chromium.payments.mojom.PaymentOptions options) {
if (options == null) return true;
if (options.requestShipping && !mShippingAddress) return false;
if (options.requestPayerName && !mPayerName) return false;
if (options.requestPayerPhone && !mPayerPhone) return false;
if (options.requestPayerEmail && !mPayerEmail) return false;
return true;
}
public boolean getShippingAddress() {
return mShippingAddress;
}
......
......@@ -67,7 +67,7 @@ public class AndroidPaymentAppFinderTest
private GURL mTestServerUrl;
/**
* @param uri The URL of the test server.
* @param url The URL of the test server.
*/
/* package */ void setTestServerUrl(GURL url) {
assert mTestServerUrl == null : "Test server URL should be set only once";
......
......@@ -17,6 +17,7 @@ source_set("browsertests") {
"payment_handler_capabilities_browsertest.cc",
"payment_handler_change_shipping_address_option_browsertest.cc",
"payment_handler_enable_delegations_browsertest.cc",
"payment_handler_enforce_full_delegation_browsertest.cc",
"payment_handler_exploit_browsertest.cc",
"payment_handler_just_in_time_installation_browsertest.cc",
"payment_request_app_store_billing_browsertest.cc",
......
// 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 "base/macros.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/test/payments/payment_request_platform_browsertest_base.h"
#include "components/payments/core/features.h"
#include "content/public/test/browser_test.h"
namespace payments {
namespace {
enum EnforceFullDelegationFlag {
ENABLED,
DISABLED,
};
class PaymentHandlerEnforceFullDelegationTest
: public PaymentRequestPlatformBrowserTestBase,
public testing::WithParamInterface<EnforceFullDelegationFlag> {
public:
PaymentHandlerEnforceFullDelegationTest() {
if (GetParam() == ENABLED) {
scoped_feature_list_.InitAndEnableFeature(
payments::features::kEnforceFullDelegation);
} else {
scoped_feature_list_.InitAndDisableFeature(
payments::features::kEnforceFullDelegation);
}
}
~PaymentHandlerEnforceFullDelegationTest() override = default;
void SetUpOnMainThread() override {
PaymentRequestPlatformBrowserTestBase::SetUpOnMainThread();
NavigateTo("/enforce_full_delegation.com/index.html");
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_P(PaymentHandlerEnforceFullDelegationTest,
ShowPaymentSheetWhenEnabledRejectWhenDisabled) {
std::string expected = "success";
EXPECT_EQ(expected, content::EvalJs(GetActiveWebContents(), "install()"));
EXPECT_EQ(expected, content::EvalJs(GetActiveWebContents(),
"addDefaultSupportedMethod()"));
EXPECT_EQ(expected,
content::EvalJs(GetActiveWebContents(), "enableDelegations([])"));
EXPECT_EQ(expected,
content::EvalJs(
GetActiveWebContents(),
"createPaymentRequestWithOptions({requestPayerName: true})"));
if (GetParam() == ENABLED) {
ResetEventWaiterForSingleEvent(TestEvent::kNotSupportedError);
} else {
ResetEventWaiterForSingleEvent(TestEvent::kAppListReady);
}
EXPECT_EQ(expected, content::EvalJs(GetActiveWebContents(), "show()"));
WaitForObservedEvent();
if (GetParam() == ENABLED) {
EXPECT_GE(1u, test_controller()->app_descriptions().size());
}
}
// Run all tests with both values for
// features::kEnforceFullDelegation.
INSTANTIATE_TEST_SUITE_P(All,
PaymentHandlerEnforceFullDelegationTest,
::testing::Values(ENABLED, DISABLED));
} // namespace
} // namespace payments
......@@ -17,6 +17,7 @@ import org.chromium.base.annotations.NativeMethods;
public class PaymentFeatureList {
/** Alphabetical: */
public static final String ANDROID_APP_PAYMENT_UPDATE_EVENTS = "AndroidAppPaymentUpdateEvents";
public static final String ENFORCE_FULL_DELEGATION = "EnforceFullDelegation";
public static final String PAYMENT_REQUEST_SKIP_TO_GPAY = "PaymentRequestSkipToGPay";
public static final String PAYMENT_REQUEST_SKIP_TO_GPAY_IF_NO_CARD =
"PaymentRequestSkipToGPayIfNoCard";
......
......@@ -25,6 +25,7 @@ const base::Feature* kFeaturesExposedToJava[] = {
&::features::kWebPaymentsMinimalUI,
&features::kAlwaysAllowJustInTimePaymentApp,
&features::kAppStoreBillingDebug,
&features::kEnforceFullDelegation,
&features::kPaymentRequestSkipToGPay,
&features::kPaymentRequestSkipToGPayIfNoCard,
&features::kReturnGooglePayInBasicCard,
......
......@@ -119,6 +119,8 @@ class PaymentRequestSpec : public PaymentOptionsProvider,
bool request_payer_email() const override;
PaymentShippingType shipping_type() const override;
const mojom::PaymentOptionsPtr& payment_options() const { return options_; }
// Returns the query to be used for the quota on hasEnrolledInstrument()
// calls. Generally this returns the payment method identifiers and their
// corresponding data. However, in the case of basic-card with
......
......@@ -13,6 +13,10 @@
#include "components/payments/content/payment_manifest_web_data_service.h"
#include "components/payments/content/service_worker_payment_app.h"
#include "components/payments/content/service_worker_payment_app_finder.h"
#include "components/payments/core/features.h"
#include "components/payments/core/method_strings.h"
#include "content/public/browser/stored_payment_app.h"
#include "content/public/browser/supported_delegations.h"
#include "content/public/browser/web_contents.h"
namespace payments {
......@@ -50,15 +54,22 @@ class ServiceWorkerPaymentAppCreator {
if (!error_message.empty())
delegate_->OnPaymentAppCreationError(error_message);
number_of_pending_sw_payment_apps_ = apps.size() + installable_apps.size();
if (number_of_pending_sw_payment_apps_ == 0U) {
FinishAndCleanup();
return;
}
base::RepeatingClosure show_processing_spinner = base::BindRepeating(
&PaymentAppFactory::Delegate::ShowProcessingSpinner, delegate_);
for (auto& installed_app : apps) {
std::vector<std::string> enabled_methods =
installed_app.second->enabled_methods;
bool has_app_store_billing_method =
enabled_methods.end() != std::find(enabled_methods.begin(),
enabled_methods.end(),
methods::kGooglePlayBilling);
if (ShouldSkipAppForPartialDelegation(
installed_app.second->supported_delegations, delegate_,
has_app_store_billing_method)) {
// TODO(crbug.com/1100656): give the developer an error message.
continue;
}
auto app = std::make_unique<ServiceWorkerPaymentApp>(
delegate_->GetWebContents(), delegate_->GetTopOrigin(),
delegate_->GetFrameOrigin(), delegate_->GetSpec(),
......@@ -69,9 +80,18 @@ class ServiceWorkerPaymentAppCreator {
weak_ptr_factory_.GetWeakPtr()));
PaymentApp* raw_payment_app_pointer = app.get();
available_apps_[raw_payment_app_pointer] = std::move(app);
number_of_pending_sw_payment_apps_++;
}
for (auto& installable_app : installable_apps) {
bool is_app_store_billing_method =
installable_app.first.spec() == methods::kGooglePlayBilling;
if (ShouldSkipAppForPartialDelegation(
installable_app.second->supported_delegations, delegate_,
is_app_store_billing_method)) {
// TODO(crbug.com/1100656): give the developer an error message.
continue;
}
auto app = std::make_unique<ServiceWorkerPaymentApp>(
delegate_->GetWebContents(), delegate_->GetTopOrigin(),
delegate_->GetFrameOrigin(), delegate_->GetSpec(),
......@@ -82,7 +102,21 @@ class ServiceWorkerPaymentAppCreator {
weak_ptr_factory_.GetWeakPtr()));
PaymentApp* raw_payment_app_pointer = app.get();
available_apps_[raw_payment_app_pointer] = std::move(app);
number_of_pending_sw_payment_apps_++;
}
if (number_of_pending_sw_payment_apps_ == 0U)
FinishAndCleanup();
}
bool ShouldSkipAppForPartialDelegation(
const content::SupportedDelegations& supported_delegations,
const base::WeakPtr<PaymentAppFactory::Delegate>& delegate,
bool has_app_store_billing_method) const {
return (base::FeatureList::IsEnabled(features::kEnforceFullDelegation) ||
has_app_store_billing_method) &&
!supported_delegations.ProvidesAll(
delegate->GetSpec()->payment_options());
}
base::WeakPtr<ServiceWorkerPaymentAppCreator> GetWeakPtr() {
......
......@@ -32,6 +32,7 @@ const char kProhibitedOriginOrInvalidSslExplanation[] = "No UI will be shown. Ca
const char kShippingAddressInvalid[] = "Payment app returned invalid shipping address in response.";
const char kShippingOptionEmpty[] = "Payment app returned invalid response. Missing field \"shipping option\".";
const char kShippingOptionIdRequired[] = "Shipping option identifier required.";
const char kSkipAppForPartialDelegation[] = "Skipping \"$1\" because it does not provide all of the requested PaymentOptions.";
const char kStrictBasicCardShowReject[] = "User does not have valid information on file.";
const char kTotalRequired[] = "Total required.";
const char kUserCancelled[] = "User closed the Payment Request UI.";
......
......@@ -83,6 +83,10 @@ extern const char kShippingOptionEmpty[];
// Used when non-empty "shippingOptionId": "" is required, but not provided.
extern const char kShippingOptionIdRequired[];
// Used when an app is skipped for supporting only part of the requested payment
// options.
extern const char kSkipAppForPartialDelegation[];
// Used when rejecting show() with NotSupportedError, because the user did not
// have all valid autofill data.
extern const char kStrictBasicCardShowReject[];
......
......@@ -69,5 +69,7 @@ const base::Feature kAllowJITInstallationWhenAppIconIsMissing{
const base::Feature kPaymentHandlerLockIcon{"PaymentHandlerLockIcon",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kEnforceFullDelegation{"EnforceFullDelegation",
base::FEATURE_DISABLED_BY_DEFAULT};
} // namespace features
} // namespace payments
......@@ -80,6 +80,9 @@ extern const base::Feature kAllowJITInstallationWhenAppIconIsMissing;
// allowed inside the payment handler.
extern const base::Feature kPaymentHandlerLockIcon;
// Used to reject the apps with partial delegation.
extern const base::Feature kEnforceFullDelegation;
} // namespace features
} // namespace payments
......
/*
* 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.
*/
let methodName;
self.addEventListener('canmakepayment', (evt) => {
evt.respondWith(true);
});
self.addEventListener('paymentrequest', (evt) => {
methodName = evt.methodData[0].supportedMethods;
evt.respondWith(new Promise((responder) => {
const payerName = (evt.paymentOptions &&
evt.paymentOptions.requestPayerName) ? 'John Smith' : '';
responder({methodName, details: {status: 'success'}, payerName});
}));
});
{
"name": "MaxPay",
"icons": [{
"src": "../icon.png",
"sizes": "50x50",
"type": "image/jpg"
}]
}
<!DOCTYPE html>
<!--
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.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="manifest" href="app_manifest.json">
<title>Enforce Full Delegation Tests</title>
</head>
<style>
pre {
white-space: initial;
}
</style>
<body>
<div><button onclick="install()">install</button>
<button onclick="uninstall()">uninstall</button></div>
<div><button onclick="enableDelegations(['payerName'])">enableDelegations(['payerName'])</button></div>
<div><button onclick="addSupportedMethod('https://play.google.com/billing')">addSupportedMethod('https://play.google.com/billing')</button>
<button onclick="addDefaultSupportedMethod()">addDefaultSupportedMethod()</button></div>
<div><button onclick="createPaymentRequestWithOptions({requestPayerName: true})">createPaymentRequestWithOptions({requestPayerName: true})</button></div>
<div><button onclick="show()">show</button></div>
<pre id="msg"></pre>
</body>
<script src="index.js"></script>
<script src="util.js"></script>
</html>
/*
* 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.
*/
const methodName = window.location.origin + '/method_manifest.json';
const swSrcUrl = 'app.js';
let request;
let supportedInstruments = [];
/**
* Install a payment app.
* @return {string} - a message indicating whether the installation is
* successful.
*/
async function install() { // eslint-disable-line no-unused-vars
info('installing');
await navigator.serviceWorker.register(swSrcUrl);
const registration = await navigator.serviceWorker.ready;
if (!registration.paymentManager) {
return 'No payment handler capability in this browser. Is' +
'chrome://flags/#service-worker-payment-apps enabled?';
}
if (!registration.paymentManager.instruments) {
return 'Payment handler is not fully implemented. ' +
'Cannot set the instruments.';
}
await registration.paymentManager.instruments.set('instrument-key', {
name: 'MaxPay',
method: methodName,
});
return 'success';
}
/**
* Uninstall the payment handler.
* @return {string} - the message about the uninstallation result.
*/
async function uninstall() { // eslint-disable-line no-unused-vars
info('uninstall');
let registration = await navigator.serviceWorker.getRegistration(swSrcUrl);
if (!registration) {
return 'The Payment handler has not been installed yet.';
}
await registration.unregister();
return 'success';
}
/**
* Delegates handling of the provided options to the payment handler.
* @param {Array<string>} delegations The list of payment options to delegate.
* @return {string} The 'success' or error message.
*/
async function enableDelegations(delegations) { // eslint-disable-line no-unused-vars, max-len
info('enableDelegations: ' + JSON.stringify(delegations));
try {
await navigator.serviceWorker.ready;
let registration =
await navigator.serviceWorker.getRegistration(swSrcUrl);
if (!registration) {
return 'The payment handler is not installed.';
}
if (!registration.paymentManager) {
return 'PaymentManager API not found.';
}
if (!registration.paymentManager.enableDelegations) {
return 'PaymentManager does not support enableDelegations method';
}
await registration.paymentManager.enableDelegations(delegations);
return 'success';
} catch (e) {
return e.toString();
}
}
/**
* Add a payment method to the payment request.
* @param {string} method - the payment method.
* @return {string} - a message indicating whether the operation is successful.
*/
function addSupportedMethod(method) { // eslint-disable-line no-unused-vars
info('addSupportedMethod: ' + method);
supportedInstruments.push({
supportedMethods: [
method,
],
});
return 'success';
}
/**
* Add the payment method of this test to the payment request.
* @return {string} - a message indicating whether the operation is successful.
*/
function addDefaultSupportedMethod() { // eslint-disable-line no-unused-vars
return addSupportedMethod(methodName);
}
/**
* Create a PaymentRequest.
* @param {PaymentOptions} options - the payment options.
* @return {string} - a message indicating whether the operation is successful.
*/
function createPaymentRequestWithOptions(options) { // eslint-disable-line no-unused-vars, max-len
info('createPaymentRequestWithOptions: ' +
JSON.stringify(supportedInstruments) + ', ' + JSON.stringify(options));
const details = {
total: {
label: 'Donation',
amount: {
currency: 'USD',
value: '55.00',
},
},
};
request = new PaymentRequest(supportedInstruments, details, options);
return 'success';
}
/**
* Show the payment sheet. This method is not blocking.
* @return {string} - a message indicating whether the operation is successful.
*/
function show() { // eslint-disable-line no-unused-vars
info('show');
request.show().then((response) => {
info('complete: status=' + response.details.status + ', payerName='
+ response.payerName);
response.complete(response.details.status).then(() => {
info('complete success');
}).catch((e) => {
info('complete error: ' + e);
}).finally(() => {
info('show finished');
});
}).catch((e) => {
info('show error: ' + e);
});
info('show on going');
return 'success';
}
{
"default_applications": ['app.js'],
"supported_origins": []
}
/*
* 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.
*/
/**
* Prints the given informational message.
* @param {string} msg - The information message to print.
*/
function info(msg) { // eslint-disable-line no-unused-vars
let element = document.createElement('pre');
element.innerHTML = msg;
element.className = 'info';
document.getElementById('msg').appendChild(element);
}
......@@ -10,4 +10,19 @@ SupportedDelegations::SupportedDelegations() = default;
SupportedDelegations::~SupportedDelegations() = default;
} // namespace content
\ No newline at end of file
bool SupportedDelegations::ProvidesAll(
const payments::mojom::PaymentOptionsPtr& payment_options) const {
if (!payment_options)
return true;
if (payment_options->request_shipping && !shipping_address)
return false;
if (payment_options->request_payer_name && !payer_name)
return false;
if (payment_options->request_payer_phone && !payer_phone)
return false;
if (payment_options->request_payer_email && !payer_email)
return false;
return true;
}
} // namespace content
......@@ -6,6 +6,7 @@
#define CONTENT_PUBLIC_BROWSER_SUPPORTED_DELEGATIONS_H_
#include "content/common/content_export.h"
#include "third_party/blink/public/mojom/payments/payment_request.mojom.h"
namespace content {
......@@ -18,6 +19,9 @@ struct CONTENT_EXPORT SupportedDelegations {
bool payer_name = false;
bool payer_phone = false;
bool payer_email = false;
bool ProvidesAll(
const payments::mojom::PaymentOptionsPtr& payment_options) const;
};
} // namespace content
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment