summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/credential-management
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/credential-management')
-rw-r--r--testing/web-platform/tests/credential-management/META.yml3
-rw-r--r--testing/web-platform/tests/credential-management/credentialscontainer-create-basics.https.html135
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-cross-origin-policy.https.html18
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-cross-origin-policy.https.html.sub.headers1
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-csp.https.html17
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-csp.https.html.sub.headers2
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-iframe.https.html61
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-logout-rps.https.html38
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-logout-rps.https.html.headers1
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-after-onload.https.html37
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-during-onload.https.html32
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-onload-and-during-dom-content-loaded.https.html32
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-onload.https.html29
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-before-onload.https.html29
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-during-onload.https.html29
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html25
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html30
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html25
-rw-r--r--testing/web-platform/tests/credential-management/fedcm-network-requests.https.html192
-rw-r--r--testing/web-platform/tests/credential-management/federatedcredential-framed-get.sub.https.html76
-rw-r--r--testing/web-platform/tests/credential-management/idlharness.https.window.js37
-rw-r--r--testing/web-platform/tests/credential-management/otpcredential-get-basics.https.html77
-rw-r--r--testing/web-platform/tests/credential-management/otpcredential-iframe.https.html54
-rw-r--r--testing/web-platform/tests/credential-management/passwordcredential-framed-get.sub.https.html76
-rw-r--r--testing/web-platform/tests/credential-management/require_securecontext.html13
-rw-r--r--testing/web-platform/tests/credential-management/support/README.md54
-rw-r--r--testing/web-platform/tests/credential-management/support/echoing-nester.html16
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js99
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm-iframe-level2.html23
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm-iframe.html23
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm-mock.js105
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm-mojojs-helper.js20
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/accounts.py24
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py30
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py.headers1
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/client_metadata_clear_count.py15
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/intercept_service_worker.js10
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/keys.py2
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/manifest-not-in-list.json5
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/manifest.py20
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_accounts.json5
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_token.json6
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/select_manifest_in_root_manifest.py17
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/simple.html3
-rw-r--r--testing/web-platform/tests/credential-management/support/fedcm/token.py24
-rw-r--r--testing/web-platform/tests/credential-management/support/federatedcredential-get.html17
-rw-r--r--testing/web-platform/tests/credential-management/support/otpcredential-helper.js114
-rw-r--r--testing/web-platform/tests/credential-management/support/otpcredential-iframe.html26
-rw-r--r--testing/web-platform/tests/credential-management/support/passwordcredential-get.html17
-rw-r--r--testing/web-platform/tests/credential-management/support/set_cookie12
-rw-r--r--testing/web-platform/tests/credential-management/support/set_cookie.headers2
51 files changed, 1759 insertions, 0 deletions
diff --git a/testing/web-platform/tests/credential-management/META.yml b/testing/web-platform/tests/credential-management/META.yml
new file mode 100644
index 0000000000..7b18609213
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/webappsec-credential-management/
+suggested_reviewers:
+ - mikewest
diff --git a/testing/web-platform/tests/credential-management/credentialscontainer-create-basics.https.html b/testing/web-platform/tests/credential-management/credentialscontainer-create-basics.https.html
new file mode 100644
index 0000000000..ea2326c4ae
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/credentialscontainer-create-basics.https.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<title>Credential Management API: create() basics.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(function(t) {
+ return promise_rejects_dom(t, "NotSupportedError",
+ navigator.credentials.create());
+}, "navigator.credentials.create() with no argument.");
+
+promise_test(function(t) {
+ return promise_rejects_dom(t, "NotSupportedError",
+ navigator.credentials.create({}));
+}, "navigator.credentials.create() with empty argument.");
+
+promise_test(function(t) {
+ var credential_data = {
+ id: 'id',
+ password: 'pencil',
+ };
+
+ return navigator.credentials.create({password: credential_data})
+ .then(function(credential) {
+ assert_equals(credential.id, 'id');
+ assert_equals(credential.name, '');
+ assert_equals(credential.iconURL, '');
+ assert_equals(credential.type, 'password');
+ assert_equals(credential.password, 'pencil');
+ });
+}, "navigator.credentials.create() with valid PasswordCredentialData");
+
+promise_test(function(t) {
+ var f = document.createElement('form');
+ f.innerHTML = "<input type='text' name='theId' value='musterman' autocomplete='username'>"
+ + "<input type='text' name='thePassword' value='sekrit' autocomplete='current-password'>"
+ + "<input type='text' name='theIcon' value='https://example.com/photo' autocomplete='photo'>"
+ + "<input type='text' name='theExtraField' value='extra'>"
+ + "<input type='text' name='theName' value='friendly name' autocomplete='name'>";
+
+ return navigator.credentials.create({password: f})
+ .then(function(credential) {
+ assert_equals(credential.id, 'musterman');
+ assert_equals(credential.name, 'friendly name');
+ assert_equals(credential.iconURL, 'https://example.com/photo');
+ assert_equals(credential.type, 'password');
+ assert_equals(credential.password, 'sekrit');
+ });
+}, "navigator.credentials.create() with valid HTMLFormElement");
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError,
+ navigator.credentials.create({password: "bogus password data"}));
+}, "navigator.credentials.create() with bogus password data");
+
+promise_test(function(t) {
+ var federated_data = {
+ id: 'id',
+ provider: 'https://example.com/',
+ };
+
+ return navigator.credentials.create({federated: federated_data})
+ .then(function(credential) {
+ assert_equals(credential.id, 'id');
+ assert_equals(credential.name, '');
+ assert_equals(credential.iconURL, '');
+ assert_equals(credential.type, 'federated');
+ });
+}, "navigator.credentials.create() with valid FederatedCredentialData");
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError,
+ navigator.credentials.create({federated: "bogus federated data"}));
+}, "navigator.credentials.create() with bogus federated data");
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError,
+ navigator.credentials.create({publicKey: "bogus publicKey data"}));
+}, "navigator.credentials.create() with bogus publicKey data");
+
+promise_test(function(t) {
+ var credential_data = {
+ id: 'id',
+ password: 'pencil',
+ };
+
+ var federated_data = {
+ id: 'id',
+ provider: 'https://example.com/',
+ };
+
+ return promise_rejects_dom(t, "NotSupportedError",
+ navigator.credentials.create({
+ password: credential_data,
+ federated: federated_data,
+ }));
+}, "navigator.credentials.create() with both PasswordCredentialData and FederatedCredentialData");
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError,
+ navigator.credentials.create({
+ password: "bogus password data",
+ federated: "bogus federated data",
+ }));
+}, "navigator.credentials.create() with bogus password and federated data");
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError,
+ navigator.credentials.create({
+ federated: "bogus federated data",
+ publicKey: "bogus publicKey data",
+ }));
+}, "navigator.credentials.create() with bogus federated and publicKey data");
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError,
+ navigator.credentials.create({
+ password: "bogus password data",
+ publicKey: "bogus publicKey data",
+ }));
+}, "navigator.credentials.create() with bogus password and publicKey data");
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError,
+ navigator.credentials.create({
+ password: "bogus password data",
+ federated: "bogus federated data",
+ publicKey: "bogus publicKey data",
+ }));
+}, "navigator.credentials.create() with bogus password, federated, and publicKey data");
+
+promise_test(function(t) {
+ return promise_rejects_dom(t, "NotSupportedError",
+ navigator.credentials.create({bogus_key: "bogus data"}));
+}, "navigator.credentials.create() with bogus data");
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-cross-origin-policy.https.html b/testing/web-platform/tests/credential-management/fedcm-cross-origin-policy.https.html
new file mode 100644
index 0000000000..808b7f6ea7
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-cross-origin-policy.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API Cross-Origin-Embedder-Policy tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {default_request_options,
+ fedcm_test} from './support/fedcm-helper.sub.js';
+
+fedcm_test(async t => {
+ const cred = await navigator.credentials.get(default_request_options());
+ assert_equals(cred.token, 'token');
+}, 'Test that COEP policy do not apply to FedCM requests');
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-cross-origin-policy.https.html.sub.headers b/testing/web-platform/tests/credential-management/fedcm-cross-origin-policy.https.html.sub.headers
new file mode 100644
index 0000000000..32523a6978
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-cross-origin-policy.https.html.sub.headers
@@ -0,0 +1 @@
+Cross-Origin-Embedder-Policy: credentialless
diff --git a/testing/web-platform/tests/credential-management/fedcm-csp.https.html b/testing/web-platform/tests/credential-management/fedcm-csp.https.html
new file mode 100644
index 0000000000..59c97e8c38
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-csp.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API network request tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {default_request_options, fedcm_test, set_fedcm_cookie} from './support/fedcm-helper.sub.js';
+
+fedcm_test(async t => {
+ const cred = navigator.credentials.get(default_request_options());
+ return promise_rejects_dom(t, "NetworkError", cred);
+}, "Provider configURL should honor Content-Security-Policy.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-csp.https.html.sub.headers b/testing/web-platform/tests/credential-management/fedcm-csp.https.html.sub.headers
new file mode 100644
index 0000000000..c1e6fd6c4c
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-csp.https.html.sub.headers
@@ -0,0 +1,2 @@
+Cross-Origin-Embedder-Policy: credentialless
+Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-inline'; img-src 'self'
diff --git a/testing/web-platform/tests/credential-management/fedcm-iframe.https.html b/testing/web-platform/tests/credential-management/fedcm-iframe.https.html
new file mode 100644
index 0000000000..43fe39f7bc
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-iframe.https.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<link rel="help" href="https://wicg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<div id=log>
+<script type="module">
+'use strict';
+
+import {fedcm_test, set_fedcm_cookie} from './support/fedcm-helper.sub.js';
+
+const host = get_host_info();
+const remoteBaseURL =
+ host.HTTPS_REMOTE_ORIGIN +
+ window.location.pathname.replace(/\/[^\/]*$/, '/');
+
+async function createIframeAndWaitForMessage(test, iframeUrl, setPermissionPolicy) {
+ const messageWatcher = new EventWatcher(test, window, "message");
+ var iframe = document.createElement("iframe");
+ iframe.src = iframeUrl;
+ if (setPermissionPolicy) {
+ iframe.allow = "identity-credentials-get";
+ }
+ document.body.appendChild(iframe);
+ const message = await messageWatcher.wait_for("message");
+ return message.data;
+}
+
+fedcm_test(async t => {
+ const message = await createIframeAndWaitForMessage(
+ t, remoteBaseURL + "support/fedcm-iframe.html",
+ /*setPermissionPolicy=*/false);
+ assert_equals(message.result, "Fail");
+ assert_equals(message.errorType, "NotAllowedError");
+}, "FedCM disabled in cross origin iframe without permissions policy");
+
+fedcm_test(async t => {
+ const message = await createIframeAndWaitForMessage(
+ t, remoteBaseURL + "support/fedcm-iframe-level2.html",
+ /*setPermissionPolicy=*/true);
+ assert_equals(message.result, "Pass");
+ assert_equals(message.token, "token");
+}, "FedCM enabled in 2 level deep nested iframe. FedCM should be enabled regardless of iframe nesting depth");
+
+fedcm_test(async t => {
+ const message = await createIframeAndWaitForMessage(
+ t, remoteBaseURL + "support/fedcm-iframe-level2.html",
+ /*setPermissionPolicy=*/false);
+ assert_equals(message.result, "Fail");
+ assert_equals(message.errorType, "NotAllowedError");
+}, "FedCM disabled in 2 level deep nested iframe where middle iframe does not have permission policy");
+
+fedcm_test(async t => {
+ const message = await createIframeAndWaitForMessage(
+ t, remoteBaseURL + "support/fedcm-iframe-level2.html?permission=0",
+ /*setPermissionPolicy=*/true);
+ assert_equals(message.result, "Fail");
+ assert_equals(message.errorType, "NotAllowedError");
+}, "FedCM disabled in 2 level deep nested iframe where innermost iframe does not have permission policy");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-logout-rps.https.html b/testing/web-platform/tests/credential-management/fedcm-logout-rps.https.html
new file mode 100644
index 0000000000..51b123003c
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-logout-rps.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>IdentityCredential.logoutRPs() promise resolution</title>
+<link rel="author" title="Peter Kotwicz" href="mailto:pkotwicz@chromium.org">
+<link rel="help" href="https://wicg.github.io/FedCM/#browser-api-idp-sign-out">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script type="module">
+ import {fedcm_mojo_mock_test} from './support/fedcm-mojojs-helper.js';
+
+ fedcm_mojo_mock_test(async (t, mock) => {
+ mock.logoutRpsReturn("kError");
+ return promise_rejects_dom(t, "NetworkError",
+ IdentityCredential.logoutRPs([{
+ accountId: "1234",
+ url: "https://rp.example/logout.php"
+ }])
+ );
+ }, "IdentityCredential.logoutRPs() error.");
+
+ fedcm_mojo_mock_test(async (t, mock) => {
+ mock.logoutRpsReturn("kSuccess");
+ await IdentityCredential.logoutRPs([{
+ accountId: "1234",
+ url: "https://rp.example/logout.php"
+ }]);
+ }, "IdentityCredential.logoutRPs() success.");
+
+ fedcm_mojo_mock_test(async (t, mock) => {
+ return promise_rejects_dom(t, "NetworkError",
+ IdentityCredential.logoutRPs([{
+ accountId: "1234",
+ url: "https://other-rp.example/logout.php"
+ }])
+ );
+ }, "Logout URL should honor Content-Security-Policy.");
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-logout-rps.https.html.headers b/testing/web-platform/tests/credential-management/fedcm-logout-rps.https.html.headers
new file mode 100644
index 0000000000..90454dbb22
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-logout-rps.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src https://rp.example
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-after-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-after-onload.https.html
new file mode 100644
index 0000000000..fa9ec7b52f
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-after-onload.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP get before and after onload test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {set_fedcm_cookie, set_alt_fedcm_cookie, default_request_options, default_alt_request_options} from '../support/fedcm-helper.sub.js';
+
+let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
+let has_window_loaded = false;
+const window_loaded = new Promise(resolve => {
+ window.addEventListener('load', () => {
+ has_window_loaded = true;
+ resolve();
+ });
+});
+
+promise_test(async t => {
+ let first_cred_resolved = false;
+ assert_false(has_window_loaded);
+ // First navigator.credentials.get() is called prior to window.onload
+ const first_cred = navigator.credentials.get(default_request_options()).finally(() => { first_cred_resolved = true; });
+ await Promise.all([cookies_promise, window_loaded]);
+ assert_true(has_window_loaded);
+ assert_false(first_cred_resolved);
+ // Second navigator.credentials.get() is called after window.onload but before first navigator.credentials.get() resolves
+ const second_cred = navigator.credentials.get(default_alt_request_options());
+ const rejection = promise_rejects_dom(t, 'NotAllowedError', second_cred);
+ const first = await first_cred;
+ assert_equals(first.token, "token");
+ return rejection;
+}, "When there's a `get` call before onload, a `get` call which occurs after onload but before the first `get` call resolves, should be rejected.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-during-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-during-onload.https.html
new file mode 100644
index 0000000000..93ee2075fe
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-and-during-onload.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP get before and during onload test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {set_fedcm_cookie, set_alt_fedcm_cookie, default_request_options, default_alt_request_options} from '../support/fedcm-helper.sub.js';
+
+let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
+
+promise_test(async t => {
+ let has_window_loaded = false;
+ const window_loaded = new Promise(resolve => {
+ window.addEventListener('load', async () => {
+ const second_cred = navigator.credentials.get(default_alt_request_options());
+ await promise_rejects_dom(t, 'NetworkError', second_cred);
+ has_window_loaded = true;
+ resolve();
+ });
+ });
+ assert_false(has_window_loaded);
+ const first_cred = navigator.credentials.get(default_request_options());
+ await Promise.all([cookies_promise, window_loaded]);
+ assert_true(has_window_loaded);
+ const first = await first_cred;
+ assert_equals(first.token, "token");
+}, "A `get` call before onload and a `get` call during onload should be combined.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-onload-and-during-dom-content-loaded.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-onload-and-during-dom-content-loaded.https.html
new file mode 100644
index 0000000000..edc293daff
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/get-before-onload-and-during-dom-content-loaded.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP get before onload and during DOMContentLoaded test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {set_fedcm_cookie, set_alt_fedcm_cookie, default_request_options, default_alt_request_options} from '../support/fedcm-helper.sub.js';
+
+let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
+
+promise_test(async t => {
+ let has_dom_content_loaded = false;
+ const dom_content_loaded = new Promise(resolve => {
+ document.addEventListener('DOMContentLoaded', async () => {
+ const second_cred = navigator.credentials.get(default_alt_request_options());
+ await promise_rejects_dom(t, 'NetworkError', second_cred);
+ has_dom_content_loaded = true;
+ resolve();
+ });
+ });
+ assert_false(has_dom_content_loaded);
+ const first_cred = navigator.credentials.get(default_request_options());
+ await Promise.all([cookies_promise, dom_content_loaded]);
+ assert_true(has_dom_content_loaded);
+ const first = await first_cred;
+ assert_equals(first.token, "token");
+}, "A `get` call before onload and a `get` call during DOMContentLoaded event should combine despite being called from different tasks.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-onload.https.html
new file mode 100644
index 0000000000..1a806a2049
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-after-onload.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP multiple gets after onload test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {set_fedcm_cookie, set_alt_fedcm_cookie, default_request_options, default_alt_request_options} from '../support/fedcm-helper.sub.js';
+
+let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
+const window_loaded = new Promise(resolve => {
+ window.addEventListener('load', () => {
+ resolve();
+ });
+});
+
+promise_test(async t => {
+ await Promise.all([cookies_promise, window_loaded]);
+ const first_cred = navigator.credentials.get(default_request_options());
+ const second_cred = navigator.credentials.get(default_alt_request_options());
+ // NetworkError is returned when another IDP is selected.
+ await promise_rejects_dom(t, 'NetworkError', second_cred);
+ const first = await first_cred;
+ assert_equals(first.token, "token");
+}, "No `get` calls before or during onload, multiple `get` calls after onload in the same task are allowed.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-before-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-before-onload.https.html
new file mode 100644
index 0000000000..69a70ce47d
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-before-onload.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP multiple gets before onload test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {set_fedcm_cookie, set_alt_fedcm_cookie, default_request_options, default_alt_request_options} from '../support/fedcm-helper.sub.js';
+
+let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
+let has_window_loaded = false;
+window.addEventListener('load', () => {
+ has_window_loaded = true;
+});
+
+// TODO(crbug.com/1374869): Add multi IDP test where second IDP is selected.
+promise_test(async t => {
+ assert_false(has_window_loaded);
+ const first_cred = navigator.credentials.get(default_alt_request_options());
+ const second_cred = navigator.credentials.get(default_request_options());
+ await cookies_promise;
+ await promise_rejects_dom(t, 'NetworkError', second_cred);
+ const first = await first_cred;
+ assert_equals(first.token, "token");
+}, "Multiple get calls before window onload are allowed.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-during-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-during-onload.https.html
new file mode 100644
index 0000000000..e98b63ff33
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/multiple-gets-during-onload.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP multiple gets during onload test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {set_fedcm_cookie, set_alt_fedcm_cookie, default_request_options, default_alt_request_options} from '../support/fedcm-helper.sub.js';
+
+let cookies_promise = Promise.all([set_fedcm_cookie(), set_alt_fedcm_cookie()]);
+
+promise_test(async t => {
+ const window_loaded = new Promise(resolve => {
+ window.addEventListener('load', async () => {
+ const first_cred = navigator.credentials.get(default_request_options());
+ const second_cred = navigator.credentials.get(default_alt_request_options());
+ await cookies_promise;
+ await promise_rejects_dom(t, 'NetworkError', second_cred);
+ const first = await first_cred;
+ assert_equals(first.token, "token");
+ resolve();
+ });
+ });
+ await window_loaded;
+}, "No `get` calls before onload, multiple `get` calls during onload are allowed.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html
new file mode 100644
index 0000000000..a34ff84aab
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP single get after onload test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {set_fedcm_cookie, default_request_options} from '../support/fedcm-helper.sub.js';
+
+const window_loaded = new Promise(resolve => {
+ window.addEventListener('load', () => {
+ resolve();
+ });
+});
+
+promise_test(async t => {
+ await set_fedcm_cookie();
+ await window_loaded;
+ const cred = await navigator.credentials.get(default_request_options());
+ assert_equals(cred.token, "token");
+}, "Single `get` call after onload is allowed.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html
new file mode 100644
index 0000000000..91195fc5a7
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP single get before onload test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {set_fedcm_cookie, default_request_options} from '../support/fedcm-helper.sub.js';
+
+let has_window_loaded = false;
+const window_loaded = new Promise(resolve => {
+ window.addEventListener('load', () => {
+ has_window_loaded = true;
+ resolve();
+ });
+});
+
+promise_test(async t => {
+ const first_cred = navigator.credentials.get(default_request_options());
+ assert_false(has_window_loaded);
+ await set_fedcm_cookie();
+ await window_loaded;
+ assert_true(has_window_loaded);
+ const first = await first_cred;
+ assert_equals(first.token, "token");
+}, "Single `get` call before onload is allowed.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html
new file mode 100644
index 0000000000..17939d2d93
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API multi IDP single get during onload test.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+
+<script type="module">
+import {default_request_options, set_fedcm_cookie} from '../support/fedcm-helper.sub.js';
+
+promise_test(async t => {
+ const window_loaded = new Promise(resolve => {
+ window.addEventListener('load', async () => {
+ const first_cred = navigator.credentials.get(default_request_options());
+ await set_fedcm_cookie();
+ const first = await first_cred;
+ assert_equals(first.token, "token");
+ resolve();
+ });
+ });
+ await window_loaded;
+}, "Single `get` call during onload is allowed.");
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/fedcm-network-requests.https.html b/testing/web-platform/tests/credential-management/fedcm-network-requests.https.html
new file mode 100644
index 0000000000..ebcf61ba18
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/fedcm-network-requests.https.html
@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API network request tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<body>
+
+<script type="module">
+import {default_request_options,
+ default_alt_request_options,
+ fedcm_test,
+ select_manifest,
+ set_fedcm_cookie} from './support/fedcm-helper.sub.js';
+
+function loadUrlInIframe(url) {
+ let iframe = document.createElement("iframe");
+ return new Promise(resolve => {
+ iframe.src = url;
+ iframe.onload = function() { resolve(iframe); };
+ document.body.appendChild(iframe);
+ });
+}
+
+fedcm_test(async t => {
+ const cred = await navigator.credentials.get(default_request_options());
+ assert_equals(cred.token, "token");
+}, "Successfully obtaining token should resolve the promise.");
+
+fedcm_test(async t => {
+ const first = navigator.credentials.get(default_request_options());
+ const second = navigator.credentials.get(default_alt_request_options());
+
+ // We have to call promise_rejects_dom here, because if we call it after
+ // the promise gets rejected, the unhandled rejection event handler is called
+ // and fails the test even if we handle the rejection later.
+ const rej = promise_rejects_dom(t, 'AbortError', second);
+
+ const first_cred = await first;
+ assert_equals(first_cred.token, "token");
+
+ return rej;
+},
+"When there's a pending request, a second `get` call should be rejected. ");
+
+fedcm_test(async t => {
+ let test_options = default_request_options();
+ test_options.identity.providers = [];
+ const cred = navigator.credentials.get(test_options);
+ return promise_rejects_js(t, TypeError, cred);
+}, "Reject when provider list is empty");
+
+fedcm_test(async t => {
+ let test_options = default_request_options();
+ delete test_options.identity.providers[0].configURL;
+ const cred = navigator.credentials.get(test_options);
+ return promise_rejects_js(t, TypeError, cred);
+}, "Reject when configURL is missing" );
+
+fedcm_test(async t => {
+ let test_options = default_request_options();
+ test_options.identity.providers[0].configURL = 'test';
+ const cred = navigator.credentials.get(test_options);
+ return promise_rejects_dom(t, "InvalidStateError", cred);
+}, "Reject when configURL is invalid");
+
+fedcm_test(async t => {
+ let test_options = default_request_options();
+ test_options.identity.providers[0].clientId = '';
+ const cred = navigator.credentials.get(test_options);
+ return promise_rejects_dom(t, "InvalidStateError", cred);
+}, "Reject when clientId is empty");
+
+fedcm_test(async t => {
+ let test_options = default_request_options();
+ assert_true("nonce" in test_options.identity.providers[0]);
+ delete test_options.identity.providers[0].nonce;
+ const cred = await navigator.credentials.get(test_options);
+ assert_equals(cred.token, "token");
+}, "nonce is not required in FederatedIdentityProvider.");
+
+fedcm_test(async t => {
+ let test_options = default_request_options();
+ delete test_options.identity.providers[0].clientId;
+ const cred = navigator.credentials.get(test_options);
+ return promise_rejects_js(t, TypeError, cred);
+}, "Reject when clientId is missing" );
+
+fedcm_test(async t => {
+ let controller = new AbortController();
+ let test_options = default_request_options();
+ test_options.signal = controller.signal;
+ const cred = navigator.credentials.get(test_options);
+ controller.abort();
+ return promise_rejects_dom(t, 'AbortError', cred);
+}, "Test the abort signal");
+
+fedcm_test(async t => {
+ let controller = new AbortController();
+ let test_options = default_request_options();
+ test_options.signal = controller.signal;
+ const first_cred = navigator.credentials.get(test_options);
+ controller.abort();
+ await promise_rejects_dom(t, 'AbortError', first_cred);
+
+ const second_cred = await navigator.credentials.get(default_request_options());
+ assert_equals(second_cred.token, "token");
+}, "Get after abort should work");
+
+fedcm_test(async t => {
+ let test_options = default_request_options('manifest-not-in-list.json');
+ const cred = navigator.credentials.get(test_options);
+ return promise_rejects_dom(t, 'NetworkError', cred);
+}, 'Test that the promise is rejected if the manifest is not in the manifest list');
+
+fedcm_test(async t => {
+ let test_options = default_request_options("manifest_redirect_accounts.json");
+ await select_manifest(t, test_options);
+
+ const cred = navigator.credentials.get(test_options);
+ return promise_rejects_dom(t, 'NetworkError', cred);
+}, 'Test that promise is rejected if accounts endpoint redirects');
+// A malicious site might impersonate an IDP, redirecting the accounts endpoint to a
+// legitimate IDP in order to get the list of user accounts.
+
+fedcm_test(async t => {
+ let test_options = default_request_options("manifest_redirect_token.json");
+ await select_manifest(t, test_options);
+
+ const cred = navigator.credentials.get(test_options);
+ return promise_rejects_dom(t, 'NetworkError', cred);
+}, 'Test that token endpoint does not follow redirects');
+// The token endpoint should not follow redirects because the user has not consented
+// to share their identity with the redirect destination.
+
+fedcm_test(async t => {
+ // Reset the client_metadata fetch count.
+ const clear_metadata_count_path = `support/fedcm/client_metadata_clear_count.py`;
+ await fetch(clear_metadata_count_path);
+
+ const cred = await navigator.credentials.get(default_request_options());
+ assert_equals(cred.token, "token");
+
+ await new Promise(resolve => {
+ let popup_window = window.open('support/fedcm/client_metadata.py?skip_checks=1');
+ const popup_window_load_handler = (event) => {
+ popup_window.removeEventListener('load', popup_window_load_handler);
+ popup_window.close();
+ resolve();
+ };
+ popup_window.addEventListener('load', popup_window_load_handler);
+ });
+
+ const client_metadata_counter = await fetch(clear_metadata_count_path);
+ const client_metadata_counter_text = await client_metadata_counter.text()
+ assert_equals(client_metadata_counter_text, "2");
+}, 'Test client_metadata request');
+// Test:
+// - Headers sent to client metadata endpoint. (Counter is not incremented if the headers are
+// wrong.)
+// - That the client metadata response is not cached. If the client metadata response were
+// cached, when the user visits the IDP as a first party, the IDP would be able to determine the
+// last RP the user visited regardless of whether the user granted consent via the FedCM prompt.
+
+fedcm_test(async t => {
+ const service_worker_url = 'support/fedcm/intercept_service_worker.js';
+ const sw_scope_url = '/credential-management/support/fedcm/';
+ // URL for querying number of page loads observed by service worker.
+ const query_sw_intercepts_url = 'support/fedcm/query_service_worker_intercepts.html';
+ const page_in_sw_scope_url = 'support/fedcm/simple.html';
+
+ const sw_registration = await service_worker_unregister_and_register(
+ t, service_worker_url, sw_scope_url);
+ t.add_cleanup(() => service_worker_unregister(t, sw_scope_url));
+ await wait_for_state(t, sw_registration.installing, 'activated');
+
+ // Verify that service worker works.
+ await loadUrlInIframe(page_in_sw_scope_url);
+ let query_sw_iframe = await loadUrlInIframe(query_sw_intercepts_url);
+ assert_equals(query_sw_iframe.contentDocument.body.textContent, "1");
+
+ await set_fedcm_cookie();
+ const cred = await navigator.credentials.get(default_request_options());
+ assert_equals(cred.token, "token");
+
+ // Use cache buster query parameter to avoid cached response.
+ let query_sw_iframe2 = await loadUrlInIframe(query_sw_intercepts_url + "?2");
+ assert_equals(query_sw_iframe2.contentDocument.body.textContent, "1");
+}, 'Test that service worker cannot observe fetches performed by FedCM API');
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/federatedcredential-framed-get.sub.https.html b/testing/web-platform/tests/credential-management/federatedcredential-framed-get.sub.https.html
new file mode 100644
index 0000000000..7883c8ebf2
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/federatedcredential-framed-get.sub.https.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+assert_implements('FederatedCredential' in window, "`FederatedCredential` is supported.");
+
+// Ensure that the check is "same origin", not "same origin-domain".
+document.domain = window.location.hostname;
+
+function create_iframe_test(origin, expectation) {
+ return function (t) {
+ window.addEventListener("load", _ => {
+ var iframe = document.createElement("iframe");
+ iframe.src = origin + "/credential-management/support/federatedcredential-get.html";
+ window.addEventListener("message", t.step_func(e => {
+ if (e.source == iframe.contentWindow) {
+ if (expectation == "blocked") {
+ assert_equals(e.data.exception, "NotAllowedError");
+ } else {
+ if (e.data.exception)
+ assert_not_equals(e.data.exception, "NotAllowedError");
+ }
+ t.done();
+ }
+ }));
+ document.body.appendChild(iframe);
+ });
+ };
+}
+
+function create_nested_iframe_test(outerOrigin, innerOrigin, expectation) {
+ return function (t) {
+ window.addEventListener("load", _ => {
+ var iframe = document.createElement("iframe");
+ iframe.src = outerOrigin + "/credential-management/support/echoing-nester.html?origin=" + innerOrigin + "&file=federatedcredential-get.html";
+ window.addEventListener("message", t.step_func(e => {
+ if (e.source == iframe.contentWindow) {
+ if (expectation == "blocked") {
+ assert_equals(e.data.exception, "NotAllowedError");
+ } else {
+ assert_equals(e.data.exception, null);
+ }
+ t.done();
+ }
+ }));
+ document.body.appendChild(iframe);
+ });
+ };
+}
+
+const SAME_ORIGIN = window.origin;
+const CROSS_ORIGIN = "https://{{domains[élève]}}:{{ports[https][0]}}";
+
+async_test(
+ create_iframe_test(SAME_ORIGIN, "allowed"),
+ "Same-origin IFrame does not throw.");
+async_test(
+ create_iframe_test(CROSS_ORIGIN, "blocked"),
+ "Cross-origin IFrame throws 'NotAllowedError'.");
+
+async_test(
+ create_nested_iframe_test(SAME_ORIGIN, SAME_ORIGIN, "allowed"),
+ "Same-origin IFrame in same-origin IFrame does not throw.");
+
+async_test(
+ create_nested_iframe_test(SAME_ORIGIN, CROSS_ORIGIN, "blocked"),
+ "Same-origin IFrame in same-origin IFrame throws 'NotAllowedError'.");
+
+async_test(
+ create_nested_iframe_test(CROSS_ORIGIN, SAME_ORIGIN, "blocked"),
+ "Cross-origin IFrame in same-origin IFrame throws 'NotAllowedError'.");
+
+async_test(
+ create_nested_iframe_test(CROSS_ORIGIN, CROSS_ORIGIN, "blocked"),
+ "Cross-origin IFrame in same-cross-origin throws 'NotAllowedError'.");
+</script>
diff --git a/testing/web-platform/tests/credential-management/idlharness.https.window.js b/testing/web-platform/tests/credential-management/idlharness.https.window.js
new file mode 100644
index 0000000000..26d7c493b0
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/idlharness.https.window.js
@@ -0,0 +1,37 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+// https://w3c.github.io/webappsec-credential-management/
+
+'use strict';
+
+idl_test(
+ ['credential-management'],
+ ['html', 'dom'],
+ idl_array => {
+ idl_array.add_objects({
+ CredentialsContainer: ['navigator.credentials'],
+ PasswordCredential: ['passwordCredential'],
+ FederatedCredential: ['federatedCredential'],
+ });
+
+ try {
+ self.passwordCredential = new PasswordCredential({
+ id: "id",
+ password: "pencil",
+ iconURL: "https://example.com/",
+ name: "name"
+ });
+ } catch (e) {}
+
+ try {
+ self.federatedCredential = new FederatedCredential({
+ id: "id",
+ provider: "https://example.com",
+ iconURL: "https://example.com/",
+ name: "name"
+ });
+ } catch (e) {}
+ }
+)
diff --git a/testing/web-platform/tests/credential-management/otpcredential-get-basics.https.html b/testing/web-platform/tests/credential-management/otpcredential-get-basics.https.html
new file mode 100644
index 0000000000..3907f85450
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/otpcredential-get-basics.https.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<link rel="help" href="https://github.com/WICG/WebOTP">
+<title>Tests OTPCredential</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="module">
+import {Status, expectOTPRequest} from './support/otpcredential-helper.js';
+
+promise_test(async t => {
+ await expectOTPRequest().andReturn(
+ () => ({status: Status.SUCCESS, otp: "ABC"}));
+
+ let cred = await navigator.credentials.get({otp: {transport: ["sms"]}});
+
+ assert_equals(cred.code, "ABC");
+}, 'Basic usage');
+
+promise_test(async t => {
+ await expectOTPRequest().andReturn(
+ () => ({status: Status.SUCCESS, otp: "ABC"}));
+ await expectOTPRequest().andReturn(
+ () => ({status: Status.SUCCESS, otp: "ABC2"}));
+
+ let sms1 = navigator.credentials.get({otp: {transport: ["sms"]}});
+ let sms2 = navigator.credentials.get({otp: {transport: ["sms"]}});
+
+ let cred2 = await sms2;
+ let cred1 = await sms1;
+
+ assert_equals(cred1.code, "ABC");
+ assert_equals(cred2.code, "ABC2");
+}, 'Handle multiple requests in different order.');
+
+promise_test(async t => {
+ await expectOTPRequest().andReturn(() => ({status: Status.CANCELLED}));
+ await expectOTPRequest().andReturn(
+ () => ({status: Status.SUCCESS, otp: "success"}));
+
+ let cancelledRequest = navigator.credentials.get({otp: {transport: ["sms"]}});
+ let successfulCred =
+ await navigator.credentials.get({otp: {transport: ["sms"]}});
+
+ assert_equals(successfulCred.code, "success");
+
+ try {
+ await cancelledRequest;
+ assert_unreached('Expected AbortError to be thrown.');
+ } catch (error) {
+ assert_equals(error.name, "AbortError");
+ }
+}, 'Handle multiple requests with success and error.');
+
+promise_test(async t => {
+ await expectOTPRequest().andReturn(() => ({status: Status.CANCELLED}));
+
+ await promise_rejects_dom(t, 'AbortError', navigator.credentials.get(
+ {otp: {transport: ["sms"]}}));
+}, 'Deal with cancelled requests');
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ controller.abort();
+ await promise_rejects_dom(t, 'AbortError', navigator.credentials.get(
+ {otp: {transport: ["sms"]}, signal: signal}));
+}, 'Should abort request');
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ controller.abort('CustomError');
+ await promise_rejects_exactly(t, 'CustomError', navigator.credentials.get(
+ {otp: {transport: ["sms"]}, signal: signal}));
+}, 'Should abort request with reason');
+</script>
diff --git a/testing/web-platform/tests/credential-management/otpcredential-iframe.https.html b/testing/web-platform/tests/credential-management/otpcredential-iframe.https.html
new file mode 100644
index 0000000000..da3e572b6b
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/otpcredential-iframe.https.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<div id=log>
+<script>
+'use strict';
+
+const host = get_host_info();
+const remoteBaseURL =
+ host.HTTPS_REMOTE_ORIGIN +
+ window.location.pathname.replace(/\/[^\/]*$/, '/');
+const localBaseURL =
+ host.HTTPS_ORIGIN +
+ window.location.pathname.replace(/\/[^\/]*$/, '/');
+
+promise_test(async t => {
+ const messageWatcher = new EventWatcher(t, window, "message");
+ var iframe = document.createElement("iframe");
+ iframe.src = localBaseURL + "support/otpcredential-iframe.html";
+
+ document.body.appendChild(iframe);
+
+ const message = await messageWatcher.wait_for("message");
+ assert_equals(message.data.result, "Pass");
+ assert_equals(message.data.code, "ABC123");
+
+}, "Test OTPCredential enabled in same origin iframes");
+
+promise_test(async t => {
+ const messageWatcher = new EventWatcher(t, window, "message");
+ var iframe = document.createElement("iframe");
+ iframe.src = remoteBaseURL + "support/otpcredential-iframe.html"
+ iframe.allow = "otp-credentials";
+ document.body.appendChild(iframe);
+
+ const message = await messageWatcher.wait_for("message");
+ assert_equals(message.data.result, "Pass");
+ assert_equals(message.data.code, "ABC123");
+
+}, "OTPCredential enabled in cross origin iframes with permissions policy");
+
+promise_test(async t => {
+ const messageWatcher = new EventWatcher(t, window, "message");
+ var iframe = document.createElement("iframe");
+ iframe.src = remoteBaseURL + "support/otpcredential-iframe.html"
+ document.body.appendChild(iframe);
+
+ const message = await messageWatcher.wait_for("message");
+ assert_equals(message.data.result, "Fail");
+ assert_equals(message.data.errorType, "NotAllowedError");
+
+}, "OTPCredential disabled in cross origin iframes without permissions policy");
+</script>
diff --git a/testing/web-platform/tests/credential-management/passwordcredential-framed-get.sub.https.html b/testing/web-platform/tests/credential-management/passwordcredential-framed-get.sub.https.html
new file mode 100644
index 0000000000..b4afc1eb91
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/passwordcredential-framed-get.sub.https.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+assert_implements('PasswordCredential' in window, "`PasswordCredential` is supported.");
+
+// Ensure that the check is "same origin", not "same origin-domain".
+document.domain = window.location.hostname;
+
+function create_iframe_test(origin, expectation) {
+ return function (t) {
+ window.addEventListener("load", _ => {
+ var iframe = document.createElement("iframe");
+ iframe.src = origin + "/credential-management/support/passwordcredential-get.html";
+ window.addEventListener("message", t.step_func(e => {
+ if (e.source == iframe.contentWindow) {
+ if (expectation == "blocked") {
+ assert_equals(e.data.exception, "NotAllowedError");
+ } else {
+ if (e.data.exception)
+ assert_not_equals(e.data.exception, "NotAllowedError");
+ }
+ t.done();
+ }
+ }));
+ document.body.appendChild(iframe);
+ });
+ };
+}
+
+function create_nested_iframe_test(outerOrigin, innerOrigin, expectation) {
+ return function (t) {
+ window.addEventListener("load", _ => {
+ var iframe = document.createElement("iframe");
+ iframe.src = outerOrigin + "/credential-management/support/echoing-nester.html?origin=" + innerOrigin + "&file=passwordcredential-get.html";
+ window.addEventListener("message", t.step_func(e => {
+ if (e.source == iframe.contentWindow) {
+ if (expectation == "blocked") {
+ assert_equals(e.data.exception, "NotAllowedError");
+ } else {
+ assert_equals(e.data.exception, null);
+ }
+ t.done();
+ }
+ }));
+ document.body.appendChild(iframe);
+ });
+ };
+}
+
+const SAME_ORIGIN = window.origin;
+const CROSS_ORIGIN = "https://{{domains[élève]}}:{{ports[https][0]}}";
+
+async_test(
+ create_iframe_test(SAME_ORIGIN, "allowed"),
+ "Same-origin IFrame does not throw.");
+async_test(
+ create_iframe_test(CROSS_ORIGIN, "blocked"),
+ "Cross-origin IFrame throws 'NotAllowedError'.");
+
+async_test(
+ create_nested_iframe_test(SAME_ORIGIN, SAME_ORIGIN, "allowed"),
+ "Same-origin IFrame in same-origin IFrame does not throw.");
+
+async_test(
+ create_nested_iframe_test(SAME_ORIGIN, CROSS_ORIGIN, "blocked"),
+ "Same-origin IFrame in same-origin IFrame throws 'NotAllowedError'.");
+
+async_test(
+ create_nested_iframe_test(CROSS_ORIGIN, SAME_ORIGIN, "blocked"),
+ "Cross-origin IFrame in same-origin IFrame throws 'NotAllowedError'.");
+
+async_test(
+ create_nested_iframe_test(CROSS_ORIGIN, CROSS_ORIGIN, "blocked"),
+ "Cross-origin IFrame in same-cross-origin throws 'NotAllowedError'.");
+</script>
diff --git a/testing/web-platform/tests/credential-management/require_securecontext.html b/testing/web-platform/tests/credential-management/require_securecontext.html
new file mode 100644
index 0000000000..b1f3103da0
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/require_securecontext.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test that Credential Management requires secure contexts</title>
+<link rel="help" href="https://w3c.github.io/webappsec-credential-management/#idl-index">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+ test(() => {
+ assert_false(isSecureContext);
+ assert_false('credentials' in navigator);
+ }, "Credential Management must not be accessible in insecure contexts");
+</script>
diff --git a/testing/web-platform/tests/credential-management/support/README.md b/testing/web-platform/tests/credential-management/support/README.md
new file mode 100644
index 0000000000..a6d33ff6f7
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/README.md
@@ -0,0 +1,54 @@
+# CredentialManagement Testing
+
+## OTPCredential Testing
+
+In this test suite `otpcredential-helper.js` is a testing framework that enables
+engines to test OTPCredential by intercepting the connection between the browser
+and the underlying operating system and mock its behavior.
+
+Usage:
+
+1. Include the following in your test:
+```html
+<script src="/resources/test-only-api.js"></script>
+<script src="support/otpcredential-helper.js"></script>
+```
+2. Set expectations
+```javascript
+await expect(receive).andReturn(() => {
+ // mock behavior
+})
+```
+3. Call `navigator.credentials.get({otp: {transport: ["sms"]}})`
+4. Verify results
+
+The mocking API is browser agnostic and is designed such that other engines
+could implement it too.
+
+Here are the symbols that are exposed to tests that need to be implemented
+per engine:
+
+- function receive(): the main/only function that can be mocked
+- function expect(): the main/only function that enables us to mock it
+- enum State {kSuccess, kTimeout}: allows you to mock success/failures
+
+## FedCM Testing
+
+`fedcm-mojojs-helper.js` exposes `fedcm_mojo_mock_test` which is a specialized
+`promise_test` which comes pre-setup with the appropriate mocking infrastructure
+to emulate platform federated auth backend. The mock is passed to the test
+function as the second parameter.
+
+Example usage:
+```
+<script type="module">
+ import {fedcm_mojo_mock_test} from './support/fedcm-mojojs-helper.js';
+
+ fedcm_mojo_mock_test(async (t, mock) => {
+ mock.returnToken("https://idp.test/fedcm.json", "a_token");
+ assert_equals("a_token", await navigator.credentials.get(options));
+ }, "Successfully obtaining a token using mock.");
+</script>
+```
+
+The chromium implementation uses the MojoJS shim.
diff --git a/testing/web-platform/tests/credential-management/support/echoing-nester.html b/testing/web-platform/tests/credential-management/support/echoing-nester.html
new file mode 100644
index 0000000000..d4f5899da7
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/echoing-nester.html
@@ -0,0 +1,16 @@
+<body>
+ <script>
+ window.addEventListener('message', m => {
+ window.parent.postMessage(m.data, '*');
+ });
+
+ var u = new URL(window.location.href);
+ var origin = u.searchParams.has('origin') ? u.searchParams.get('origin') : window.origin;
+ var file = u.searchParams.has('file') ? u.searchParams.get('file') : 'passwordcredential-get.html';
+
+ var url = origin + "/credential-management/support/" + file;
+ var i = document.createElement('iframe');
+ i.src = url;
+ document.body.appendChild(i);
+ </script>
+</body>
diff --git a/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js b/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js
new file mode 100644
index 0000000000..87a1337a06
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js
@@ -0,0 +1,99 @@
+const alt_manifest_origin = 'https://{{hosts[alt][]}}:{{ports[https][0]}}';
+
+// Set the identity provider cookie.
+export function set_fedcm_cookie(host) {
+ return new Promise(resolve => {
+ if (host == undefined) {
+ document.cookie = 'cookie=1; SameSite=Strict; Path=/credential-management/support; Secure';
+ resolve();
+ } else {
+ let popup_window = window.open(host + '/credential-management/support/set_cookie');
+
+ const popup_message_handler = (event) => {
+ if (event.origin == host) {
+ popup_window.close();
+ window.removeEventListener('message', popup_message_handler);
+ resolve();
+ }
+ };
+
+ window.addEventListener('message', popup_message_handler);
+ }
+ });
+}
+
+// Set the alternate identity provider cookie.
+export function set_alt_fedcm_cookie() {
+ return set_fedcm_cookie(alt_manifest_origin);
+}
+
+// Returns FedCM CredentialRequestOptions for which navigator.credentials.get()
+// succeeds.
+export function default_request_options(manifest_filename) {
+ if (manifest_filename === undefined) {
+ manifest_filename = "manifest.py";
+ }
+ const manifest_path = `https://{{host}}:{{ports[https][0]}}/\
+credential-management/support/fedcm/${manifest_filename}`;
+ return {
+ identity: {
+ providers: [{
+ configURL: manifest_path,
+ clientId: '1',
+ nonce: '2',
+ }]
+ }
+ };
+}
+
+// Returns alternate FedCM CredentialRequestOptions for which navigator.credentials.get()
+// succeeds.
+export function default_alt_request_options(manifest_filename) {
+ if (manifest_filename === undefined) {
+ manifest_filename = "manifest.py";
+ }
+ const manifest_path = `${alt_manifest_origin}/\
+credential-management/support/fedcm/${manifest_filename}`;
+ return {
+ identity: {
+ providers: [{
+ configURL: manifest_path,
+ clientId: '1',
+ nonce: '2',
+ }]
+ }
+ };
+}
+
+// Test wrapper which does FedCM-specific setup.
+export function fedcm_test(test_func, test_name) {
+ promise_test(async t => {
+ await set_fedcm_cookie();
+ await set_alt_fedcm_cookie();
+ await test_func(t);
+ }, test_name);
+}
+
+function select_manifest_impl(manifest_url) {
+ const url_query = (manifest_url === undefined)
+ ? '' : '?manifest_url=${manifest_url}';
+
+ return new Promise(resolve => {
+ const img = document.createElement('img');
+ img.src = 'support/fedcm/select_manifest_in_root_manifest.py?${url_query}';
+ img.addEventListener('error', resolve);
+ document.body.appendChild(img);
+ });
+}
+
+// Sets the manifest returned by the next fetch of /.well-known/web_identity
+// select_manifest() only affects the next fetch and not any subsequent fetches
+// (ex second next fetch).
+export function select_manifest(test, test_options) {
+ // Add cleanup in case that /.well-known/web_identity is not fetched at all.
+ test.add_cleanup(async () => {
+ await select_manifest_impl();
+ });
+ const manifest_url = test_options.identity.providers[0].configURL;
+ return select_manifest_impl(manifest_url);
+}
diff --git a/testing/web-platform/tests/credential-management/support/fedcm-iframe-level2.html b/testing/web-platform/tests/credential-management/support/fedcm-iframe-level2.html
new file mode 100644
index 0000000000..7622d988ff
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm-iframe-level2.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<link rel="help" href="https://wicg.github.io/FedCM">
+<script src="/common/get-host-info.sub.js"></script>
+<div id=log>
+<script>
+'use strict';
+
+const host = get_host_info();
+const remoteBaseURL =
+ host.AUTHENTICATED_ORIGIN +
+ window.location.pathname.replace(/\/[^\/]*$/, '/');
+
+window.onload = async () => {
+ const urlParams = new URLSearchParams(window.location.search);
+ var iframe = document.createElement("iframe");
+ iframe.src = remoteBaseURL + "fedcm-iframe.html";
+ if (urlParams.get("permission") != '0') {
+ iframe.allow = "identity-credentials-get";
+ }
+ document.body.appendChild(iframe);
+};
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/support/fedcm-iframe.html b/testing/web-platform/tests/credential-management/support/fedcm-iframe.html
new file mode 100644
index 0000000000..c57f54e1da
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm-iframe.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script type="module">
+import {default_request_options} from './fedcm-helper.sub.js';
+
+// Loading fedcm-iframe.html in the test will make a FedCM call on load, and
+// trigger a postMessage upon completion.
+//
+// message {
+// string result: "Pass" | "Fail"
+// string token: token.token
+// string errorType: error.name
+// }
+
+window.onload = async () => {
+ try {
+ const cred = await navigator.credentials.get(default_request_options());
+ window.top.postMessage({result: "Pass", token: cred.token}, '*');
+ } catch (error) {
+ window.top.postMessage({result: "Fail", errorType: error.name}, '*');
+ }
+};
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/support/fedcm-mock.js b/testing/web-platform/tests/credential-management/support/fedcm-mock.js
new file mode 100644
index 0000000000..f52bd6e0e7
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm-mock.js
@@ -0,0 +1,105 @@
+import { RequestTokenStatus, LogoutRpsStatus, FederatedAuthRequest, FederatedAuthRequestReceiver } from '/gen/third_party/blink/public/mojom/webid/federated_auth_request.mojom.m.js';
+
+function toMojoTokenStatus(status) {
+ return RequestTokenStatus["k" + status];
+}
+
+// A mock service for responding to federated auth requests.
+export class MockFederatedAuthRequest {
+ constructor() {
+ this.receiver_ = new FederatedAuthRequestReceiver(this);
+ this.interceptor_ = new MojoInterfaceInterceptor(FederatedAuthRequest.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ this.receiver_.$.bindHandle(e.handle);
+ }
+ this.interceptor_.start();
+ this.token_ = null;
+ this.selected_identity_provider_config_url_ = null;
+ this.status_ = RequestTokenStatus.kError;
+ this.logoutRpsStatus_ = LogoutRpsStatus.kError;
+ this.returnPending_ = false;
+ this.pendingPromiseResolve_ = null;
+ }
+
+ // Causes the subsequent `navigator.credentials.get()` to resolve with the token.
+ returnToken(selected_identity_provider_config_url, token) {
+ this.status_ = RequestTokenStatus.kSuccess;
+ this.selected_identity_provider_config_url_ = selected_identity_provider_config_url;
+ this.token_ = token;
+ this.returnPending_ = false;
+ }
+
+ // Causes the subsequent `navigator.credentials.get()` to reject with the error.
+ returnError(error) {
+ if (error == "Success")
+ throw new Error("Success is not a valid error");
+ this.status_ = toMojoTokenStatus(error);
+ this.selected_identity_provider_config_url_ = null;
+ this.token_ = null;
+ this.returnPending_ = false;
+ }
+
+ // Causes the subsequent `navigator.credentials.get()` to return a pending promise
+ // that can be cancelled using `cancelTokenRequest()`.
+ returnPendingPromise() {
+ this.returnPending_ = true;
+ }
+
+ logoutRpsReturn(status) {
+ let validated = LogoutRpsStatus[status];
+ if (validated === undefined)
+ throw new Error("Invalid status: " + status);
+ this.logoutRpsStatus_ = validated;
+ }
+
+ // Implements
+ // RequestToken(array<IdentityProviderGetParameters> idp_get_params) =>
+ // (RequestTokenStatus status,
+ // url.mojom.Url? selected_identity_provider_config_url,
+ // string? token);
+ async requestToken(idp_get_params) {
+ if (this.returnPending_) {
+ this.pendingPromise_ = new Promise((resolve, reject) => {
+ this.pendingPromiseResolve_ = resolve;
+ });
+ return this.pendingPromise_;
+ }
+ return Promise.resolve({
+ status: this.status_,
+ selected_identity_provider_config_url: this.selected_identity_provider_config_url_,
+ token: this.token_
+ });
+ }
+
+ async cancelTokenRequest() {
+ this.pendingPromiseResolve_({
+ status: toMojoTokenStatus("ErrorCanceled"),
+ selected_identity_provider_config_url: null,
+ token: null
+ });
+ this.pendingPromiseResolve_ = null;
+ }
+
+ async logoutRps(logout_endpoints) {
+ return Promise.resolve({
+ status: this.logoutRpsStatus_
+ });
+ }
+
+ async setIdpSigninStatus(origin, status) {
+ }
+
+ async reset() {
+ this.token_ = null;
+ this.selected_identity_provider_config_url_ = null;
+ this.status_ = RequestTokenStatus.kError;
+ this.logoutRpsStatus_ = LogoutRpsStatus.kError;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+
+ // Clean up and reset mock stubs asynchronously, so that the blink side
+ // closes its proxies and notifies JS sensor objects before new test is
+ // started.
+ await new Promise(resolve => { step_timeout(resolve, 0); });
+ }
+}
diff --git a/testing/web-platform/tests/credential-management/support/fedcm-mojojs-helper.js b/testing/web-platform/tests/credential-management/support/fedcm-mojojs-helper.js
new file mode 100644
index 0000000000..40ab729b1f
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm-mojojs-helper.js
@@ -0,0 +1,20 @@
+// The testing infra for FedCM is loaded using mojo js shim. To enable these
+// tests the browser must be run with these options:
+//
+// --enable-blink-features=MojoJS,MojoJSTest
+
+import { MockFederatedAuthRequest } from './fedcm-mock.js';
+
+export function fedcm_mojo_mock_test(test_func, name, exception, properties) {
+ promise_test(async (t) => {
+ assert_implements(navigator.credentials, 'missing navigator.credentials');
+ const mock = new MockFederatedAuthRequest();
+ try {
+ await test_func(t, mock);
+ } catch (e) {
+ assert_equals(exception, e.message)
+ } finally {
+ await mock.reset();
+ }
+ }, name, properties);
+}
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/accounts.py b/testing/web-platform/tests/credential-management/support/fedcm/accounts.py
new file mode 100644
index 0000000000..088ce5967c
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/accounts.py
@@ -0,0 +1,24 @@
+def main(request, response):
+ if request.cookies.get(b"cookie") != b"1":
+ return (530, [], "Missing cookie")
+ if request.headers.get(b"Accept") != b"application/json":
+ return (531, [], "Wrong Accept")
+ if request.headers.get(b"Sec-Fetch-Dest") != b"webidentity":
+ return (532, [], "Wrong Sec-Fetch-Dest header")
+ if request.headers.get(b"Referer"):
+ return (533, [], "Should not have Referer")
+ if request.headers.get(b"Origin"):
+ return (534, [], "Should not have Origin")
+
+ return """
+{
+ "accounts": [{
+ "id": "1234",
+ "given_name": "John",
+ "name": "John Doe",
+ "email": "john_doe@idp.example",
+ "picture": "https://idp.example/profile/123",
+ "approved_clients": ["123", "456", "789"]
+ }]
+}
+"""
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py
new file mode 100644
index 0000000000..e6f6a77632
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py
@@ -0,0 +1,30 @@
+# 'import credential-management.support.fedcm.keys' does not work.
+import importlib
+keys = importlib.import_module("credential-management.support.fedcm.keys")
+
+def main(request, response):
+ if (request.GET.get(b'skip_checks', b'0') != b'1'):
+ if len(request.cookies) > 0:
+ return (530, [], "Cookie should not be sent to this endpoint")
+ if request.headers.get(b"Accept") != b"application/json":
+ return (531, [], "Wrong Accept")
+ if request.headers.get(b"Sec-Fetch-Dest") != b"webidentity":
+ return (532, [], "Wrong Sec-Fetch-Dest header")
+ if request.headers.get(b"Referer"):
+ return (533, [], "Should not have Referer")
+ if not request.headers.get(b"Origin"):
+ return (534, [], "Missing Origin")
+
+ counter = request.server.stash.take(keys.CLIENT_METADATA_COUNTER_KEY)
+ try:
+ counter = int(counter) + 1
+ except (TypeError, ValueError):
+ counter = 1
+
+ request.server.stash.put(keys.CLIENT_METADATA_COUNTER_KEY, str(counter).encode())
+
+ return """
+{{
+ "privacy_policy_url": "https://privacypolicy{0}.com"
+}}
+""".format(str(counter))
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py.headers b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py.headers
new file mode 100644
index 0000000000..7164e5f818
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py.headers
@@ -0,0 +1 @@
+Cache-Control: public, max-age=86400
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/client_metadata_clear_count.py b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata_clear_count.py
new file mode 100644
index 0000000000..3c31bf5077
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata_clear_count.py
@@ -0,0 +1,15 @@
+# 'import credential-management.support.fedcm.keys' does not work.
+import importlib
+keys = importlib.import_module("credential-management.support.fedcm.keys")
+
+def main(request, response):
+ client_metadata_url = "/credential-management/support/fedcm/client_metadata.py"
+ counter = request.server.stash.take(keys.CLIENT_METADATA_COUNTER_KEY,
+ client_metadata_url)
+
+ try:
+ counter = counter.decode()
+ except (UnicodeDecodeError, AttributeError):
+ pass
+
+ return counter
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/intercept_service_worker.js b/testing/web-platform/tests/credential-management/support/fedcm/intercept_service_worker.js
new file mode 100644
index 0000000000..773e38fd21
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/intercept_service_worker.js
@@ -0,0 +1,10 @@
+var num_overridden = 0;
+
+self.addEventListener('fetch', event => {
+ const url = event.request.url;
+ if (url.indexOf('query_service_worker_intercepts.html') != -1) {
+ event.respondWith(new Response(num_overridden));
+ } else if (url.indexOf('credential-management/support') != -1) {
+ ++num_overridden;
+ }
+});
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/keys.py b/testing/web-platform/tests/credential-management/support/fedcm/keys.py
new file mode 100644
index 0000000000..6b7d67e21e
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/keys.py
@@ -0,0 +1,2 @@
+CLIENT_METADATA_COUNTER_KEY = b"bdc14e3e-b8bc-44a1-8eec-78da5fdacbc3"
+MANIFEST_URL_IN_MANIFEST_LIST_KEY = b"7f3f7478-b7f0-41c5-b357-f3ac16f5f25a"
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/manifest-not-in-list.json b/testing/web-platform/tests/credential-management/support/fedcm/manifest-not-in-list.json
new file mode 100644
index 0000000000..c66903cfd2
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/manifest-not-in-list.json
@@ -0,0 +1,5 @@
+{
+ "accounts_endpoint": "accounts.py",
+ "client_metadata_endpoint": "client_metadata.py",
+ "id_assertion_endpoint": "token.py"
+}
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/manifest.py b/testing/web-platform/tests/credential-management/support/fedcm/manifest.py
new file mode 100644
index 0000000000..3e72faf75a
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/manifest.py
@@ -0,0 +1,20 @@
+def main(request, response):
+ if len(request.cookies) > 0:
+ return (530, [], "Cookie should not be sent to manifest endpoint")
+ if request.headers.get(b"Accept") != b"application/json":
+ return (531, [], "Wrong Accept")
+ if request.headers.get(b"Sec-Fetch-Dest") != b"webidentity":
+ return (532, [], "Wrong Sec-Fetch-Dest header")
+ if request.headers.get(b"Referer"):
+ return (533, [], "Should not have Referer")
+ if request.headers.get(b"Origin"):
+ return (534, [], "Should not have Origin")
+
+ return """
+{
+ "accounts_endpoint": "accounts.py",
+ "client_metadata_endpoint": "client_metadata.py",
+ "id_assertion_endpoint": "token.py",
+ "revocation_endpoint": "revoke.py"
+}
+"""
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_accounts.json b/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_accounts.json
new file mode 100644
index 0000000000..590704cfeb
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_accounts.json
@@ -0,0 +1,5 @@
+{
+ "accounts_endpoint": "/common/redirect.py?location=/credential-management/support/fedcm/accounts.py",
+ "client_metadata_endpoint": "client_metadata.py",
+ "id_assertion_endpoint": "token.py"
+}
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_token.json b/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_token.json
new file mode 100644
index 0000000000..190420736d
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_token.json
@@ -0,0 +1,6 @@
+{
+ "accounts_endpoint": "accounts.py",
+ "client_metadata_endpoint": "client_metadata.py",
+ "id_assertion_endpoint": "/common/redirect.py?location=/credential-management/support/fedcm/token.py&status=308",
+ "revocation_endpoint": "revoke.py"
+}
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/select_manifest_in_root_manifest.py b/testing/web-platform/tests/credential-management/support/fedcm/select_manifest_in_root_manifest.py
new file mode 100644
index 0000000000..d4f1efff6a
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/select_manifest_in_root_manifest.py
@@ -0,0 +1,17 @@
+import importlib
+from urllib.parse import urlsplit
+
+# 'import credential-management.support.fedcm.keys' does not work.
+keys = importlib.import_module("credential-management.support.fedcm.keys")
+
+def main(request, response):
+ root_manifest_url = "/.well-known/web-identity"
+
+ # Clear stash so that a new value can be written.
+ request.server.stash.take(keys.MANIFEST_URL_IN_MANIFEST_LIST_KEY, root_manifest_url)
+
+ request.server.stash.put(keys.MANIFEST_URL_IN_MANIFEST_LIST_KEY,
+ request.GET.first(b"manifest_url", b""),
+ root_manifest_url)
+
+ return root_manifest_url
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/simple.html b/testing/web-platform/tests/credential-management/support/fedcm/simple.html
new file mode 100644
index 0000000000..d62419ce8a
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/simple.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<html><body>
+Simple
diff --git a/testing/web-platform/tests/credential-management/support/fedcm/token.py b/testing/web-platform/tests/credential-management/support/fedcm/token.py
new file mode 100644
index 0000000000..867dab9592
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/fedcm/token.py
@@ -0,0 +1,24 @@
+def main(request, response):
+ if request.cookies.get(b"cookie") != b"1":
+ return (530, [], "Missing cookie")
+ if request.method != "POST":
+ return (531, [], "Method is not POST")
+ if request.headers.get(b"Content-Type") != b"application/x-www-form-urlencoded":
+ return (532, [], "Wrong Content-Type")
+ if request.headers.get(b"Accept") != b"application/json":
+ return (533, [], "Wrong Accept")
+ if request.headers.get(b"Sec-Fetch-Dest") != b"webidentity":
+ return (500, [], "Wrong Sec-Fetch-Dest header")
+ if request.headers.get(b"Referer"):
+ return (534, [], "Should not have Referer")
+ if not request.headers.get(b"Origin"):
+ return (535, [], "Missing Origin")
+
+ if not request.POST.get(b"client_id"):
+ return (536, [], "Missing 'client_id' POST parameter")
+ if not request.POST.get(b"account_id"):
+ return (537, [], "Missing 'account_id' POST parameter")
+ if not request.POST.get(b"disclosure_text_shown"):
+ return (538, [], "Missing 'disclosure_text_shown' POST parameter")
+
+ return "{\"token\": \"token\"}"
diff --git a/testing/web-platform/tests/credential-management/support/federatedcredential-get.html b/testing/web-platform/tests/credential-management/support/federatedcredential-get.html
new file mode 100644
index 0000000000..476f32688f
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/federatedcredential-get.html
@@ -0,0 +1,17 @@
+<script>
+ navigator.credentials.get({ 'federated': { 'providers': ['https://example.com' ] } })
+ .then(c => {
+ window.parent.postMessage({
+ "status": "resolved",
+ "credential": c,
+ "exception": null
+ }, "*");
+ })
+ .catch(omg => {
+ window.parent.postMessage({
+ "status": "rejected",
+ "credential": null,
+ "exception": omg.name
+ }, "*");
+ });
+</script>
diff --git a/testing/web-platform/tests/credential-management/support/otpcredential-helper.js b/testing/web-platform/tests/credential-management/support/otpcredential-helper.js
new file mode 100644
index 0000000000..e07e9f5be3
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/otpcredential-helper.js
@@ -0,0 +1,114 @@
+// These tests rely on the User Agent providing an implementation of
+// MockWebOTPService.
+//
+// In Chromium-based browsers this implementation is provided by a polyfill
+// in order to reduce the amount of test-only code shipped to users. To enable
+// these tests the browser must be run with these options:
+// // --enable-blink-features=MojoJS,MojoJSTest
+
+import {isChromiumBased} from '/resources/test-only-api.m.js';
+
+/**
+ * This enumeration is used by WebOTP WPTs to control mock backend behavior.
+ * See MockWebOTPService below.
+ */
+export const Status = {
+ SUCCESS: 0,
+ UNHANDLED_REQUEST: 1,
+ CANCELLED: 2,
+ ABORTED: 3,
+};
+
+/**
+ * A interface which must be implemented by browsers to support WebOTP WPTs.
+ */
+export class MockWebOTPService {
+ /**
+ * Accepts a function to be invoked in response to the next OTP request
+ * received by the mock. The (optionally async) function, when executed, must
+ * return an object with a `status` field holding one of the `Status` values
+ * defined above, and -- if successful -- an `otp` field containing a
+ * simulated OTP string.
+ *
+ * Tests will call this method directly to inject specific response behavior
+ * into the browser-specific mock implementation.
+ */
+ async handleNextOTPRequest(responseFunc) {}
+}
+
+/**
+ * Returns a Promise resolving to a browser-specific MockWebOTPService subclass
+ * instance if one is available.
+ */
+async function createBrowserSpecificMockImpl() {
+ if (isChromiumBased) {
+ return await createChromiumMockImpl();
+ }
+ throw new Error('Unsupported browser.');
+}
+
+const asyncMock = createBrowserSpecificMockImpl();
+
+export function expectOTPRequest() {
+ return {
+ async andReturn(callback) {
+ const mock = await asyncMock;
+ mock.handleNextOTPRequest(callback);
+ }
+ }
+}
+
+/**
+ * Instantiates a Chromium-specific subclass of MockWebOTPService.
+ */
+async function createChromiumMockImpl() {
+ const {SmsStatus, WebOTPService, WebOTPServiceReceiver} = await import(
+ '/gen/third_party/blink/public/mojom/sms/webotp_service.mojom.m.js');
+ const MockWebOTPServiceChromium = class extends MockWebOTPService {
+ constructor() {
+ super();
+ this.mojoReceiver_ = new WebOTPServiceReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(WebOTPService.$interfaceName);
+ this.interceptor_.oninterfacerequest = (e) => {
+ this.mojoReceiver_.$.bindHandle(e.handle);
+ };
+ this.interceptor_.start();
+ this.requestHandlers_ = [];
+ Object.freeze(this);
+ }
+
+ handleNextOTPRequest(responseFunc) {
+ this.requestHandlers_.push(responseFunc);
+ }
+
+ async receive() {
+ if (this.requestHandlers_.length == 0) {
+ throw new Error('Mock received unexpected OTP request.');
+ }
+
+ const responseFunc = this.requestHandlers_.shift();
+ const response = await responseFunc();
+ switch (response.status) {
+ case Status.SUCCESS:
+ if (typeof response.otp != 'string') {
+ throw new Error('Mock success results require an OTP string.');
+ }
+ return {status: SmsStatus.kSuccess, otp: response.otp};
+ case Status.UNHANDLED_REQUEST:
+ return {status: SmsStatus.kUnhandledRequest};
+ case Status.CANCELLED:
+ return {status: SmsStatus.kCancelled};
+ case Status.ABORTED:
+ return {status: SmsStatus.kAborted};
+ default:
+ throw new Error(
+ `Mock result contains unknown status: ${response.status}`);
+ }
+ }
+
+ async abort() {}
+ };
+ return new MockWebOTPServiceChromium();
+}
+
diff --git a/testing/web-platform/tests/credential-management/support/otpcredential-iframe.html b/testing/web-platform/tests/credential-management/support/otpcredential-iframe.html
new file mode 100644
index 0000000000..4affc00ecd
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/otpcredential-iframe.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script type="module">
+import {Status, expectOTPRequest} from './otpcredential-helper.js';
+
+// Loading otpcredential-iframe.html in the test will make an OTPCredentials
+// call on load, and trigger a postMessage upon completion.
+//
+// message {
+// string result: "Pass" | "Fail"
+// string code: credentials.code
+// string errorType: error.name
+// }
+
+window.onload = async () => {
+ try {
+ await expectOTPRequest().andReturn(
+ () => ({status: Status.SUCCESS, otp: "ABC123"}));
+ const credentials =
+ await navigator.credentials.get({otp: {transport: ["sms"]}});
+ window.parent.postMessage({result: "Pass", code: credentials.code}, '*');
+ } catch (error) {
+ window.parent.postMessage({result: "Fail", errorType: error.name}, '*');
+ }
+}
+
+</script>
diff --git a/testing/web-platform/tests/credential-management/support/passwordcredential-get.html b/testing/web-platform/tests/credential-management/support/passwordcredential-get.html
new file mode 100644
index 0000000000..0ec584d73d
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/passwordcredential-get.html
@@ -0,0 +1,17 @@
+<script>
+ navigator.credentials.get({ 'password': true })
+ .then(c => {
+ window.parent.postMessage({
+ "status": "resolved",
+ "credential": c,
+ "exception": null
+ }, "*");
+ })
+ .catch(omg => {
+ window.parent.postMessage({
+ "status": "rejected",
+ "credential": null,
+ "exception": omg.name
+ }, "*");
+ });
+</script>
diff --git a/testing/web-platform/tests/credential-management/support/set_cookie b/testing/web-platform/tests/credential-management/support/set_cookie
new file mode 100644
index 0000000000..1080b366e4
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/set_cookie
@@ -0,0 +1,12 @@
+<html>
+<body>
+<script>
+// The important part of this page are the headers.
+
+// If this page was opened as a popup, notify the opener.
+if (window.opener) {
+ window.opener.postMessage("done_loading", "*");
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/credential-management/support/set_cookie.headers b/testing/web-platform/tests/credential-management/support/set_cookie.headers
new file mode 100644
index 0000000000..cf5ea7fff9
--- /dev/null
+++ b/testing/web-platform/tests/credential-management/support/set_cookie.headers
@@ -0,0 +1,2 @@
+Content-Type: text/html
+Set-Cookie: cookie=1; SameSite=Strict; Secure