Skip to content

Commit

Permalink
[SPC] Require user activation for cross-origin enrollment
Browse files Browse the repository at this point in the history
This adds a user activation check and consumption for the enrollment of
a credential with the payment extension in a cross-origin frame.

Test coverage is added to the SPC iframe enrollment WPTs, and the
existing SPC authentication WPTs validate that this does not apply to
same-origin credential enrollments. Also tested manually on
https://rsolomakhin.github.io/pr/spc-iframe-no-ph/.

Intent to ship:
https://groups.google.com/a/chromium.org/g/blink-dev/c/GSoWLFb_jF0

Bug: 1322603
Change-Id: I5bca6d3fdf9a8687fa5d9d08b162287e1e7e4f98
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3584295
Commit-Queue: Nick Burris <nburris@chromium.org>
Reviewed-by: Stephen McGruer <smcgruer@chromium.org>
Reviewed-by: Ken Buchanan <kenrb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1002052}
  • Loading branch information
nickburris authored and Chromium LUCI CQ committed May 11, 2022
1 parent 31f8a10 commit de11fb8
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,19 @@ bool IsPaymentExtensionValid(const CredentialCreationOptions* options,
if (!payment->hasIsPayment() || !payment->isPayment())
return true;

if (!IsSameOriginWithAncestors(resolver->DomWindow()->GetFrame())) {
bool has_user_activation = LocalFrame::ConsumeTransientUserActivation(
resolver->DomWindow()->GetFrame(),
UserActivationUpdateSource::kRenderer);
if (!has_user_activation) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"A user activation is required to create a credential in a "
"cross-origin iframe."));
return false;
}
}

const auto* context = resolver->GetExecutionContext();
DCHECK(RuntimeEnabledFeatures::SecurePaymentConfirmationEnabled(context));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},
}
};
await window.test_driver.bless('user activation');
await navigator.credentials.create({
publicKey
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@
frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
'/secure-payment-confirmation/resources/iframe-enroll.html';

// Wait for the iframe to load.
const readyPromise = new Promise(resolve => {
window.addEventListener('message', function handler(evt) {
if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
window.removeEventListener('message', handler);

resolve(evt.data);
}
});
});
document.body.appendChild(frame);
await readyPromise;

const resultPromise = new Promise(resolve => {
window.addEventListener('message', function handler(evt) {
if (evt.source === frame.contentWindow) {
Expand All @@ -36,15 +49,65 @@
}
});
});
document.body.appendChild(frame);
frame.contentWindow.postMessage({ userActivation: true }, '*');
const result = await resultPromise;

// Because we specified the 'payment' permission, the enrollment should work.
// Because we specified the 'payment' permission and the iframe had a user
// activation, the enrollment should work.
assert_equals(result.error, null);
assert_own_property(result, 'id');
assert_own_property(result, 'rawId');
assert_not_own_property(result, 'error');
}, 'SPC enrollment in cross-origin iframe');

promise_test(async t => {
// Make sure that we are testing enrolling an SPC credential in a
// cross-origin iframe.
assert_not_equals(window.location.hostname, '{{hosts[alt][]}}',
'This test must not be run on the alt hostname.');

const authenticator = await window.test_driver.add_virtual_authenticator(
AUTHENTICATOR_OPTS);
t.add_cleanup(() => {
return window.test_driver.remove_virtual_authenticator(authenticator);
});

const frame = document.createElement('iframe');
frame.allow = 'payment';
frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
'/secure-payment-confirmation/resources/iframe-enroll.html';

// Wait for the iframe to load.
const readyPromise = new Promise(resolve => {
window.addEventListener('message', function handler(evt) {
if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
window.removeEventListener('message', handler);

resolve(evt.data);
}
});
});
document.body.appendChild(frame);
await readyPromise;

const resultPromise = new Promise(resolve => {
window.addEventListener('message', function handler(evt) {
if (evt.source === frame.contentWindow) {
window.removeEventListener('message', handler);
document.body.removeChild(frame);
resolve(evt.data);
}
});
});
frame.contentWindow.postMessage({ userActivation: false }, '*');
const result = await resultPromise;

// Without a user activation, we expect a SecurityError.
assert_true(result.error instanceof DOMException);
assert_equals(result.error.name, 'SecurityError');
assert_not_own_property(result, 'id');
assert_not_own_property(result, 'rawId');
}, 'SPC enrollment in cross-origin iframe fails without user activation');

promise_test(async t => {
// Make sure that we are testing enrolling an SPC credential in a
// cross-origin iframe.
Expand All @@ -63,6 +126,19 @@
frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
'/secure-payment-confirmation/resources/iframe-enroll.html';

// Wait for the iframe to load.
const readyPromise = new Promise(resolve => {
window.addEventListener('message', function handler(evt) {
if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
window.removeEventListener('message', handler);

resolve(evt.data);
}
});
});
document.body.appendChild(frame);
await readyPromise;

const resultPromise = new Promise(resolve => {
window.addEventListener('message', function handler(evt) {
if (evt.source === frame.contentWindow) {
Expand All @@ -72,7 +148,7 @@
}
});
});
document.body.appendChild(frame);
frame.contentWindow.postMessage({ userActivation: true }, '*');
const result = await resultPromise;

// Because we didn't specify the 'payment' permission, the enrollment should
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>SPC Enrollment iframe</title>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="../utils.sub.js"></script>
<script>
'use strict';

// Assume that our parent has already created a virtual authenticator device.
createCredential().then(credential => {
parent.postMessage({id: credential.id, rawId: credential.rawId}, '*');
}).catch(e => {
parent.postMessage({error: e}, '*');
// Setup the listener first, to avoid race conditions.
window.addEventListener('message', async function handler(evt) {
window.removeEventListener('message', handler);

if (evt.data.userActivation) {
test_driver.set_test_context(window.parent);
await test_driver.bless('user activation');
}
// Assume that our parent has already created a virtual authenticator device.
await createCredential().then(credential => {
parent.postMessage({id: credential.id, rawId: credential.rawId, error: null}, '*');
}).catch(e => {
parent.postMessage({error: e}, '*');
});
});

// Now let our parent know that we are ready to enroll.
window.parent.postMessage({ type: 'loaded' }, '*');
</script>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
This is a testharness.js-based test.
FAIL SPC enrollment in cross-origin iframe assert_own_property: expected property "id" missing
FAIL SPC enrollment in cross-origin iframe assert_equals: expected null but got object "NotSupportedError: The user agent does not support public key credentials."
PASS SPC enrollment in cross-origin iframe fails without user activation
PASS SPC enrollment in cross-origin iframe without payment permission
Harness: the test ran to completion.

0 comments on commit de11fb8

Please sign in to comment.