diff options
Diffstat (limited to 'toolkit/components/credentialmanagement')
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, ®isteredInt); + 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] |