summaryrefslogtreecommitdiffstats
path: root/toolkit/components/credentialmanagement
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/credentialmanagement
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/credentialmanagement')
-rw-r--r--toolkit/components/credentialmanagement/IdentityCredentialPromptService.sys.mjs751
-rw-r--r--toolkit/components/credentialmanagement/IdentityCredentialStorageService.cpp887
-rw-r--r--toolkit/components/credentialmanagement/IdentityCredentialStorageService.h185
-rw-r--r--toolkit/components/credentialmanagement/components.conf27
-rw-r--r--toolkit/components/credentialmanagement/moz.build34
-rw-r--r--toolkit/components/credentialmanagement/nsIIdentityCredentialPromptService.idl22
-rw-r--r--toolkit/components/credentialmanagement/nsIIdentityCredentialStorageService.idl38
-rw-r--r--toolkit/components/credentialmanagement/tests/browser/browser.toml18
-rw-r--r--toolkit/components/credentialmanagement/tests/browser/browser_account_dialog.js462
-rw-r--r--toolkit/components/credentialmanagement/tests/browser/browser_policy_dialog.js204
-rw-r--r--toolkit/components/credentialmanagement/tests/browser/browser_provider_dialog.js396
-rw-r--r--toolkit/components/credentialmanagement/tests/browser/custom.svg8
-rw-r--r--toolkit/components/credentialmanagement/tests/browser/head.js2
-rw-r--r--toolkit/components/credentialmanagement/tests/xpcshell/head.js9
-rw-r--r--toolkit/components/credentialmanagement/tests/xpcshell/test_identity_credential_storage_service.js300
-rw-r--r--toolkit/components/credentialmanagement/tests/xpcshell/xpcshell.toml5
16 files changed, 3348 insertions, 0 deletions
diff --git a/toolkit/components/credentialmanagement/IdentityCredentialPromptService.sys.mjs b/toolkit/components/credentialmanagement/IdentityCredentialPromptService.sys.mjs
new file mode 100644
index 0000000000..3a6b43b5b1
--- /dev/null
+++ b/toolkit/components/credentialmanagement/IdentityCredentialPromptService.sys.mjs
@@ -0,0 +1,751 @@
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "IDNService",
+ "@mozilla.org/network/idn-service;1",
+ "nsIIDNService"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "SELECT_FIRST_IN_UI_LISTS",
+ "dom.security.credentialmanagement.identity.select_first_in_ui_lists",
+ false
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ GeckoViewIdentityCredential:
+ "resource://gre/modules/GeckoViewIdentityCredential.sys.mjs",
+});
+const BEST_HEADER_ICON_SIZE = 16;
+const BEST_ICON_SIZE = 32;
+
+// Used in plain mochitests to enable automation
+function fulfilledPromiseFromFirstListElement(list) {
+ if (list.length) {
+ return Promise.resolve(0);
+ }
+ return Promise.reject();
+}
+
+// Converts a "blob" to a data URL
+function blobToDataUrl(blob) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.addEventListener("loadend", function () {
+ if (reader.error) {
+ reject(reader.error);
+ }
+ resolve(reader.result);
+ });
+ reader.readAsDataURL(blob);
+ });
+}
+
+// Converts a URL into a data:// url, suitable for inclusion in Chrome UI
+async function fetchToDataUrl(url) {
+ let result = await fetch(url);
+ if (!result.ok) {
+ throw result.status;
+ }
+ let blob = await result.blob();
+ let data = blobToDataUrl(blob);
+ return data;
+}
+
+/**
+ * Class implementing the nsIIdentityCredentialPromptService
+ * */
+export class IdentityCredentialPromptService {
+ classID = Components.ID("{936007db-a957-4f1d-a23d-f7d9403223e6}");
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIIdentityCredentialPromptService",
+ ]);
+
+ async loadIconFromManifest(
+ providerManifest,
+ bestIconSize = BEST_ICON_SIZE,
+ defaultIcon = null
+ ) {
+ if (providerManifest?.branding?.icons?.length) {
+ // Prefer a vector icon, then an exactly sized icon,
+ // the the largest icon available.
+ let iconsArray = providerManifest.branding.icons;
+ let vectorIcon = iconsArray.find(icon => !icon.size);
+ if (vectorIcon) {
+ return fetchToDataUrl(vectorIcon.url);
+ }
+ let exactIcon = iconsArray.find(icon => icon.size == bestIconSize);
+ if (exactIcon) {
+ return fetchToDataUrl(exactIcon.url);
+ }
+ let biggestIcon = iconsArray.sort(
+ (iconA, iconB) => iconB.size - iconA.size
+ )[0];
+ if (biggestIcon) {
+ return fetchToDataUrl(biggestIcon.url);
+ }
+ }
+
+ return defaultIcon;
+ }
+
+ /**
+ * Ask the user, using a PopupNotification, to select an Identity Provider from a provided list.
+ * @param {BrowsingContext} browsingContext - The BrowsingContext of the document requesting an identity credential via navigator.credentials.get()
+ * @param {IdentityProviderConfig[]} identityProviders - The list of identity providers the user selects from
+ * @param {IdentityProviderAPIConfig[]} identityManifests - The manifests corresponding 1-to-1 with identityProviders
+ * @returns {Promise<number>} The user-selected identity provider
+ */
+ async showProviderPrompt(
+ browsingContext,
+ identityProviders,
+ identityManifests
+ ) {
+ // For testing only.
+ if (lazy.SELECT_FIRST_IN_UI_LISTS) {
+ return fulfilledPromiseFromFirstListElement(identityProviders);
+ }
+ let browser = browsingContext.top.embedderElement;
+ if (!browser) {
+ throw new Error("Null browser provided");
+ }
+
+ if (identityProviders.length != identityManifests.length) {
+ throw new Error("Mismatch argument array length");
+ }
+
+ // Map each identity manifest to a promise that would resolve to its icon
+ let promises = identityManifests.map(async providerManifest => {
+ // we don't need to set default icon because default icon is already set on popup-notifications.inc
+ const iconResult = await this.loadIconFromManifest(providerManifest);
+ // If we didn't have a manifest with an icon, push a rejection.
+ // This will be replaced with the default icon.
+ return iconResult ? iconResult : Promise.reject();
+ });
+
+ const providerNames = identityManifests.map(
+ providerManifest => providerManifest?.branding?.name
+ );
+
+ // Sanity check that we made one promise per IDP.
+ if (promises.length != identityManifests.length) {
+ throw new Error("Mismatch promise array length");
+ }
+
+ let iconResults = await Promise.allSettled(promises);
+ if (AppConstants.platform === "android") {
+ const providers = [];
+ for (const [providerIndex, provider] of identityProviders.entries()) {
+ let providerURL = new URL(provider.configURL);
+ let displayDomain = lazy.IDNService.convertToDisplayIDN(
+ providerURL.host,
+ {}
+ );
+
+ let iconResult = iconResults[providerIndex];
+ const data = {
+ id: providerIndex,
+ icon: iconResult.value,
+ name: providerNames[providerIndex],
+ domain: displayDomain,
+ };
+ providers.push(data);
+ }
+
+ return new Promise((resolve, reject) => {
+ lazy.GeckoViewIdentityCredential.onShowProviderPrompt(
+ browsingContext,
+ providers,
+ resolve,
+ reject
+ );
+ });
+ }
+
+ // Localize all strings to be used
+ // Bug 1797154 - Convert localization calls to use the async formatValues.
+ let localization = new Localization(
+ ["browser/identityCredentialNotification.ftl"],
+ true
+ );
+ let headerMessage = localization.formatValueSync(
+ "identity-credential-header-providers"
+ );
+ let [accept, cancel] = localization.formatMessagesSync([
+ { id: "identity-credential-accept-button" },
+ { id: "identity-credential-cancel-button" },
+ ]);
+
+ let cancelLabel = cancel.attributes.find(x => x.name == "label").value;
+ let cancelKey = cancel.attributes.find(x => x.name == "accesskey").value;
+ let acceptLabel = accept.attributes.find(x => x.name == "label").value;
+ let acceptKey = accept.attributes.find(x => x.name == "accesskey").value;
+
+ // Build the choices into the panel
+ let listBox = browser.ownerDocument.getElementById(
+ "identity-credential-provider-selector-container"
+ );
+ while (listBox.firstChild) {
+ listBox.removeChild(listBox.lastChild);
+ }
+ let itemTemplate = browser.ownerDocument.getElementById(
+ "template-credential-provider-list-item"
+ );
+ for (const [providerIndex, provider] of identityProviders.entries()) {
+ let providerURL = new URL(provider.configURL);
+ let displayDomain = lazy.IDNService.convertToDisplayIDN(
+ providerURL.host,
+ {}
+ );
+ let newItem = itemTemplate.content.firstElementChild.cloneNode(true);
+
+ // Create the radio button,
+ // including the check callback and the initial state
+ let newRadio = newItem.getElementsByClassName(
+ "identity-credential-list-item-radio"
+ )[0];
+ newRadio.value = providerIndex;
+ newRadio.addEventListener("change", function (event) {
+ for (let item of listBox.children) {
+ item.classList.remove("checked");
+ }
+ if (event.target.checked) {
+ event.target.parentElement.classList.add("checked");
+ }
+ });
+ if (providerIndex == 0) {
+ newRadio.checked = true;
+ newItem.classList.add("checked");
+ }
+
+ // Set the icon to the data url if we have one
+ let iconResult = iconResults[providerIndex];
+ if (iconResult.status == "fulfilled") {
+ let newIcon = newItem.getElementsByClassName(
+ "identity-credential-list-item-icon"
+ )[0];
+ newIcon.setAttribute("src", iconResult.value);
+ }
+
+ // Set the words that the user sees in the selection
+ newItem.getElementsByClassName(
+ "identity-credential-list-item-label-primary"
+ )[0].textContent = providerNames[providerIndex] || displayDomain;
+ newItem.getElementsByClassName(
+ "identity-credential-list-item-label-secondary"
+ )[0].hidden = true;
+
+ if (providerNames[providerIndex] && displayDomain) {
+ newItem.getElementsByClassName(
+ "identity-credential-list-item-label-secondary"
+ )[0].hidden = false;
+ newItem.getElementsByClassName(
+ "identity-credential-list-item-label-secondary"
+ )[0].textContent = displayDomain;
+ }
+
+ // Add the new item to the DOM!
+ listBox.append(newItem);
+ }
+
+ // Create a new promise to wrap the callbacks of the popup buttons
+ return new Promise((resolve, reject) => {
+ // Construct the necessary arguments for notification behavior
+ let options = {
+ hideClose: true,
+ eventCallback: (topic, nextRemovalReason, isCancel) => {
+ if (topic == "removed" && isCancel) {
+ reject();
+ }
+ },
+ };
+ let mainAction = {
+ label: acceptLabel,
+ accessKey: acceptKey,
+ callback(event) {
+ let result = listBox.querySelector(
+ ".identity-credential-list-item-radio:checked"
+ ).value;
+ resolve(parseInt(result));
+ },
+ };
+ let secondaryActions = [
+ {
+ label: cancelLabel,
+ accessKey: cancelKey,
+ callback(event) {
+ reject();
+ },
+ },
+ ];
+
+ // Show the popup
+ browser.ownerDocument.getElementById(
+ "identity-credential-provider"
+ ).hidden = false;
+ browser.ownerDocument.getElementById(
+ "identity-credential-policy"
+ ).hidden = true;
+ browser.ownerDocument.getElementById(
+ "identity-credential-account"
+ ).hidden = true;
+ browser.ownerDocument.getElementById(
+ "identity-credential-header"
+ ).hidden = true;
+ browser.ownerGlobal.PopupNotifications.show(
+ browser,
+ "identity-credential",
+ headerMessage,
+ "identity-credential-notification-icon",
+ mainAction,
+ secondaryActions,
+ options
+ );
+ });
+ }
+
+ /**
+ * Ask the user, using a PopupNotification, to approve or disapprove of the policies of the Identity Provider.
+ * @param {BrowsingContext} browsingContext - The BrowsingContext of the document requesting an identity credential via navigator.credentials.get()
+ * @param {IdentityProviderConfig} identityProvider - The Identity Provider that the user has selected to use
+ * @param {IdentityProviderAPIConfig} identityManifest - The Identity Provider that the user has selected to use's manifest
+ * @param {IdentityCredentialMetadata} identityCredentialMetadata - The metadata displayed to the user
+ * @returns {Promise<bool>} A boolean representing the user's acceptance of the metadata.
+ */
+ async showPolicyPrompt(
+ browsingContext,
+ identityProvider,
+ identityManifest,
+ identityCredentialMetadata
+ ) {
+ // For testing only.
+ if (lazy.SELECT_FIRST_IN_UI_LISTS) {
+ return Promise.resolve(true);
+ }
+ if (
+ !identityCredentialMetadata ||
+ !identityCredentialMetadata.privacy_policy_url ||
+ !identityCredentialMetadata.terms_of_service_url
+ ) {
+ return Promise.resolve(true);
+ }
+
+ let iconResult = await this.loadIconFromManifest(
+ identityManifest,
+ BEST_HEADER_ICON_SIZE,
+ "chrome://global/skin/icons/defaultFavicon.svg"
+ );
+
+ const providerName = identityManifest?.branding?.name;
+
+ return new Promise(function (resolve, reject) {
+ let browser = browsingContext.top.embedderElement;
+ if (!browser) {
+ reject();
+ return;
+ }
+
+ let providerURL = new URL(identityProvider.configURL);
+ let providerDisplayDomain = lazy.IDNService.convertToDisplayIDN(
+ providerURL.host,
+ {}
+ );
+ let currentBaseDomain =
+ browsingContext.currentWindowContext.documentPrincipal.baseDomain;
+
+ if (AppConstants.platform === "android") {
+ lazy.GeckoViewIdentityCredential.onShowPolicyPrompt(
+ browsingContext,
+ identityCredentialMetadata.privacy_policy_url,
+ identityCredentialMetadata.terms_of_service_url,
+ providerDisplayDomain,
+ currentBaseDomain,
+ iconResult,
+ resolve,
+ reject
+ );
+ } else {
+ // Localize the description
+ // Bug 1797154 - Convert localization calls to use the async formatValues.
+ let localization = new Localization(
+ ["browser/identityCredentialNotification.ftl"],
+ true
+ );
+ let [accept, cancel] = localization.formatMessagesSync([
+ { id: "identity-credential-accept-button" },
+ { id: "identity-credential-cancel-button" },
+ ]);
+
+ let cancelLabel = cancel.attributes.find(x => x.name == "label").value;
+ let cancelKey = cancel.attributes.find(
+ x => x.name == "accesskey"
+ ).value;
+ let acceptLabel = accept.attributes.find(x => x.name == "label").value;
+ let acceptKey = accept.attributes.find(
+ x => x.name == "accesskey"
+ ).value;
+
+ let title = localization.formatValueSync(
+ "identity-credential-policy-title",
+ {
+ provider: providerName || providerDisplayDomain,
+ }
+ );
+
+ if (iconResult) {
+ let headerIcon = browser.ownerDocument.getElementsByClassName(
+ "identity-credential-header-icon"
+ )[0];
+ headerIcon.setAttribute("src", iconResult);
+ }
+
+ const headerText = browser.ownerDocument.getElementById(
+ "identity-credential-header-text"
+ );
+ headerText.textContent = title;
+
+ let privacyPolicyAnchor = browser.ownerDocument.getElementById(
+ "identity-credential-privacy-policy"
+ );
+ privacyPolicyAnchor.href =
+ identityCredentialMetadata.privacy_policy_url;
+ let termsOfServiceAnchor = browser.ownerDocument.getElementById(
+ "identity-credential-terms-of-service"
+ );
+ termsOfServiceAnchor.href =
+ identityCredentialMetadata.terms_of_service_url;
+
+ // Populate the content of the policy panel
+ let description = browser.ownerDocument.getElementById(
+ "identity-credential-policy-explanation"
+ );
+ browser.ownerDocument.l10n.setAttributes(
+ description,
+ "identity-credential-policy-description",
+ {
+ host: currentBaseDomain,
+ provider: providerDisplayDomain,
+ }
+ );
+
+ // Construct the necessary arguments for notification behavior
+ let options = {
+ hideClose: true,
+ eventCallback: (topic, nextRemovalReason, isCancel) => {
+ if (topic == "removed" && isCancel) {
+ reject();
+ }
+ },
+ };
+ let mainAction = {
+ label: acceptLabel,
+ accessKey: acceptKey,
+ callback(event) {
+ resolve(true);
+ },
+ };
+ let secondaryActions = [
+ {
+ label: cancelLabel,
+ accessKey: cancelKey,
+ callback(event) {
+ resolve(false);
+ },
+ },
+ ];
+
+ // Show the popup
+ let ownerDocument = browser.ownerDocument;
+ ownerDocument.getElementById(
+ "identity-credential-provider"
+ ).hidden = true;
+ ownerDocument.getElementById(
+ "identity-credential-policy"
+ ).hidden = false;
+ ownerDocument.getElementById(
+ "identity-credential-account"
+ ).hidden = true;
+ ownerDocument.getElementById(
+ "identity-credential-header"
+ ).hidden = false;
+ browser.ownerGlobal.PopupNotifications.show(
+ browser,
+ "identity-credential",
+ "",
+ "identity-credential-notification-icon",
+ mainAction,
+ secondaryActions,
+ options
+ );
+ }
+ });
+ }
+
+ /**
+ * Ask the user, using a PopupNotification, to select an account from a provided list.
+ * @param {BrowsingContext} browsingContext - The BrowsingContext of the document requesting an identity credential via navigator.credentials.get()
+ * @param {IdentityProviderAccountList} accountList - The list of accounts the user selects from
+ * @param {IdentityProviderConfig} provider - The selected identity provider
+ * @param {IdentityProviderAPIConfig} providerManifest - The manifest of the selected identity provider
+ * @returns {Promise<IdentityProviderAccount>} The user-selected account
+ */
+ async showAccountListPrompt(
+ browsingContext,
+ accountList,
+ provider,
+ providerManifest
+ ) {
+ // For testing only.
+ if (lazy.SELECT_FIRST_IN_UI_LISTS) {
+ return fulfilledPromiseFromFirstListElement(accountList.accounts);
+ }
+
+ let browser = browsingContext.top.embedderElement;
+ if (!browser) {
+ throw new Error("Null browser provided");
+ }
+
+ // Map to an array of promises that resolve to a data URL,
+ // encoding the corresponding account's picture
+ let promises = accountList.accounts.map(async account => {
+ if (!account?.picture) {
+ throw new Error("Missing picture");
+ }
+ return fetchToDataUrl(account.picture);
+ });
+
+ // Sanity check that we made one promise per account.
+ if (promises.length != accountList.accounts.length) {
+ throw new Error("Incorrect number of promises obtained");
+ }
+
+ let pictureResults = await Promise.allSettled(promises);
+
+ // Localize all strings to be used
+ // Bug 1797154 - Convert localization calls to use the async formatValues.
+ let localization = new Localization(
+ ["browser/identityCredentialNotification.ftl"],
+ true
+ );
+ const providerName = providerManifest?.branding?.name;
+ let providerURL = new URL(provider.configURL);
+ let displayDomain = lazy.IDNService.convertToDisplayIDN(
+ providerURL.host,
+ {}
+ );
+
+ let headerIconResult = await this.loadIconFromManifest(
+ providerManifest,
+ BEST_HEADER_ICON_SIZE,
+ "chrome://global/skin/icons/defaultFavicon.svg"
+ );
+
+ if (AppConstants.platform === "android") {
+ const accounts = [];
+
+ for (const [accountIndex, account] of accountList.accounts.entries()) {
+ var picture = "";
+ let pictureResult = pictureResults[accountIndex];
+ if (pictureResult.status == "fulfilled") {
+ picture = pictureResult.value;
+ }
+ account.name;
+ account.email;
+ const data = {
+ id: accountIndex,
+ icon: picture,
+ name: account.name,
+ email: account.email,
+ };
+ accounts.push(data);
+ console.log(data);
+ }
+
+ const provider = {
+ name: providerName || displayDomain,
+ domain: displayDomain,
+ icon: headerIconResult,
+ };
+
+ const result = {
+ provider,
+ accounts,
+ };
+
+ return new Promise((resolve, reject) => {
+ lazy.GeckoViewIdentityCredential.onShowAccountsPrompt(
+ browsingContext,
+ result,
+ resolve,
+ reject
+ );
+ });
+ }
+
+ let headerMessage = localization.formatValueSync(
+ "identity-credential-header-accounts",
+ {
+ provider: providerName || displayDomain,
+ }
+ );
+
+ let [accept, cancel] = localization.formatMessagesSync([
+ { id: "identity-credential-sign-in-button" },
+ { id: "identity-credential-cancel-button" },
+ ]);
+
+ let cancelLabel = cancel.attributes.find(x => x.name == "label").value;
+ let cancelKey = cancel.attributes.find(x => x.name == "accesskey").value;
+ let acceptLabel = accept.attributes.find(x => x.name == "label").value;
+ let acceptKey = accept.attributes.find(x => x.name == "accesskey").value;
+
+ // Build the choices into the panel
+ let listBox = browser.ownerDocument.getElementById(
+ "identity-credential-account-selector-container"
+ );
+ while (listBox.firstChild) {
+ listBox.removeChild(listBox.lastChild);
+ }
+ let itemTemplate = browser.ownerDocument.getElementById(
+ "template-credential-account-list-item"
+ );
+ for (const [accountIndex, account] of accountList.accounts.entries()) {
+ let newItem = itemTemplate.content.firstElementChild.cloneNode(true);
+
+ // Add the new radio button, including pre-selection and the callback
+ let newRadio = newItem.getElementsByClassName(
+ "identity-credential-list-item-radio"
+ )[0];
+ newRadio.value = accountIndex;
+ newRadio.addEventListener("change", function (event) {
+ for (let item of listBox.children) {
+ item.classList.remove("checked");
+ }
+ if (event.target.checked) {
+ event.target.parentElement.classList.add("checked");
+ }
+ });
+ if (accountIndex == 0) {
+ newRadio.checked = true;
+ newItem.classList.add("checked");
+ }
+
+ // Change the default picture if one exists
+ let pictureResult = pictureResults[accountIndex];
+ if (pictureResult.status == "fulfilled") {
+ let newPicture = newItem.getElementsByClassName(
+ "identity-credential-list-item-icon"
+ )[0];
+ newPicture.setAttribute("src", pictureResult.value);
+ }
+
+ // Add information to the label
+ newItem.getElementsByClassName(
+ "identity-credential-list-item-label-primary"
+ )[0].textContent = account.name;
+ newItem.getElementsByClassName(
+ "identity-credential-list-item-label-secondary"
+ )[0].textContent = account.email;
+
+ // Add the item to the DOM!
+ listBox.append(newItem);
+ }
+
+ // Create a new promise to wrap the callbacks of the popup buttons
+ return new Promise(function (resolve, reject) {
+ // Construct the necessary arguments for notification behavior
+ let options = {
+ hideClose: true,
+ eventCallback: (topic, nextRemovalReason, isCancel) => {
+ if (topic == "removed" && isCancel) {
+ reject();
+ }
+ },
+ };
+ let mainAction = {
+ label: acceptLabel,
+ accessKey: acceptKey,
+ callback(event) {
+ let result = listBox.querySelector(
+ ".identity-credential-list-item-radio:checked"
+ ).value;
+ resolve(parseInt(result));
+ },
+ };
+ let secondaryActions = [
+ {
+ label: cancelLabel,
+ accessKey: cancelKey,
+ callback(event) {
+ reject();
+ },
+ },
+ ];
+
+ if (headerIconResult) {
+ let headerIcon = browser.ownerDocument.getElementsByClassName(
+ "identity-credential-header-icon"
+ )[0];
+ headerIcon.setAttribute("src", headerIconResult);
+ }
+
+ const headerText = browser.ownerDocument.getElementById(
+ "identity-credential-header-text"
+ );
+ headerText.textContent = headerMessage;
+
+ // Show the popup
+ browser.ownerDocument.getElementById(
+ "identity-credential-provider"
+ ).hidden = true;
+ browser.ownerDocument.getElementById(
+ "identity-credential-policy"
+ ).hidden = true;
+ browser.ownerDocument.getElementById(
+ "identity-credential-account"
+ ).hidden = false;
+ browser.ownerDocument.getElementById(
+ "identity-credential-header"
+ ).hidden = false;
+ browser.ownerGlobal.PopupNotifications.show(
+ browser,
+ "identity-credential",
+ "",
+ "identity-credential-notification-icon",
+ mainAction,
+ secondaryActions,
+ options
+ );
+ });
+ }
+
+ /**
+ * Close all UI from the other methods of this module for the provided window.
+ * @param {BrowsingContext} browsingContext - The BrowsingContext of the document requesting an identity credential via navigator.credentials.get()
+ * @returns
+ */
+ close(browsingContext) {
+ let browser = browsingContext.top.embedderElement;
+ if (!browser || AppConstants.platform === "android") {
+ return;
+ }
+ let notification = browser.ownerGlobal.PopupNotifications.getNotification(
+ "identity-credential",
+ browser
+ );
+ if (notification) {
+ browser.ownerGlobal.PopupNotifications.remove(notification, true);
+ }
+ }
+}
diff --git a/toolkit/components/credentialmanagement/IdentityCredentialStorageService.cpp b/toolkit/components/credentialmanagement/IdentityCredentialStorageService.cpp
new file mode 100644
index 0000000000..f3994d1a86
--- /dev/null
+++ b/toolkit/components/credentialmanagement/IdentityCredentialStorageService.cpp
@@ -0,0 +1,887 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ErrorList.h"
+#include "IdentityCredentialStorageService.h"
+#include "MainThreadUtils.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/Base64.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Components.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "mozIStorageService.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageStatement.h"
+#include "mozStorageCID.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCRT.h"
+#include "nsDebug.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIObserverService.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsVariant.h"
+#include "prtime.h"
+
+#define ACCOUNT_STATE_FILENAME "credentialstate.sqlite"_ns
+#define SCHEMA_VERSION 1
+#define MODIFIED_NOW PR_Now()
+
+namespace mozilla {
+
+StaticRefPtr<IdentityCredentialStorageService>
+ gIdentityCredentialStorageService;
+
+NS_IMPL_ISUPPORTS(IdentityCredentialStorageService,
+ nsIIdentityCredentialStorageService, nsIObserver,
+ nsIAsyncShutdownBlocker)
+
+already_AddRefed<IdentityCredentialStorageService>
+IdentityCredentialStorageService::GetSingleton() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!gIdentityCredentialStorageService) {
+ gIdentityCredentialStorageService = new IdentityCredentialStorageService();
+ ClearOnShutdown(&gIdentityCredentialStorageService);
+ nsresult rv = gIdentityCredentialStorageService->Init();
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ RefPtr<IdentityCredentialStorageService> service =
+ gIdentityCredentialStorageService;
+ return service.forget();
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::GetName(nsAString& aName) {
+ aName = u"IdentityCredentialStorageService: Flushing data"_ns;
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::BlockShutdown(
+ nsIAsyncShutdownClient* aClient) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsresult rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MonitorAutoLock lock(mMonitor);
+ mShuttingDown.Flip();
+
+ if (mMemoryDatabaseConnection) {
+ Unused << mMemoryDatabaseConnection->Close();
+ mMemoryDatabaseConnection = nullptr;
+ }
+
+ RefPtr<IdentityCredentialStorageService> self = this;
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction(
+ "IdentityCredentialStorageService::BlockShutdown",
+ [self]() {
+ MonitorAutoLock lock(self->mMonitor);
+
+ MOZ_ASSERT(self->mPendingWrites == 0);
+
+ if (self->mDiskDatabaseConnection) {
+ Unused << self->mDiskDatabaseConnection->Close();
+ self->mDiskDatabaseConnection = nullptr;
+ }
+
+ self->mFinalized.Flip();
+ self->mMonitor.NotifyAll();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "IdentityCredentialStorageService::BlockShutdown "
+ "- mainthread callback",
+ [self]() { self->Finalize(); }));
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+IdentityCredentialStorageService::GetState(nsIPropertyBag** aBagOut) {
+ return NS_OK;
+}
+
+already_AddRefed<nsIAsyncShutdownClient>
+IdentityCredentialStorageService::GetAsyncShutdownBarrier() const {
+ nsresult rv;
+ nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service();
+ MOZ_RELEASE_ASSERT(svc);
+
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ rv = svc->GetProfileBeforeChange(getter_AddRefs(client));
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_RELEASE_ASSERT(client);
+ return client.forget();
+}
+
+nsresult IdentityCredentialStorageService::Init() {
+ AssertIsOnMainThread();
+
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ MonitorAutoLock lock(mMonitor);
+ mShuttingDown.Flip();
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ nsCOMPtr<nsIAsyncShutdownClient> asc = GetAsyncShutdownBarrier();
+ if (!asc) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ nsresult rv = asc->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
+ __LINE__, u""_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mDatabaseFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mDatabaseFile->AppendNative(ACCOUNT_STATE_FILENAME);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Register the PBMode cleaner (IdentityCredentialStorageService::Observe) as
+ // an observer.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE);
+ observerService->AddObserver(this, "last-pb-context-exited", false);
+
+ rv = GetMemoryDatabaseConnection();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MonitorAutoLock lock(mMonitor);
+ mErrored.Flip();
+ return rv;
+ }
+
+ NS_ENSURE_SUCCESS(
+ NS_CreateBackgroundTaskQueue("IdentityCredentialStorage",
+ getter_AddRefs(mBackgroundThread)),
+ NS_ERROR_FAILURE);
+
+ RefPtr<IdentityCredentialStorageService> self = this;
+
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("IdentityCredentialStorageService::Init",
+ [self]() {
+ MonitorAutoLock lock(self->mMonitor);
+ nsresult rv = self->GetDiskDatabaseConnection();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ self->mErrored.Flip();
+ self->mMonitor.Notify();
+ return;
+ }
+
+ rv = self->LoadMemoryTableFromDisk();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ self->mErrored.Flip();
+ self->mMonitor.Notify();
+ return;
+ }
+
+ self->mInitialized.Flip();
+ self->mMonitor.Notify();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+nsresult IdentityCredentialStorageService::WaitForInitialization() {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Must only wait for initialization in the main thread.");
+ MonitorAutoLock lock(mMonitor);
+ while (!mInitialized && !mErrored && !mShuttingDown) {
+ mMonitor.Wait();
+ }
+ if (mErrored) {
+ return NS_ERROR_FAILURE;
+ }
+ if (mShuttingDown) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return NS_OK;
+}
+
+void IdentityCredentialStorageService::Finalize() {
+ nsCOMPtr<nsIAsyncShutdownClient> asc = GetAsyncShutdownBarrier();
+ MOZ_ASSERT(asc);
+ DebugOnly<nsresult> rv = asc->RemoveBlocker(this);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+// static
+nsresult IdentityCredentialStorageService::ValidatePrincipal(
+ nsIPrincipal* aPrincipal) {
+ // We add some constraints on the RP principal where it is provided to reduce
+ // edge cases in implementation. These are reasonable constraints with the
+ // semantics of the store: it must be a http or https content principal.
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ NS_ENSURE_TRUE(aPrincipal->GetIsContentPrincipal(), NS_ERROR_FAILURE);
+ nsCString scheme;
+ nsresult rv = aPrincipal->GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(scheme.Equals("http"_ns) || scheme.Equals("https"_ns),
+ NS_ERROR_FAILURE);
+ return NS_OK;
+}
+
+nsresult IdentityCredentialStorageService::GetMemoryDatabaseConnection() {
+ return IdentityCredentialStorageService::GetDatabaseConnectionInternal(
+ getter_AddRefs(mMemoryDatabaseConnection), nullptr);
+}
+
+nsresult IdentityCredentialStorageService::GetDiskDatabaseConnection() {
+ NS_ENSURE_TRUE(mDatabaseFile, NS_ERROR_NULL_POINTER);
+ return IdentityCredentialStorageService::GetDatabaseConnectionInternal(
+ getter_AddRefs(mDiskDatabaseConnection), mDatabaseFile);
+}
+
+// static
+nsresult IdentityCredentialStorageService::GetDatabaseConnectionInternal(
+ mozIStorageConnection** aDatabase, nsIFile* aFile) {
+ NS_ENSURE_TRUE(aDatabase, NS_ERROR_UNEXPECTED);
+ NS_ENSURE_STATE(!(*aDatabase));
+ nsCOMPtr<mozIStorageService> storage =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(storage, NS_ERROR_UNEXPECTED);
+ nsresult rv;
+
+ if (aFile) {
+ rv = storage->OpenDatabase(aFile, mozIStorageService::CONNECTION_DEFAULT,
+ aDatabase);
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ rv = aFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = storage->OpenDatabase(aFile, mozIStorageService::CONNECTION_DEFAULT,
+ aDatabase);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = storage->OpenSpecialDatabase(
+ kMozStorageMemoryStorageKey, "icsprivatedb"_ns,
+ mozIStorageService::CONNECTION_DEFAULT, aDatabase);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_ENSURE_TRUE(*aDatabase, NS_ERROR_UNEXPECTED);
+ bool ready = false;
+ (*aDatabase)->GetConnectionReady(&ready);
+ NS_ENSURE_TRUE(ready, NS_ERROR_UNEXPECTED);
+ rv = EnsureTable(*aDatabase);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// static
+nsresult IdentityCredentialStorageService::EnsureTable(
+ mozIStorageConnection* aDatabase) {
+ NS_ENSURE_ARG_POINTER(aDatabase);
+ bool tableExists = false;
+ aDatabase->TableExists("identity"_ns, &tableExists);
+ if (!tableExists) {
+ // Currently there is only one schema version, so we just need to create the
+ // table. The definition uses no explicit rowid column, instead primary
+ // keying on the tuple defined in the spec. We store two bits and some
+ // additional data to make integration with the ClearDataService
+ // easier/possible.
+ nsresult rv = aDatabase->SetSchemaVersion(SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aDatabase->ExecuteSimpleSQL(
+ "CREATE TABLE identity ("
+ "rpOrigin TEXT NOT NULL"
+ ",idpOrigin TEXT NOT NULL"
+ ",credentialId TEXT NOT NULL"
+ ",registered INTEGER"
+ ",allowLogout INTEGER"
+ ",modificationTime INTEGER"
+ ",rpBaseDomain TEXT"
+ ",PRIMARY KEY (rpOrigin, idpOrigin, credentialId)"
+ ")"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult IdentityCredentialStorageService::LoadMemoryTableFromDisk() {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "Must not load the table from disk in the main thread.");
+ auto constexpr selectAllQuery =
+ "SELECT rpOrigin, idpOrigin, credentialId, registered, allowLogout, "
+ "modificationTime, rpBaseDomain FROM identity;"_ns;
+ auto constexpr insertQuery =
+ "INSERT INTO identity(rpOrigin, idpOrigin, credentialId, registered, "
+ "allowLogout, modificationTime, rpBaseDomain) VALUES (?1, ?2, ?3, ?4, "
+ "?5, ?6, ?7);"_ns;
+
+ nsCOMPtr<mozIStorageStatement> writeStmt;
+ nsresult rv = mMemoryDatabaseConnection->CreateStatement(
+ insertQuery, getter_AddRefs(writeStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> readStmt;
+ rv = mDiskDatabaseConnection->CreateStatement(selectAllQuery,
+ getter_AddRefs(readStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ while (NS_SUCCEEDED(readStmt->ExecuteStep(&hasResult)) && hasResult) {
+ int64_t registered, allowLogout, modificationTime;
+ nsCString rpOrigin, idpOrigin, credentialID, rpBaseDomain;
+
+ // Read values from disk query
+ rv = readStmt->GetUTF8String(0, rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetUTF8String(1, idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetUTF8String(2, credentialID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetInt64(3, &registered);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetInt64(4, &allowLogout);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetInt64(5, &modificationTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetUTF8String(6, rpBaseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Write values to memory database
+ rv = writeStmt->BindUTF8StringByIndex(0, rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = writeStmt->BindUTF8StringByIndex(1, idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = writeStmt->BindUTF8StringByIndex(2, credentialID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = writeStmt->BindInt64ByIndex(3, registered);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = writeStmt->BindInt64ByIndex(4, allowLogout);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = writeStmt->BindInt64ByIndex(5, modificationTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = writeStmt->BindUTF8StringByIndex(6, rpBaseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = writeStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+void IdentityCredentialStorageService::IncrementPendingWrites() {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mPendingWrites < std::numeric_limits<uint32_t>::max());
+ mPendingWrites++;
+}
+
+void IdentityCredentialStorageService::DecrementPendingWrites() {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mPendingWrites > 0);
+ mPendingWrites--;
+}
+
+// static
+nsresult IdentityCredentialStorageService::UpsertData(
+ mozIStorageConnection* aDatabaseConnection, nsIPrincipal* aRPPrincipal,
+ nsIPrincipal* aIDPPrincipal, nsACString const& aCredentialID,
+ bool aRegistered, bool aAllowLogout) {
+ NS_ENSURE_ARG_POINTER(aDatabaseConnection);
+ NS_ENSURE_ARG_POINTER(aRPPrincipal);
+ NS_ENSURE_ARG_POINTER(aIDPPrincipal);
+ nsresult rv;
+ constexpr auto upsert_query =
+ "INSERT INTO identity(rpOrigin, idpOrigin, credentialId, "
+ "registered, allowLogout, modificationTime, rpBaseDomain)"
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
+ "ON CONFLICT(rpOrigin, idpOrigin, credentialId)"
+ "DO UPDATE SET registered=excluded.registered, "
+ "allowLogout=excluded.allowLogout, "
+ "modificationTime=excluded.modificationTime"_ns;
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = aDatabaseConnection->CreateStatement(upsert_query, getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString rpOrigin;
+ rv = aRPPrincipal->GetOrigin(rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString idpOrigin;
+ rv = aIDPPrincipal->GetOrigin(idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString rpBaseDomain;
+ rv = aRPPrincipal->GetBaseDomain(rpBaseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(0, rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(1, idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(2, aCredentialID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByIndex(3, aRegistered ? 1 : 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByIndex(4, aAllowLogout ? 1 : 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByIndex(5, MODIFIED_NOW);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(6, rpBaseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// static
+nsresult IdentityCredentialStorageService::DeleteData(
+ mozIStorageConnection* aDatabaseConnection, nsIPrincipal* aRPPrincipal,
+ nsIPrincipal* aIDPPrincipal, nsACString const& aCredentialID) {
+ NS_ENSURE_ARG_POINTER(aDatabaseConnection);
+ NS_ENSURE_ARG_POINTER(aRPPrincipal);
+ NS_ENSURE_ARG_POINTER(aIDPPrincipal);
+ auto constexpr deleteQuery =
+ "DELETE FROM identity WHERE rpOrigin=?1 AND idpOrigin=?2 AND "
+ "credentialId=?3"_ns;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv =
+ aDatabaseConnection->CreateStatement(deleteQuery, getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString rpOrigin;
+ rv = aRPPrincipal->GetOrigin(rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString idpOrigin;
+ rv = aIDPPrincipal->GetOrigin(idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(0, rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(1, idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(2, aCredentialID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// static
+nsresult IdentityCredentialStorageService::ClearData(
+ mozIStorageConnection* aDatabaseConnection) {
+ NS_ENSURE_ARG_POINTER(aDatabaseConnection);
+ nsresult rv =
+ aDatabaseConnection->ExecuteSimpleSQL("DELETE FROM identity;"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// static
+nsresult
+IdentityCredentialStorageService::DeleteDataFromOriginAttributesPattern(
+ mozIStorageConnection* aDatabaseConnection,
+ OriginAttributesPattern const& aOriginAttributesPattern) {
+ NS_ENSURE_ARG_POINTER(aDatabaseConnection);
+ nsCOMPtr<mozIStorageFunction> patternMatchFunction(
+ new OriginAttrsPatternMatchOriginSQLFunction(aOriginAttributesPattern));
+
+ nsresult rv = aDatabaseConnection->CreateFunction(
+ "ORIGIN_ATTRS_PATTERN_MATCH_ORIGIN"_ns, 1, patternMatchFunction);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aDatabaseConnection->ExecuteSimpleSQL(
+ "DELETE FROM identity WHERE "
+ "ORIGIN_ATTRS_PATTERN_MATCH_ORIGIN(rpOrigin);"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aDatabaseConnection->RemoveFunction(
+ "ORIGIN_ATTRS_PATTERN_MATCH_ORIGIN"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// static
+nsresult IdentityCredentialStorageService::DeleteDataFromTimeRange(
+ mozIStorageConnection* aDatabaseConnection, int64_t aStart, int64_t aEnd) {
+ NS_ENSURE_ARG_POINTER(aDatabaseConnection);
+ auto constexpr deleteTimeQuery =
+ "DELETE FROM identity WHERE modificationTime > ?1 and modificationTime "
+ "< ?2"_ns;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = aDatabaseConnection->CreateStatement(deleteTimeQuery,
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByIndex(0, aStart);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByIndex(1, aEnd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// static
+nsresult IdentityCredentialStorageService::DeleteDataFromPrincipal(
+ mozIStorageConnection* aDatabaseConnection, nsIPrincipal* aPrincipal) {
+ NS_ENSURE_ARG_POINTER(aDatabaseConnection);
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+
+ nsCString rpOrigin;
+ nsresult rv = aPrincipal->GetOrigin(rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ auto constexpr deletePrincipalQuery =
+ "DELETE FROM identity WHERE rpOrigin=?1"_ns;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = aDatabaseConnection->CreateStatement(deletePrincipalQuery,
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(0, rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// static
+nsresult IdentityCredentialStorageService::DeleteDataFromBaseDomain(
+ mozIStorageConnection* aDatabaseConnection, nsACString const& aBaseDomain) {
+ NS_ENSURE_ARG_POINTER(aDatabaseConnection);
+
+ auto constexpr deleteBaseDomainQuery =
+ "DELETE FROM identity WHERE rpBaseDomain=?1"_ns;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = aDatabaseConnection->CreateStatement(deleteBaseDomainQuery,
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(0, aBaseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::SetState(
+ nsIPrincipal* aRPPrincipal, nsIPrincipal* aIDPPrincipal,
+ nsACString const& aCredentialID, bool aRegistered, bool aAllowLogout) {
+ AssertIsOnMainThread();
+ NS_ENSURE_ARG_POINTER(aRPPrincipal);
+ NS_ENSURE_ARG_POINTER(aIDPPrincipal);
+
+ nsresult rv;
+ rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = IdentityCredentialStorageService::ValidatePrincipal(aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = UpsertData(mMemoryDatabaseConnection, aRPPrincipal, aIDPPrincipal,
+ aCredentialID, aRegistered, aAllowLogout);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ IncrementPendingWrites();
+ RefPtr<IdentityCredentialStorageService> self = this;
+ RefPtr<nsIPrincipal> rpPrincipal = aRPPrincipal;
+ RefPtr<nsIPrincipal> idpPrincipal = aIDPPrincipal;
+ nsCString credentialID(aCredentialID);
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("IdentityCredentialStorageService::Init",
+ [self, rpPrincipal, idpPrincipal, credentialID,
+ aRegistered, aAllowLogout]() {
+ nsresult rv = UpsertData(
+ self->mDiskDatabaseConnection, rpPrincipal,
+ idpPrincipal, credentialID, aRegistered,
+ aAllowLogout);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ self->DecrementPendingWrites();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::GetState(
+ nsIPrincipal* aRPPrincipal, nsIPrincipal* aIDPPrincipal,
+ nsACString const& aCredentialID, bool* aRegistered, bool* aAllowLogout) {
+ AssertIsOnMainThread();
+ NS_ENSURE_ARG_POINTER(aRPPrincipal);
+ NS_ENSURE_ARG_POINTER(aIDPPrincipal);
+
+ nsresult rv;
+ rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = IdentityCredentialStorageService::ValidatePrincipal(aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto constexpr selectQuery =
+ "SELECT registered, allowLogout FROM identity WHERE rpOrigin=?1 AND "
+ "idpOrigin=?2 AND credentialId=?3"_ns;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mMemoryDatabaseConnection->CreateStatement(selectQuery,
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString rpOrigin;
+ nsCString idpOrigin;
+ rv = aRPPrincipal->GetOrigin(rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aIDPPrincipal->GetOrigin(idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindUTF8StringByIndex(0, rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(1, idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByIndex(2, aCredentialID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ // If we find a result, return it
+ if (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ int64_t registeredInt, allowLogoutInt;
+ rv = stmt->GetInt64(0, &registeredInt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(1, &allowLogoutInt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aRegistered = registeredInt != 0;
+ *aAllowLogout = allowLogoutInt != 0;
+ return NS_OK;
+ }
+
+ // The tuple was not found on disk or in memory, use the defaults.
+ *aRegistered = false;
+ *aAllowLogout = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::Delete(
+ nsIPrincipal* aRPPrincipal, nsIPrincipal* aIDPPrincipal,
+ nsACString const& aCredentialID) {
+ AssertIsOnMainThread();
+ NS_ENSURE_ARG_POINTER(aRPPrincipal);
+ NS_ENSURE_ARG_POINTER(aIDPPrincipal);
+
+ nsresult rv;
+ rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = IdentityCredentialStorageService::ValidatePrincipal(aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = DeleteData(mMemoryDatabaseConnection, aRPPrincipal, aIDPPrincipal,
+ aCredentialID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ IncrementPendingWrites();
+ RefPtr<IdentityCredentialStorageService> self = this;
+ RefPtr<nsIPrincipal> rpPrincipal = aRPPrincipal;
+ RefPtr<nsIPrincipal> idpPrincipal = aIDPPrincipal;
+ nsCString credentialID(aCredentialID);
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("IdentityCredentialStorageService::Init",
+ [self, rpPrincipal, idpPrincipal, credentialID]() {
+ nsresult rv = DeleteData(
+ self->mDiskDatabaseConnection, rpPrincipal,
+ idpPrincipal, credentialID);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ self->DecrementPendingWrites();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::Clear() {
+ AssertIsOnMainThread();
+ nsresult rv;
+ rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = ClearData(mMemoryDatabaseConnection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ IncrementPendingWrites();
+ RefPtr<IdentityCredentialStorageService> self = this;
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("IdentityCredentialStorageService::Init",
+ [self]() {
+ nsresult rv =
+ ClearData(self->mDiskDatabaseConnection);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ self->DecrementPendingWrites();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+IdentityCredentialStorageService::DeleteFromOriginAttributesPattern(
+ nsAString const& aOriginAttributesPattern) {
+ AssertIsOnMainThread();
+ NS_ENSURE_FALSE(aOriginAttributesPattern.IsEmpty(), NS_ERROR_FAILURE);
+ OriginAttributesPattern oaPattern;
+ if (!oaPattern.Init(aOriginAttributesPattern)) {
+ NS_ERROR("Could not parse the argument for OriginAttributes");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv;
+ rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = DeleteDataFromOriginAttributesPattern(mMemoryDatabaseConnection,
+ oaPattern);
+ NS_ENSURE_SUCCESS(rv, rv);
+ IncrementPendingWrites();
+ RefPtr<IdentityCredentialStorageService> self = this;
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction(
+ "IdentityCredentialStorageService::Init",
+ [self, oaPattern]() {
+ nsresult rv = DeleteDataFromOriginAttributesPattern(
+ self->mDiskDatabaseConnection, oaPattern);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ self->DecrementPendingWrites();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::DeleteFromTimeRange(
+ int64_t aStart, int64_t aEnd) {
+ AssertIsOnMainThread();
+ nsresult rv;
+ rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = DeleteDataFromTimeRange(mMemoryDatabaseConnection, aStart, aEnd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ IncrementPendingWrites();
+ RefPtr<IdentityCredentialStorageService> self = this;
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("IdentityCredentialStorageService::Init",
+ [self, aStart, aEnd]() {
+ nsresult rv = DeleteDataFromTimeRange(
+ self->mDiskDatabaseConnection, aStart, aEnd);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ self->DecrementPendingWrites();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::
+ IdentityCredentialStorageService::DeleteFromPrincipal(
+ nsIPrincipal* aRPPrincipal) {
+ AssertIsOnMainThread();
+ NS_ENSURE_ARG_POINTER(aRPPrincipal);
+ nsresult rv =
+ IdentityCredentialStorageService::ValidatePrincipal(aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = DeleteDataFromPrincipal(mMemoryDatabaseConnection, aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+ IncrementPendingWrites();
+ RefPtr<IdentityCredentialStorageService> self = this;
+ RefPtr<nsIPrincipal> principal = aRPPrincipal;
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("IdentityCredentialStorageService::Init",
+ [self, principal]() {
+ nsresult rv = DeleteDataFromPrincipal(
+ self->mDiskDatabaseConnection, principal);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ self->DecrementPendingWrites();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::DeleteFromBaseDomain(
+ nsACString const& aBaseDomain) {
+ AssertIsOnMainThread();
+ nsresult rv;
+ rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = DeleteDataFromBaseDomain(mMemoryDatabaseConnection, aBaseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+ IncrementPendingWrites();
+ RefPtr<IdentityCredentialStorageService> self = this;
+ nsCString baseDomain(aBaseDomain);
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("IdentityCredentialStorageService::Init",
+ [self, baseDomain]() {
+ nsresult rv = DeleteDataFromBaseDomain(
+ self->mDiskDatabaseConnection, baseDomain);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ self->DecrementPendingWrites();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+IdentityCredentialStorageService::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ AssertIsOnMainThread();
+ // Double check that we have the right topic.
+ if (!nsCRT::strcmp(aTopic, "last-pb-context-exited")) {
+ MonitorAutoLock lock(mMonitor);
+ if (mInitialized && mMemoryDatabaseConnection) {
+ nsCOMPtr<mozIStorageFunction> patternMatchFunction(
+ new PrivateBrowsingOriginSQLFunction());
+ nsresult rv = mMemoryDatabaseConnection->CreateFunction(
+ "PRIVATE_BROWSING_PATTERN_MATCH_ORIGIN"_ns, 1, patternMatchFunction);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mMemoryDatabaseConnection->ExecuteSimpleSQL(
+ "DELETE FROM identity WHERE "
+ "PRIVATE_BROWSING_PATTERN_MATCH_ORIGIN(rpOrigin);"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mMemoryDatabaseConnection->RemoveFunction(
+ "PRIVATE_BROWSING_PATTERN_MATCH_ORIGIN"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(OriginAttrsPatternMatchOriginSQLFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+OriginAttrsPatternMatchOriginSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ nsresult rv;
+
+ nsAutoCString origin;
+ rv = aFunctionArguments->GetUTF8String(0, origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString originNoSuffix;
+ OriginAttributes oa;
+ bool parsedSuccessfully = oa.PopulateFromOrigin(origin, originNoSuffix);
+ NS_ENSURE_TRUE(parsedSuccessfully, NS_ERROR_FAILURE);
+ bool result = mPattern.Matches(oa);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsBool(result);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(PrivateBrowsingOriginSQLFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+PrivateBrowsingOriginSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ nsresult rv;
+
+ nsAutoCString origin;
+ rv = aFunctionArguments->GetUTF8String(0, origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool result = OriginAttributes::IsPrivateBrowsing(origin);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsBool(result);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/credentialmanagement/IdentityCredentialStorageService.h b/toolkit/components/credentialmanagement/IdentityCredentialStorageService.h
new file mode 100644
index 0000000000..ec69c6e0e1
--- /dev/null
+++ b/toolkit/components/credentialmanagement/IdentityCredentialStorageService.h
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_IDENTITYCREDENTIALSTORAGESERVICE_H_
+#define MOZILLA_IDENTITYCREDENTIALSTORAGESERVICE_H_
+
+#include "ErrorList.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageFunction.h"
+#include "nsIAsyncShutdown.h"
+#include "nsIIdentityCredentialStorageService.h"
+#include "nsIObserver.h"
+#include "nsISupports.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+class IdentityCredentialStorageService final
+ : public nsIIdentityCredentialStorageService,
+ public nsIObserver,
+ public nsIAsyncShutdownBlocker {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIDENTITYCREDENTIALSTORAGESERVICE
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ // Returns the singleton instance. Used by the component manager
+ static already_AddRefed<IdentityCredentialStorageService> GetSingleton();
+
+ // Singletons shouldn't have copy constructors or assignment operators
+ IdentityCredentialStorageService(const IdentityCredentialStorageService&) =
+ delete;
+ IdentityCredentialStorageService& operator=(
+ const IdentityCredentialStorageService&) = delete;
+
+ private:
+ IdentityCredentialStorageService()
+ : mMonitor("mozilla::IdentityCredentialStorageService::mMonitor"),
+ mPendingWrites(0){};
+ ~IdentityCredentialStorageService() = default;
+
+ // Spins up the service. This includes firing off async work in a worker
+ // thread. This should always be called before other use of the service to
+ // prevent deadlock.
+ nsresult Init();
+
+ // Wait (non-blocking) until the service is fully initialized. We may be
+ // waiting for that async work started by Init().
+ nsresult WaitForInitialization();
+
+ // Utility function to grab the correct barrier this service needs to shut
+ // down by
+ already_AddRefed<nsIAsyncShutdownClient> GetAsyncShutdownBarrier() const;
+
+ // Called to indicate to the async shutdown service that we are all wrapped
+ // up. This also spins down the worker thread, since it is called after all
+ // disk database connections are closed.
+ void Finalize();
+
+ // Utility function to make sure a principal is an acceptable primary (RP)
+ // principal
+ static nsresult ValidatePrincipal(nsIPrincipal* aPrincipal);
+
+ // Helper functions to initialize the database connections. Also makes sure
+ // the tables are present and have up to date schemas.
+ nsresult GetMemoryDatabaseConnection();
+ nsresult GetDiskDatabaseConnection();
+ static nsresult GetDatabaseConnectionInternal(
+ mozIStorageConnection** aDatabase, nsIFile* aFile);
+
+ // Helper function for the Get*DatabaseConnection functions to ensure the
+ // tables are present and have up to date schemas.
+ static nsresult EnsureTable(mozIStorageConnection* aDatabase);
+
+ // Grab all data from the disk database and insert it into the memory
+ // database/ This is used at start up
+ nsresult LoadMemoryTableFromDisk();
+
+ // Used to (thread-safely) track how many operations have been launched to the
+ // worker thread so that we can wait for it to hit zero before close the disk
+ // database connection
+ void IncrementPendingWrites();
+ void DecrementPendingWrites();
+
+ // Helper functions for database writes.
+ // We have helper functions here for all operations we perform on the
+ // databases that invlolve writes. These are split out and take a connection
+ // as an argument because we have to write to both the memory database and
+ // the disk database, where we only read from the memory database after
+ // initialization. See nsIIdentityCredentialStorageService.idl for more info
+ // on these functions' semantics
+
+ // Upsert == Update or Insert! Uses some nice SQLite syntax to make this easy.
+ static nsresult UpsertData(mozIStorageConnection* aDatabaseConnection,
+ nsIPrincipal* aRPPrincipal,
+ nsIPrincipal* aIDPPrincipal,
+ nsACString const& aCredentialID, bool aRegistered,
+ bool aAllowLogout);
+
+ static nsresult DeleteData(mozIStorageConnection* aDatabaseConnection,
+ nsIPrincipal* aRPPrincipal,
+ nsIPrincipal* aIDPPrincipal,
+ nsACString const& aCredentialID);
+
+ static nsresult ClearData(mozIStorageConnection* aDatabaseConnection);
+
+ static nsresult DeleteDataFromOriginAttributesPattern(
+ mozIStorageConnection* aDatabaseConnection,
+ OriginAttributesPattern const& aOriginAttributesPattern);
+
+ static nsresult DeleteDataFromTimeRange(
+ mozIStorageConnection* aDatabaseConnection, int64_t aStart, int64_t aEnd);
+
+ static nsresult DeleteDataFromPrincipal(
+ mozIStorageConnection* aDatabaseConnection, nsIPrincipal* aPrincipal);
+
+ static nsresult DeleteDataFromBaseDomain(
+ mozIStorageConnection* aDatabaseConnection,
+ nsACString const& aBaseDomain);
+
+ // Database connections. Guaranteed to be non-null and working once
+ // initialized and not-yet finalized
+ RefPtr<mozIStorageConnection> mDiskDatabaseConnection; // Worker thread only
+ RefPtr<mozIStorageConnection>
+ mMemoryDatabaseConnection; // Main thread only after initialization,
+ // worker thread only before initialization.
+
+ // Worker thread. This should be a valid thread after Init() returns and be
+ // destroyed when we finalize
+ nsCOMPtr<nsISerialEventTarget> mBackgroundThread; // main thread only
+
+ // The database file handle. We can only create this in the main thread and
+ // need it in the worker to perform blocking disk IO. So we put it on this,
+ // since we pass this to the worker anyway
+ nsCOMPtr<nsIFile> mDatabaseFile; // initialized in the main thread, read-only
+ // in worker thread
+
+ // Service state management. We protect these variables with a monitor. This
+ // monitor is also used to signal the completion of initialization and
+ // finalization performed in the worker thread.
+ Monitor mMonitor;
+ FlippedOnce<false> mInitialized MOZ_GUARDED_BY(mMonitor);
+ FlippedOnce<false> mErrored MOZ_GUARDED_BY(mMonitor);
+ FlippedOnce<false> mShuttingDown MOZ_GUARDED_BY(mMonitor);
+ FlippedOnce<false> mFinalized MOZ_GUARDED_BY(mMonitor);
+ uint32_t mPendingWrites MOZ_GUARDED_BY(mMonitor);
+};
+
+class OriginAttrsPatternMatchOriginSQLFunction final
+ : public mozIStorageFunction {
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ explicit OriginAttrsPatternMatchOriginSQLFunction(
+ OriginAttributesPattern const& aPattern)
+ : mPattern(aPattern) {}
+ OriginAttrsPatternMatchOriginSQLFunction() = delete;
+
+ private:
+ ~OriginAttrsPatternMatchOriginSQLFunction() = default;
+
+ OriginAttributesPattern mPattern;
+};
+
+class PrivateBrowsingOriginSQLFunction final : public mozIStorageFunction {
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ PrivateBrowsingOriginSQLFunction() = default;
+
+ private:
+ ~PrivateBrowsingOriginSQLFunction() = default;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_IDENTITYCREDENTIALSTORAGESERVICE_H_ */
diff --git a/toolkit/components/credentialmanagement/components.conf b/toolkit/components/credentialmanagement/components.conf
new file mode 100644
index 0000000000..eb991b86b2
--- /dev/null
+++ b/toolkit/components/credentialmanagement/components.conf
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{936007db-a957-4f1d-a23d-f7d9403223e6}',
+ 'contract_ids': ['@mozilla.org/browser/identitycredentialpromptservice;1'],
+ 'esModule': 'resource://gre/modules/IdentityCredentialPromptService.sys.mjs',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ 'constructor': 'IdentityCredentialPromptService',
+ 'name': 'IdentityCredentialPromptService',
+ },
+ {
+ 'cid': '{029823d0-0448-46c5-af1f-25cd4501d0d7}',
+ 'contract_ids': ['@mozilla.org/browser/identity-credential-storage-service;1'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ 'singleton' : True,
+ 'type': 'mozilla::IdentityCredentialStorageService',
+ 'headers': ['mozilla/IdentityCredentialStorageService.h'],
+ 'interfaces': ['nsIIdentityCredentialStorageService'],
+ 'name': 'IdentityCredentialStorageService',
+ 'constructor': 'mozilla::IdentityCredentialStorageService::GetSingleton',
+ },
+]
diff --git a/toolkit/components/credentialmanagement/moz.build b/toolkit/components/credentialmanagement/moz.build
new file mode 100644
index 0000000000..b3216a33a9
--- /dev/null
+++ b/toolkit/components/credentialmanagement/moz.build
@@ -0,0 +1,34 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Credential Management")
+
+EXPORTS.mozilla += [
+ "IdentityCredentialStorageService.h",
+]
+
+UNIFIED_SOURCES += ["IdentityCredentialStorageService.cpp"]
+
+FINAL_LIBRARY = "xul"
+
+XPIDL_SOURCES += [
+ "nsIIdentityCredentialPromptService.idl",
+ "nsIIdentityCredentialStorageService.idl",
+]
+
+XPIDL_MODULE = "dom_identitycredential"
+
+EXTRA_JS_MODULES += [
+ "IdentityCredentialPromptService.sys.mjs",
+]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/toolkit/components/credentialmanagement/nsIIdentityCredentialPromptService.idl b/toolkit/components/credentialmanagement/nsIIdentityCredentialPromptService.idl
new file mode 100644
index 0000000000..dd0a61500b
--- /dev/null
+++ b/toolkit/components/credentialmanagement/nsIIdentityCredentialPromptService.idl
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+webidl BrowsingContext;
+
+[scriptable, uuid(936007db-a957-4f1d-a23d-f7d9403223e6)]
+interface nsIIdentityCredentialPromptService : nsISupports {
+ // Display to the user an interface to choose from among the identity providers listed
+ // Resolves with an index referring to one pair of the elements of the lists.
+ Promise showProviderPrompt(in BrowsingContext browsingContext, in jsval identityProviders, in jsval identityManifests);
+ // Display to the user an interface to approve (or disapprove) of the terms of service for
+ // the identity provider when used on the current site.
+ Promise showPolicyPrompt(in BrowsingContext browsingContext, in jsval identityProvider, in jsval identityManifest, in jsval identityClientMetadata);
+ // Display to the user an interface to choose from among the accounts listed with the information of the provider.
+ // Resolves with an index referring to one of the elements of the list.
+ Promise showAccountListPrompt(in BrowsingContext browsingContext, in jsval accountList, in jsval identityProvider, in jsval identityManifest);
+ // Close all UI from the other methods of this module
+ void close(in BrowsingContext browsingContext);
+};
diff --git a/toolkit/components/credentialmanagement/nsIIdentityCredentialStorageService.idl b/toolkit/components/credentialmanagement/nsIIdentityCredentialStorageService.idl
new file mode 100644
index 0000000000..0d993549e3
--- /dev/null
+++ b/toolkit/components/credentialmanagement/nsIIdentityCredentialStorageService.idl
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIPrincipal.idl"
+
+webidl IdentityCredential;
+
+[scriptable, builtinclass, uuid(029823d0-0448-46c5-af1f-25cd4501d0d7)]
+interface nsIIdentityCredentialStorageService : nsISupports {
+ // Store the registered and allowLogout bit for the tuple (rpPrincipal, idpPrincipal, credentialID).
+ // This goes straight to disk if rpPrincipal is not in Private Browsing mode and stays in memory otherwise.
+ // Additionally, if rpPrincipal is private, it will be cleared when the user closes the last private browsing window.
+ void setState(in nsIPrincipal rpPrincipal, in nsIPrincipal idpPrincipal, in ACString credentialID, in boolean registered, in boolean allowLogout);
+
+ // Retrieve the registered and allowLogout bits for the tuple (rpPrincipal, idpPrincipal, credentialID).
+ // This will always return defaults, even if there was never a value stored or it was deleted.
+ void getState(in nsIPrincipal rpPrincipal, in nsIPrincipal idpPrincipal, in ACString credentialID, out boolean registered, out boolean allowLogout);
+
+ // Delete the entry for the tuple (rpPrincipal, idpPrincipal, credentialID).
+ void delete(in nsIPrincipal rpPrincipal, in nsIPrincipal idpPrincipal, in ACString credentialID);
+
+ // Delete all data in this service.
+ void clear();
+
+ // Delete all data stored under a tuple with rpPrincipal that has the given base domain
+ void deleteFromBaseDomain(in ACString baseDomain);
+
+ // Delete all data stored under a tuple with a given rpPrincipal
+ void deleteFromPrincipal(in nsIPrincipal rpPrincipal);
+
+ // Delete all data stored in the given time range (microseconds since epoch)
+ void deleteFromTimeRange(in PRTime aFrom, in PRTime aTo);
+
+ // Delete all data matching the given Origin Attributes pattern
+ void deleteFromOriginAttributesPattern(in AString aPattern);
+};
diff --git a/toolkit/components/credentialmanagement/tests/browser/browser.toml b/toolkit/components/credentialmanagement/tests/browser/browser.toml
new file mode 100644
index 0000000000..a7202c9190
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/browser/browser.toml
@@ -0,0 +1,18 @@
+[DEFAULT]
+head = "head.js"
+prefs = [
+ "dom.security.credentialmanagement.identity.enabled=true",
+ "dom.security.credentialmanagement.identity.ignore_well_known=true",
+ "privacy.antitracking.enableWebcompat=false",
+] # disables opener heuristic
+scheme = "https"
+
+support-files = ["custom.svg"]
+
+["browser_account_dialog.js"]
+fail-if = ["a11y_checks"] # Bug 1854509 clicked identity-credential-list-item may not be focusable
+
+["browser_policy_dialog.js"]
+
+["browser_provider_dialog.js"]
+fail-if = ["a11y_checks"] # Bug 1854509 clicked identity-credential-list-item may not be focusable
diff --git a/toolkit/components/credentialmanagement/tests/browser/browser_account_dialog.js b/toolkit/components/credentialmanagement/tests/browser/browser_account_dialog.js
new file mode 100644
index 0000000000..f6e74d2bad
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/browser/browser_account_dialog.js
@@ -0,0 +1,462 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "IdentityCredentialPromptService",
+ "@mozilla.org/browser/identitycredentialpromptservice;1",
+ "nsIIdentityCredentialPromptService"
+);
+
+const TEST_URL = "https://example.com/";
+
+// Test that a single account shows up in a dialog and is chosen when "continue" is clicked
+add_task(async function test_single_acccount_dialog() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show the single account
+ let prompt = IdentityCredentialPromptService.showAccountListPrompt(
+ tab.linkedBrowser.browsingContext,
+ {
+ accounts: [
+ {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Test Account",
+ email: "test@idp.example",
+ },
+ ],
+ },
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ }
+ );
+
+ // Wait for the popup to appear
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ // Validate the popup contents
+ let inputs = document
+ .getElementById("identity-credential-account")
+ .getElementsByClassName("identity-credential-list-item");
+ is(inputs.length, 1, "One account expected");
+ let label = inputs[0].getElementsByClassName(
+ "identity-credential-list-item-label-stack"
+ )[0];
+ ok(
+ label.textContent.includes("Test Account"),
+ "Label includes the account name"
+ );
+ ok(
+ label.textContent.includes("test@idp.example"),
+ "Label includes the account email"
+ );
+
+ let title = document.getElementById("identity-credential-header-text");
+ ok(
+ title.textContent.includes("idp.example"),
+ "Popup title includes the IDP Site"
+ );
+
+ // Click Continue
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ // Make sure that the prompt resolves with the index of the only argument element
+ let value = await prompt;
+ is(value, 0);
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Test that no account is chosen when "cancel" is clicked
+add_task(async function test_single_acccount_deny() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show a prompt with one account
+ let prompt = IdentityCredentialPromptService.showAccountListPrompt(
+ tab.linkedBrowser.browsingContext,
+ {
+ accounts: [
+ {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Test Account",
+ email: "test@idp.example",
+ },
+ ],
+ },
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ }
+ );
+
+ // Wait for that popup
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ // Validate the popup contents
+ let inputs = document
+ .getElementById("identity-credential-account")
+ .getElementsByClassName("identity-credential-list-item");
+ is(inputs.length, 1, "One account expected");
+ let label = inputs[0].getElementsByClassName(
+ "identity-credential-list-item-label-stack"
+ )[0];
+ ok(
+ label.textContent.includes("Test Account"),
+ "Label includes the account name"
+ );
+ ok(
+ label.textContent.includes("test@idp.example"),
+ "Label includes the account email"
+ );
+
+ // Click cancel
+ document
+ .getElementsByClassName("popup-notification-secondary-button")[0]
+ .click();
+
+ // Make sure we reject
+ try {
+ await prompt;
+ ok(false, "Prompt should not resolve when denied.");
+ } catch (e) {
+ ok(true, "Prompt should reject when denied.");
+ }
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Show multiple accounts and select the first one
+add_task(async function test_multiple_acccount_dialog() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show a prompt with multiple accounts
+ let prompt = IdentityCredentialPromptService.showAccountListPrompt(
+ tab.linkedBrowser.browsingContext,
+ {
+ accounts: [
+ {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Test Account",
+ email: "test@idp.example",
+ },
+ {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Test Account 2",
+ email: "test2@idp.example",
+ },
+ ],
+ },
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ }
+ );
+
+ // Wait for that popup to appear
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ // Validate the account list contents and ordering
+ let inputs = document
+ .getElementById("identity-credential-account")
+ .getElementsByClassName("identity-credential-list-item");
+ is(inputs.length, 2, "Two accounts expected");
+ let label0 = inputs[0].getElementsByClassName(
+ "identity-credential-list-item-label-stack"
+ )[0];
+ ok(
+ label0.textContent.includes("Test Account"),
+ "The first account name should be in the label"
+ );
+ ok(
+ label0.textContent.includes("test@idp.example"),
+ "The first account email should be in the label"
+ );
+ let label1 = inputs[1].getElementsByClassName(
+ "identity-credential-list-item-label-stack"
+ )[0];
+ ok(
+ label1.textContent.includes("Test Account 2"),
+ "The second account name should be in the label"
+ );
+ ok(
+ label1.textContent.includes("test2@idp.example"),
+ "The second account email should be in the label"
+ );
+
+ // Click continue
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ // Validate that the caller gets a resolving promise with index of the first element
+ let value = await prompt;
+ is(value, 0, "The first account is chosen by default");
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Show multiple accounts and select the second one
+add_task(async function test_multiple_acccount_choose_second() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show a prompt with multiple accounts
+ let prompt = IdentityCredentialPromptService.showAccountListPrompt(
+ tab.linkedBrowser.browsingContext,
+ {
+ accounts: [
+ {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Test Account",
+ email: "test@idp.example",
+ },
+ {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Test Account 2",
+ email: "test2@idp.example",
+ },
+ ],
+ },
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ }
+ );
+
+ // Wait for that popup
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ // Validate the account list contents and ordering
+ let inputs = document
+ .getElementById("identity-credential-account")
+ .getElementsByClassName("identity-credential-list-item");
+ is(inputs.length, 2, "Two accounts expected");
+ let label0 = inputs[0].getElementsByClassName(
+ "identity-credential-list-item-label-stack"
+ )[0];
+ ok(
+ label0.textContent.includes("Test Account"),
+ "The first account name should be in the label"
+ );
+ ok(
+ label0.textContent.includes("test@idp.example"),
+ "The first account email should be in the label"
+ );
+ let label1 = inputs[1].getElementsByClassName(
+ "identity-credential-list-item-label-stack"
+ )[0];
+ ok(
+ label1.textContent.includes("Test Account 2"),
+ "The second account name should be in the label"
+ );
+ ok(
+ label1.textContent.includes("test2@idp.example"),
+ "The second account email should be in the label"
+ );
+
+ // Click the second account
+ inputs[1].click();
+
+ // Click continue
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ // Make sure we selected the second account
+ let value = await prompt;
+ is(value, 1, "The prompt should resolve to 1, indicating the second account");
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Test that account pictures are rendered for the user
+add_task(async function test_multiple_acccount_show_picture() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show a prompt with two accounts, but only the first has a custom picture
+ let prompt = IdentityCredentialPromptService.showAccountListPrompt(
+ tab.linkedBrowser.browsingContext,
+ {
+ accounts: [
+ {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Test Account",
+ email: "test@idp.example",
+ picture:
+ "https://example.net/browser/toolkit/components/credentialmanagement/tests/browser/custom.svg",
+ },
+ {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Test Account 2",
+ email: "test2@idp.example",
+ },
+ ],
+ },
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ privacy_policy_url: "https://idp.example/privacy-policy.html",
+ terms_of_service_url: "https://idp.example/terms-of-service.html",
+ branding: {
+ background_color: "0x6200ee",
+ color: "0xffffff",
+ icons: [
+ {
+ size: 256,
+ url: "https://example.net/browser/toolkit/components/credentialmanagement/tests/browser/custom.svg",
+ },
+ ],
+ name: "demo ip",
+ },
+ }
+ );
+
+ // Wait for that popup
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ let icons = document
+ .getElementById("identity-credential-account")
+ .getElementsByClassName("identity-credential-list-item-icon");
+ is(icons.length, 2, "Two accounts expected");
+ Assert.notEqual(icons[0].src, icons[1].src, "The icons are different");
+ ok(
+ icons[0].src.startsWith(
+ "data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDE2IDE2IiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9ImNvbnRleHQtZmlsbCIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSI+CiAgPHBhdGggZD0iTS42MjUgMTNhLjYyNS42MjUgMCAwIDEgMC0xLjI1bDMuMjUgMEE0Ljg4IDQuODggMCAwIDAgOC43NSA2Ljg3NWwwLS4yNWEuNjI1LjYyNSAwIDAgMSAxLjI1IDBsMCAuMjVBNi4xMzIgNi4xMzIgMCAwIDEgMy44NzUgMTNsLTMuMjUgMHoiLz"
+ ),
+ "The first icon matches the custom.svg"
+ );
+
+ const headerIcon = document.getElementsByClassName(
+ "identity-credential-header-icon"
+ )[0];
+
+ let title = document.getElementById("identity-credential-header-text");
+ ok(
+ title.textContent.includes("demo ip"),
+ "Popup title appears as business short name"
+ );
+
+ ok(BrowserTestUtils.isVisible(headerIcon), "Header Icon is showing");
+ ok(
+ headerIcon.src.startsWith(
+ "data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDE2IDE2IiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9ImNvbnRleHQtZmlsbCIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSI+CiAgPHBhdGggZD0iTS42MjUgMTNhLjYyNS42MjUgMCAwIDEgMC0xLjI1bDMuMjUgMEE0Ljg4IDQuODggMCAwIDAgOC43NSA2Ljg3NWwwLS4yNWEuNjI1LjYyNSAwIDAgMSAxLjI1IDBsMCAuMjVBNi4xMzIgNi4xMzIgMCAwIDEgMy44NzUgMTNsLTMuMjUgMHoiLz"
+ ),
+ "The header icon matches the icon resource from manifest"
+ );
+
+ // Click Continue
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ // Make sure that the prompt resolves with the index of the only argument element
+ let value = await prompt;
+ is(value, 0);
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/credentialmanagement/tests/browser/browser_policy_dialog.js b/toolkit/components/credentialmanagement/tests/browser/browser_policy_dialog.js
new file mode 100644
index 0000000000..eb1026934c
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/browser/browser_policy_dialog.js
@@ -0,0 +1,204 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "IdentityCredentialPromptService",
+ "@mozilla.org/browser/identitycredentialpromptservice;1",
+ "nsIIdentityCredentialPromptService"
+);
+
+const TEST_URL = "https://example.com/";
+
+// Test that a policy dialog does not appear when no policies are given
+add_task(async function test_policy_dialog_empty() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let prompt = IdentityCredentialPromptService.showPolicyPrompt(
+ tab.linkedBrowser.browsingContext,
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ {} // No policies!
+ );
+
+ // Make sure we resolve with true without interaction
+ let value = await prompt;
+ is(value, true, "Automatically accept the missing policies");
+
+ // Close tab
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Make sure that a policy dialog shows up when we have policies to show.
+// Also test the accept path.
+add_task(async function test_policy_dialog() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show a prompt- the operative argument is the last one
+ let prompt = IdentityCredentialPromptService.showPolicyPrompt(
+ tab.linkedBrowser.browsingContext,
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ branding: {
+ background_color: "0x6200ee",
+ color: "0xffffff",
+ icons: [
+ {
+ size: 256,
+ url: "https://example.net/browser/toolkit/components/credentialmanagement/tests/browser/custom.svg",
+ },
+ ],
+ name: "demo ip",
+ },
+ },
+ {
+ privacy_policy_url: "https://idp.example/privacy-policy.html",
+ terms_of_service_url: "https://idp.example/terms-of-service.html",
+ }
+ );
+
+ // Make sure the popup shows up
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ // Validate the contents of the popup
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ let description = document.getElementById(
+ "identity-credential-policy-explanation"
+ );
+
+ ok(
+ description.textContent.includes("idp.example"),
+ "IDP domain in the policy prompt text"
+ );
+ ok(
+ description.textContent.includes("example.com"),
+ "RP domain in the policy prompt text"
+ );
+ ok(
+ description.textContent.includes("Privacy Policy"),
+ "Link to the privacy policy in the policy prompt text"
+ );
+ ok(
+ description.textContent.includes("Terms of Service"),
+ "Link to the ToS in the policy prompt text"
+ );
+
+ let title = document.getElementById("identity-credential-header-text");
+ ok(
+ title.textContent.includes("demo ip"),
+ "IDP domain in the policy prompt header as business short name"
+ );
+
+ const headerIcon = document.getElementsByClassName(
+ "identity-credential-header-icon"
+ )[0];
+
+ ok(BrowserTestUtils.isVisible(headerIcon), "Header Icon is showing");
+ ok(
+ headerIcon.src.startsWith(
+ "data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDE2IDE2IiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9ImNvbnRleHQtZmlsbCIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSI+CiAgPHBhdGggZD0iTS42MjUgMTNhLjYyNS42MjUgMCAwIDEgMC0xLjI1bDMuMjUgMEE0Ljg4IDQuODggMCAwIDAgOC43NSA2Ljg3NWwwLS4yNWEuNjI1LjYyNSAwIDAgMSAxLjI1IDBsMCAuMjVBNi4xMzIgNi4xMzIgMCAwIDEgMy44NzUgMTNsLTMuMjUgMHoiLz"
+ ),
+ "The header icon matches the icon resource from manifest"
+ );
+
+ // Accept the policies
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ // Make sure the call to the propmt resolves with true
+ let value = await prompt;
+ is(value, true, "User clicking accept resolves with true");
+
+ // Wait for the prompt to go away
+ await popupHiding;
+
+ // Close tab
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Test that rejecting the policies works
+add_task(async function test_policy_reject() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show the same prompt with policies
+ let prompt = IdentityCredentialPromptService.showPolicyPrompt(
+ tab.linkedBrowser.browsingContext,
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ {
+ privacy_policy_url: "https://idp.example/privacy-policy.html",
+ terms_of_service_url: "https://idp.example/terms-of-service.html",
+ }
+ );
+
+ // Wait for the prompt to show up
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ let title = document.getElementById("identity-credential-header-text");
+ ok(
+ title.textContent.includes("idp.example"),
+ "IDP domain in the policy prompt header as domain"
+ );
+
+ // Click reject.
+ document
+ .getElementsByClassName("popup-notification-secondary-button")[0]
+ .click();
+
+ // Make sure the prompt call accepts with an indication of the user's reject choice.
+ let value = await prompt;
+ is(value, false, "User clicking reject causes the promise to resolve(false)");
+
+ // Wait for the popup to go away.
+ await popupHiding;
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/credentialmanagement/tests/browser/browser_provider_dialog.js b/toolkit/components/credentialmanagement/tests/browser/browser_provider_dialog.js
new file mode 100644
index 0000000000..ca81675bb2
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/browser/browser_provider_dialog.js
@@ -0,0 +1,396 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "IdentityCredentialPromptService",
+ "@mozilla.org/browser/identitycredentialpromptservice;1",
+ "nsIIdentityCredentialPromptService"
+);
+
+const TEST_URL = "https://example.com/";
+
+// Test that a single provider shows up in a dialog and is chosen when "continue" is clicked
+add_task(async function test_single_provider_dialog() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show one IDP
+ let prompt = IdentityCredentialPromptService.showProviderPrompt(
+ tab.linkedBrowser.browsingContext,
+ [
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ ],
+ [
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ ]
+ );
+
+ // Make sure a popup shows up.
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ // Make sure there is only one option
+ let inputs = document
+ .getElementById("identity-credential-provider")
+ .getElementsByClassName("identity-credential-list-item");
+ is(inputs.length, 1, "One IDP");
+
+ // Make sure the IDP Site is in the label
+ let label = inputs[0].getElementsByClassName(
+ "identity-credential-list-item-label-primary"
+ )[0];
+ ok(label.textContent.includes("idp.example"), "IDP site in label");
+
+ // Validate the title of the popup
+ let title = document.querySelector(
+ 'description[popupid="identity-credential"]'
+ );
+ ok(
+ title.textContent.includes("Sign in with a login provider"),
+ "Popup title correct"
+ );
+
+ // Click "Continue"
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ // Make sure the prompt promise resolves
+ let value = await prompt;
+ is(value, 0, "Selected the only IDP");
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Test that a single provider shows up in a dialog and is not chosen when "cancel" is clicked
+add_task(async function test_single_provider_deny() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show one IDP
+ let prompt = IdentityCredentialPromptService.showProviderPrompt(
+ tab.linkedBrowser.browsingContext,
+ [
+ {
+ configURL: "https://idp.example/",
+ clientId: "123",
+ },
+ ],
+ [
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ ]
+ );
+
+ // Make sure a popup shows up.
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ // Click cancel
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+ document
+ .getElementsByClassName("popup-notification-secondary-button")[0]
+ .click();
+
+ try {
+ await prompt;
+ ok(false, "Prompt should not resolve when denied.");
+ } catch (e) {
+ ok(true, "Prompt should reject when denied.");
+ }
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Show multiple IDPs and select the first one
+add_task(async function test_multiple_provider_dialog() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show two providers, in order. (we don't need the metadata because we aren't testing branding)
+ let prompt = IdentityCredentialPromptService.showProviderPrompt(
+ tab.linkedBrowser.browsingContext,
+ [
+ {
+ configURL: "https://idp1.example/",
+ clientId: "123",
+ },
+ {
+ configURL: "https://idp2.example/",
+ clientId: "123",
+ },
+ ],
+ [
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ ]
+ );
+
+ // Make sure the popup shows up
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ let inputs = document
+ .getElementById("identity-credential-provider")
+ .getElementsByClassName("identity-credential-list-item");
+ is(inputs.length, 2, "Two IDPs visible");
+
+ let label1 = inputs[0].getElementsByClassName(
+ "identity-credential-list-item-label-primary"
+ )[0];
+ ok(
+ label1.textContent.includes("idp1.example"),
+ "First IDP label includes its site"
+ );
+ let label2 = inputs[1].getElementsByClassName(
+ "identity-credential-list-item-label-primary"
+ )[0];
+ ok(
+ label2.textContent.includes("idp2.example"),
+ "Second IDP label includes its site"
+ );
+
+ // Click continue
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ let value = await prompt;
+ is(value, 0, "The default is the first option in the list");
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Show multiple IDPs and select the second one
+add_task(async function test_multiple_provider_choose_second() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show two providers, in order. (we don't need the metadata because we aren't testing branding)
+ let prompt = IdentityCredentialPromptService.showProviderPrompt(
+ tab.linkedBrowser.browsingContext,
+ [
+ {
+ configURL: "https://idp1.example/",
+ clientId: "123",
+ },
+ {
+ configURL: "https://idp2.example/",
+ clientId: "123",
+ },
+ ],
+ [
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ ]
+ );
+
+ // Wait for the popup
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ let inputs = document
+ .getElementById("identity-credential-provider")
+ .getElementsByClassName("identity-credential-list-item");
+ is(inputs.length, 2, "Two IDPs visible");
+
+ let label1 = inputs[0].getElementsByClassName(
+ "identity-credential-list-item-label-primary"
+ )[0];
+ ok(
+ label1.textContent.includes("idp1.example"),
+ "First IDP label includes its site"
+ );
+ let label2 = inputs[1].getElementsByClassName(
+ "identity-credential-list-item-label-primary"
+ )[0];
+ ok(
+ label2.textContent.includes("idp2.example"),
+ "Second IDP label includes its site"
+ );
+
+ // Click the second list item
+ inputs[1].click();
+
+ // Click continue
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ // Make sure the caller gets the second IDP
+ let value = await prompt;
+ is(value, 1, "Choosing a different option makes a change");
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Validate that the branding information is rendered correctly
+add_task(async function test_multiple_provider_show_branding() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Show the prompt, but include an icon for the second IDP
+ let prompt = IdentityCredentialPromptService.showProviderPrompt(
+ tab.linkedBrowser.browsingContext,
+ [
+ {
+ configURL: "https://idp1.example/",
+ clientId: "123",
+ },
+ {
+ configURL: "https://idp2.example/",
+ clientId: "123",
+ },
+ ],
+ [
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ },
+ {
+ accounts_endpoint: "",
+ client_metadata_endpoint: "",
+ id_assertion_endpoint: "",
+ branding: {
+ icons: [
+ {
+ url: "https://example.net/browser/toolkit/components/credentialmanagement/tests/browser/custom.svg",
+ },
+ ],
+ name: "demo ip",
+ },
+ },
+ ]
+ );
+
+ // Wait for that popup
+ await popupShown;
+
+ let popupHiding = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphiding"
+ );
+
+ let document = tab.linkedBrowser.browsingContext.topChromeWindow.document;
+
+ // Validate the icons
+ let icons = document
+ .getElementById("identity-credential-provider")
+ .getElementsByClassName("identity-credential-list-item-icon");
+ is(icons.length, 2, "Two icons in the popup");
+ Assert.notEqual(icons[0].src, icons[1].src, "Icons are different");
+ ok(
+ icons[1].src.startsWith(
+ "data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDE2IDE2IiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9ImNvbnRleHQtZmlsbCIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSI+CiAgPHBhdGggZD0iTS42MjUgMTNhLjYyNS42MjUgMCAwIDEgMC0xLjI1bDMuMjUgMEE0Ljg4IDQuODggMCAwIDAgOC43NSA2Ljg3NWwwLS4yNWEuNjI1LjYyNSAwIDAgMSAxLjI1IDBsMCAuMjVBNi4xMzIgNi4xMzIgMCAwIDEgMy44NzUgMTNsLTMuMjUgMHoiLz"
+ ),
+ "The second icon matches the custom.svg"
+ );
+
+ let inputs = document
+ .getElementById("identity-credential-provider")
+ .getElementsByClassName("identity-credential-list-item");
+ is(inputs.length, 2, "One IDP");
+ let label = inputs[1].getElementsByClassName(
+ "identity-credential-list-item-label-primary"
+ )[0];
+ ok(label.textContent.includes("demo ip"), "should show business short time");
+
+ // Click continue
+ document
+ .getElementsByClassName("popup-notification-primary-button")[0]
+ .click();
+
+ // Make sure the caller gets the first provider still
+ let value = await prompt;
+ is(value, 0, "The default is the first option in the list");
+
+ await popupHiding;
+
+ // Close tabs.
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/credentialmanagement/tests/browser/custom.svg b/toolkit/components/credentialmanagement/tests/browser/custom.svg
new file mode 100644
index 0000000000..320d516b07
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/browser/custom.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="M.625 13a.625.625 0 0 1 0-1.25l3.25 0A4.88 4.88 0 0 0 8.75 6.875l0-.25a.625.625 0 0 1 1.25 0l0 .25A6.132 6.132 0 0 1 3.875 13l-3.25 0z"/>
+ <path d="M12.096 15.248a.625.625 0 0 1-.491-1.012c4.281-5.446 3.261-8.813 2.271-10.337-1.346-2.07-4.169-3.106-6.71-2.464-2.428.615-3.951 2.583-4.178 5.397-.027.345-.364.585-.673.574a.626.626 0 0 1-.573-.673C2.011 3.4 3.924.967 6.859.224c3.044-.768 6.437.489 8.064 2.995 1.172 1.803 2.426 5.732-2.337 11.791a.622.622 0 0 1-.49.238z"/>
+ <path d="M5.734 16a.624.624 0 0 1-.165-1.228c5.912-1.62 6.147-7.768 6.148-7.829a2.113 2.113 0 0 0-.457-1.708 2.713 2.713 0 0 0-1.689-.947c-.683-.109-1.357.021-1.902.371-.521.334-.859.83-.95 1.396l-.217 1.209a3.621 3.621 0 0 1-3.567 2.986l-2.31 0a.625.625 0 0 1 0-1.25l2.31 0a2.373 2.373 0 0 0 2.337-1.957l.215-1.196c.146-.912.683-1.711 1.509-2.24a3.95 3.95 0 0 1 2.774-.553 3.964 3.964 0 0 1 2.461 1.394c.618.762.878 1.688.729 2.611.005 0-.27 7.059-7.061 8.919a.619.619 0 0 1-.165.022z"/>
+</svg>
diff --git a/toolkit/components/credentialmanagement/tests/browser/head.js b/toolkit/components/credentialmanagement/tests/browser/head.js
new file mode 100644
index 0000000000..d646d2b39d
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/browser/head.js
@@ -0,0 +1,2 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
diff --git a/toolkit/components/credentialmanagement/tests/xpcshell/head.js b/toolkit/components/credentialmanagement/tests/xpcshell/head.js
new file mode 100644
index 0000000000..4614e91961
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/xpcshell/head.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
diff --git a/toolkit/components/credentialmanagement/tests/xpcshell/test_identity_credential_storage_service.js b/toolkit/components/credentialmanagement/tests/xpcshell/test_identity_credential_storage_service.js
new file mode 100644
index 0000000000..95ee8042cd
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/xpcshell/test_identity_credential_storage_service.js
@@ -0,0 +1,300 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "IdentityCredentialStorageService",
+ "@mozilla.org/browser/identity-credential-storage-service;1",
+ "nsIIdentityCredentialStorageService"
+);
+
+do_get_profile();
+
+add_task(async function test_insert_and_delete() {
+ let rpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://rp.com/"),
+ {}
+ );
+ let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://idp.com/"),
+ {}
+ );
+ const credentialID = "ID";
+
+ // Test initial value
+ let registered = {};
+ let allowLogout = {};
+ IdentityCredentialStorageService.getState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(!registered.value, "Should not be registered initially.");
+ Assert.ok(!allowLogout.value, "Should not allow logout initially.");
+
+ // Set and read a value
+ IdentityCredentialStorageService.setState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+ IdentityCredentialStorageService.getState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(registered.value, "Should be registered by set.");
+ Assert.ok(allowLogout.value, "Should now allow logout by set.");
+
+ IdentityCredentialStorageService.delete(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID
+ );
+ IdentityCredentialStorageService.getState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(!registered.value, "Should not be registered after deletion.");
+ Assert.ok(!allowLogout.value, "Should not allow logout after deletion.");
+ IdentityCredentialStorageService.clear();
+});
+
+add_task(async function test_basedomain_delete() {
+ let rpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://rp.com/"),
+ {}
+ );
+ let rpPrincipal2 = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://www.rp.com/"),
+ {}
+ );
+ let rpPrincipal3 = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://www.other.com/"),
+ {}
+ );
+ let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://idp.com/"),
+ {}
+ );
+ const credentialID = "ID";
+ let registered = {};
+ let allowLogout = {};
+
+ // Set values
+ IdentityCredentialStorageService.setState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+ IdentityCredentialStorageService.setState(
+ rpPrincipal2,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+ IdentityCredentialStorageService.setState(
+ rpPrincipal3,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+
+ IdentityCredentialStorageService.deleteFromBaseDomain(
+ rpPrincipal2.baseDomain
+ );
+ IdentityCredentialStorageService.getState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(!registered.value, "Should not be registered after deletion.");
+ Assert.ok(!allowLogout.value, "Should not allow logout after deletion.");
+ IdentityCredentialStorageService.getState(
+ rpPrincipal2,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(!registered.value, "Should not be registered after deletion.");
+ Assert.ok(!allowLogout.value, "Should not allow logout after deletion.");
+ IdentityCredentialStorageService.getState(
+ rpPrincipal3,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(registered.value, "Should be registered by set.");
+ Assert.ok(allowLogout.value, "Should now allow logout by set.");
+ IdentityCredentialStorageService.clear();
+});
+
+add_task(async function test_principal_delete() {
+ let rpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://rp.com/"),
+ {}
+ );
+ let rpPrincipal2 = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://www.rp.com/"),
+ {}
+ );
+ let rpPrincipal3 = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://www.other.com/"),
+ {}
+ );
+ let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://idp.com/"),
+ {}
+ );
+ const credentialID = "ID";
+ let registered = {};
+ let allowLogout = {};
+
+ // Set values
+ IdentityCredentialStorageService.setState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+ IdentityCredentialStorageService.setState(
+ rpPrincipal2,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+ IdentityCredentialStorageService.setState(
+ rpPrincipal3,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+
+ IdentityCredentialStorageService.deleteFromPrincipal(rpPrincipal2);
+ IdentityCredentialStorageService.getState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(registered.value, "Should be registered by set.");
+ Assert.ok(allowLogout.value, "Should now allow logout by set.");
+ IdentityCredentialStorageService.getState(
+ rpPrincipal2,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(!registered.value, "Should not be registered after deletion.");
+ Assert.ok(!allowLogout.value, "Should not allow logout after deletion.");
+ IdentityCredentialStorageService.getState(
+ rpPrincipal3,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(registered.value, "Should be registered by set.");
+ Assert.ok(allowLogout.value, "Should now allow logout by set.");
+ IdentityCredentialStorageService.clear();
+});
+
+add_task(async function test_principal_delete() {
+ let rpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://rp.com/"),
+ {}
+ );
+ let rpPrincipal2 = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://rp.com/"),
+ { privateBrowsingId: 1 }
+ );
+ let rpPrincipal3 = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://www.other.com/"),
+ { privateBrowsingId: 1 }
+ );
+ let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://idp.com/"),
+ {}
+ );
+ const credentialID = "ID";
+ let registered = {};
+ let allowLogout = {};
+
+ // Set values
+ IdentityCredentialStorageService.setState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+ IdentityCredentialStorageService.setState(
+ rpPrincipal2,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+ IdentityCredentialStorageService.setState(
+ rpPrincipal3,
+ idpPrincipal,
+ credentialID,
+ true,
+ true
+ );
+
+ IdentityCredentialStorageService.deleteFromOriginAttributesPattern(
+ '{ "privateBrowsingId": 1 }'
+ );
+ IdentityCredentialStorageService.getState(
+ rpPrincipal,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(registered.value, "Should be registered by set.");
+ Assert.ok(allowLogout.value, "Should now allow logout by set.");
+ IdentityCredentialStorageService.getState(
+ rpPrincipal2,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(!registered.value, "Should not be registered after deletion.");
+ Assert.ok(!allowLogout.value, "Should not allow logout after deletion.");
+ IdentityCredentialStorageService.getState(
+ rpPrincipal3,
+ idpPrincipal,
+ credentialID,
+ registered,
+ allowLogout
+ );
+ Assert.ok(!registered.value, "Should not be registered after deletion.");
+ Assert.ok(!allowLogout.value, "Should not allow logout after deletion.");
+ IdentityCredentialStorageService.clear();
+});
diff --git a/toolkit/components/credentialmanagement/tests/xpcshell/xpcshell.toml b/toolkit/components/credentialmanagement/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..b9c1234449
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+head = "head.js"
+prefs = ["dom.security.credentialmanagement.identity.enabled=true"]
+
+["test_identity_credential_storage_service.js"]