Commit 54e0d1eb authored by Rouslan Solomakhin's avatar Rouslan Solomakhin Committed by Commit Bot

[Web Payment] Enable secure payment confirmation from an iframe.

Before this patch, invoking PaymentRequest API with secure payment
confirmation method in a cross-origin iframe would display the secure
payment confirmation dialog UI, but proceeding to verify the payment
with WebAuthn would return NOT_ALLOWED_ERROR.

This patch creates autofill::InternalAuthenticator on the top-level
RenderFrameHost of the WebContents instead of the RenderFrameHost of the
exact iframe that invoked PaymentRequest API.

After this patch, it is possible to use secure payment confirmation
method in a cross-origin iframe.

Bug: 1123217
Change-Id: Ifc3ec58a5bb57a2544c51d15aaa1035b15da6190
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2383452Reviewed-by: default avatarKen Buchanan <kenrb@chromium.org>
Reviewed-by: default avatarNick Burris <nburris@chromium.org>
Commit-Queue: Rouslan Solomakhin <rouslan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#802974}
parent 1b6200aa
......@@ -183,9 +183,14 @@ bool ChromePaymentRequestDelegate::IsBrowserWindowActive() const {
}
std::unique_ptr<autofill::InternalAuthenticator>
ChromePaymentRequestDelegate::CreateInternalAuthenticator(
content::RenderFrameHost* rfh) const {
return std::make_unique<content::InternalAuthenticatorImpl>(rfh);
ChromePaymentRequestDelegate::CreateInternalAuthenticator() const {
// This authenticator can be used in a cross-origin iframe only if the
// top-level frame allowed it with Feature Policy, e.g., with allow="payment"
// iframe attribute. The secure payment confirmation dialog displays the
// top-level origin in its UI before the user can click on the [Verify] button
// to invoke this authenticator.
return std::make_unique<content::InternalAuthenticatorImpl>(
web_contents_->GetMainFrame());
}
scoped_refptr<PaymentManifestWebDataService>
......@@ -244,4 +249,8 @@ std::string ChromePaymentRequestDelegate::GetTwaPackageName() const {
#endif // OS_CHROMEOS
}
PaymentRequestDialog* ChromePaymentRequestDelegate::GetDialogForTesting() {
return shown_dialog_.get();
}
} // namespace payments
......@@ -48,8 +48,8 @@ class ChromePaymentRequestDelegate : public ContentPaymentRequestDelegate {
bool IsBrowserWindowActive() const override;
// ContentPaymentRequestDelegate:
std::unique_ptr<autofill::InternalAuthenticator> CreateInternalAuthenticator(
content::RenderFrameHost* rfh) const override;
std::unique_ptr<autofill::InternalAuthenticator> CreateInternalAuthenticator()
const override;
scoped_refptr<PaymentManifestWebDataService>
GetPaymentManifestWebDataService() const override;
PaymentRequestDisplayManager* GetDisplayManager() override;
......@@ -60,6 +60,7 @@ class ChromePaymentRequestDelegate : public ContentPaymentRequestDelegate {
std::string GetInvalidSslCertificateErrorMessage() override;
bool SkipUiForBasicCard() const override;
std::string GetTwaPackageName() const override;
PaymentRequestDialog* GetDialogForTesting() override;
protected:
// Reference to the dialog so that we can satisfy calls to CloseDialog(). This
......
......@@ -116,7 +116,14 @@ class SecurePaymentConfirmationTest
databse_write_responded_ = true;
}
void OnAppListReady() override {
PaymentRequestPlatformBrowserTestBase::OnAppListReady();
if (confirm_payment_)
ASSERT_TRUE(test_controller()->ConfirmPayment());
}
bool databse_write_responded_ = false;
bool confirm_payment_ = false;
};
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, NoAuthenticator) {
......@@ -327,9 +334,13 @@ class SecurePaymentConfirmationCreationTest
// that still needs to be investigated.
#define MAYBE_CreatePaymentCredential DISABLED_CreatePaymentCredential
#define MAYBE_LookupPaymentCredential DISABLED_LookupPaymentCredential
#define MAYBE_ConfirmPaymentInCrossOriginIframe \
DISABLED_ConfirmPaymentInCrossOriginIframe
#else
#define MAYBE_CreatePaymentCredential CreatePaymentCredential
#define MAYBE_LookupPaymentCredential LookupPaymentCredential
#define MAYBE_ConfirmPaymentInCrossOriginIframe \
ConfirmPaymentInCrossOriginIframe
#endif
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
MAYBE_CreatePaymentCredential) {
......@@ -373,6 +384,8 @@ IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
"}}])",
credentialIdentifier);
// Cross the origin boundary.
NavigateTo("b.com", "/payment_handler_status.html");
test_controller()->SetHasAuthenticator(true);
ResetEventWaiterForSingleEvent(TestEvent::kAppListReady);
......@@ -386,6 +399,37 @@ IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
EXPECT_EQ("display_name_for_instrument",
test_controller()->app_descriptions().front().label);
}
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
MAYBE_ConfirmPaymentInCrossOriginIframe) {
NavigateTo("a.com", "/payment_handler_status.html");
ReplaceFidoDiscoveryFactory();
std::string credentialIdentifier =
content::EvalJs(
GetActiveWebContents(),
base::StrCat(
{"async function createPaymentCredential() {",
getPaymentCreationOptions(GetDefaultIconURL()),
" const c = await navigator.credentials.create("
" {payment: PAYMENT_CREATION_OPTIONS});"
" return btoa(String.fromCharCode(...new Uint8Array(c.rawId)));"
"};"
"createPaymentCredential();"}))
.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)));
}
#endif // !defined(OS_ANDROID)
} // namespace
......
......@@ -212,6 +212,10 @@ void PaymentRequestDialogView::RetryDialog() {
}
}
void PaymentRequestDialogView::ConfirmPaymentForTesting() {
Pay();
}
void PaymentRequestDialogView::OnStartUpdating(
PaymentRequestSpec::UpdateReason reason) {
ShowProcessingSpinner();
......
......@@ -116,6 +116,7 @@ class PaymentRequestDialogView : public views::DialogDelegateView,
const GURL& url,
PaymentHandlerOpenWindowCallback callback) override;
void RetryDialog() override;
void ConfirmPaymentForTesting() override;
// PaymentRequestSpec::Observer:
void OnStartUpdating(PaymentRequestSpec::UpdateReason reason) override;
......
......@@ -9,6 +9,7 @@
#include <string>
#include <vector>
#include "base/memory/weak_ptr.h"
#include "build/build_config.h"
#if !defined(OS_ANDROID)
......@@ -23,6 +24,8 @@ class WebContents;
namespace payments {
class ContentPaymentRequestDelegate;
struct AppDescription {
std::string label;
std::string sublabel;
......@@ -82,6 +85,11 @@ class PaymentRequestTestController {
bool ClickPaymentHandlerSecurityIcon();
#endif
// Confirms payment in a browser payment sheet, be it either PAYMENT_REQUEST
// or SECURE_PAYMENT_CONFIRMATION type. Returns true if the dialog was
// available.
bool ConfirmPayment();
// Confirms payment in minimal UI. Returns true on success or if the minimal
// UI is not implemented on the current platform.
bool ConfirmMinimalUI();
......@@ -135,6 +143,8 @@ class PaymentRequestTestController {
class ObserverConverter;
std::unique_ptr<ObserverConverter> observer_converter_;
base::WeakPtr<ContentPaymentRequestDelegate> delegate_;
#endif
};
......
......@@ -5,6 +5,7 @@
#include "chrome/test/payments/payment_request_test_controller.h"
#include "base/bind.h"
#include "base/notreached.h"
#include "chrome/browser/android/background_task_scheduler/chrome_background_task_factory.h"
#include "chrome/test/payments/android/payment_request_test_bridge.h"
......@@ -23,6 +24,11 @@ bool PaymentRequestTestController::ClickPaymentHandlerSecurityIcon() {
return ClickPaymentHandlerSecurityIconForTest();
}
bool PaymentRequestTestController::ConfirmPayment() {
NOTIMPLEMENTED();
return false;
}
bool PaymentRequestTestController::ConfirmMinimalUI() {
return ConfirmMinimalUIForTest();
}
......
......@@ -54,6 +54,7 @@ class ChromePaymentRequestTestDelegate : public ChromePaymentRequestDelegate {
const std::string& twa_package_name,
bool has_authenticator)
: ChromePaymentRequestDelegate(web_contents),
web_contents_(web_contents),
is_off_the_record_(is_off_the_record),
valid_ssl_(valid_ssl),
prefs_(prefs),
......@@ -67,12 +68,14 @@ class ChromePaymentRequestTestDelegate : public ChromePaymentRequestDelegate {
PrefService* GetPrefService() override { return prefs_; }
bool IsBrowserWindowActive() const override { return true; }
std::string GetTwaPackageName() const override { return twa_package_name_; }
std::unique_ptr<autofill::InternalAuthenticator> CreateInternalAuthenticator(
content::RenderFrameHost* rfh) const override {
return std::make_unique<TestAuthenticator>(rfh, has_authenticator_);
std::unique_ptr<autofill::InternalAuthenticator> CreateInternalAuthenticator()
const override {
return std::make_unique<TestAuthenticator>(web_contents_->GetMainFrame(),
has_authenticator_);
}
private:
content::WebContents* web_contents_;
const bool is_off_the_record_;
const bool valid_ssl_;
PrefService* const prefs_;
......@@ -140,6 +143,18 @@ PaymentRequestTestController::GetPaymentHandlerWebContents() {
return nullptr;
}
bool PaymentRequestTestController::ConfirmPayment() {
if (!delegate_)
return false;
PaymentRequestDialog* dialog = delegate_->GetDialogForTesting();
if (!dialog)
return false;
dialog->ConfirmPaymentForTesting();
return true;
}
bool PaymentRequestTestController::ConfirmMinimalUI() {
// Desktop does not have a minimal UI.
return true;
......@@ -210,6 +225,7 @@ void PaymentRequestTestController::UpdateDelegateFactory() {
const std::string& twa_package_name, bool has_authenticator,
const std::string& twa_payment_app_method_name,
const std::string& twa_payment_app_response,
base::WeakPtr<ContentPaymentRequestDelegate>* delegate_weakptr,
mojo::PendingReceiver<payments::mojom::PaymentRequest> receiver,
content::RenderFrameHost* render_frame_host) {
content::WebContents* web_contents =
......@@ -218,6 +234,7 @@ void PaymentRequestTestController::UpdateDelegateFactory() {
auto delegate = std::make_unique<ChromePaymentRequestTestDelegate>(
web_contents, is_off_the_record, valid_ssl, prefs, twa_package_name,
has_authenticator);
*delegate_weakptr = delegate->GetContentWeakPtr();
PaymentRequestWebContentsManager* manager =
PaymentRequestWebContentsManager::GetOrCreateForWebContents(
web_contents);
......@@ -233,7 +250,7 @@ void PaymentRequestTestController::UpdateDelegateFactory() {
},
observer_converter_.get(), is_off_the_record_, valid_ssl_, prefs_.get(),
twa_package_name_, has_authenticator_, twa_payment_app_method_name_,
twa_payment_app_response_));
twa_payment_app_response_, &delegate_));
}
void PaymentRequestTestController::OnCanMakePaymentCalled() {
......
......@@ -88,6 +88,7 @@ static_library("content") {
sources += [ "secure_payment_confirmation_view_stub.cc" ]
} else {
sources += [
"content_payment_request_delegate.cc",
"content_payment_request_delegate.h",
"payment_credential.cc",
"payment_credential.h",
......@@ -173,8 +174,6 @@ source_set("unit_tests") {
"android_payment_app_unittest.cc",
"payment_method_manifest_table_unittest.cc",
"service_worker_payment_app_finder_unittest.cc",
"test_content_payment_request_delegate.cc",
"test_content_payment_request_delegate.h",
"web_app_manifest_section_table_unittest.cc",
]
......@@ -188,6 +187,8 @@ source_set("unit_tests") {
"secure_payment_confirmation_app_unittest.cc",
"secure_payment_confirmation_model_unittest.cc",
"service_worker_payment_app_unittest.cc",
"test_content_payment_request_delegate.cc",
"test_content_payment_request_delegate.h",
]
}
......
// 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 "components/payments/content/content_payment_request_delegate.h"
namespace payments {
ContentPaymentRequestDelegate::~ContentPaymentRequestDelegate() = default;
base::WeakPtr<ContentPaymentRequestDelegate>
ContentPaymentRequestDelegate::GetContentWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
ContentPaymentRequestDelegate::ContentPaymentRequestDelegate() = default;
} // namespace payments
......@@ -8,6 +8,7 @@
#include <memory>
#include <string>
#include "base/memory/weak_ptr.h"
#include "components/payments/content/payment_request_display_manager.h"
#include "components/payments/core/payment_request_delegate.h"
......@@ -18,24 +19,21 @@ namespace autofill {
class InternalAuthenticator;
} // namespace autofill
namespace content {
class RenderFrameHost;
} // namespace content
namespace payments {
class PaymentManifestWebDataService;
class PaymentRequestDialog;
class PaymentRequestDisplayManager;
// The delegate for PaymentRequest that can use content.
class ContentPaymentRequestDelegate : public PaymentRequestDelegate {
public:
~ContentPaymentRequestDelegate() override {}
~ContentPaymentRequestDelegate() override;
// Creates and returns an instance of the InternalAuthenticator interface for
// communication with WebAuthn.
virtual std::unique_ptr<autofill::InternalAuthenticator>
CreateInternalAuthenticator(content::RenderFrameHost* rfh) const = 0;
CreateInternalAuthenticator() const = 0;
// Returns the web data service for caching payment method manifests.
virtual scoped_refptr<PaymentManifestWebDataService>
......@@ -73,6 +71,17 @@ class ContentPaymentRequestDelegate : public PaymentRequestDelegate {
// Returns the Android package name of the Trusted Web Activity that invoked
// this browser, if any. Otherwise, an empty string.
virtual std::string GetTwaPackageName() const = 0;
virtual PaymentRequestDialog* GetDialogForTesting() = 0;
// Returns a weak pointer to this delegate.
base::WeakPtr<ContentPaymentRequestDelegate> GetContentWeakPtr();
protected:
ContentPaymentRequestDelegate();
private:
base::WeakPtrFactory<ContentPaymentRequestDelegate> weak_ptr_factory_{this};
};
} // namespace payments
......
......@@ -47,6 +47,9 @@ class PaymentRequestDialog {
virtual void ShowPaymentHandlerScreen(
const GURL& url,
PaymentHandlerOpenWindowCallback callback) = 0;
// Confirms payment. Used only in tests.
virtual void ConfirmPaymentForTesting() = 0;
};
} // namespace payments
......
......@@ -139,8 +139,7 @@ PaymentRequestState::GetMethodData() const {
std::unique_ptr<autofill::InternalAuthenticator>
PaymentRequestState::CreateInternalAuthenticator() const {
return GetPaymentRequestDelegate()->CreateInternalAuthenticator(
initiator_render_frame_host_);
return GetPaymentRequestDelegate()->CreateInternalAuthenticator();
}
scoped_refptr<PaymentManifestWebDataService>
......
......@@ -161,6 +161,10 @@ void SecurePaymentConfirmationController::ShowPaymentHandlerScreen(
NOTREACHED();
}
void SecurePaymentConfirmationController::ConfirmPaymentForTesting() {
OnConfirm();
}
void SecurePaymentConfirmationController::OnInitialized(
InitializationTask* initialization_task) {
if (--number_of_initialization_tasks_ == 0)
......
......@@ -44,6 +44,7 @@ class SecurePaymentConfirmationController
void ShowPaymentHandlerScreen(
const GURL& url,
PaymentHandlerOpenWindowCallback callback) override;
void ConfirmPaymentForTesting() override;
// InitializationTask::Observer:
void OnInitialized(InitializationTask* initialization_task) override;
......
......@@ -16,8 +16,7 @@ TestContentPaymentRequestDelegate::TestContentPaymentRequestDelegate(
TestContentPaymentRequestDelegate::~TestContentPaymentRequestDelegate() {}
std::unique_ptr<autofill::InternalAuthenticator>
TestContentPaymentRequestDelegate::CreateInternalAuthenticator(
content::RenderFrameHost* rfh) const {
TestContentPaymentRequestDelegate::CreateInternalAuthenticator() const {
return nullptr;
}
......@@ -63,6 +62,10 @@ std::string TestContentPaymentRequestDelegate::GetTwaPackageName() const {
return "";
}
PaymentRequestDialog* TestContentPaymentRequestDelegate::GetDialogForTesting() {
return nullptr;
}
autofill::PersonalDataManager*
TestContentPaymentRequestDelegate::GetPersonalDataManager() {
return core_delegate_.GetPersonalDataManager();
......
......@@ -22,8 +22,8 @@ class TestContentPaymentRequestDelegate : public ContentPaymentRequestDelegate {
~TestContentPaymentRequestDelegate() override;
// ContentPaymentRequestDelegate:
std::unique_ptr<autofill::InternalAuthenticator> CreateInternalAuthenticator(
content::RenderFrameHost* rfh) const override;
std::unique_ptr<autofill::InternalAuthenticator> CreateInternalAuthenticator()
const override;
scoped_refptr<PaymentManifestWebDataService>
GetPaymentManifestWebDataService() const override;
PaymentRequestDisplayManager* GetDisplayManager() override;
......@@ -35,6 +35,7 @@ class TestContentPaymentRequestDelegate : public ContentPaymentRequestDelegate {
bool IsBrowserWindowActive() const override;
bool SkipUiForBasicCard() const override;
std::string GetTwaPackageName() const override;
PaymentRequestDialog* GetDialogForTesting() override;
autofill::PersonalDataManager* GetPersonalDataManager() override;
const std::string& GetApplicationLocale() const override;
bool IsOffTheRecord() const override;
......
<!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, maximum-scale=1, minimum-scale=1">
<title>Iframe Poster</title>
</head>
<body>
<iframe id="iframe" allow="payment"></iframe>
<script src="iframe_poster.js"></script>
</body>
</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.
*/
/**
* Opens the given `url` in an iframe, posts the `msg` to it, and waits for the
* iframe to return a response.
* @param {string} url - The url to open in the iframe.
* @param {object} msg - The message to post to the iframe.
* @return {Promise<object>} - What the iframe returned.
*/
async function postToIframe(url, msg) { // eslint-disable-line no-unused-vars
let resolveFunction = null;
const promise = new Promise((resolve) => {
resolveFunction = resolve;
});
window.onmessage = (e) => {
resolveFunction(e.data);
};
const iframe = document.getElementById('iframe');
iframe.onload = (e) => {
iframe.contentWindow.postMessage(msg, url);
};
iframe.src = url;
return promise;
}
<!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, maximum-scale=1, minimum-scale=1">
<title>Iframe Receiver</title>
</head>
<body>
<script src="iframe_receiver.js"></script>
</body>
</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.
*/
window.onmessage = (e) => {
requestPayment(e.data).then((result) => {
e.source.postMessage(result, e.origin);
}).catch((error) => {
e.source.postMessage(error, e.origin);
});
};
/**
* Requests a secure payment confirmation payment for the given credential
* identifier.
* @param {string} credentialId - The base64 encoded identifier of the
* credential to use for payment.
* @return {Promise<object>} - Either the string 'success' or an error message.
*/
async function requestPayment(credentialId) {
try {
const request = new PaymentRequest(
[{supportedMethods: 'secure-payment-confirmation',
data: {
action: 'authenticate',
credentialIds: [Uint8Array.from(atob(credentialId),
(b) => b.charCodeAt(0))],
networkData: new TextEncoder().encode('hello world'),
timeout: 6000,
fallbackUrl: 'https://fallback.example/url',
}}],
{total: {label: 'TEST', amount: {currency: 'USD', value: '0.01'}}});
const response = await request.show();
await response.complete();
return 'success';
} catch (e) {
return e.message;
}
}
......@@ -66,3 +66,28 @@ async function getStatusForMethodDataAfterCanMakePayment(
return e.message;
}
}
/**
* Returns the status field from the payment handler's response for given
* payment method data. Passes a promise into PaymentRequest.show() to delay
* initialization by 1 second.
* @param {array<PaymentMethodData>} methodData - The method data to use.
* @return {string} - The status field or error message.
*/
async function getStatusForMethodDataWithShowPromise(methodData) { // eslint-disable-line no-unused-vars, max-len
try {
const details = {total: {label: 'TEST',
amount: {currency: 'USD', value: '0.01'}}};
const request = new PaymentRequest(methodData, details);
const response = await request.show(new Promise((resolve) => {
window.setTimeout(() => resolve(details), 1000);
}));
await response.complete();
if (!response.details.status) {
return 'Payment handler did not specify the status.';
}
return response.details.status;
} catch (e) {
return e.message;
}
}
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