| <!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 {alt_manifest_origin, |
| default_request_options, |
| default_alt_request_options, |
| request_options_with_auto_reauthn, |
| 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); |
| }); |
| } |
| |
| async function createIframeWithPermissionPolicyAndWaitForMessage(test, iframeUrl) { |
| const messageWatcher = new EventWatcher(test, window, "message"); |
| let iframe = document.createElement("iframe"); |
| iframe.src = iframeUrl; |
| iframe.allow = "identity-credentials-get"; |
| document.body.appendChild(iframe); |
| const message = await messageWatcher.wait_for("message"); |
| return message.data; |
| } |
| |
| 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'); |
| |
| fedcm_test(async t => { |
| const cred = await navigator.credentials.get(default_alt_request_options()); |
| assert_equals(cred.token, "token"); |
| |
| const iframe_in_idp_scope = `${alt_manifest_origin}/\ |
| credential-management/support/fedcm/userinfo-iframe.html`; |
| const message = await createIframeWithPermissionPolicyAndWaitForMessage(t, iframe_in_idp_scope); |
| assert_equals(message.result, "Pass"); |
| assert_equals(message.numAccounts, 1); |
| assert_equals(message.firstAccountEmail, "john_doe@idp.example"); |
| assert_equals(message.firstAccountName, "John Doe"); |
| assert_equals(message.firstAccountGivenName, "John"); |
| assert_equals(message.firstAccountPicture, "https://idp.example/profile/123"); |
| }, 'Test basic User InFo API flow'); |
| |
| fedcm_test(async t => { |
| const cred = await navigator.credentials.get(default_alt_request_options()); |
| assert_equals(cred.token, "token"); |
| |
| const iframe_in_idp_scope = `support/fedcm/userinfo-iframe.html`; |
| const message = await createIframeWithPermissionPolicyAndWaitForMessage(t, iframe_in_idp_scope); |
| assert_equals(message.result, "Fail"); |
| }, 'Test that User Info API only works when invoked from iframe that is same origin as the IDP'); |
| |
| fedcm_test(async t => { |
| const cred = await navigator.credentials.get(default_alt_request_options()); |
| assert_equals(cred.token, "token"); |
| |
| try { |
| const manifest_path = `${alt_manifest_origin}/\ |
| credential-management/support/fedcm/manifest.py`; |
| const user_info = await IdentityProvider.getUserInfo({ |
| configURL: manifest_path, |
| // Approved client |
| clientId: '123', |
| }); |
| assert_unreached("Failure message"); |
| } catch (error) { |
| assert_equals(error.message, "UserInfo request must be initiated from a frame that is the same origin with the provider."); |
| // Expect failure |
| } |
| }, 'Test that User Info API does not work in the top frame'); |
| |
| fedcm_test(async t => { |
| let test_options = request_options_with_auto_reauthn("manifest_with_single_account.json"); |
| await select_manifest(t, test_options); |
| |
| // Signs in john_doe so that they will be a returning user |
| let cred = await navigator.credentials.get(test_options); |
| assert_equals(cred.token, "account_id=john_doe"); |
| |
| test_options = request_options_with_auto_reauthn("manifest_with_two_accounts.json"); |
| await select_manifest(t, test_options); |
| |
| // There are two accounts "Jane" and "John" returned in that order. Without |
| // auto re-authn, the first account "Jane" would be selected and an token |
| // would be issued to that account. However, since "John" is returning and |
| // "Jane" is a new user, the second account "John" will be selected. |
| cred = await navigator.credentials.get(test_options); |
| assert_equals(cred.token, "account_id=john_doe"); |
| }, "Test that the returning account from the two accounts will be auto re-authenticated."); |
| |
| </script> |