summaryrefslogtreecommitdiffstats
path: root/toolkit/components/credentialmanagement
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/credentialmanagement')
-rw-r--r--toolkit/components/credentialmanagement/IdentityCredentialPromptService.sys.mjs399
-rw-r--r--toolkit/components/credentialmanagement/IdentityCredentialStorageService.cpp613
-rw-r--r--toolkit/components/credentialmanagement/IdentityCredentialStorageService.h47
-rw-r--r--toolkit/components/credentialmanagement/components.conf27
-rw-r--r--toolkit/components/credentialmanagement/moz.build33
-rw-r--r--toolkit/components/credentialmanagement/nsIIdentityCredentialPromptService.idl22
-rw-r--r--toolkit/components/credentialmanagement/nsIIdentityCredentialStorageService.idl38
-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.ini6
10 files changed, 1494 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..3557f911e1
--- /dev/null
+++ b/toolkit/components/credentialmanagement/IdentityCredentialPromptService.sys.mjs
@@ -0,0 +1,399 @@
+/**
+ * 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";
+
+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
+);
+
+function fulfilledPromiseFromFirstListElement(list) {
+ if (list.length) {
+ return Promise.resolve(list[0]);
+ }
+ return Promise.reject();
+}
+
+/**
+ * Class implementing the nsIIdentityCredentialPromptService
+ * */
+export class IdentityCredentialPromptService {
+ classID = Components.ID("{936007db-a957-4f1d-a23d-f7d9403223e6}");
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIIdentityCredentialPromptService",
+ ]);
+
+ /**
+ * 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 {IdentityProvider[]} identityProviders - The list of identity providers the user selects from
+ * @returns {Promise<IdentityProvider>} The user-selected identity provider
+ */
+ showProviderPrompt(browsingContext, identityProviders) {
+ // For testing only.
+ if (lazy.SELECT_FIRST_IN_UI_LISTS) {
+ return fulfilledPromiseFromFirstListElement(identityProviders);
+ }
+ return new Promise(function(resolve, reject) {
+ let browser = browsingContext.top.embedderElement;
+ if (!browser) {
+ reject();
+ return;
+ }
+
+ // Localize all strings to be used
+ // Bug 1797154 - Convert localization calls to use the async formatValues.
+ let localization = new Localization(
+ ["preview/identityCredentialNotification.ftl"],
+ true
+ );
+ let headerMessage = localization.formatValueSync(
+ "identity-credential-header-providers",
+ {
+ host: "<>",
+ }
+ );
+ let [cancel] = localization.formatMessagesSync([
+ { 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;
+
+ // 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 (let providerIndex in identityProviders) {
+ let provider = identityProviders[providerIndex];
+ let providerURI = new URL(provider.configURL);
+ let displayDomain = lazy.IDNService.convertToDisplayIDN(
+ providerURI.host,
+ {}
+ );
+ let newItem = itemTemplate.content.firstElementChild.cloneNode(true);
+ newItem.firstElementChild.textContent = displayDomain;
+ newItem.setAttribute("oncommand", `this.callback(event)`);
+ newItem.callback = function(event) {
+ let notification = browser.ownerGlobal.PopupNotifications.getNotification(
+ "identity-credential",
+ browser
+ );
+ browser.ownerGlobal.PopupNotifications.remove(notification);
+ resolve(provider);
+ event.stopPropagation();
+ };
+ listBox.append(newItem);
+ }
+
+ // Construct the necessary arguments for notification behavior
+ let currentOrigin =
+ browsingContext.currentWindowContext.documentPrincipal.originNoSuffix;
+ let options = {
+ name: currentOrigin,
+ };
+ let mainAction = {
+ 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.ownerGlobal.PopupNotifications.show(
+ browser,
+ "identity-credential",
+ headerMessage,
+ "identity-credential-notification-icon",
+ mainAction,
+ null,
+ 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 {IdentityProvider} identityProvider - The Identity Provider that the user has selected to use
+ * @param {IdentityCredentialMetadata} identityCredentialMetadata - The metadata displayed to the user
+ * @returns {Promise<bool>} A boolean representing the user's acceptance of the metadata.
+ */
+ showPolicyPrompt(
+ browsingContext,
+ identityProvider,
+ 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);
+ }
+ return new Promise(function(resolve, reject) {
+ let browser = browsingContext.top.embedderElement;
+ if (!browser) {
+ reject();
+ return;
+ }
+
+ let providerURI = new URL(identityProvider.configURL);
+ let providerDisplayDomain = lazy.IDNService.convertToDisplayIDN(
+ providerURI.host,
+ {}
+ );
+ let currentBaseDomain =
+ browsingContext.currentWindowContext.documentPrincipal.baseDomain;
+ // Localize the description
+ // Bug 1797154 - Convert localization calls to use the async formatValues.
+ let localization = new Localization(
+ ["preview/identityCredentialNotification.ftl"],
+ true
+ );
+ let descriptionMessage = localization.formatValueSync(
+ "identity-credential-policy-description",
+ {
+ host: currentBaseDomain,
+ provider: providerDisplayDomain,
+ }
+ );
+ 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"
+ );
+
+ // Populate the content of the policy panel
+ let description = browser.ownerDocument.getElementById(
+ "identity-credential-policy-explanation"
+ );
+ description.textContent = descriptionMessage;
+ let privacyPolicyAnchor = browser.ownerDocument.getElementById(
+ "identity-credential-privacy-policy"
+ );
+ privacyPolicyAnchor.hidden = true;
+ if (identityCredentialMetadata.privacy_policy_url) {
+ privacyPolicyAnchor.href =
+ identityCredentialMetadata.privacy_policy_url;
+ privacyPolicyAnchor.hidden = false;
+ }
+ let termsOfServiceAnchor = browser.ownerDocument.getElementById(
+ "identity-credential-terms-of-service"
+ );
+ termsOfServiceAnchor.hidden = true;
+ if (identityCredentialMetadata.terms_of_service_url) {
+ termsOfServiceAnchor.href =
+ identityCredentialMetadata.terms_of_service_url;
+ termsOfServiceAnchor.hidden = false;
+ }
+
+ // Construct the necessary arguments for notification behavior
+ let options = {};
+ 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;
+ browser.ownerGlobal.PopupNotifications.show(
+ browser,
+ "identity-credential",
+ title,
+ "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 {IdentityAccountList} accountList - The list of accounts the user selects from
+ * @returns {Promise<IdentityAccount>} The user-selected account
+ */
+ showAccountListPrompt(browsingContext, accountList) {
+ // For testing only.
+ if (lazy.SELECT_FIRST_IN_UI_LISTS) {
+ return fulfilledPromiseFromFirstListElement(accountList.accounts);
+ }
+ return new Promise(function(resolve, reject) {
+ let browser = browsingContext.top.embedderElement;
+ if (!browser) {
+ reject();
+ return;
+ }
+ let currentOrigin =
+ browsingContext.currentWindowContext.documentPrincipal.originNoSuffix;
+
+ // Localize all strings to be used
+ // Bug 1797154 - Convert localization calls to use the async formatValues.
+ let localization = new Localization(
+ ["preview/identityCredentialNotification.ftl"],
+ true
+ );
+ let headerMessage = localization.formatValueSync(
+ "identity-credential-header-accounts",
+ {
+ host: "<>",
+ }
+ );
+ let descriptionMessage = localization.formatValueSync(
+ "identity-credential-description-account-explanation",
+ {
+ host: currentOrigin,
+ }
+ );
+ let [cancel] = localization.formatMessagesSync([
+ { 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;
+
+ // Add the description text
+ browser.ownerDocument.getElementById(
+ "credential-account-explanation"
+ ).textContent = descriptionMessage;
+
+ // 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 (let accountIndex in accountList.accounts) {
+ let account = accountList.accounts[accountIndex];
+ let newItem = itemTemplate.content.firstElementChild.cloneNode(true);
+ newItem.firstElementChild.textContent = account.email;
+ newItem.setAttribute("oncommand", "this.callback()");
+ newItem.callback = function() {
+ let notification = browser.ownerGlobal.PopupNotifications.getNotification(
+ "identity-credential",
+ browser
+ );
+ browser.ownerGlobal.PopupNotifications.remove(notification);
+ resolve(account);
+ };
+ listBox.append(newItem);
+ }
+
+ // Construct the necessary arguments for notification behavior
+ let options = {
+ name: currentOrigin,
+ };
+ let mainAction = {
+ label: cancelLabel,
+ accessKey: cancelKey,
+ callback(event) {
+ reject();
+ },
+ };
+
+ // 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.ownerGlobal.PopupNotifications.show(
+ browser,
+ "identity-credential",
+ headerMessage,
+ "identity-credential-notification-icon",
+ mainAction,
+ null,
+ 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) {
+ return;
+ }
+ let notification = browser.ownerGlobal.PopupNotifications.getNotification(
+ "identity-credential",
+ browser
+ );
+ if (notification) {
+ browser.ownerGlobal.PopupNotifications.remove(notification);
+ }
+ }
+}
diff --git a/toolkit/components/credentialmanagement/IdentityCredentialStorageService.cpp b/toolkit/components/credentialmanagement/IdentityCredentialStorageService.cpp
new file mode 100644
index 0000000000..c5fb698b80
--- /dev/null
+++ b/toolkit/components/credentialmanagement/IdentityCredentialStorageService.cpp
@@ -0,0 +1,613 @@
+/* -*- 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 "IdentityCredentialStorageService.h"
+#include "ErrorList.h"
+#include "MainThreadUtils.h"
+#include "mozIStorageService.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageStatement.h"
+#include "mozStorageCID.h"
+#include "mozilla/Base64.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsCRT.h"
+#include "nsDebug.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIObserverService.h"
+#include "nsServiceManagerUtils.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)
+
+IdentityCredentialStorageService::~IdentityCredentialStorageService() {
+ AssertIsOnMainThread();
+}
+
+already_AddRefed<IdentityCredentialStorageService>
+IdentityCredentialStorageService::GetSingleton() {
+ AssertIsOnMainThread();
+ if (!gIdentityCredentialStorageService) {
+ gIdentityCredentialStorageService = new IdentityCredentialStorageService();
+ ClearOnShutdown(&gIdentityCredentialStorageService);
+ nsresult rv = gIdentityCredentialStorageService->Init();
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ RefPtr<IdentityCredentialStorageService> service =
+ gIdentityCredentialStorageService;
+ return service.forget();
+}
+
+nsresult createTable(mozIStorageConnection* aDatabase) {
+ // 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.
+ NS_ENSURE_ARG_POINTER(aDatabase);
+ nsresult rv = aDatabase->SetSchemaVersion(SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aDatabase->ExecuteSimpleSQL(
+ nsLiteralCString("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_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult getMemoryDatabaseConnection(mozIStorageConnection** aDatabase) {
+ nsCOMPtr<mozIStorageService> storage =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(storage, NS_ERROR_UNEXPECTED);
+ nsresult rv = storage->OpenSpecialDatabase(
+ kMozStorageMemoryStorageKey, "icsprivatedb"_ns,
+ mozIStorageService::CONNECTION_DEFAULT, aDatabase);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool ready = false;
+ (*aDatabase)->GetConnectionReady(&ready);
+ NS_ENSURE_TRUE(ready, NS_ERROR_UNEXPECTED);
+ bool tableExists = false;
+ (*aDatabase)->TableExists("identity"_ns, &tableExists);
+ if (!tableExists) {
+ rv = createTable(*aDatabase);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult getDiskDatabaseConnection(mozIStorageConnection** aDatabase) {
+ // Create the file we store the database in.
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = profileDir->AppendNative(nsLiteralCString(ACCOUNT_STATE_FILENAME));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageService> storage =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(storage, NS_ERROR_UNEXPECTED);
+ rv = storage->OpenDatabase(profileDir, mozIStorageService::CONNECTION_DEFAULT,
+ aDatabase);
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ rv = profileDir->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = storage->OpenDatabase(
+ profileDir, mozIStorageService::CONNECTION_DEFAULT, aDatabase);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool ready = false;
+ (*aDatabase)->GetConnectionReady(&ready);
+ NS_ENSURE_TRUE(ready, NS_ERROR_UNEXPECTED);
+ return NS_OK;
+}
+
+nsresult IdentityCredentialStorageService::Init() {
+ AssertIsOnMainThread();
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ RefPtr<mozIStorageConnection> database;
+ rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create the database table for memory and disk if it doesn't already exist
+ bool tableExists = false;
+ database->TableExists("identity"_ns, &tableExists);
+ if (!tableExists) {
+ rv = createTable(database);
+ 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);
+
+ mInitialized.Flip();
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::SetState(
+ nsIPrincipal* aRPPrincipal, nsIPrincipal* aIDPPrincipal,
+ nsACString const& aCredentialID, bool aRegistered, bool aAllowLogout) {
+ AssertIsOnMainThread();
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ return NS_OK;
+ }
+ MOZ_ASSERT(XRE_IsParentProcess());
+ NS_ENSURE_ARG_POINTER(aRPPrincipal);
+ NS_ENSURE_ARG_POINTER(aIDPPrincipal);
+
+ nsresult rv;
+ rv = IdentityCredentialStorageService::ValidatePrincipal(aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozIStorageConnection> database;
+ rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Build the statement on one of our databases. This is in memory if the RP
+ // principal is private, disk otherwise. The queries are the same, using the
+ // SQLite3 UPSERT syntax.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsCString rpOrigin;
+ rv = aRPPrincipal->GetOrigin(rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ constexpr auto upsert_query = nsLiteralCString(
+ "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");
+ if (OriginAttributes::IsPrivateBrowsing(rpOrigin)) {
+ rv = privateBrowsingDatabase->CreateStatement(upsert_query,
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = database->CreateStatement(upsert_query, getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Bind the arguments to the query and execute it.
+ 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->BindInt32ByIndex(3, aRegistered ? 1 : 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByIndex(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;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::GetState(
+ nsIPrincipal* aRPPrincipal, nsIPrincipal* aIDPPrincipal,
+ nsACString const& aCredentialID, bool* aRegistered, bool* aAllowLogout) {
+ AssertIsOnMainThread();
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ *aRegistered = false;
+ *aAllowLogout = false;
+ return NS_OK;
+ }
+ nsresult rv;
+ rv = IdentityCredentialStorageService::ValidatePrincipal(aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozIStorageConnection> database;
+ rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto constexpr selectQuery = nsLiteralCString(
+ "SELECT registered, allowLogout FROM identity WHERE rpOrigin=?1 AND "
+ "idpOrigin=?2 AND credentialId=?3");
+
+ nsCString rpOrigin;
+ nsCString idpOrigin;
+ rv = aRPPrincipal->GetOrigin(rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aIDPPrincipal->GetOrigin(idpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If the RP principal is private, query the provided tuple in memory
+ nsCOMPtr<mozIStorageStatement> stmt;
+ if (OriginAttributes::IsPrivateBrowsing(rpOrigin)) {
+ rv = privateBrowsingDatabase->CreateStatement(selectQuery,
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = database->CreateStatement(selectQuery, getter_AddRefs(stmt));
+ 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();
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ return NS_OK;
+ }
+ nsresult rv;
+ rv = IdentityCredentialStorageService::ValidatePrincipal(aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozIStorageConnection> database;
+ rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto constexpr deleteQuery = nsLiteralCString(
+ "DELETE FROM identity WHERE rpOrigin=?1 AND idpOrigin=?2 AND "
+ "credentialId=?3");
+
+ // Delete all entries matching this tuple.
+ // We only have to execute one statement because we don't want to delete
+ // entries on disk from PB mode
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsCString rpOrigin;
+ rv = aRPPrincipal->GetOrigin(rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (OriginAttributes::IsPrivateBrowsing(rpOrigin)) {
+ rv = privateBrowsingDatabase->CreateStatement(deleteQuery,
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = database->CreateStatement(deleteQuery, getter_AddRefs(stmt));
+ 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;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::Clear() {
+ AssertIsOnMainThread();
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ return NS_OK;
+ }
+ RefPtr<mozIStorageConnection> database;
+ nsresult rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We just clear all rows from both databases.
+ rv = privateBrowsingDatabase->ExecuteSimpleSQL(
+ nsLiteralCString("DELETE FROM identity;"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = database->ExecuteSimpleSQL(nsLiteralCString("DELETE FROM identity;"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+IdentityCredentialStorageService::DeleteFromOriginAttributesPattern(
+ nsAString const& aOriginAttributesPattern) {
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ return NS_OK;
+ }
+ nsresult rv;
+ NS_ENSURE_FALSE(aOriginAttributesPattern.IsEmpty(), NS_ERROR_FAILURE);
+
+ // parse the JSON origin attribute argument
+ OriginAttributesPattern oaPattern;
+ if (!oaPattern.Init(aOriginAttributesPattern)) {
+ NS_ERROR("Could not parse the argument for OriginAttributes");
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<mozIStorageConnection> database;
+ rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozIStorageConnection> chosenDatabase;
+ if (!oaPattern.mPrivateBrowsingId.WasPassed() ||
+ oaPattern.mPrivateBrowsingId.Value() ==
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) {
+ chosenDatabase = database;
+ } else {
+ chosenDatabase = privateBrowsingDatabase;
+ }
+
+ chosenDatabase->BeginTransaction();
+ Vector<int64_t> rowIdsToDelete;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = chosenDatabase->CreateStatement(
+ nsLiteralCString("SELECT rowid, rpOrigin FROM identity;"),
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ int64_t rowId;
+ nsCString rpOrigin;
+ nsCString rpOriginNoSuffix;
+ rv = stmt->GetInt64(0, &rowId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ rv = stmt->GetUTF8String(1, rpOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ OriginAttributes oa;
+ bool parsedSuccessfully = oa.PopulateFromOrigin(rpOrigin, rpOriginNoSuffix);
+ NS_ENSURE_TRUE(parsedSuccessfully, NS_ERROR_FAILURE);
+ if (oaPattern.Matches(oa)) {
+ bool appendedSuccessfully = rowIdsToDelete.append(rowId);
+ NS_ENSURE_TRUE(appendedSuccessfully, NS_ERROR_FAILURE);
+ }
+ }
+
+ for (auto rowId : rowIdsToDelete) {
+ rv = chosenDatabase->CreateStatement(
+ nsLiteralCString("DELETE FROM identity WHERE rowid = ?1"),
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByIndex(0, rowId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = chosenDatabase->CommitTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::DeleteFromTimeRange(
+ int64_t aStart, int64_t aEnd) {
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ return NS_OK;
+ }
+ nsresult rv;
+
+ RefPtr<mozIStorageConnection> database;
+ rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ auto constexpr deleteTimeQuery = nsLiteralCString(
+ "DELETE FROM identity WHERE modificationTime > ?1 and modificationTime < "
+ "?2");
+
+ // We just clear all matching rows from both databases.
+ rv = privateBrowsingDatabase->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);
+
+ rv = database->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;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::
+ IdentityCredentialStorageService::DeleteFromPrincipal(
+ nsIPrincipal* aRPPrincipal) {
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ return NS_OK;
+ }
+ nsresult rv =
+ IdentityCredentialStorageService::ValidatePrincipal(aRPPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozIStorageConnection> database;
+ rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto constexpr deletePrincipalQuery =
+ nsLiteralCString("DELETE FROM identity WHERE rpOrigin=?1");
+
+ // create the (identical) statement on the database we need to clear from.
+ // Like delete, a given argument is either private or not, so there is only
+ // one argument to execute.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsCString rpOrigin;
+ rv = aRPPrincipal->GetOrigin(rpOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (OriginAttributes::IsPrivateBrowsing(rpOrigin)) {
+ rv = privateBrowsingDatabase->CreateStatement(deletePrincipalQuery,
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = database->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;
+}
+
+NS_IMETHODIMP IdentityCredentialStorageService::DeleteFromBaseDomain(
+ nsACString const& aBaseDomain) {
+ if (!StaticPrefs::dom_security_credentialmanagement_identity_enabled()) {
+ return NS_OK;
+ }
+ nsresult rv;
+
+ RefPtr<mozIStorageConnection> database;
+ rv = getDiskDatabaseConnection(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ rv = getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ auto constexpr deleteBaseDomainQuery =
+ nsLiteralCString("DELETE FROM identity WHERE rpBaseDomain=?1");
+
+ // We just clear all matching rows from both databases.
+ // This is very easy because we store the relevant base domain.
+ rv = database->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);
+
+ rv = privateBrowsingDatabase->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::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")) {
+ RefPtr<mozIStorageConnection> privateBrowsingDatabase;
+ nsresult rv =
+ getMemoryDatabaseConnection(getter_AddRefs(privateBrowsingDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Delete exactly all of the private browsing data
+ rv = privateBrowsingDatabase->ExecuteSimpleSQL(
+ nsLiteralCString("DELETE FROM identity;"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+// 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;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/credentialmanagement/IdentityCredentialStorageService.h b/toolkit/components/credentialmanagement/IdentityCredentialStorageService.h
new file mode 100644
index 0000000000..2fe285726f
--- /dev/null
+++ b/toolkit/components/credentialmanagement/IdentityCredentialStorageService.h
@@ -0,0 +1,47 @@
+/* -*- 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 "mozIStorageConnection.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "nsIIdentityCredentialStorageService.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "nsIObserver.h"
+#include "nsISupports.h"
+
+namespace mozilla {
+
+class IdentityCredentialStorageService final
+ : public nsIIdentityCredentialStorageService,
+ public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIDENTITYCREDENTIALSTORAGESERVICE
+ NS_DECL_NSIOBSERVER
+
+ // Returns the singleton instance which is addreffed.
+ static already_AddRefed<IdentityCredentialStorageService> GetSingleton();
+
+ IdentityCredentialStorageService(const IdentityCredentialStorageService&) =
+ delete;
+ IdentityCredentialStorageService& operator=(
+ const IdentityCredentialStorageService&) = delete;
+
+ private:
+ IdentityCredentialStorageService() = default;
+ ~IdentityCredentialStorageService();
+ nsresult Init();
+ static nsresult ValidatePrincipal(nsIPrincipal* aPrincipal);
+
+ FlippedOnce<false> mInitialized;
+};
+
+} // 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..ee07c01e21
--- /dev/null
+++ b/toolkit/components/credentialmanagement/moz.build
@@ -0,0 +1,33 @@
+# -*- 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",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/toolkit/components/credentialmanagement/nsIIdentityCredentialPromptService.idl b/toolkit/components/credentialmanagement/nsIIdentityCredentialPromptService.idl
new file mode 100644
index 0000000000..483689c933
--- /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 one of the elements of the list.
+ Promise showProviderPrompt(in BrowsingContext browsingContext, in jsval identityProviders);
+ // 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 identityClientMetadata);
+ // Display to the user an interface to choose from among the accounts listed.
+ // Resolves with one of the elements of the list.
+ Promise showAccountListPrompt(in BrowsingContext browsingContext, in jsval accountList);
+ // 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/xpcshell/head.js b/toolkit/components/credentialmanagement/tests/xpcshell/head.js
new file mode 100644
index 0000000000..3e7b3c2ae6
--- /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.import(
+ "resource://testing-common/TestUtils.jsm"
+);
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.ini b/toolkit/components/credentialmanagement/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..79536069e2
--- /dev/null
+++ b/toolkit/components/credentialmanagement/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = head.js
+prefs =
+ dom.security.credentialmanagement.identity.enabled=true
+
+[test_identity_credential_storage_service.js]