summaryrefslogtreecommitdiffstats
path: root/toolkit/identity
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/identity')
-rw-r--r--toolkit/identity/FirefoxAccounts.jsm320
-rw-r--r--toolkit/identity/Identity.jsm309
-rw-r--r--toolkit/identity/IdentityCryptoService.cpp571
-rw-r--r--toolkit/identity/IdentityProvider.jsm496
-rw-r--r--toolkit/identity/IdentityStore.jsm97
-rw-r--r--toolkit/identity/IdentityUtils.jsm111
-rw-r--r--toolkit/identity/LogUtils.jsm103
-rw-r--r--toolkit/identity/MinimalIdentity.jsm242
-rw-r--r--toolkit/identity/RelyingParty.jsm367
-rw-r--r--toolkit/identity/Sandbox.jsm152
-rw-r--r--toolkit/identity/jwcrypto.jsm180
-rw-r--r--toolkit/identity/moz.build36
-rw-r--r--toolkit/identity/nsIIdentityCryptoService.idl106
-rw-r--r--toolkit/identity/tests/chrome/.eslintrc.js7
-rw-r--r--toolkit/identity/tests/chrome/chrome.ini13
-rw-r--r--toolkit/identity/tests/chrome/sandbox_content.html32
-rw-r--r--toolkit/identity/tests/chrome/sandbox_content.sjs36
-rw-r--r--toolkit/identity/tests/chrome/sandbox_content_alert.html20
-rw-r--r--toolkit/identity/tests/chrome/sandbox_content_framed.html17
-rw-r--r--toolkit/identity/tests/chrome/sandbox_content_perms.html64
-rw-r--r--toolkit/identity/tests/chrome/sandbox_content_popup.html25
-rw-r--r--toolkit/identity/tests/chrome/sandbox_content_redirect.html2
-rw-r--r--toolkit/identity/tests/chrome/sandbox_content_redirect.html^headers^2
-rw-r--r--toolkit/identity/tests/chrome/test_sandbox.xul324
-rw-r--r--toolkit/identity/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/identity/tests/unit/data/idp_1/.well-known/browserid5
-rw-r--r--toolkit/identity/tests/unit/data/idp_invalid_1/.well-known/browserid5
-rw-r--r--toolkit/identity/tests/unit/head_identity.js256
-rw-r--r--toolkit/identity/tests/unit/tail_identity.js8
-rw-r--r--toolkit/identity/tests/unit/test_authentication.js159
-rw-r--r--toolkit/identity/tests/unit/test_crypto_service.js122
-rw-r--r--toolkit/identity/tests/unit/test_firefox_accounts.js270
-rw-r--r--toolkit/identity/tests/unit/test_identity.js114
-rw-r--r--toolkit/identity/tests/unit/test_identity_utils.js46
-rw-r--r--toolkit/identity/tests/unit/test_jwcrypto.js281
-rw-r--r--toolkit/identity/tests/unit/test_load_modules.js20
-rw-r--r--toolkit/identity/tests/unit/test_log_utils.js74
-rw-r--r--toolkit/identity/tests/unit/test_minimalidentity.js223
-rw-r--r--toolkit/identity/tests/unit/test_observer_topics.js114
-rw-r--r--toolkit/identity/tests/unit/test_provisioning.js242
-rw-r--r--toolkit/identity/tests/unit/test_relying_party.js255
-rw-r--r--toolkit/identity/tests/unit/test_store.js64
-rw-r--r--toolkit/identity/tests/unit/test_well-known.js90
-rw-r--r--toolkit/identity/tests/unit/xpcshell.ini24
44 files changed, 6011 insertions, 0 deletions
diff --git a/toolkit/identity/FirefoxAccounts.jsm b/toolkit/identity/FirefoxAccounts.jsm
new file mode 100644
index 000000000..54efaf3b4
--- /dev/null
+++ b/toolkit/identity/FirefoxAccounts.jsm
@@ -0,0 +1,320 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FirefoxAccounts"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
+ "resource://gre/modules/identity/IdentityUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
+ "resource://gre/modules/identity/IdentityUtils.jsm");
+
+// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
+// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
+// default.
+const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
+try {
+ this.LOG_LEVEL =
+ Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
+ && Services.prefs.getCharPref(PREF_LOG_LEVEL);
+} catch (e) {
+ this.LOG_LEVEL = Log.Level.Error;
+}
+
+var log = Log.repository.getLogger("Identity.FxAccounts");
+log.level = LOG_LEVEL;
+log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+
+#ifdef MOZ_B2G
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsManager",
+ "resource://gre/modules/FxAccountsManager.jsm",
+ "FxAccountsManager");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+#else
+log.warn("The FxAccountsManager is only functional in B2G at this time.");
+var FxAccountsManager = null;
+var ONVERIFIED_NOTIFICATION = null;
+var ONLOGIN_NOTIFICATION = null;
+var ONLOGOUT_NOTIFICATION = null;
+#endif
+
+function FxAccountsService() {
+ Services.obs.addObserver(this, "quit-application-granted", false);
+ if (ONVERIFIED_NOTIFICATION) {
+ Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, false);
+ Services.obs.addObserver(this, ONLOGIN_NOTIFICATION, false);
+ Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
+ }
+
+ // Maintain interface parity with Identity.jsm and MinimalIdentity.jsm
+ this.RP = this;
+
+ this._rpFlows = new Map();
+
+ // Enable us to mock FxAccountsManager service in testing
+ this.fxAccountsManager = FxAccountsManager;
+}
+
+FxAccountsService.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case null:
+ // Guard against matching null ON*_NOTIFICATION
+ break;
+ case ONVERIFIED_NOTIFICATION:
+ log.debug("Received " + ONVERIFIED_NOTIFICATION + "; firing request()s");
+ for (let [rpId,] of this._rpFlows) {
+ this.request(rpId);
+ }
+ break;
+ case ONLOGIN_NOTIFICATION:
+ log.debug("Received " + ONLOGIN_NOTIFICATION + "; doLogin()s fired");
+ for (let [rpId,] of this._rpFlows) {
+ this.request(rpId);
+ }
+ break;
+ case ONLOGOUT_NOTIFICATION:
+ log.debug("Received " + ONLOGOUT_NOTIFICATION + "; doLogout()s fired");
+ for (let [rpId,] of this._rpFlows) {
+ this.doLogout(rpId);
+ }
+ break;
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ if (ONVERIFIED_NOTIFICATION) {
+ Services.obs.removeObserver(this, ONVERIFIED_NOTIFICATION);
+ Services.obs.removeObserver(this, ONLOGIN_NOTIFICATION);
+ Services.obs.removeObserver(this, ONLOGOUT_NOTIFICATION);
+ }
+ break;
+ }
+ },
+
+ cleanupRPRequest: function(aRp) {
+ aRp.pendingRequest = false;
+ this._rpFlows.set(aRp.id, aRp);
+ },
+
+ /**
+ * Register a listener for a given windowID as a result of a call to
+ * navigator.id.watch().
+ *
+ * @param aRPCaller
+ * (Object) an object that represents the caller document, and
+ * is expected to have properties:
+ * - id (unique, e.g. uuid)
+ * - origin (string)
+ *
+ * and a bunch of callbacks
+ * - doReady()
+ * - doLogin()
+ * - doLogout()
+ * - doError()
+ * - doCancel()
+ *
+ */
+ watch: function watch(aRpCaller) {
+ this._rpFlows.set(aRpCaller.id, aRpCaller);
+ log.debug("watch: " + aRpCaller.id);
+ log.debug("Current rp flows: " + this._rpFlows.size);
+
+ // Log the user in, if possible, and then call ready().
+ let runnable = {
+ run: () => {
+ this.fxAccountsManager.getAssertion(aRpCaller.audience,
+ aRpCaller.principal,
+ { silent:true }).then(
+ data => {
+ if (data) {
+ this.doLogin(aRpCaller.id, data);
+ } else {
+ this.doLogout(aRpCaller.id);
+ }
+ this.doReady(aRpCaller.id);
+ },
+ error => {
+ log.error("get silent assertion failed: " + JSON.stringify(error));
+ this.doError(aRpCaller.id, error);
+ }
+ );
+ }
+ };
+ Services.tm.currentThread.dispatch(runnable,
+ Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ /**
+ * Delete the flow when the screen is unloaded
+ */
+ unwatch: function(aRpCallerId, aTargetMM) {
+ log.debug("unwatching: " + aRpCallerId);
+ this._rpFlows.delete(aRpCallerId);
+ },
+
+ /**
+ * Initiate a login with user interaction as a result of a call to
+ * navigator.id.request().
+ *
+ * @param aRPId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ * @param aOptions
+ * (Object) options including privacyPolicy, termsOfService
+ */
+ request: function request(aRPId, aOptions) {
+ aOptions = aOptions || {};
+ let rp = this._rpFlows.get(aRPId);
+ if (!rp) {
+ log.error("request() called before watch()");
+ return;
+ }
+
+ // We check if we already have a pending request for this RP and in that
+ // case we just bail out. We don't want duplicated onlogin or oncancel
+ // events.
+ if (rp.pendingRequest) {
+ log.debug("request() already called");
+ return;
+ }
+
+ // Otherwise, we set the RP flow with the pending request flag.
+ rp.pendingRequest = true;
+ this._rpFlows.set(rp.id, rp);
+
+ let options = makeMessageObject(rp);
+ objectCopy(aOptions, options);
+
+ log.debug("get assertion for " + rp.audience);
+
+ this.fxAccountsManager.getAssertion(rp.audience, rp.principal, options)
+ .then(
+ data => {
+ log.debug("got assertion for " + rp.audience + ": " + data);
+ this.doLogin(aRPId, data);
+ },
+ error => {
+ log.debug("get assertion failed: " + JSON.stringify(error));
+ // Cancellation is passed through an error channel; here we reroute.
+ if ((error.error && (error.error.details == "DIALOG_CLOSED_BY_USER")) ||
+ (error.details == "DIALOG_CLOSED_BY_USER")) {
+ return this.doCancel(aRPId);
+ }
+ this.doError(aRPId, error);
+ }
+ )
+ .then(
+ () => {
+ this.cleanupRPRequest(rp);
+ }
+ )
+ .catch(
+ () => {
+ this.cleanupRPRequest(rp);
+ }
+ );
+ },
+
+ /**
+ * Invoked when a user wishes to logout of a site (for instance, when clicking
+ * on an in-content logout button).
+ *
+ * @param aRpCallerId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ */
+ logout: function logout(aRpCallerId) {
+ // XXX Bug 945363 - Resolve the SSO story for FXA and implement
+ // logout accordingly.
+ //
+ // For now, it makes no sense to logout from a specific RP in
+ // Firefox Accounts, so just directly call the logout callback.
+ if (!this._rpFlows.has(aRpCallerId)) {
+ log.error("logout() called before watch()");
+ return;
+ }
+
+ // Call logout() on the next tick
+ let runnable = {
+ run: () => {
+ this.fxAccountsManager.signOut().then(() => {
+ this.doLogout(aRpCallerId);
+ });
+ }
+ };
+ Services.tm.currentThread.dispatch(runnable,
+ Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ childProcessShutdown: function childProcessShutdown(messageManager) {
+ for (let [key,] of this._rpFlows) {
+ if (this._rpFlows.get(key)._mm === messageManager) {
+ this._rpFlows.delete(key);
+ }
+ }
+ },
+
+ doLogin: function doLogin(aRpCallerId, aAssertion) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doLogin found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doLogin(aAssertion);
+ },
+
+ doLogout: function doLogout(aRpCallerId) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doLogout found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doLogout();
+ },
+
+ doReady: function doReady(aRpCallerId) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doReady found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doReady();
+ },
+
+ doCancel: function doCancel(aRpCallerId) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doCancel found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doCancel();
+ },
+
+ doError: function doError(aRpCallerId, aError) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doError found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doError(aError);
+ }
+};
+
+this.FirefoxAccounts = new FxAccountsService();
+
diff --git a/toolkit/identity/Identity.jsm b/toolkit/identity/Identity.jsm
new file mode 100644
index 000000000..a27b7c93d
--- /dev/null
+++ b/toolkit/identity/Identity.jsm
@@ -0,0 +1,309 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["IdentityService"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
+Cu.import("resource://gre/modules/identity/RelyingParty.jsm");
+Cu.import("resource://gre/modules/identity/IdentityProvider.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["core"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs));
+}
+
+function IDService() {
+ Services.obs.addObserver(this, "quit-application-granted", false);
+ Services.obs.addObserver(this, "identity-auth-complete", false);
+
+ this._store = IdentityStore;
+ this.RP = RelyingParty;
+ this.IDP = IdentityProvider;
+}
+
+IDService.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.shutdown();
+ break;
+ case "identity-auth-complete":
+ if (!aSubject || !aSubject.wrappedJSObject)
+ break;
+ let subject = aSubject.wrappedJSObject;
+ log("Auth complete:", aSubject.wrappedJSObject);
+ // We have authenticated in order to provision an identity.
+ // So try again.
+ this.selectIdentity(subject.rpId, subject.identity);
+ break;
+ }
+ },
+
+ reset: function reset() {
+ // Explicitly call reset() on our RP and IDP classes.
+ // This is here to make testing easier. When the
+ // quit-application-granted signal is emitted, reset() will be
+ // called here, on RP, on IDP, and on the store. So you don't
+ // need to use this :)
+ this._store.reset();
+ this.RP.reset();
+ this.IDP.reset();
+ },
+
+ shutdown: function shutdown() {
+ log("shutdown");
+ Services.obs.removeObserver(this, "identity-auth-complete");
+ // try to prevent abort/crash during shutdown of mochitest-browser2...
+ try {
+ Services.obs.removeObserver(this, "quit-application-granted");
+ } catch (e) {}
+ },
+
+ /**
+ * Parse an email into username and domain if it is valid, else return null
+ */
+ parseEmail: function parseEmail(email) {
+ var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/);
+ if (match) {
+ return {
+ username: match[1],
+ domain: match[2]
+ };
+ }
+ return null;
+ },
+
+ /**
+ * The UX wants to add a new identity
+ * often followed by selectIdentity()
+ *
+ * @param aIdentity
+ * (string) the email chosen for login
+ */
+ addIdentity: function addIdentity(aIdentity) {
+ if (this._store.fetchIdentity(aIdentity) === null) {
+ this._store.addIdentity(aIdentity, null, null);
+ }
+ },
+
+ /**
+ * The UX comes back and calls selectIdentity once the user has picked
+ * an identity.
+ *
+ * @param aRPId
+ * (integer) the id of the doc object obtained in .watch() and
+ * passed to the UX component.
+ *
+ * @param aIdentity
+ * (string) the email chosen for login
+ */
+ selectIdentity: function selectIdentity(aRPId, aIdentity) {
+ log("selectIdentity: RP id:", aRPId, "identity:", aIdentity);
+
+ // Get the RP that was stored when watch() was invoked.
+ let rp = this.RP._rpFlows[aRPId];
+ if (!rp) {
+ reportError("selectIdentity", "Invalid RP id: ", aRPId);
+ return;
+ }
+
+ // It's possible that we are in the process of provisioning an
+ // identity.
+ let provId = rp.provId;
+
+ let rpLoginOptions = {
+ loggedInUser: aIdentity,
+ origin: rp.origin
+ };
+ log("selectIdentity: provId:", provId, "origin:", rp.origin);
+
+ // Once we have a cert, and once the user is authenticated with the
+ // IdP, we can generate an assertion and deliver it to the doc.
+ let self = this;
+ this.RP._generateAssertion(rp.origin, aIdentity, function hadReadyAssertion(err, assertion) {
+ if (!err && assertion) {
+ self.RP._doLogin(rp, rpLoginOptions, assertion);
+ return;
+
+ }
+ // Need to provision an identity first. Begin by discovering
+ // the user's IdP.
+ self._discoverIdentityProvider(aIdentity, function gotIDP(err, idpParams) {
+ if (err) {
+ rp.doError(err);
+ return;
+ }
+
+ // The idpParams tell us where to go to provision and authenticate
+ // the identity.
+ self.IDP._provisionIdentity(aIdentity, idpParams, provId, function gotID(err, aProvId) {
+
+ // Provision identity may have created a new provision flow
+ // for us. To make it easier to relate provision flows with
+ // RP callers, we cross index the two here.
+ rp.provId = aProvId;
+ self.IDP._provisionFlows[aProvId].rpId = aRPId;
+
+ // At this point, we already have a cert. If the user is also
+ // already authenticated with the IdP, then we can try again
+ // to generate an assertion and login.
+ if (err) {
+ // We are not authenticated. If we have already tried to
+ // authenticate and failed, then this is a "hard fail" and
+ // we give up. Otherwise we try to authenticate with the
+ // IdP.
+
+ if (self.IDP._provisionFlows[aProvId].didAuthentication) {
+ self.IDP._cleanUpProvisionFlow(aProvId);
+ self.RP._cleanUpProvisionFlow(aRPId, aProvId);
+ log("ERROR: selectIdentity: authentication hard fail");
+ rp.doError("Authentication fail.");
+ return;
+ }
+ // Try to authenticate with the IdP. Note that we do
+ // not clean up the provision flow here. We will continue
+ // to use it.
+ self.IDP._doAuthentication(aProvId, idpParams);
+ return;
+ }
+
+ // Provisioning flows end when a certificate has been registered.
+ // Thus IdentityProvider's registerCertificate() cleans up the
+ // current provisioning flow. We only do this here on error.
+ self.RP._generateAssertion(rp.origin, aIdentity, function gotAssertion(err, assertion) {
+ if (err) {
+ rp.doError(err);
+ return;
+ }
+ self.RP._doLogin(rp, rpLoginOptions, assertion);
+ self.RP._cleanUpProvisionFlow(aRPId, aProvId);
+ return;
+ });
+ });
+ });
+ });
+ },
+
+ // methods for chrome and add-ons
+
+ /**
+ * Discover the IdP for an identity
+ *
+ * @param aIdentity
+ * (string) the email we're logging in with
+ *
+ * @param aCallback
+ * (function) callback to invoke on completion
+ * with first-positional parameter the error.
+ */
+ _discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) {
+ // XXX bug 767610 - validate email address call
+ // When that is available, we can remove this custom parser
+ var parsedEmail = this.parseEmail(aIdentity);
+ if (parsedEmail === null) {
+ aCallback("Could not parse email: " + aIdentity);
+ return;
+ }
+ log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain);
+
+ this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) {
+ // idpParams includes the pk, authorization url, and
+ // provisioning url.
+
+ // XXX bug 769861 follow any authority delegations
+ // if no well-known at any point in the delegation
+ // fall back to browserid.org as IdP
+ return aCallback(err, idpParams);
+ });
+ },
+
+ /**
+ * Fetch the well-known file from the domain.
+ *
+ * @param aDomain
+ *
+ * @param aScheme
+ * (string) (optional) Protocol to use. Default is https.
+ * This is necessary because we are unable to test
+ * https.
+ *
+ * @param aCallback
+ *
+ */
+ _fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme='https') {
+ // XXX bug 769854 make tests https and remove aScheme option
+ let url = aScheme + '://' + aDomain + "/.well-known/browserid";
+ log("_fetchWellKnownFile:", url);
+
+ // this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window
+ let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+
+ // XXX bug 769865 gracefully handle being off-line
+ // XXX bug 769866 decide on how to handle redirects
+ req.open("GET", url, true);
+ req.responseType = "json";
+ req.mozBackgroundRequest = true;
+ req.onload = function _fetchWellKnownFile_onload() {
+ if (req.status < 200 || req.status >= 400) {
+ log("_fetchWellKnownFile", url, ": server returned status:", req.status);
+ return aCallback("Error");
+ }
+ try {
+ let idpParams = req.response;
+
+ // Verify that the IdP returned a valid configuration
+ if (! (idpParams.provisioning &&
+ idpParams.authentication &&
+ idpParams['public-key'])) {
+ let errStr= "Invalid well-known file from: " + aDomain;
+ log("_fetchWellKnownFile:", errStr);
+ return aCallback(errStr);
+ }
+
+ let callbackObj = {
+ domain: aDomain,
+ idpParams: idpParams,
+ };
+ log("_fetchWellKnownFile result: ", callbackObj);
+ // Yay. Valid IdP configuration for the domain.
+ return aCallback(null, callbackObj);
+
+ } catch (err) {
+ reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err);
+ return aCallback(err.toString());
+ }
+ };
+ req.onerror = function _fetchWellKnownFile_onerror() {
+ log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText);
+ log("ERROR: _fetchWellKnownFile:", err);
+ return aCallback("Error");
+ };
+ req.send(null);
+ },
+
+};
+
+this.IdentityService = new IDService();
diff --git a/toolkit/identity/IdentityCryptoService.cpp b/toolkit/identity/IdentityCryptoService.cpp
new file mode 100644
index 000000000..cafe07999
--- /dev/null
+++ b/toolkit/identity/IdentityCryptoService.cpp
@@ -0,0 +1,571 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=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 "nsIIdentityCryptoService.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsNSSShutDown.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "nsCOMPtr.h"
+#include "nsProxyRelease.h"
+#include "nsString.h"
+#include "mozilla/ArrayUtils.h" // ArrayLength
+#include "mozilla/Base64.h"
+#include "ScopedNSSTypes.h"
+#include "NSSErrorsService.h"
+
+#include "nss.h"
+#include "pk11pub.h"
+#include "secmod.h"
+#include "secerr.h"
+#include "keyhi.h"
+#include "cryptohi.h"
+
+#include <limits.h>
+
+using namespace mozilla;
+
+namespace {
+
+void
+HexEncode(const SECItem * it, nsACString & result)
+{
+ const char * digits = "0123456789ABCDEF";
+ result.SetCapacity((it->len * 2) + 1);
+ result.SetLength(it->len * 2);
+ char * p = result.BeginWriting();
+ for (unsigned int i = 0; i < it->len; ++i) {
+ *p++ = digits[it->data[i] >> 4];
+ *p++ = digits[it->data[i] & 0x0f];
+ }
+}
+
+#define DSA_KEY_TYPE_STRING (NS_LITERAL_CSTRING("DS160"))
+#define RSA_KEY_TYPE_STRING (NS_LITERAL_CSTRING("RS256"))
+
+class KeyPair : public nsIIdentityKeyPair, public nsNSSShutDownObject
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIDENTITYKEYPAIR
+
+ KeyPair(SECKEYPrivateKey* aPrivateKey, SECKEYPublicKey* aPublicKey,
+ nsIEventTarget* aOperationThread);
+
+private:
+ ~KeyPair()
+ {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown()) {
+ return;
+ }
+ destructorSafeDestroyNSSReference();
+ shutdown(ShutdownCalledFrom::Object);
+ }
+
+ void virtualDestroyNSSReference() override
+ {
+ destructorSafeDestroyNSSReference();
+ }
+
+ void destructorSafeDestroyNSSReference()
+ {
+ SECKEY_DestroyPrivateKey(mPrivateKey);
+ mPrivateKey = nullptr;
+ SECKEY_DestroyPublicKey(mPublicKey);
+ mPublicKey = nullptr;
+ }
+
+ SECKEYPrivateKey * mPrivateKey;
+ SECKEYPublicKey * mPublicKey;
+ nsCOMPtr<nsIEventTarget> mThread;
+
+ KeyPair(const KeyPair &) = delete;
+ void operator=(const KeyPair &) = delete;
+};
+
+NS_IMPL_ISUPPORTS(KeyPair, nsIIdentityKeyPair)
+
+class KeyGenRunnable : public Runnable, public nsNSSShutDownObject
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ KeyGenRunnable(KeyType keyType, nsIIdentityKeyGenCallback * aCallback,
+ nsIEventTarget* aOperationThread);
+
+private:
+ ~KeyGenRunnable()
+ {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown()) {
+ return;
+ }
+ destructorSafeDestroyNSSReference();
+ shutdown(ShutdownCalledFrom::Object);
+ }
+
+ virtual void virtualDestroyNSSReference() override
+ {
+ destructorSafeDestroyNSSReference();
+ }
+
+ void destructorSafeDestroyNSSReference()
+ {
+ }
+
+ const KeyType mKeyType; // in
+ nsMainThreadPtrHandle<nsIIdentityKeyGenCallback> mCallback; // in
+ nsresult mRv; // out
+ nsCOMPtr<nsIIdentityKeyPair> mKeyPair; // out
+ nsCOMPtr<nsIEventTarget> mThread;
+
+ KeyGenRunnable(const KeyGenRunnable &) = delete;
+ void operator=(const KeyGenRunnable &) = delete;
+};
+
+class SignRunnable : public Runnable, public nsNSSShutDownObject
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ SignRunnable(const nsACString & textToSign, SECKEYPrivateKey * privateKey,
+ nsIIdentitySignCallback * aCallback);
+
+private:
+ ~SignRunnable()
+ {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown()) {
+ return;
+ }
+ destructorSafeDestroyNSSReference();
+ shutdown(ShutdownCalledFrom::Object);
+ }
+
+ void virtualDestroyNSSReference() override
+ {
+ destructorSafeDestroyNSSReference();
+ }
+
+ void destructorSafeDestroyNSSReference()
+ {
+ SECKEY_DestroyPrivateKey(mPrivateKey);
+ mPrivateKey = nullptr;
+ }
+
+ const nsCString mTextToSign; // in
+ SECKEYPrivateKey* mPrivateKey; // in
+ nsMainThreadPtrHandle<nsIIdentitySignCallback> mCallback; // in
+ nsresult mRv; // out
+ nsCString mSignature; // out
+
+private:
+ SignRunnable(const SignRunnable &) = delete;
+ void operator=(const SignRunnable &) = delete;
+};
+
+class IdentityCryptoService final : public nsIIdentityCryptoService
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIIDENTITYCRYPTOSERVICE
+
+ IdentityCryptoService() { }
+ nsresult Init()
+ {
+ nsresult rv;
+ nsCOMPtr<nsISupports> dummyUsedToEnsureNSSIsInitialized
+ = do_GetService("@mozilla.org/psm;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIThread> thread;
+ rv = NS_NewNamedThread("IdentityCrypto", getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mThread = thread.forget();
+
+ return NS_OK;
+ }
+
+private:
+ ~IdentityCryptoService() { }
+ IdentityCryptoService(const KeyPair &) = delete;
+ void operator=(const IdentityCryptoService &) = delete;
+
+ nsCOMPtr<nsIEventTarget> mThread;
+};
+
+NS_IMPL_ISUPPORTS(IdentityCryptoService, nsIIdentityCryptoService)
+
+NS_IMETHODIMP
+IdentityCryptoService::GenerateKeyPair(
+ const nsACString & keyTypeString, nsIIdentityKeyGenCallback * callback)
+{
+ KeyType keyType;
+ if (keyTypeString.Equals(RSA_KEY_TYPE_STRING)) {
+ keyType = rsaKey;
+ } else if (keyTypeString.Equals(DSA_KEY_TYPE_STRING)) {
+ keyType = dsaKey;
+ } else {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCOMPtr<nsIRunnable> r = new KeyGenRunnable(keyType, callback, mThread);
+ nsresult rv = mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+IdentityCryptoService::Base64UrlEncode(const nsACString & utf8Input,
+ nsACString & result)
+{
+ return Base64URLEncode(utf8Input.Length(),
+ reinterpret_cast<const uint8_t*>(utf8Input.BeginReading()),
+ Base64URLEncodePaddingPolicy::Include, result);
+}
+
+KeyPair::KeyPair(SECKEYPrivateKey * privateKey, SECKEYPublicKey * publicKey,
+ nsIEventTarget* operationThread)
+ : mPrivateKey(privateKey)
+ , mPublicKey(publicKey)
+ , mThread(operationThread)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexRSAPublicKeyExponent(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == rsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.rsa.publicExponent, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexRSAPublicKeyModulus(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == rsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.rsa.modulus, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAPrime(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.prime, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSASubPrime(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.subPrime, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAGenerator(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.base, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAPublicValue(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.publicValue, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetKeyType(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+
+ switch (mPublicKey->keyType) {
+ case rsaKey: result = RSA_KEY_TYPE_STRING; return NS_OK;
+ case dsaKey: result = DSA_KEY_TYPE_STRING; return NS_OK;
+ default: return NS_ERROR_UNEXPECTED;
+ }
+}
+
+NS_IMETHODIMP
+KeyPair::Sign(const nsACString & textToSign,
+ nsIIdentitySignCallback* callback)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIRunnable> r = new SignRunnable(textToSign, mPrivateKey,
+ callback);
+
+ return mThread->Dispatch(r, NS_DISPATCH_NORMAL);
+}
+
+KeyGenRunnable::KeyGenRunnable(KeyType keyType,
+ nsIIdentityKeyGenCallback * callback,
+ nsIEventTarget* operationThread)
+ : mKeyType(keyType)
+ , mCallback(new nsMainThreadPtrHolder<nsIIdentityKeyGenCallback>(callback))
+ , mRv(NS_ERROR_NOT_INITIALIZED)
+ , mThread(operationThread)
+{
+}
+
+MOZ_MUST_USE nsresult
+GenerateKeyPair(PK11SlotInfo * slot,
+ SECKEYPrivateKey ** privateKey,
+ SECKEYPublicKey ** publicKey,
+ CK_MECHANISM_TYPE mechanism,
+ void * params)
+{
+ *publicKey = nullptr;
+ *privateKey = PK11_GenerateKeyPair(slot, mechanism, params, publicKey,
+ PR_FALSE /*isPerm*/,
+ PR_TRUE /*isSensitive*/,
+ nullptr /*&pwdata*/);
+ if (!*privateKey) {
+ MOZ_ASSERT(!*publicKey);
+ return mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
+ }
+ if (!*publicKey) {
+ SECKEY_DestroyPrivateKey(*privateKey);
+ *privateKey = nullptr;
+ MOZ_CRASH("PK11_GnerateKeyPair returned private key without public key");
+ }
+
+ return NS_OK;
+}
+
+
+MOZ_MUST_USE nsresult
+GenerateRSAKeyPair(PK11SlotInfo * slot,
+ SECKEYPrivateKey ** privateKey,
+ SECKEYPublicKey ** publicKey)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ PK11RSAGenParams rsaParams;
+ rsaParams.keySizeInBits = 2048;
+ rsaParams.pe = 0x10001;
+ return GenerateKeyPair(slot, privateKey, publicKey, CKM_RSA_PKCS_KEY_PAIR_GEN,
+ &rsaParams);
+}
+
+MOZ_MUST_USE nsresult
+GenerateDSAKeyPair(PK11SlotInfo * slot,
+ SECKEYPrivateKey ** privateKey,
+ SECKEYPublicKey ** publicKey)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // XXX: These could probably be static const arrays, but this way we avoid
+ // compiler warnings and also we avoid having to worry much about whether the
+ // functions that take these inputs will (unexpectedly) modify them.
+
+ // Using NIST parameters. Some other BrowserID components require that these
+ // exact parameters are used.
+ uint8_t P[] = {
+ 0xFF,0x60,0x04,0x83,0xDB,0x6A,0xBF,0xC5,0xB4,0x5E,0xAB,0x78,
+ 0x59,0x4B,0x35,0x33,0xD5,0x50,0xD9,0xF1,0xBF,0x2A,0x99,0x2A,
+ 0x7A,0x8D,0xAA,0x6D,0xC3,0x4F,0x80,0x45,0xAD,0x4E,0x6E,0x0C,
+ 0x42,0x9D,0x33,0x4E,0xEE,0xAA,0xEF,0xD7,0xE2,0x3D,0x48,0x10,
+ 0xBE,0x00,0xE4,0xCC,0x14,0x92,0xCB,0xA3,0x25,0xBA,0x81,0xFF,
+ 0x2D,0x5A,0x5B,0x30,0x5A,0x8D,0x17,0xEB,0x3B,0xF4,0xA0,0x6A,
+ 0x34,0x9D,0x39,0x2E,0x00,0xD3,0x29,0x74,0x4A,0x51,0x79,0x38,
+ 0x03,0x44,0xE8,0x2A,0x18,0xC4,0x79,0x33,0x43,0x8F,0x89,0x1E,
+ 0x22,0xAE,0xEF,0x81,0x2D,0x69,0xC8,0xF7,0x5E,0x32,0x6C,0xB7,
+ 0x0E,0xA0,0x00,0xC3,0xF7,0x76,0xDF,0xDB,0xD6,0x04,0x63,0x8C,
+ 0x2E,0xF7,0x17,0xFC,0x26,0xD0,0x2E,0x17
+ };
+
+ uint8_t Q[] = {
+ 0xE2,0x1E,0x04,0xF9,0x11,0xD1,0xED,0x79,0x91,0x00,0x8E,0xCA,
+ 0xAB,0x3B,0xF7,0x75,0x98,0x43,0x09,0xC3
+ };
+
+ uint8_t G[] = {
+ 0xC5,0x2A,0x4A,0x0F,0xF3,0xB7,0xE6,0x1F,0xDF,0x18,0x67,0xCE,
+ 0x84,0x13,0x83,0x69,0xA6,0x15,0x4F,0x4A,0xFA,0x92,0x96,0x6E,
+ 0x3C,0x82,0x7E,0x25,0xCF,0xA6,0xCF,0x50,0x8B,0x90,0xE5,0xDE,
+ 0x41,0x9E,0x13,0x37,0xE0,0x7A,0x2E,0x9E,0x2A,0x3C,0xD5,0xDE,
+ 0xA7,0x04,0xD1,0x75,0xF8,0xEB,0xF6,0xAF,0x39,0x7D,0x69,0xE1,
+ 0x10,0xB9,0x6A,0xFB,0x17,0xC7,0xA0,0x32,0x59,0x32,0x9E,0x48,
+ 0x29,0xB0,0xD0,0x3B,0xBC,0x78,0x96,0xB1,0x5B,0x4A,0xDE,0x53,
+ 0xE1,0x30,0x85,0x8C,0xC3,0x4D,0x96,0x26,0x9A,0xA8,0x90,0x41,
+ 0xF4,0x09,0x13,0x6C,0x72,0x42,0xA3,0x88,0x95,0xC9,0xD5,0xBC,
+ 0xCA,0xD4,0xF3,0x89,0xAF,0x1D,0x7A,0x4B,0xD1,0x39,0x8B,0xD0,
+ 0x72,0xDF,0xFA,0x89,0x62,0x33,0x39,0x7A
+ };
+
+ static_assert(MOZ_ARRAY_LENGTH(P) == 1024 / CHAR_BIT, "bad DSA P");
+ static_assert(MOZ_ARRAY_LENGTH(Q) == 160 / CHAR_BIT, "bad DSA Q");
+ static_assert(MOZ_ARRAY_LENGTH(G) == 1024 / CHAR_BIT, "bad DSA G");
+
+ PQGParams pqgParams = {
+ nullptr /*arena*/,
+ { siBuffer, P, static_cast<unsigned int>(mozilla::ArrayLength(P)) },
+ { siBuffer, Q, static_cast<unsigned int>(mozilla::ArrayLength(Q)) },
+ { siBuffer, G, static_cast<unsigned int>(mozilla::ArrayLength(G)) }
+ };
+
+ return GenerateKeyPair(slot, privateKey, publicKey, CKM_DSA_KEY_PAIR_GEN,
+ &pqgParams);
+}
+
+NS_IMETHODIMP
+KeyGenRunnable::Run()
+{
+ if (!NS_IsMainThread()) {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown()) {
+ mRv = NS_ERROR_NOT_AVAILABLE;
+ } else {
+ // We always want to use the internal slot for BrowserID; in particular,
+ // we want to avoid smartcard slots.
+ PK11SlotInfo *slot = PK11_GetInternalSlot();
+ if (!slot) {
+ mRv = NS_ERROR_UNEXPECTED;
+ } else {
+ SECKEYPrivateKey *privk = nullptr;
+ SECKEYPublicKey *pubk = nullptr;
+
+ switch (mKeyType) {
+ case rsaKey:
+ mRv = GenerateRSAKeyPair(slot, &privk, &pubk);
+ break;
+ case dsaKey:
+ mRv = GenerateDSAKeyPair(slot, &privk, &pubk);
+ break;
+ default:
+ MOZ_CRASH("unknown key type");
+ }
+
+ PK11_FreeSlot(slot);
+
+ if (NS_SUCCEEDED(mRv)) {
+ MOZ_ASSERT(privk);
+ MOZ_ASSERT(pubk);
+ // mKeyPair will take over ownership of privk and pubk
+ mKeyPair = new KeyPair(privk, pubk, mThread);
+ }
+ }
+ }
+
+ NS_DispatchToMainThread(this);
+ } else {
+ // Back on Main Thread
+ (void) mCallback->GenerateKeyPairFinished(mRv, mKeyPair);
+ }
+ return NS_OK;
+}
+
+SignRunnable::SignRunnable(const nsACString & aText,
+ SECKEYPrivateKey * privateKey,
+ nsIIdentitySignCallback * aCallback)
+ : mTextToSign(aText)
+ , mPrivateKey(SECKEY_CopyPrivateKey(privateKey))
+ , mCallback(new nsMainThreadPtrHolder<nsIIdentitySignCallback>(aCallback))
+ , mRv(NS_ERROR_NOT_INITIALIZED)
+{
+}
+
+NS_IMETHODIMP
+SignRunnable::Run()
+{
+ if (!NS_IsMainThread()) {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown()) {
+ mRv = NS_ERROR_NOT_AVAILABLE;
+ } else {
+ // We need the output in PKCS#11 format, not DER encoding, so we must use
+ // PK11_HashBuf and PK11_Sign instead of SEC_SignData.
+
+ SECItem sig = { siBuffer, nullptr, 0 };
+ int sigLength = PK11_SignatureLen(mPrivateKey);
+ if (sigLength <= 0) {
+ mRv = mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
+ } else if (!SECITEM_AllocItem(nullptr, &sig, sigLength)) {
+ mRv = mozilla::psm::GetXPCOMFromNSSError(PR_GetError());
+ } else {
+ uint8_t hash[32]; // big enough for SHA-1 or SHA-256
+ SECOidTag hashAlg = mPrivateKey->keyType == dsaKey ? SEC_OID_SHA1
+ : SEC_OID_SHA256;
+ SECItem hashItem = { siBuffer, hash,
+ hashAlg == SEC_OID_SHA1 ? 20u : 32u };
+
+ mRv = MapSECStatus(PK11_HashBuf(hashAlg, hash,
+ const_cast<uint8_t*>(reinterpret_cast<const uint8_t *>(
+ mTextToSign.get())),
+ mTextToSign.Length()));
+ if (NS_SUCCEEDED(mRv)) {
+ mRv = MapSECStatus(PK11_Sign(mPrivateKey, &sig, &hashItem));
+ }
+ if (NS_SUCCEEDED(mRv)) {
+ mRv = Base64URLEncode(sig.len, sig.data,
+ Base64URLEncodePaddingPolicy::Include,
+ mSignature);
+ }
+ SECITEM_FreeItem(&sig, false);
+ }
+ }
+
+ NS_DispatchToMainThread(this);
+ } else {
+ // Back on Main Thread
+ (void) mCallback->SignFinished(mRv, mSignature);
+ }
+
+ return NS_OK;
+}
+
+// XPCOM module registration
+
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(IdentityCryptoService, Init)
+
+#define NS_IDENTITYCRYPTOSERVICE_CID \
+ {0xbea13a3a, 0x44e8, 0x4d7f, {0xa0, 0xa2, 0x2c, 0x67, 0xf8, 0x4e, 0x3a, 0x97}}
+
+NS_DEFINE_NAMED_CID(NS_IDENTITYCRYPTOSERVICE_CID);
+
+const mozilla::Module::CIDEntry kCIDs[] = {
+ { &kNS_IDENTITYCRYPTOSERVICE_CID, false, nullptr, IdentityCryptoServiceConstructor },
+ { nullptr }
+};
+
+const mozilla::Module::ContractIDEntry kContracts[] = {
+ { "@mozilla.org/identity/crypto-service;1", &kNS_IDENTITYCRYPTOSERVICE_CID },
+ { nullptr }
+};
+
+const mozilla::Module kModule = {
+ mozilla::Module::kVersion,
+ kCIDs,
+ kContracts
+};
+
+} // unnamed namespace
+
+NSMODULE_DEFN(identity) = &kModule;
diff --git a/toolkit/identity/IdentityProvider.jsm b/toolkit/identity/IdentityProvider.jsm
new file mode 100644
index 000000000..11529bfba
--- /dev/null
+++ b/toolkit/identity/IdentityProvider.jsm
@@ -0,0 +1,496 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/identity/Sandbox.jsm");
+
+this.EXPORTED_SYMBOLS = ["IdentityProvider"];
+const FALLBACK_PROVIDER = "browserid.org";
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["IDP"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError.apply(Logger, ["IDP"].concat(aMessageArgs));
+}
+
+
+function IdentityProviderService() {
+ XPCOMUtils.defineLazyModuleGetter(this,
+ "_store",
+ "resource://gre/modules/identity/IdentityStore.jsm",
+ "IdentityStore");
+
+ this.reset();
+}
+
+IdentityProviderService.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+ _sandboxConfigured: false,
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.shutdown();
+ break;
+ }
+ },
+
+ reset: function IDP_reset() {
+ // Clear the provisioning flows. Provision flows contain an
+ // identity, idpParams (how to reach the IdP to provision and
+ // authenticate), a callback (a completion callback for when things
+ // are done), and a provisioningFrame (which is the provisioning
+ // sandbox). Additionally, two callbacks will be attached:
+ // beginProvisioningCallback and genKeyPairCallback.
+ this._provisionFlows = {};
+
+ // Clear the authentication flows. Authentication flows attach
+ // to provision flows. In the process of provisioning an id, it
+ // may be necessary to authenticate with an IdP. The authentication
+ // flow maintains the state of that authentication process.
+ this._authenticationFlows = {};
+ },
+
+ getProvisionFlow: function getProvisionFlow(aProvId, aErrBack) {
+ let provFlow = this._provisionFlows[aProvId];
+ if (provFlow) {
+ return provFlow;
+ }
+
+ let err = "No provisioning flow found with id " + aProvId;
+ log("ERROR:", err);
+ if (typeof aErrBack === 'function') {
+ aErrBack(err);
+ }
+
+ return undefined;
+ },
+
+ shutdown: function RP_shutdown() {
+ this.reset();
+
+ if (this._sandboxConfigured) {
+ // Tear down message manager listening on the hidden window
+ Cu.import("resource://gre/modules/DOMIdentity.jsm");
+ DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, false);
+ this._sandboxConfigured = false;
+ }
+
+ Services.obs.removeObserver(this, "quit-application-granted");
+ },
+
+ get securityLevel() {
+ return 1;
+ },
+
+ get certDuration() {
+ switch (this.securityLevel) {
+ default:
+ return 3600;
+ }
+ },
+
+ /**
+ * Provision an Identity
+ *
+ * @param aIdentity
+ * (string) the email we're logging in with
+ *
+ * @param aIDPParams
+ * (object) parameters of the IdP
+ *
+ * @param aCallback
+ * (function) callback to invoke on completion
+ * with first-positional parameter the error.
+ */
+ _provisionIdentity: function _provisionIdentity(aIdentity, aIDPParams, aProvId, aCallback) {
+ let provPath = aIDPParams.idpParams.provisioning;
+ let url = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(provPath);
+ log("_provisionIdentity: identity:", aIdentity, "url:", url);
+
+ // If aProvId is not null, then we already have a flow
+ // with a sandbox. Otherwise, get a sandbox and create a
+ // new provision flow.
+
+ if (aProvId) {
+ // Re-use an existing sandbox
+ log("_provisionIdentity: re-using sandbox in provisioning flow with id:", aProvId);
+ this._provisionFlows[aProvId].provisioningSandbox.reload();
+
+ } else {
+ this._createProvisioningSandbox(url, function createdSandbox(aSandbox) {
+ // create a provisioning flow, using the sandbox id, and
+ // stash callback associated with this provisioning workflow.
+
+ let provId = aSandbox.id;
+ this._provisionFlows[provId] = {
+ identity: aIdentity,
+ idpParams: aIDPParams,
+ securityLevel: this.securityLevel,
+ provisioningSandbox: aSandbox,
+ callback: function doCallback(aErr) {
+ aCallback(aErr, provId);
+ },
+ };
+
+ log("_provisionIdentity: Created sandbox and provisioning flow with id:", provId);
+ // XXX bug 769862 - provisioning flow should timeout after N seconds
+
+ }.bind(this));
+ }
+ },
+
+ // DOM Methods
+ /**
+ * the provisioning iframe sandbox has called navigator.id.beginProvisioning()
+ *
+ * @param aCaller
+ * (object) the iframe sandbox caller with all callbacks and
+ * other information. Callbacks include:
+ * - doBeginProvisioningCallback(id, duration_s)
+ * - doGenKeyPairCallback(pk)
+ */
+ beginProvisioning: function beginProvisioning(aCaller) {
+ log("beginProvisioning:", aCaller.id);
+
+ // Expect a flow for this caller already to be underway.
+ let provFlow = this.getProvisionFlow(aCaller.id, aCaller.doError);
+
+ // keep the caller object around
+ provFlow.caller = aCaller;
+
+ let identity = provFlow.identity;
+ let frame = provFlow.provisioningFrame;
+
+ // Determine recommended length of cert.
+ let duration = this.certDuration;
+
+ // Make a record that we have begun provisioning. This is required
+ // for genKeyPair.
+ provFlow.didBeginProvisioning = true;
+
+ // Let the sandbox know to invoke the callback to beginProvisioning with
+ // the identity and cert length.
+ return aCaller.doBeginProvisioningCallback(identity, duration);
+ },
+
+ /**
+ * the provisioning iframe sandbox has called
+ * navigator.id.raiseProvisioningFailure()
+ *
+ * @param aProvId
+ * (int) the identifier of the provisioning flow tied to that sandbox
+ * @param aReason
+ */
+ raiseProvisioningFailure: function raiseProvisioningFailure(aProvId, aReason) {
+ reportError("Provisioning failure", aReason);
+
+ // look up the provisioning caller and its callback
+ let provFlow = this.getProvisionFlow(aProvId);
+
+ // Sandbox is deleted in _cleanUpProvisionFlow in case we re-use it.
+
+ // This may be either a "soft" or "hard" fail. If it's a
+ // soft fail, we'll flow through setAuthenticationFlow, where
+ // the provision flow data will be copied into a new auth
+ // flow. If it's a hard fail, then the callback will be
+ // responsible for cleaning up the now defunct provision flow.
+
+ // invoke the callback with an error.
+ provFlow.callback(aReason);
+ },
+
+ /**
+ * When navigator.id.genKeyPair is called from provisioning iframe sandbox.
+ * Generates a keypair for the current user being provisioned.
+ *
+ * @param aProvId
+ * (int) the identifier of the provisioning caller tied to that sandbox
+ *
+ * It is an error to call genKeypair without receiving the callback for
+ * the beginProvisioning() call first.
+ */
+ genKeyPair: function genKeyPair(aProvId) {
+ // Look up the provisioning caller and make sure it's valid.
+ let provFlow = this.getProvisionFlow(aProvId);
+
+ if (!provFlow.didBeginProvisioning) {
+ let errStr = "ERROR: genKeyPair called before beginProvisioning";
+ log(errStr);
+ provFlow.callback(errStr);
+ return;
+ }
+
+ // Ok generate a keypair
+ jwcrypto.generateKeyPair(jwcrypto.ALGORITHMS.DS160, function gkpCb(err, kp) {
+ log("in gkp callback");
+ if (err) {
+ log("ERROR: genKeyPair:", err);
+ provFlow.callback(err);
+ return;
+ }
+
+ provFlow.kp = kp;
+
+ // Serialize the publicKey of the keypair and send it back to the
+ // sandbox.
+ log("genKeyPair: generated keypair for provisioning flow with id:", aProvId);
+ provFlow.caller.doGenKeyPairCallback(provFlow.kp.serializedPublicKey);
+ }.bind(this));
+ },
+
+ /**
+ * When navigator.id.registerCertificate is called from provisioning iframe
+ * sandbox.
+ *
+ * Sets the certificate for the user for which a certificate was requested
+ * via a preceding call to beginProvisioning (and genKeypair).
+ *
+ * @param aProvId
+ * (integer) the identifier of the provisioning caller tied to that
+ * sandbox
+ *
+ * @param aCert
+ * (String) A JWT representing the signed certificate for the user
+ * being provisioned, provided by the IdP.
+ */
+ registerCertificate: function registerCertificate(aProvId, aCert) {
+ log("registerCertificate:", aProvId, aCert);
+
+ // look up provisioning caller, make sure it's valid.
+ let provFlow = this.getProvisionFlow(aProvId);
+
+ if (!provFlow.caller) {
+ reportError("registerCertificate", "No provision flow or caller");
+ return;
+ }
+ if (!provFlow.kp) {
+ let errStr = "Cannot register a certificate without a keypair";
+ reportError("registerCertificate", errStr);
+ provFlow.callback(errStr);
+ return;
+ }
+
+ // store the keypair and certificate just provided in IDStore.
+ this._store.addIdentity(provFlow.identity, provFlow.kp, aCert);
+
+ // Great success!
+ provFlow.callback(null);
+
+ // Clean up the flow.
+ this._cleanUpProvisionFlow(aProvId);
+ },
+
+ /**
+ * Begin the authentication process with an IdP
+ *
+ * @param aProvId
+ * (int) the identifier of the provisioning flow which failed
+ *
+ * @param aCallback
+ * (function) to invoke upon completion, with
+ * first-positional-param error.
+ */
+ _doAuthentication: function _doAuthentication(aProvId, aIDPParams) {
+ log("_doAuthentication: provId:", aProvId, "idpParams:", aIDPParams);
+ // create an authentication caller and its identifier AuthId
+ // stash aIdentity, idpparams, and callback in it.
+
+ // extract authentication URL from idpParams
+ let authPath = aIDPParams.idpParams.authentication;
+ let authURI = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(authPath);
+
+ // beginAuthenticationFlow causes the "identity-auth" topic to be
+ // observed. Since it's sending a notification to the DOM, there's
+ // no callback. We wait for the DOM to trigger the next phase of
+ // provisioning.
+ this._beginAuthenticationFlow(aProvId, authURI);
+
+ // either we bind the AuthID to the sandbox ourselves, or UX does that,
+ // in which case we need to tell UX the AuthId.
+ // Currently, the UX creates the UI and gets the AuthId from the window
+ // and sets is with setAuthenticationFlow
+ },
+
+ /**
+ * The authentication frame has called navigator.id.beginAuthentication
+ *
+ * IMPORTANT: the aCaller is *always* non-null, even if this is called from
+ * a regular content page. We have to make sure, on every DOM call, that
+ * aCaller is an expected authentication-flow identifier. If not, we throw
+ * an error or something.
+ *
+ * @param aCaller
+ * (object) the authentication caller
+ *
+ */
+ beginAuthentication: function beginAuthentication(aCaller) {
+ log("beginAuthentication: caller id:", aCaller.id);
+
+ // Begin the authentication flow after having concluded a provisioning
+ // flow. The aCaller that the DOM gives us will have the same ID as
+ // the provisioning flow we just concluded. (see setAuthenticationFlow)
+ let authFlow = this._authenticationFlows[aCaller.id];
+ if (!authFlow) {
+ return aCaller.doError("beginAuthentication: no flow for caller id", aCaller.id);
+ }
+
+ authFlow.caller = aCaller;
+
+ let identity = this._provisionFlows[authFlow.provId].identity;
+
+ // tell the UI to start the authentication process
+ log("beginAuthentication: authFlow:", aCaller.id, "identity:", identity);
+ return authFlow.caller.doBeginAuthenticationCallback(identity);
+ },
+
+ /**
+ * The auth frame has called navigator.id.completeAuthentication
+ *
+ * @param aAuthId
+ * (int) the identifier of the authentication caller tied to that sandbox
+ *
+ */
+ completeAuthentication: function completeAuthentication(aAuthId) {
+ log("completeAuthentication:", aAuthId);
+
+ // look up the AuthId caller, and get its callback.
+ let authFlow = this._authenticationFlows[aAuthId];
+ if (!authFlow) {
+ reportError("completeAuthentication", "No auth flow with id", aAuthId);
+ return;
+ }
+ let provId = authFlow.provId;
+
+ // delete caller
+ delete authFlow['caller'];
+ delete this._authenticationFlows[aAuthId];
+
+ let provFlow = this.getProvisionFlow(provId);
+ provFlow.didAuthentication = true;
+ let subject = {
+ rpId: provFlow.rpId,
+ identity: provFlow.identity,
+ };
+ Services.obs.notifyObservers({ wrappedJSObject: subject }, "identity-auth-complete", aAuthId);
+ },
+
+ /**
+ * The auth frame has called navigator.id.cancelAuthentication
+ *
+ * @param aAuthId
+ * (int) the identifier of the authentication caller
+ *
+ */
+ cancelAuthentication: function cancelAuthentication(aAuthId) {
+ log("cancelAuthentication:", aAuthId);
+
+ // look up the AuthId caller, and get its callback.
+ let authFlow = this._authenticationFlows[aAuthId];
+ if (!authFlow) {
+ reportError("cancelAuthentication", "No auth flow with id:", aAuthId);
+ return;
+ }
+ let provId = authFlow.provId;
+
+ // delete caller
+ delete authFlow['caller'];
+ delete this._authenticationFlows[aAuthId];
+
+ let provFlow = this.getProvisionFlow(provId);
+ provFlow.didAuthentication = true;
+ Services.obs.notifyObservers(null, "identity-auth-complete", aAuthId);
+
+ // invoke callback with ERROR.
+ let errStr = "Authentication canceled by IDP";
+ log("ERROR: cancelAuthentication:", errStr);
+ provFlow.callback(errStr);
+ },
+
+ /**
+ * Called by the UI to set the ID and caller for the authentication flow after it gets its ID
+ */
+ setAuthenticationFlow: function(aAuthId, aProvId) {
+ // this is the transition point between the two flows,
+ // provision and authenticate. We tell the auth flow which
+ // provisioning flow it is started from.
+ log("setAuthenticationFlow: authId:", aAuthId, "provId:", aProvId);
+ this._authenticationFlows[aAuthId] = { provId: aProvId };
+ this._provisionFlows[aProvId].authId = aAuthId;
+ },
+
+ /**
+ * Load the provisioning URL in a hidden frame to start the provisioning
+ * process.
+ */
+ _createProvisioningSandbox: function _createProvisioningSandbox(aURL, aCallback) {
+ log("_createProvisioningSandbox:", aURL);
+
+ if (!this._sandboxConfigured) {
+ // Configure message manager listening on the hidden window
+ Cu.import("resource://gre/modules/DOMIdentity.jsm");
+ DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, true);
+ this._sandboxConfigured = true;
+ }
+
+ new Sandbox(aURL, aCallback);
+ },
+
+ /**
+ * Load the authentication UI to start the authentication process.
+ */
+ _beginAuthenticationFlow: function _beginAuthenticationFlow(aProvId, aURL) {
+ log("_beginAuthenticationFlow:", aProvId, aURL);
+ let propBag = {provId: aProvId};
+
+ Services.obs.notifyObservers({wrappedJSObject:propBag}, "identity-auth", aURL);
+ },
+
+ /**
+ * Clean up a provision flow and the authentication flow and sandbox
+ * that may be attached to it.
+ */
+ _cleanUpProvisionFlow: function _cleanUpProvisionFlow(aProvId) {
+ log('_cleanUpProvisionFlow:', aProvId);
+ let prov = this._provisionFlows[aProvId];
+
+ // Clean up the sandbox, if there is one.
+ if (prov.provisioningSandbox) {
+ let sandbox = this._provisionFlows[aProvId]['provisioningSandbox'];
+ if (sandbox.free) {
+ log('_cleanUpProvisionFlow: freeing sandbox');
+ sandbox.free();
+ }
+ delete this._provisionFlows[aProvId]['provisioningSandbox'];
+ }
+
+ // Clean up a related authentication flow, if there is one.
+ if (this._authenticationFlows[prov.authId]) {
+ delete this._authenticationFlows[prov.authId];
+ }
+
+ // Finally delete the provision flow
+ delete this._provisionFlows[aProvId];
+ }
+
+};
+
+this.IdentityProvider = new IdentityProviderService();
diff --git a/toolkit/identity/IdentityStore.jsm b/toolkit/identity/IdentityStore.jsm
new file mode 100644
index 000000000..1827a839e
--- /dev/null
+++ b/toolkit/identity/IdentityStore.jsm
@@ -0,0 +1,97 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["IdentityStore"];
+
+// the data store for IDService
+// written as a separate thing so it can easily be mocked
+function IDServiceStore() {
+ this.reset();
+}
+
+// Note: eventually these methods may be async, but we haven no need for this
+// for now, since we're not storing to disk.
+IDServiceStore.prototype = {
+ addIdentity: function addIdentity(aEmail, aKeyPair, aCert) {
+ this._identities[aEmail] = {keyPair: aKeyPair, cert: aCert};
+ },
+ fetchIdentity: function fetchIdentity(aEmail) {
+ return aEmail in this._identities ? this._identities[aEmail] : null;
+ },
+ removeIdentity: function removeIdentity(aEmail) {
+ let data = this._identities[aEmail];
+ delete this._identities[aEmail];
+ return data;
+ },
+ getIdentities: function getIdentities() {
+ // XXX - should clone?
+ return this._identities;
+ },
+ clearCert: function clearCert(aEmail) {
+ // XXX - should remove key from store?
+ this._identities[aEmail].cert = null;
+ this._identities[aEmail].keyPair = null;
+ },
+
+ /**
+ * set the login state for a given origin
+ *
+ * @param aOrigin
+ * (string) a web origin
+ *
+ * @param aState
+ * (boolean) whether or not the user is logged in
+ *
+ * @param aEmail
+ * (email) the email address the user is logged in with,
+ * or, if not logged in, the default email for that origin.
+ */
+ setLoginState: function setLoginState(aOrigin, aState, aEmail) {
+ if (aState && !aEmail) {
+ throw "isLoggedIn cannot be set to true without an email";
+ }
+ return this._loginStates[aOrigin] = {isLoggedIn: aState, email: aEmail};
+ },
+ getLoginState: function getLoginState(aOrigin) {
+ return aOrigin in this._loginStates ? this._loginStates[aOrigin] : null;
+ },
+ clearLoginState: function clearLoginState(aOrigin) {
+ delete this._loginStates[aOrigin];
+ },
+
+ reset: function Store_reset() {
+ // _identities associates emails with keypairs and certificates
+ this._identities = {};
+
+ // _loginStates associates. remote origins with a login status and
+ // the email the user has chosen as his or her identity when logging
+ // into that origin.
+ this._loginStates = {};
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.reset();
+ break;
+ }
+ },
+};
+
+this.IdentityStore = new IDServiceStore();
diff --git a/toolkit/identity/IdentityUtils.jsm b/toolkit/identity/IdentityUtils.jsm
new file mode 100644
index 000000000..a34c0b133
--- /dev/null
+++ b/toolkit/identity/IdentityUtils.jsm
@@ -0,0 +1,111 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=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/. */
+
+// functions common to Identity.jsm and MinimalIdentity.jsm
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "checkDeprecated",
+ "checkRenamed",
+ "getRandomId",
+ "objectCopy",
+ "makeMessageObject",
+];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Logger",
+ "resource://gre/modules/identity/LogUtils.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["Identity"].concat(aMessageArgs));
+}
+
+function defined(item) {
+ return typeof item !== 'undefined';
+}
+
+var checkDeprecated = this.checkDeprecated = function checkDeprecated(aOptions, aField) {
+ if (defined(aOptions[aField])) {
+ log("WARNING: field is deprecated:", aField);
+ return true;
+ }
+ return false;
+};
+
+this.checkRenamed = function checkRenamed(aOptions, aOldName, aNewName) {
+ if (defined(aOptions[aOldName]) &&
+ defined(aOptions[aNewName])) {
+ let err = "You cannot provide both " + aOldName + " and " + aNewName;
+ Logger.reportError(err);
+ throw new Error(err);
+ }
+
+ if (checkDeprecated(aOptions, aOldName)) {
+ aOptions[aNewName] = aOptions[aOldName];
+ delete(aOptions[aOldName]);
+ }
+};
+
+this.getRandomId = function getRandomId() {
+ return uuidgen.generateUUID().toString();
+};
+
+/*
+ * copy source object into target, excluding private properties
+ * (those whose names begin with an underscore)
+ */
+this.objectCopy = function objectCopy(source, target) {
+ let desc;
+ Object.getOwnPropertyNames(source).forEach(function(name) {
+ if (name[0] !== '_') {
+ desc = Object.getOwnPropertyDescriptor(source, name);
+ Object.defineProperty(target, name, desc);
+ }
+ });
+};
+
+this.makeMessageObject = function makeMessageObject(aRpCaller) {
+ let options = {};
+
+ options.id = aRpCaller.id;
+ options.origin = aRpCaller.origin;
+
+ // Backwards compatibility with Persona beta:
+ // loggedInUser can be undefined, null, or a string
+ options.loggedInUser = aRpCaller.loggedInUser;
+
+ // Special flag for internal calls for Persona in b2g
+ options._internal = aRpCaller._internal;
+
+ Object.keys(aRpCaller).forEach(function(option) {
+ // Duplicate the callerobject, scrubbing out functions and other
+ // internal variables (like _mm, the message manager object)
+ if (!Object.hasOwnProperty(this, option)
+ && option[0] !== '_'
+ && typeof aRpCaller[option] !== 'function') {
+ options[option] = aRpCaller[option];
+ }
+ });
+
+ // check validity of message structure
+ if ((typeof options.id === 'undefined') ||
+ (typeof options.origin === 'undefined')) {
+ let err = "id and origin required in relying-party message: " + JSON.stringify(options);
+ reportError(err);
+ throw new Error(err);
+ }
+
+ return options;
+}
+
diff --git a/toolkit/identity/LogUtils.jsm b/toolkit/identity/LogUtils.jsm
new file mode 100644
index 000000000..ec0f4c420
--- /dev/null
+++ b/toolkit/identity/LogUtils.jsm
@@ -0,0 +1,103 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Logger"];
+const PREF_DEBUG = "toolkit.identity.debug";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function IdentityLogger() {
+ Services.prefs.addObserver(PREF_DEBUG, this, false);
+ this._debug = Services.prefs.getBoolPref(PREF_DEBUG);
+ return this;
+}
+
+IdentityLogger.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "nsPref:changed":
+ this._debug = Services.prefs.getBoolPref(PREF_DEBUG);
+ break;
+
+ case "quit-application-granted":
+ Services.prefs.removeObserver(PREF_DEBUG, this);
+ break;
+
+ default:
+ this.log("Logger observer", "Unknown topic:", aTopic);
+ break;
+ }
+ },
+
+ _generateLogMessage: function _generateLogMessage(aPrefix, args) {
+ // create a string representation of a list of arbitrary things
+ let strings = [];
+
+ // XXX bug 770418 - args look like flattened array, not list of strings
+
+ args.forEach(function(arg) {
+ if (typeof arg === 'string') {
+ strings.push(arg);
+ } else if (typeof arg === 'undefined') {
+ strings.push('undefined');
+ } else if (arg === null) {
+ strings.push('null');
+ } else {
+ try {
+ strings.push(JSON.stringify(arg, null, 2));
+ } catch (err) {
+ strings.push("<<something>>");
+ }
+ }
+ });
+ return 'Identity ' + aPrefix + ': ' + strings.join(' ');
+ },
+
+ /**
+ * log() - utility function to print a list of arbitrary things
+ *
+ * Enable with about:config pref toolkit.identity.debug
+ */
+ log: function log(aPrefix, ...args) {
+ if (!this._debug) {
+ return;
+ }
+ let output = this._generateLogMessage(aPrefix, args);
+ dump(output + "\n");
+
+ // Additionally, make the output visible in the Error Console
+ Services.console.logStringMessage(output);
+ },
+
+ /**
+ * reportError() - report an error through component utils as well as
+ * our log function
+ */
+ reportError: function reportError(aPrefix, ...aArgs) {
+ let prefix = aPrefix + ' ERROR';
+
+ // Report the error in the browser
+ let output = this._generateLogMessage(aPrefix, aArgs);
+ Cu.reportError(output);
+ dump("ERROR: " + output + "\n");
+ for (let frame = Components.stack.caller; frame; frame = frame.caller) {
+ dump(frame + "\n");
+ }
+ }
+
+};
+
+this.Logger = new IdentityLogger();
diff --git a/toolkit/identity/MinimalIdentity.jsm b/toolkit/identity/MinimalIdentity.jsm
new file mode 100644
index 000000000..bceb65659
--- /dev/null
+++ b/toolkit/identity/MinimalIdentity.jsm
@@ -0,0 +1,242 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=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/. */
+
+/*
+ * This alternate implementation of IdentityService provides just the
+ * channels for navigator.id, leaving the certificate storage to a
+ * server-provided app.
+ *
+ * On b2g, the messages identity-controller-watch, -request, and
+ * -logout, are observed by the component SignInToWebsite.jsm.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["IdentityService"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
+ "resource://gre/modules/identity/IdentityUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
+ "resource://gre/modules/identity/IdentityUtils.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["minimal core"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs));
+}
+
+function IDService() {
+ Services.obs.addObserver(this, "quit-application-granted", false);
+
+ // simplify, it's one object
+ this.RP = this;
+ this.IDP = this;
+
+ // keep track of flows
+ this._rpFlows = {};
+ this._authFlows = {};
+ this._provFlows = {};
+}
+
+IDService.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ this.shutdown();
+ break;
+ }
+ },
+
+ shutdown: function() {
+ Services.obs.removeObserver(this, "quit-application-granted");
+ },
+
+ /**
+ * Parse an email into username and domain if it is valid, else return null
+ */
+ parseEmail: function parseEmail(email) {
+ var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/);
+ if (match) {
+ return {
+ username: match[1],
+ domain: match[2]
+ };
+ }
+ return null;
+ },
+
+ /**
+ * Register a listener for a given windowID as a result of a call to
+ * navigator.id.watch().
+ *
+ * @param aCaller
+ * (Object) an object that represents the caller document, and
+ * is expected to have properties:
+ * - id (unique, e.g. uuid)
+ * - loggedInUser (string or null)
+ * - origin (string)
+ *
+ * and a bunch of callbacks
+ * - doReady()
+ * - doLogin()
+ * - doLogout()
+ * - doError()
+ * - doCancel()
+ *
+ */
+ watch: function watch(aRpCaller) {
+ // store the caller structure and notify the UI observers
+ this._rpFlows[aRpCaller.id] = aRpCaller;
+
+ log("flows:", Object.keys(this._rpFlows).join(', '));
+
+ let options = makeMessageObject(aRpCaller);
+ log("sending identity-controller-watch:", options);
+ Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-watch", null);
+ },
+
+ /*
+ * The RP has gone away; remove handles to the hidden iframe.
+ * It's probable that the frame will already have been cleaned up.
+ */
+ unwatch: function unwatch(aRpId, aTargetMM) {
+ let rp = this._rpFlows[aRpId];
+ if (!rp) {
+ return;
+ }
+
+ let options = makeMessageObject({
+ id: aRpId,
+ origin: rp.origin,
+ messageManager: aTargetMM
+ });
+ log("sending identity-controller-unwatch for id", options.id, options.origin);
+ Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-unwatch", null);
+
+ // Stop sending messages to this window
+ delete this._rpFlows[aRpId];
+ },
+
+ /**
+ * Initiate a login with user interaction as a result of a call to
+ * navigator.id.request().
+ *
+ * @param aRPId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ * @param aOptions
+ * (Object) options including privacyPolicy, termsOfService
+ */
+ request: function request(aRPId, aOptions) {
+ let rp = this._rpFlows[aRPId];
+ if (!rp) {
+ reportError("request() called before watch()");
+ return;
+ }
+
+ // Notify UX to display identity picker.
+ // Pass the doc id to UX so it can pass it back to us later.
+ let options = makeMessageObject(rp);
+ objectCopy(aOptions, options);
+ Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-request", null);
+ },
+
+ /**
+ * Invoked when a user wishes to logout of a site (for instance, when clicking
+ * on an in-content logout button).
+ *
+ * @param aRpCallerId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ */
+ logout: function logout(aRpCallerId) {
+ let rp = this._rpFlows[aRpCallerId];
+ if (!rp) {
+ reportError("logout() called before watch()");
+ return;
+ }
+
+ let options = makeMessageObject(rp);
+ Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-logout", null);
+ },
+
+ childProcessShutdown: function childProcessShutdown(messageManager) {
+ Object.keys(this._rpFlows).forEach(function(key) {
+ if (this._rpFlows[key]._mm === messageManager) {
+ log("child process shutdown for rp", key, "- deleting flow");
+ delete this._rpFlows[key];
+ }
+ }, this);
+ },
+
+ /*
+ * once the UI-and-display-logic components have received
+ * notifications, they call back with direct invocation of the
+ * following functions (doLogin, doLogout, or doReady)
+ */
+
+ doLogin: function doLogin(aRpCallerId, aAssertion, aInternalParams) {
+ let rp = this._rpFlows[aRpCallerId];
+ if (!rp) {
+ log("WARNING: doLogin found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doLogin(aAssertion, aInternalParams);
+ },
+
+ doLogout: function doLogout(aRpCallerId) {
+ let rp = this._rpFlows[aRpCallerId];
+ if (!rp) {
+ log("WARNING: doLogout found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ // Logout from every site with the same origin
+ let origin = rp.origin;
+ Object.keys(this._rpFlows).forEach(function(key) {
+ let rp = this._rpFlows[key];
+ if (rp.origin === origin) {
+ rp.doLogout();
+ }
+ }.bind(this));
+ },
+
+ doReady: function doReady(aRpCallerId) {
+ let rp = this._rpFlows[aRpCallerId];
+ if (!rp) {
+ log("WARNING: doReady found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doReady();
+ },
+
+ doCancel: function doCancel(aRpCallerId) {
+ let rp = this._rpFlows[aRpCallerId];
+ if (!rp) {
+ log("WARNING: doCancel found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doCancel();
+ }
+};
+
+this.IdentityService = new IDService();
diff --git a/toolkit/identity/RelyingParty.jsm b/toolkit/identity/RelyingParty.jsm
new file mode 100644
index 000000000..5996383ca
--- /dev/null
+++ b/toolkit/identity/RelyingParty.jsm
@@ -0,0 +1,367 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
+
+this.EXPORTED_SYMBOLS = ["RelyingParty"];
+
+XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
+ "resource://gre/modules/identity/IdentityUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["RP"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError.apply(Logger, ["RP"].concat(aMessageArgs));
+}
+
+function IdentityRelyingParty() {
+ // The store is a singleton shared among Identity, RelyingParty, and
+ // IdentityProvider. The Identity module takes care of resetting
+ // state in the _store on shutdown.
+ this._store = IdentityStore;
+
+ this.reset();
+}
+
+IdentityRelyingParty.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.shutdown();
+ break;
+
+ }
+ },
+
+ reset: function RP_reset() {
+ // Forget all documents that call in. (These are sometimes
+ // referred to as callers.)
+ this._rpFlows = {};
+ },
+
+ shutdown: function RP_shutdown() {
+ this.reset();
+ Services.obs.removeObserver(this, "quit-application-granted");
+ },
+
+ /**
+ * Register a listener for a given windowID as a result of a call to
+ * navigator.id.watch().
+ *
+ * @param aCaller
+ * (Object) an object that represents the caller document, and
+ * is expected to have properties:
+ * - id (unique, e.g. uuid)
+ * - loggedInUser (string or null)
+ * - origin (string)
+ *
+ * and a bunch of callbacks
+ * - doReady()
+ * - doLogin()
+ * - doLogout()
+ * - doError()
+ * - doCancel()
+ *
+ */
+ watch: function watch(aRpCaller) {
+ this._rpFlows[aRpCaller.id] = aRpCaller;
+ let origin = aRpCaller.origin;
+ let state = this._store.getLoginState(origin) || { isLoggedIn: false, email: null };
+
+ log("watch: rpId:", aRpCaller.id,
+ "origin:", origin,
+ "loggedInUser:", aRpCaller.loggedInUser,
+ "loggedIn:", state.isLoggedIn,
+ "email:", state.email);
+
+ // If the user is already logged in, then there are three cases
+ // to deal with:
+ //
+ // 1. the email is valid and unchanged: 'ready'
+ // 2. the email is null: 'login'; 'ready'
+ // 3. the email has changed: 'login'; 'ready'
+ if (state.isLoggedIn) {
+ if (state.email && aRpCaller.loggedInUser === state.email) {
+ this._notifyLoginStateChanged(aRpCaller.id, state.email);
+ return aRpCaller.doReady();
+
+ } else if (aRpCaller.loggedInUser === null) {
+ // Generate assertion for existing login
+ let options = {loggedInUser: state.email, origin: origin};
+ return this._doLogin(aRpCaller, options);
+ }
+ // A loggedInUser different from state.email has been specified.
+ // Change login identity.
+
+ let options = {loggedInUser: state.email, origin: origin};
+ return this._doLogin(aRpCaller, options);
+
+ // If the user is not logged in, there are two cases:
+ //
+ // 1. a logged in email was provided: 'ready'; 'logout'
+ // 2. not logged in, no email given: 'ready';
+
+ }
+ if (aRpCaller.loggedInUser) {
+ return this._doLogout(aRpCaller, {origin: origin});
+ }
+ return aRpCaller.doReady();
+ },
+
+ /**
+ * A utility for watch() to set state and notify the dom
+ * on login
+ *
+ * Note that this calls _getAssertion
+ */
+ _doLogin: function _doLogin(aRpCaller, aOptions, aAssertion) {
+ log("_doLogin: rpId:", aRpCaller.id, "origin:", aOptions.origin);
+
+ let loginWithAssertion = function loginWithAssertion(assertion) {
+ this._store.setLoginState(aOptions.origin, true, aOptions.loggedInUser);
+ this._notifyLoginStateChanged(aRpCaller.id, aOptions.loggedInUser);
+ aRpCaller.doLogin(assertion);
+ aRpCaller.doReady();
+ }.bind(this);
+
+ if (aAssertion) {
+ loginWithAssertion(aAssertion);
+ } else {
+ this._getAssertion(aOptions, function gotAssertion(err, assertion) {
+ if (err) {
+ reportError("_doLogin:", "Failed to get assertion on login attempt:", err);
+ this._doLogout(aRpCaller);
+ } else {
+ loginWithAssertion(assertion);
+ }
+ }.bind(this));
+ }
+ },
+
+ /**
+ * A utility for watch() to set state and notify the dom
+ * on logout.
+ */
+ _doLogout: function _doLogout(aRpCaller, aOptions) {
+ log("_doLogout: rpId:", aRpCaller.id, "origin:", aOptions.origin);
+
+ let state = this._store.getLoginState(aOptions.origin) || {};
+
+ state.isLoggedIn = false;
+ this._notifyLoginStateChanged(aRpCaller.id, null);
+
+ aRpCaller.doLogout();
+ aRpCaller.doReady();
+ },
+
+ /**
+ * For use with login or logout, emit 'identity-login-state-changed'
+ *
+ * The notification will send the rp caller id in the properties,
+ * and the email of the user in the message.
+ *
+ * @param aRpCallerId
+ * (integer) The id of the RP caller
+ *
+ * @param aIdentity
+ * (string) The email of the user whose login state has changed
+ */
+ _notifyLoginStateChanged: function _notifyLoginStateChanged(aRpCallerId, aIdentity) {
+ log("_notifyLoginStateChanged: rpId:", aRpCallerId, "identity:", aIdentity);
+
+ let options = {rpId: aRpCallerId};
+ Services.obs.notifyObservers({wrappedJSObject: options},
+ "identity-login-state-changed",
+ aIdentity);
+ },
+
+ /**
+ * Initiate a login with user interaction as a result of a call to
+ * navigator.id.request().
+ *
+ * @param aRPId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ * @param aOptions
+ * (Object) options including privacyPolicy, termsOfService
+ */
+ request: function request(aRPId, aOptions) {
+ log("request: rpId:", aRPId);
+ let rp = this._rpFlows[aRPId];
+
+ // Notify UX to display identity picker.
+ // Pass the doc id to UX so it can pass it back to us later.
+ let options = {rpId: aRPId, origin: rp.origin};
+ objectCopy(aOptions, options);
+
+ // Append URLs after resolving
+ let baseURI = Services.io.newURI(rp.origin, null, null);
+ for (let optionName of ["privacyPolicy", "termsOfService"]) {
+ if (aOptions[optionName]) {
+ options[optionName] = baseURI.resolve(aOptions[optionName]);
+ }
+ }
+
+ Services.obs.notifyObservers({wrappedJSObject: options}, "identity-request", null);
+ },
+
+ /**
+ * Invoked when a user wishes to logout of a site (for instance, when clicking
+ * on an in-content logout button).
+ *
+ * @param aRpCallerId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ */
+ logout: function logout(aRpCallerId) {
+ log("logout: RP caller id:", aRpCallerId);
+ let rp = this._rpFlows[aRpCallerId];
+ if (rp && rp.origin) {
+ let origin = rp.origin;
+ log("logout: origin:", origin);
+ this._doLogout(rp, {origin: origin});
+ } else {
+ log("logout: no RP found with id:", aRpCallerId);
+ }
+ // We don't delete this._rpFlows[aRpCallerId], because
+ // the user might log back in again.
+ },
+
+ getDefaultEmailForOrigin: function getDefaultEmailForOrigin(aOrigin) {
+ let identities = this.getIdentitiesForSite(aOrigin);
+ let result = identities.lastUsed || null;
+ log("getDefaultEmailForOrigin:", aOrigin, "->", result);
+ return result;
+ },
+
+ /**
+ * Return the list of identities a user may want to use to login to aOrigin.
+ */
+ getIdentitiesForSite: function getIdentitiesForSite(aOrigin) {
+ let rv = { result: [] };
+ for (let id in this._store.getIdentities()) {
+ rv.result.push(id);
+ }
+ let loginState = this._store.getLoginState(aOrigin);
+ if (loginState && loginState.email)
+ rv.lastUsed = loginState.email;
+ return rv;
+ },
+
+ /**
+ * Obtain a BrowserID assertion with the specified characteristics.
+ *
+ * @param aCallback
+ * (Function) Callback to be called with (err, assertion) where 'err'
+ * can be an Error or NULL, and 'assertion' can be NULL or a valid
+ * BrowserID assertion. If no callback is provided, an exception is
+ * thrown.
+ *
+ * @param aOptions
+ * (Object) An object that may contain the following properties:
+ *
+ * "audience" : The audience for which the assertion is to be
+ * issued. If this property is not set an exception
+ * will be thrown.
+ *
+ * Any properties not listed above will be ignored.
+ */
+ _getAssertion: function _getAssertion(aOptions, aCallback) {
+ let audience = aOptions.origin;
+ let email = aOptions.loggedInUser || this.getDefaultEmailForOrigin(audience);
+ log("_getAssertion: audience:", audience, "email:", email);
+ if (!audience) {
+ throw "audience required for _getAssertion";
+ }
+
+ // We might not have any identity info for this email
+ if (!this._store.fetchIdentity(email)) {
+ this._store.addIdentity(email, null, null);
+ }
+
+ let cert = this._store.fetchIdentity(email)['cert'];
+ if (cert) {
+ this._generateAssertion(audience, email, function generatedAssertion(err, assertion) {
+ if (err) {
+ log("ERROR: _getAssertion:", err);
+ }
+ log("_getAssertion: generated assertion:", assertion);
+ return aCallback(err, assertion);
+ });
+ }
+ },
+
+ /**
+ * Generate an assertion, including provisioning via IdP if necessary,
+ * but no user interaction, so if provisioning fails, aCallback is invoked
+ * with an error.
+ *
+ * @param aAudience
+ * (string) web origin
+ *
+ * @param aIdentity
+ * (string) the email we're logging in with
+ *
+ * @param aCallback
+ * (function) callback to invoke on completion
+ * with first-positional parameter the error.
+ */
+ _generateAssertion: function _generateAssertion(aAudience, aIdentity, aCallback) {
+ log("_generateAssertion: audience:", aAudience, "identity:", aIdentity);
+
+ let id = this._store.fetchIdentity(aIdentity);
+ if (! (id && id.cert)) {
+ let errStr = "Cannot generate an assertion without a certificate";
+ log("ERROR: _generateAssertion:", errStr);
+ aCallback(errStr);
+ return;
+ }
+
+ let kp = id.keyPair;
+
+ if (!kp) {
+ let errStr = "Cannot generate an assertion without a keypair";
+ log("ERROR: _generateAssertion:", errStr);
+ aCallback(errStr);
+ return;
+ }
+
+ jwcrypto.generateAssertion(id.cert, kp, aAudience, aCallback);
+ },
+
+ /**
+ * Clean up references to the provisioning flow for the specified RP.
+ */
+ _cleanUpProvisionFlow: function RP_cleanUpProvisionFlow(aRPId, aProvId) {
+ let rp = this._rpFlows[aRPId];
+ if (rp) {
+ delete rp['provId'];
+ } else {
+ log("Error: Couldn't delete provision flow ", aProvId, " for RP ", aRPId);
+ }
+ },
+
+};
+
+this.RelyingParty = new IdentityRelyingParty();
diff --git a/toolkit/identity/Sandbox.jsm b/toolkit/identity/Sandbox.jsm
new file mode 100644
index 000000000..68757c212
--- /dev/null
+++ b/toolkit/identity/Sandbox.jsm
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Sandbox"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "Logger",
+ "resource://gre/modules/identity/LogUtils.jsm");
+
+/**
+ * An object that represents a sandbox in an iframe loaded with aURL. The
+ * callback provided to the constructor will be invoked when the sandbox is
+ * ready to be used. The callback will receive this object as its only argument.
+ *
+ * You must call free() when you are finished with the sandbox to explicitly
+ * free up all associated resources.
+ *
+ * @param aURL
+ * (string) URL to load in the sandbox.
+ *
+ * @param aCallback
+ * (function) Callback to be invoked with a Sandbox, when ready.
+ */
+this.Sandbox = function Sandbox(aURL, aCallback) {
+ // Normalize the URL so the comparison in _makeSandboxContentLoaded works
+ this._url = Services.io.newURI(aURL, null, null).spec;
+ this._log("Creating sandbox for:", this._url);
+ this._createFrame();
+ this._createSandbox(aCallback);
+};
+
+this.Sandbox.prototype = {
+
+ /**
+ * Use the outer window ID as the identifier of the sandbox.
+ */
+ get id() {
+ return this._frame.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ },
+
+ /**
+ * Reload the URL in the sandbox. This is useful to reuse a Sandbox (same
+ * id and URL).
+ */
+ reload: function Sandbox_reload(aCallback) {
+ this._log("reload:", this.id, ":", this._url);
+ this._createSandbox(function createdSandbox(aSandbox) {
+ this._log("reloaded sandbox id:", aSandbox.id);
+ aCallback(aSandbox);
+ }.bind(this));
+ },
+
+ /**
+ * Frees the sandbox and releases the iframe created to host it.
+ */
+ free: function Sandbox_free() {
+ this._log("free:", this.id);
+ this._container.removeChild(this._frame);
+ this._frame = null;
+ this._container = null;
+ this._url = null;
+ },
+
+ /**
+ * Creates an empty, hidden iframe and sets it to the _frame
+ * property of this object.
+ */
+ _createFrame: function Sandbox__createFrame() {
+ let hiddenWindow = Services.appShell.hiddenDOMWindow;
+ let doc = hiddenWindow.document;
+
+ // Insert iframe in to create docshell.
+ let frame = doc.createElementNS(XHTML_NS, "iframe");
+ frame.setAttribute("mozframetype", "content");
+ frame.sandbox = "allow-forms allow-scripts allow-same-origin";
+ frame.style.visibility = "collapse";
+ doc.documentElement.appendChild(frame);
+
+ let docShell = frame.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ // Stop about:blank from being loaded.
+ docShell.stop(Ci.nsIWebNavigation.STOP_NETWORK);
+
+ // Disable some types of content
+ docShell.allowAuth = false;
+ docShell.allowPlugins = false;
+ docShell.allowImages = false;
+ docShell.allowMedia = false;
+ docShell.allowWindowControl = false;
+
+ // Disable stylesheet loading since the document is not visible.
+ let markupDocViewer = docShell.contentViewer;
+ markupDocViewer.authorStyleDisabled = true;
+
+ // Set instance properties.
+ this._frame = frame;
+ this._container = doc.documentElement;
+ },
+
+ _createSandbox: function Sandbox__createSandbox(aCallback) {
+ let self = this;
+ function _makeSandboxContentLoaded(event) {
+ self._log("_makeSandboxContentLoaded:", self.id,
+ event.target.location.toString());
+ if (event.target != self._frame.contentDocument) {
+ return;
+ }
+ self._frame.removeEventListener(
+ "DOMWindowCreated", _makeSandboxContentLoaded, true
+ );
+
+ aCallback(self);
+ }
+
+ this._frame.addEventListener("DOMWindowCreated",
+ _makeSandboxContentLoaded,
+ true);
+
+ // Load the iframe.
+ let webNav = this._frame.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation);
+
+ webNav.loadURI(
+ this._url,
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE,
+ null, // referrer
+ null, // postData
+ null // headers
+ );
+
+ },
+
+ _log: function Sandbox__log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["sandbox"].concat(aMessageArgs));
+ },
+
+};
diff --git a/toolkit/identity/jwcrypto.jsm b/toolkit/identity/jwcrypto.jsm
new file mode 100644
index 000000000..4bcba730f
--- /dev/null
+++ b/toolkit/identity/jwcrypto.jsm
@@ -0,0 +1,180 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript ts=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/. */
+
+"use strict";
+
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this,
+ "IdentityCryptoService",
+ "@mozilla.org/identity/crypto-service;1",
+ "nsIIdentityCryptoService");
+
+this.EXPORTED_SYMBOLS = ["jwcrypto"];
+
+const ALGORITHMS = { RS256: "RS256", DS160: "DS160" };
+const DURATION_MS = 1000 * 60 * 2; // 2 minutes default assertion lifetime
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["jwcrypto"].concat(aMessageArgs));
+}
+
+function generateKeyPair(aAlgorithmName, aCallback) {
+ log("Generate key pair; alg =", aAlgorithmName);
+
+ IdentityCryptoService.generateKeyPair(aAlgorithmName, function(rv, aKeyPair) {
+ if (!Components.isSuccessCode(rv)) {
+ return aCallback("key generation failed");
+ }
+
+ var publicKey;
+
+ switch (aKeyPair.keyType) {
+ case ALGORITHMS.RS256:
+ publicKey = {
+ algorithm: "RS",
+ exponent: aKeyPair.hexRSAPublicKeyExponent,
+ modulus: aKeyPair.hexRSAPublicKeyModulus
+ };
+ break;
+
+ case ALGORITHMS.DS160:
+ publicKey = {
+ algorithm: "DS",
+ y: aKeyPair.hexDSAPublicValue,
+ p: aKeyPair.hexDSAPrime,
+ q: aKeyPair.hexDSASubPrime,
+ g: aKeyPair.hexDSAGenerator
+ };
+ break;
+
+ default:
+ return aCallback("unknown key type");
+ }
+
+ let keyWrapper = {
+ serializedPublicKey: JSON.stringify(publicKey),
+ _kp: aKeyPair
+ };
+
+ return aCallback(null, keyWrapper);
+ });
+}
+
+function sign(aPayload, aKeypair, aCallback) {
+ aKeypair._kp.sign(aPayload, function(rv, signature) {
+ if (!Components.isSuccessCode(rv)) {
+ log("ERROR: signer.sign failed");
+ return aCallback("Sign failed");
+ }
+ log("signer.sign: success");
+ return aCallback(null, signature);
+ });
+}
+
+function jwcryptoClass()
+{
+}
+
+jwcryptoClass.prototype = {
+ /*
+ * Determine the expiration of the assertion. Returns expiry date
+ * in milliseconds as integer.
+ *
+ * @param localtimeOffsetMsec (optional)
+ * The number of milliseconds that must be added to the local clock
+ * for it to agree with the server. For example, if the local clock
+ * if two minutes fast, localtimeOffsetMsec would be -120000
+ *
+ * @param now (options)
+ * Current date in milliseconds. Useful for mocking clock
+ * skew in testing.
+ */
+ getExpiration: function(duration=DURATION_MS, localtimeOffsetMsec=0, now=Date.now()) {
+ return now + localtimeOffsetMsec + duration;
+ },
+
+ isCertValid: function(aCert, aCallback) {
+ // XXX check expiration, bug 769850
+ aCallback(true);
+ },
+
+ generateKeyPair: function(aAlgorithmName, aCallback) {
+ log("generating");
+ generateKeyPair(aAlgorithmName, aCallback);
+ },
+
+ /*
+ * Generate an assertion and return it through the provided callback.
+ *
+ * @param aCert
+ * Identity certificate
+ *
+ * @param aKeyPair
+ * KeyPair object
+ *
+ * @param aAudience
+ * Audience of the assertion
+ *
+ * @param aOptions (optional)
+ * Can include:
+ * {
+ * localtimeOffsetMsec: <clock offset in milliseconds>,
+ * now: <current date in milliseconds>
+ * duration: <validity duration for this assertion in milliseconds>
+ * }
+ *
+ * localtimeOffsetMsec is the number of milliseconds that need to be
+ * added to the local clock time to make it concur with the server.
+ * For example, if the local clock is two minutes fast, the offset in
+ * milliseconds would be -120000.
+ *
+ * @param aCallback
+ * Function to invoke with resulting assertion. Assertion
+ * will be string or null on failure.
+ */
+ generateAssertion: function(aCert, aKeyPair, aAudience, aOptions, aCallback) {
+ if (typeof aOptions == "function") {
+ aCallback = aOptions;
+ aOptions = { };
+ }
+
+ // for now, we hack the algorithm name
+ // XXX bug 769851
+ var header = {"alg": "DS128"};
+ var headerBytes = IdentityCryptoService.base64UrlEncode(
+ JSON.stringify(header));
+
+ var payload = {
+ exp: this.getExpiration(
+ aOptions.duration, aOptions.localtimeOffsetMsec, aOptions.now),
+ aud: aAudience
+ };
+ var payloadBytes = IdentityCryptoService.base64UrlEncode(
+ JSON.stringify(payload));
+
+ log("payload bytes", payload, payloadBytes);
+ sign(headerBytes + "." + payloadBytes, aKeyPair, function(err, signature) {
+ if (err)
+ return aCallback(err);
+
+ var signedAssertion = headerBytes + "." + payloadBytes + "." + signature;
+ return aCallback(null, aCert + "~" + signedAssertion);
+ });
+ }
+
+};
+
+this.jwcrypto = new jwcryptoClass();
+this.jwcrypto.ALGORITHMS = ALGORITHMS;
diff --git a/toolkit/identity/moz.build b/toolkit/identity/moz.build
new file mode 100644
index 000000000..4c0dc8190
--- /dev/null
+++ b/toolkit/identity/moz.build
@@ -0,0 +1,36 @@
+# -*- 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/.
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsIIdentityCryptoService.idl',
+]
+
+XPIDL_MODULE = 'identity'
+
+SOURCES += [
+ 'IdentityCryptoService.cpp',
+]
+
+EXTRA_JS_MODULES.identity += [
+ 'Identity.jsm',
+ 'IdentityProvider.jsm',
+ 'IdentityStore.jsm',
+ 'IdentityUtils.jsm',
+ 'jwcrypto.jsm',
+ 'LogUtils.jsm',
+ 'MinimalIdentity.jsm',
+ 'RelyingParty.jsm',
+ 'Sandbox.jsm',
+]
+
+EXTRA_PP_JS_MODULES.identity += [
+ 'FirefoxAccounts.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/identity/nsIIdentityCryptoService.idl b/toolkit/identity/nsIIdentityCryptoService.idl
new file mode 100644
index 000000000..90149e2e8
--- /dev/null
+++ b/toolkit/identity/nsIIdentityCryptoService.idl
@@ -0,0 +1,106 @@
+/* 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"
+
+interface nsIURI;
+interface nsIIdentityKeyGenCallback;
+interface nsIIdentitySignCallback;
+
+/* Naming and calling conventions:
+ *
+ * A"hex" prefix means "hex-encoded string representation of a byte sequence"
+ * e.g. "ae34bcdf123"
+ *
+ * A "base64url" prefix means "base-64-URL-encoded string repressentation of a
+ * byte sequence.
+ * e.g. "eyJhbGciOiJSUzI1NiJ9"
+ * http://en.wikipedia.org/wiki/Base64#Variants_summary_table
+ * we use the padded approach to base64-url-encoding
+ *
+ * Callbacks take an "in nsresult rv" argument that indicates whether the async
+ * operation succeeded. On success, rv will be a success code
+ * (NS_SUCCEEDED(rv) / Components.isSuccessCode(rv)) and the remaining
+ * arguments are as defined in the documentation for the callback. When the
+ * operation fails, rv will be a failure code (NS_FAILED(rv) /
+ * !Components.isSuccessCode(rv)) and the values of the remaining arguments will
+ * be unspecified.
+ *
+ * Key Types:
+ *
+ * "RS256": RSA + SHA-256.
+ *
+ * "DS160": DSA with SHA-1. A 1024-bit prime and a 160-bit subprime with SHA-1.
+ *
+ * we use these abbreviated algorithm names as per the JWA spec
+ * http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-02
+ */
+
+// "@mozilla.org/identity/crypto-service;1"
+[scriptable, builtinclass, uuid(f087e6bc-dd33-4f6c-a106-dd786e052ee9)]
+interface nsIIdentityCryptoService : nsISupports
+{
+ void generateKeyPair(in AUTF8String algorithm,
+ in nsIIdentityKeyGenCallback callback);
+
+ ACString base64UrlEncode(in AUTF8String toEncode);
+};
+
+/**
+ * This interface provides a keypair and signing interface for Identity functionality
+ */
+[scriptable, uuid(73962dc7-8ee7-4346-a12b-b039e1d9b54d)]
+interface nsIIdentityKeyPair : nsISupports
+{
+ readonly attribute AUTF8String keyType;
+
+ // RSA properties, only accessible when keyType == "RS256"
+
+ readonly attribute AUTF8String hexRSAPublicKeyExponent;
+ readonly attribute AUTF8String hexRSAPublicKeyModulus;
+
+ // DSA properties, only accessible when keyType == "DS128"
+ readonly attribute AUTF8String hexDSAPrime; // p
+ readonly attribute AUTF8String hexDSASubPrime; // q
+ readonly attribute AUTF8String hexDSAGenerator; // g
+ readonly attribute AUTF8String hexDSAPublicValue; // y
+
+ void sign(in AUTF8String aText,
+ in nsIIdentitySignCallback callback);
+
+ // XXX implement verification bug 769856
+ // AUTF8String verify(in AUTF8String aSignature, in AUTF8String encodedPublicKey);
+
+};
+
+/**
+ * This interface provides a JavaScript callback object used to collect the
+ * nsIIdentityServeKeyPair when the keygen operation is complete
+ *
+ * though there is discussion as to whether we need the nsresult,
+ * we keep it so we can track deeper crypto errors.
+ */
+[scriptable, function, uuid(90f24ca2-2b05-4ca9-8aec-89d38e2f905a)]
+interface nsIIdentityKeyGenCallback : nsISupports
+{
+ void generateKeyPairFinished(in nsresult rv,
+ in nsIIdentityKeyPair keyPair);
+};
+
+/**
+ * This interface provides a JavaScript callback object used to collect the
+ * AUTF8String signature
+ */
+[scriptable, function, uuid(2d3e5036-374b-4b47-a430-1196b67b890f)]
+interface nsIIdentitySignCallback : nsISupports
+{
+ /** On success, base64urlSignature is the base-64-URL-encoded signature
+ *
+ * For RS256 signatures, XXX bug 769858
+ *
+ * For DSA128 signatures, the signature is the r value concatenated with the
+ * s value, each component padded with leading zeroes as necessary.
+ */
+ void signFinished(in nsresult rv, in ACString base64urlSignature);
+};
diff --git a/toolkit/identity/tests/chrome/.eslintrc.js b/toolkit/identity/tests/chrome/.eslintrc.js
new file mode 100644
index 000000000..2c669d844
--- /dev/null
+++ b/toolkit/identity/tests/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/identity/tests/chrome/chrome.ini b/toolkit/identity/tests/chrome/chrome.ini
new file mode 100644
index 000000000..ffaff0fb1
--- /dev/null
+++ b/toolkit/identity/tests/chrome/chrome.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+skip-if = buildapp == 'b2g' || os == 'android'
+support-files =
+ sandbox_content.html
+ sandbox_content.sjs
+ sandbox_content_alert.html
+ sandbox_content_framed.html
+ sandbox_content_perms.html
+ sandbox_content_popup.html
+ sandbox_content_redirect.html
+ sandbox_content_redirect.html^headers^
+
+[test_sandbox.xul]
diff --git a/toolkit/identity/tests/chrome/sandbox_content.html b/toolkit/identity/tests/chrome/sandbox_content.html
new file mode 100644
index 000000000..9a9b63ac2
--- /dev/null
+++ b/toolkit/identity/tests/chrome/sandbox_content.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+<meta charset="utf-8">
+<title>Page testing blocked content in the Sandbox</title>
+
+<link rel="stylesheet" src="sandbox_content.sjs?text/css"/>
+
+<script src="sandbox_content.sjs?application/javascript"></script>
+
+</head>
+
+<body>
+
+<img src="sandbox_content.sjs?image/jpeg"/>
+
+<!-- media -->
+<video src="sandbox_content.sjs?video/webm" autoplay="true"></video>
+<audio src="sandbox_content.sjs?audio/ogg" autoplay="true"></audio>
+
+<!-- plugins -->
+<embed src="sandbox_content.sjs?application/x-test"/>
+<object data="sandbox_content.sjs?application/x-test"></object>
+<applet code="sandbox_content.sjs?application/x-java-applet"></applet>
+
+<iframe src="sandbox_content.sjs?text/html"></iframe>
+
+</body>
+
+</html>
diff --git a/toolkit/identity/tests/chrome/sandbox_content.sjs b/toolkit/identity/tests/chrome/sandbox_content.sjs
new file mode 100644
index 000000000..2f562f214
--- /dev/null
+++ b/toolkit/identity/tests/chrome/sandbox_content.sjs
@@ -0,0 +1,36 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let loadedStateKey = "sandbox_content_loaded";
+ switch(request.queryString) {
+ case "reset": {
+ setState(loadedStateKey, "");
+ response.write("reset");
+ break;
+ }
+ case "get_loaded": {
+ response.setHeader("Content-Type", "text/plain", false);
+ let loaded = getState(loadedStateKey);
+ if (loaded)
+ response.write(loaded);
+ else
+ response.write("NOTHING");
+ break;
+ }
+ default: {
+ let contentType = decodeURIComponent(request.queryString);
+ // set the Content-Type equal to the query string
+ response.setHeader("Content-Type", contentType, false);
+ // If any content is loaded, append it's content type in state
+ let loaded = getState(loadedStateKey);
+ if (loaded)
+ loaded += ",";
+ setState(loadedStateKey, loaded + contentType);
+ break;
+ }
+ }
+}
diff --git a/toolkit/identity/tests/chrome/sandbox_content_alert.html b/toolkit/identity/tests/chrome/sandbox_content_alert.html
new file mode 100644
index 000000000..f07308e84
--- /dev/null
+++ b/toolkit/identity/tests/chrome/sandbox_content_alert.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+<meta charset="utf-8">
+<title>Page creating an alert inside the Sandbox</title>
+
+<script>
+
+alert("The user shouldn't see this");
+
+</script>
+
+</head>
+
+<body>
+
+</body>
+</html>
diff --git a/toolkit/identity/tests/chrome/sandbox_content_framed.html b/toolkit/identity/tests/chrome/sandbox_content_framed.html
new file mode 100644
index 000000000..72b0c49d0
--- /dev/null
+++ b/toolkit/identity/tests/chrome/sandbox_content_framed.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+<meta charset="utf-8">
+<title>Page testing blocked content in an iframe inside the Sandbox</title>
+
+</head>
+
+<body>
+
+<iframe src="sandbox_content.html"></iframe>
+
+</body>
+
+</html>
diff --git a/toolkit/identity/tests/chrome/sandbox_content_perms.html b/toolkit/identity/tests/chrome/sandbox_content_perms.html
new file mode 100644
index 000000000..d24c683f8
--- /dev/null
+++ b/toolkit/identity/tests/chrome/sandbox_content_perms.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Page testing content in the Sandbox can't escape</title>
+ <script type="application/javascript;version=1.8">
+ const TEST_BASE = "http://mochi.test:8888/chrome/toolkit/identity/tests/chrome/"
+ const Ci = SpecialPowers.Ci;
+
+ function expectException(aFunc) {
+ try {
+ aFunc();
+ } catch (ex) {
+ return true;
+ }
+ return false;
+ }
+
+ function CcNotPresent() {
+ if (typeof Components === 'undefined')
+ return true;
+ // Components shim doesn't define Components.classes.
+ try {
+ return typeof Components.classes === 'undefined';
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // Build an object with test results (true = pass)
+ let results = {
+ windowTop: window.top == window,
+
+ qiWindow: expectException(function() {
+ let isForced = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .docCharsetIsForced;
+ }),
+
+ ccAccess: !!CcNotPresent(),
+ };
+
+ let resultsJSON = JSON.stringify(results);
+
+ // Send the results to the mochitest server so the test file can retrieve them.
+ let stateURL = TEST_BASE + "sandbox_content.sjs"
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", stateURL + "?" + encodeURIComponent(resultsJSON), true);
+ xhr.onload = function() {
+ if (xhr.status != 200) {
+ dump("Failed sending results\n");
+ }
+ };
+ xhr.send();
+
+ </script>
+ </head>
+
+ <body>
+
+ </body>
+</html>
diff --git a/toolkit/identity/tests/chrome/sandbox_content_popup.html b/toolkit/identity/tests/chrome/sandbox_content_popup.html
new file mode 100644
index 000000000..cb21f706f
--- /dev/null
+++ b/toolkit/identity/tests/chrome/sandbox_content_popup.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+<meta charset="utf-8">
+<title>Page creating an popup inside the Sandbox</title>
+
+<script>
+
+var strWindowFeatures = "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes";
+
+var uri = "data:text/html,";
+uri += encodeURI("<body onload='setTimeout(window.close, 1000)'>");
+
+var win = window.open(uri, "sandbox_popup", strWindowFeatures);
+
+</script>
+
+</head>
+
+<body>
+
+</body>
+</html>
diff --git a/toolkit/identity/tests/chrome/sandbox_content_redirect.html b/toolkit/identity/tests/chrome/sandbox_content_redirect.html
new file mode 100644
index 000000000..7570ffad8
--- /dev/null
+++ b/toolkit/identity/tests/chrome/sandbox_content_redirect.html
@@ -0,0 +1,2 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
diff --git a/toolkit/identity/tests/chrome/sandbox_content_redirect.html^headers^ b/toolkit/identity/tests/chrome/sandbox_content_redirect.html^headers^
new file mode 100644
index 000000000..7c06340b9
--- /dev/null
+++ b/toolkit/identity/tests/chrome/sandbox_content_redirect.html^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Found
+Location: http://mochi.test:8888/chrome/toolkit/identity/tests/chrome/sandbox_content.html
diff --git a/toolkit/identity/tests/chrome/test_sandbox.xul b/toolkit/identity/tests/chrome/test_sandbox.xul
new file mode 100644
index 000000000..2b353c53b
--- /dev/null
+++ b/toolkit/identity/tests/chrome/test_sandbox.xul
@@ -0,0 +1,324 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=762993
+-->
+<window title="Mozilla Bug 762993"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="run_next_test();">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=762993"
+ target="_blank">Mozilla Bug 762993</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript;version=1.8">
+ <![CDATA[
+
+ /** Test for Bug 762993 **/
+
+"use strict";
+
+SimpleTest.expectAssertions(1);
+
+SimpleTest.waitForExplicitFinish();
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager);
+
+const TEST_URL_1 = "https://example.com/";
+// No trailing slash plus port to test normalization
+const TEST_URL_2 = "https://example.com:443";
+
+const TEST_BASE = "http://mochi.test:8888/chrome/toolkit/identity/tests/chrome/"
+const STATE_URL = TEST_BASE + "sandbox_content.sjs"
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+Services.prefs.setBoolPref("toolkit.identity.debug", true);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Sandbox",
+ "resource://gre/modules/identity/Sandbox.jsm");
+
+function check_sandbox(aSandbox, aURL) {
+ ok(aSandbox.id > 0, "valid ID");
+ is(aSandbox._url, aURL, "matching URL (with normalization)");
+ isnot(aSandbox._frame, null, "frame");
+ isnot(aSandbox._container, null, "container");
+ let docPrincipal = aSandbox._frame.contentDocument.nodePrincipal;
+ is(secMan.isSystemPrincipal(docPrincipal), false,
+ "principal must not be system");
+}
+
+/**
+ * Free the sandbox and make sure all properties that are not booleans,
+ * functions or numbers were freed.
+ */
+function free_and_check_sandbox(aSandbox) {
+ SimpleTest.executeSoon(function() {
+ aSandbox.free();
+
+ for(let prop in aSandbox) {
+ // Don't trigger the "id" getter when the frame is supposed to be freed already
+ if (prop == "id")
+ continue;
+ let propType = typeof(aSandbox[prop]);
+ if (propType == "boolean" || propType == "function" || propType == "number")
+ continue;
+ is(aSandbox[prop], null, "freed " + prop);
+ }
+ run_next_test();
+ });
+}
+
+function reset_server_state() {
+ // Now reset the server state
+ let resetReq = new XMLHttpRequest();
+ resetReq.open("GET", STATE_URL + "?reset", false);
+ resetReq.send();
+}
+
+function test_creation() {
+ new Sandbox(TEST_URL_1, function sandboxCB(aSandbox) {
+ check_sandbox(aSandbox, TEST_URL_1);
+ free_and_check_sandbox(aSandbox);
+ });
+}
+
+function test_reload() {
+ new Sandbox(TEST_URL_1, function sandboxCB(aSandbox) {
+ check_sandbox(aSandbox, TEST_URL_1);
+ let originalId = aSandbox.id;
+
+ aSandbox.reload(function sandboxReloadCB(aSandbox) {
+ check_sandbox(aSandbox, TEST_URL_1);
+ is(aSandbox.id, originalId, "Sandbox ID should be the same after reload");
+ free_and_check_sandbox(aSandbox);
+ });
+ });
+}
+
+function test_url_normalization() {
+ new Sandbox(TEST_URL_2, function sandboxCB(aSandbox) {
+ // TEST_URL_2 should be normalized into the form of TEST_URL_1
+ check_sandbox(aSandbox, TEST_URL_1);
+ free_and_check_sandbox(aSandbox);
+ });
+}
+
+/**
+ * Check with the server's state to see what content was loaded then reset it.
+ */
+function check_loaded_content(aSandbox, aNothingShouldLoad, aCallback) {
+
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", STATE_URL + "?get_loaded", true);
+ xhr.onload = function() {
+ let res = xhr.responseText;
+ is(xhr.status, 200, "Check successful response");
+
+ if (aNothingShouldLoad) {
+ is(res, "NOTHING", "Check that nothing was loaded on the server");
+ } else {
+ let allowedTypes = [ "application/javascript", "text/html", "application/x-test" ];
+ let loadedTypes = res == "NOTHING" ? [] : res.split(",");
+
+ for (let loadedType of loadedTypes) {
+ isnot(allowedTypes.indexOf(loadedType), -1, "Check that " + loadedType + " was expected to load"); // TODO
+ }
+
+ isnot(loadedTypes.indexOf("application/javascript"), -1, "Check JS was loaded");
+ isnot(loadedTypes.indexOf("text/html"), -1, "Check iframe was loaded");
+ is(loadedTypes.indexOf("video/webm"), -1, "Check webm was not loaded");
+ is(loadedTypes.indexOf("audio/ogg"), -1, "Check ogg was not loaded");
+
+ // Check that no plugin tags have a type other than TYPE_NULL (failed load)
+ // --
+ // Checking if a channel was opened is not sufficient for plugin tags --
+ // An object tag may still be allowed to load a sub-document, but not a
+ // plugin, so it will open a channel but then abort when it gets a
+ // plugin-type.
+ let doc = aSandbox._frame.contentDocument;
+ let nullType = Components.interfaces.nsIObjectLoadingContent.TYPE_NULL;
+ for (let tag of doc.querySelectorAll("embed, object, applet")) {
+ tag instanceof Components.interfaces.nsIObjectLoadingContent;
+ is(tag.displayedType, nullType, "Check that plugin did not load content");
+ }
+ }
+
+ reset_server_state();
+
+ aCallback();
+ };
+ xhr.send();
+}
+
+/**
+ * Helper to check that only certain content is loaded on creation and during reload.
+ */
+function check_disabled_content(aSandboxURL, aNothingShouldLoad = false) {
+ new Sandbox(aSandboxURL, function sandboxCB(aSandbox) {
+ check_sandbox(aSandbox, aSandboxURL);
+ let originalId = aSandbox.id;
+
+ setTimeout(function() {
+ check_loaded_content(aSandbox, aNothingShouldLoad, function checkFinished() {
+
+ info("reload the sandbox content");
+ aSandbox.reload(function sandboxReloadCB(aSandbox) {
+ check_sandbox(aSandbox, aSandboxURL);
+ is(aSandbox.id, originalId, "Sandbox ID should be the same after reload");
+
+ setTimeout(function() {
+ check_loaded_content(aSandbox, aNothingShouldLoad, function reloadCheckFinished() {
+ free_and_check_sandbox(aSandbox);
+ });
+ }, 5000);
+ });
+ });
+ }, 5000);
+ });
+}
+
+function test_disabled_content() {
+ let url = TEST_BASE + "sandbox_content.html";
+ check_disabled_content(url);
+}
+
+// Same as test above but with content in an iframe.
+function test_disabled_content_framed() {
+ let url = TEST_BASE + "sandbox_content_framed.html";
+ check_disabled_content(url);
+}
+
+function test_redirect() {
+ let url = TEST_BASE + "sandbox_content_redirect.html";
+ check_disabled_content(url);
+}
+
+function WindowObserver(aCallback) {
+ this.observe = function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+ Services.ww.unregisterNotification(this);
+
+ let domWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ ok(!domWin, "No window should be opened");
+ SimpleTest.executeSoon(function() {
+ info("Closing opened window");
+ domWin.close();
+ aCallback();
+ });
+ }
+}
+
+// Can the sandbox call window.alert() or popup other UI?
+function test_alert() {
+ let alertURL = TEST_BASE + "sandbox_content_alert.html";
+
+ new Sandbox(alertURL, function sandboxCB(aSandbox) {
+ check_sandbox(aSandbox, alertURL);
+ setTimeout(function() {
+
+ let win = Services.wm.getMostRecentWindow(null);
+ isnot(win.document.documentElement.getAttribute("id"), "commonDialog",
+ "Make sure most recent window is not a dialog");
+ if (win.document.documentElement.getAttribute("id") == "commonDialog") {
+ // If a dialog did open, close it so we don't interfere with future tests
+ win.close()
+ }
+
+ free_and_check_sandbox(aSandbox);
+ }, 1000);
+ });
+}
+
+// Can the sandboxed page open a popup with window.open?
+function test_popup() {
+ let alertURL = TEST_BASE + "sandbox_content_popup.html";
+ let theSandbox;
+ function continueTest() {
+ // avoid double-free
+ if (!theSandbox)
+ return;
+ free_and_check_sandbox(theSandbox);
+ theSandbox = null;
+ }
+ let winObs = new WindowObserver(continueTest);
+ Services.ww.registerNotification(winObs);
+ new Sandbox(alertURL, function sandboxCB(aSandbox) {
+ theSandbox = aSandbox;
+ check_sandbox(aSandbox, alertURL);
+ // Wait 5 seconds to see if the window is going to open.
+ setTimeout(function() {
+ Services.ww.unregisterNotification(winObs);
+ continueTest();
+ }, 5000);
+ });
+}
+
+// Loading a page with a bad cert
+function test_bad_cert() {
+ let url = TEST_BASE + "sandbox_content.sjs?text/html";
+ url = url.replace("http://mochi.test:8888", "https://untrusted.example.com");
+ check_disabled_content(url, /*nothingShouldLoad=*/true);
+}
+
+// Loading a page to check window.top and other permissions.
+function test_frame_perms() {
+ let url = TEST_BASE + "sandbox_content_perms.html";
+ new Sandbox(url, function sandboxCB(aSandbox) {
+ check_sandbox(aSandbox, url);
+
+ // Give the content time to load
+ setTimeout(function() {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", STATE_URL + "?get_loaded", true);
+ xhr.responseType = "json";
+ xhr.onload = function() {
+ is(xhr.status, 200, "Check successful response");
+ is(typeof(xhr.response), "object", "Check response is object");
+ is(Object.keys(xhr.response).length, 3, "Check the number of perm. tests");
+ for (let test in xhr.response) {
+ ok(xhr.response[test], "Check result of " + test);
+ }
+
+ reset_server_state();
+ free_and_check_sandbox(aSandbox);
+ };
+ xhr.send();
+ }, 3000);
+ });
+}
+
+let TESTS = [test_creation, test_reload, test_url_normalization];
+TESTS.push(test_disabled_content, test_disabled_content_framed);
+TESTS.push(test_alert, test_popup, test_bad_cert);
+TESTS.push(test_redirect, test_frame_perms);
+
+function run_next_test() {
+ if (TESTS.length) {
+ let test = TESTS.shift();
+ info(test.name);
+ test();
+ } else {
+ Services.prefs.clearUserPref("toolkit.identity.debug");
+ SimpleTest.finish();
+ }
+}
+
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/identity/tests/unit/.eslintrc.js b/toolkit/identity/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..fee088c17
--- /dev/null
+++ b/toolkit/identity/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/identity/tests/unit/data/idp_1/.well-known/browserid b/toolkit/identity/tests/unit/data/idp_1/.well-known/browserid
new file mode 100644
index 000000000..c7390457d
--- /dev/null
+++ b/toolkit/identity/tests/unit/data/idp_1/.well-known/browserid
@@ -0,0 +1,5 @@
+{
+ "public-key": {"algorithm":"RS","n":"65718905405105134410187227495885391609221288015566078542117409373192106382993306537273677557482085204736975067567111831005921322991127165013340443563713385983456311886801211241492470711576322130577278575529202840052753612576061450560588102139907846854501252327551303482213505265853706269864950437458242988327","e":"65537"},
+ "authentication": "/browserid/sign_in.html",
+ "provisioning": "/browserid/provision.html"
+}
diff --git a/toolkit/identity/tests/unit/data/idp_invalid_1/.well-known/browserid b/toolkit/identity/tests/unit/data/idp_invalid_1/.well-known/browserid
new file mode 100644
index 000000000..6bcd9de91
--- /dev/null
+++ b/toolkit/identity/tests/unit/data/idp_invalid_1/.well-known/browserid
@@ -0,0 +1,5 @@
+{
+ "public-key": {"algorithm":"RS","n":"65718905405105134410187227495885391609221288015566078542117409373192106382993306537273677557482085204736975067567111831005921322991127165013340443563713385983456311886801211241492470711576322130577278575529202840052753612576061450560588102139907846854501252327551303482213505265853706269864950437458242988327","e":"65537"},
+ "authentication": "/browserid/sign_in.html",
+ // missing "provisioning"
+}
diff --git a/toolkit/identity/tests/unit/head_identity.js b/toolkit/identity/tests/unit/head_identity.js
new file mode 100644
index 000000000..a266e7aee
--- /dev/null
+++ b/toolkit/identity/tests/unit/head_identity.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://testing-common/httpd.js");
+
+// XXX until bug 937114 is fixed
+Cu.importGlobalProperties(["atob"]);
+
+// The following boilerplate makes sure that XPCOM calls
+// that use the profile directory work.
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "IDService",
+ "resource://gre/modules/identity/Identity.jsm",
+ "IdentityService");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "IdentityStore",
+ "resource://gre/modules/identity/IdentityStore.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "Logger",
+ "resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this,
+ "uuidGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+const TEST_MESSAGE_MANAGER = "Mr McFeeley";
+const TEST_URL = "https://myfavoritebacon.com";
+const TEST_URL2 = "https://myfavoritebaconinacan.com";
+const TEST_USER = "user@mozilla.com";
+const TEST_PRIVKEY = "fake-privkey";
+const TEST_CERT = "fake-cert";
+const TEST_ASSERTION = "fake-assertion";
+const TEST_IDPPARAMS = {
+ domain: "myfavoriteflan.com",
+ authentication: "/foo/authenticate.html",
+ provisioning: "/foo/provision.html"
+};
+
+// The following are utility functions for Identity testing
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["test"].concat(aMessageArgs));
+}
+
+function get_idstore() {
+ return IdentityStore;
+}
+
+function partial(fn) {
+ let args = Array.prototype.slice.call(arguments, 1);
+ return function() {
+ return fn.apply(this, args.concat(Array.prototype.slice.call(arguments)));
+ };
+}
+
+function uuid() {
+ return uuidGenerator.generateUUID().toString();
+}
+
+function base64UrlDecode(s) {
+ s = s.replace(/-/g, "+");
+ s = s.replace(/_/g, "/");
+
+ // Replace padding if it was stripped by the sender.
+ // See http://tools.ietf.org/html/rfc4648#section-4
+ switch (s.length % 4) {
+ case 0:
+ break; // No pad chars in this case
+ case 2:
+ s += "==";
+ break; // Two pad chars
+ case 3:
+ s += "=";
+ break; // One pad char
+ default:
+ throw new InputException("Illegal base64url string!");
+ }
+
+ // With correct padding restored, apply the standard base64 decoder
+ return atob(s);
+}
+
+// create a mock "doc" object, which the Identity Service
+// uses as a pointer back into the doc object
+function mock_doc(aIdentity, aOrigin, aDoFunc) {
+ let mockedDoc = {};
+ mockedDoc.id = uuid();
+ mockedDoc.loggedInUser = aIdentity;
+ mockedDoc.origin = aOrigin;
+ mockedDoc["do"] = aDoFunc;
+ mockedDoc._mm = TEST_MESSAGE_MANAGER;
+ mockedDoc.doReady = partial(aDoFunc, "ready");
+ mockedDoc.doLogin = partial(aDoFunc, "login");
+ mockedDoc.doLogout = partial(aDoFunc, "logout");
+ mockedDoc.doError = partial(aDoFunc, "error");
+ mockedDoc.doCancel = partial(aDoFunc, "cancel");
+ mockedDoc.doCoffee = partial(aDoFunc, "coffee");
+ mockedDoc.childProcessShutdown = partial(aDoFunc, "child-process-shutdown");
+
+ mockedDoc.RP = mockedDoc;
+
+ return mockedDoc;
+}
+
+function mock_fxa_rp(aIdentity, aOrigin, aDoFunc) {
+ let mockedDoc = {};
+ mockedDoc.id = uuid();
+ mockedDoc.emailHint = aIdentity;
+ mockedDoc.origin = aOrigin;
+ mockedDoc.wantIssuer = "firefox-accounts";
+ mockedDoc._mm = TEST_MESSAGE_MANAGER;
+
+ mockedDoc.doReady = partial(aDoFunc, "ready");
+ mockedDoc.doLogin = partial(aDoFunc, "login");
+ mockedDoc.doLogout = partial(aDoFunc, "logout");
+ mockedDoc.doError = partial(aDoFunc, "error");
+ mockedDoc.doCancel = partial(aDoFunc, "cancel");
+ mockedDoc.childProcessShutdown = partial(aDoFunc, "child-process-shutdown");
+
+ mockedDoc.RP = mockedDoc;
+
+ return mockedDoc;
+}
+
+// mimicking callback funtionality for ease of testing
+// this observer auto-removes itself after the observe function
+// is called, so this is meant to observe only ONE event.
+function makeObserver(aObserveTopic, aObserveFunc) {
+ let observer = {
+ // nsISupports provides type management in C++
+ // nsIObserver is to be an observer
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function (aSubject, aTopic, aData) {
+ if (aTopic == aObserveTopic) {
+ aObserveFunc(aSubject, aTopic, aData);
+ Services.obs.removeObserver(observer, aObserveTopic);
+ }
+ }
+ };
+
+ Services.obs.addObserver(observer, aObserveTopic, false);
+}
+
+// set up the ID service with an identity with keypair and all
+// when ready, invoke callback with the identity
+function setup_test_identity(identity, cert, cb) {
+ // set up the store so that we're supposed to be logged in
+ let store = get_idstore();
+
+ function keyGenerated(err, kpo) {
+ store.addIdentity(identity, kpo, cert);
+ cb();
+ }
+
+ jwcrypto.generateKeyPair("DS160", keyGenerated);
+}
+
+// takes a list of functions and returns a function that
+// when called the first time, calls the first func,
+// then the next time the second, etc.
+function call_sequentially() {
+ let numCalls = 0;
+ let funcs = arguments;
+
+ return function() {
+ if (!funcs[numCalls]) {
+ let argString = Array.prototype.slice.call(arguments).join(",");
+ do_throw("Too many calls: " + argString);
+ return;
+ }
+ funcs[numCalls].apply(funcs[numCalls], arguments);
+ numCalls += 1;
+ };
+}
+
+/*
+ * Setup a provisioning workflow with appropriate callbacks
+ *
+ * identity is the email we're provisioning.
+ *
+ * afterSetupCallback is required.
+ *
+ * doneProvisioningCallback is optional, if the caller
+ * wants to be notified when the whole provisioning workflow is done
+ *
+ * frameCallbacks is optional, contains the callbacks that the sandbox
+ * frame would provide in response to DOM calls.
+ */
+function setup_provisioning(identity, afterSetupCallback, doneProvisioningCallback, callerCallbacks) {
+ IDService.reset();
+
+ let provId = uuid();
+ IDService.IDP._provisionFlows[provId] = {
+ identity : identity,
+ idpParams: TEST_IDPPARAMS,
+ callback: function(err) {
+ if (doneProvisioningCallback)
+ doneProvisioningCallback(err);
+ },
+ sandbox: {
+ // Emulate the free() method on the iframe sandbox
+ free: function() {}
+ }
+ };
+
+ let caller = {};
+ caller.id = provId;
+ caller.doBeginProvisioningCallback = function(id, duration_s) {
+ if (callerCallbacks && callerCallbacks.beginProvisioningCallback)
+ callerCallbacks.beginProvisioningCallback(id, duration_s);
+ };
+ caller.doGenKeyPairCallback = function(pk) {
+ if (callerCallbacks && callerCallbacks.genKeyPairCallback)
+ callerCallbacks.genKeyPairCallback(pk);
+ };
+
+ afterSetupCallback(caller);
+}
+
+// Switch debug messages on by default
+var initialPrefDebugValue = false;
+try {
+ initialPrefDebugValue = Services.prefs.getBoolPref("toolkit.identity.debug");
+} catch (noPref) {}
+Services.prefs.setBoolPref("toolkit.identity.debug", true);
+
+// Switch on firefox accounts
+var initialPrefFXAValue = false;
+try {
+ initialPrefFXAValue = Services.prefs.getBoolPref("identity.fxaccounts.enabled");
+} catch (noPref) {}
+Services.prefs.setBoolPref("identity.fxaccounts.enabled", true);
+
+// after execution, restore prefs
+do_register_cleanup(function() {
+ log("restoring prefs to their initial values");
+ Services.prefs.setBoolPref("toolkit.identity.debug", initialPrefDebugValue);
+ Services.prefs.setBoolPref("identity.fxaccounts.enabled", initialPrefFXAValue);
+});
+
+
diff --git a/toolkit/identity/tests/unit/tail_identity.js b/toolkit/identity/tests/unit/tail_identity.js
new file mode 100644
index 000000000..c263f8369
--- /dev/null
+++ b/toolkit/identity/tests/unit/tail_identity.js
@@ -0,0 +1,8 @@
+
+// pre-emptively shut down to clear resources
+if (typeof IdentityService !== "undefined") {
+ IdentityService.shutdown();
+} else if (typeof IDService !== "undefined") {
+ IDService.shutdown();
+}
+
diff --git a/toolkit/identity/tests/unit/test_authentication.js b/toolkit/identity/tests/unit/test_authentication.js
new file mode 100644
index 000000000..3f24e2e37
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_authentication.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "IDService",
+ "resource://gre/modules/identity/Identity.jsm",
+ "IdentityService");
+
+XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+function test_begin_authentication_flow() {
+ do_test_pending();
+ let _provId = null;
+
+ // set up a watch, to be consistent
+ let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {});
+ IDService.RP.watch(mockedDoc);
+
+ // The identity-auth notification is sent up to the UX from the
+ // _doAuthentication function. Be ready to receive it and call
+ // beginAuthentication
+ makeObserver("identity-auth", function (aSubject, aTopic, aData) {
+ do_check_neq(aSubject, null);
+
+ do_check_eq(aSubject.wrappedJSObject.provId, _provId);
+
+ do_test_finished();
+ run_next_test();
+ });
+
+ setup_provisioning(
+ TEST_USER,
+ function(caller) {
+ _provId = caller.id;
+ IDService.IDP.beginProvisioning(caller);
+ }, function() {},
+ {
+ beginProvisioningCallback: function(email, duration_s) {
+
+ // let's say this user needs to authenticate
+ IDService.IDP._doAuthentication(_provId, {idpParams:TEST_IDPPARAMS});
+ }
+ }
+ );
+}
+
+function test_complete_authentication_flow() {
+ do_test_pending();
+ let _provId = null;
+ let _authId = null;
+ let id = TEST_USER;
+
+ let callbacksFired = false;
+ let loginStateChanged = false;
+ let identityAuthComplete = false;
+
+ // The result of authentication should be a successful login
+ IDService.reset();
+
+ setup_test_identity(id, TEST_CERT, function() {
+ // set it up so we're supposed to be logged in to TEST_URL
+
+ get_idstore().setLoginState(TEST_URL, true, id);
+
+ // When we authenticate, our ready callback will be fired.
+ // At the same time, a separate topic will be sent up to the
+ // the observer in the UI. The test is complete when both
+ // events have occurred.
+ let mockedDoc = mock_doc(id, TEST_URL, call_sequentially(
+ function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_eq(params, undefined);
+
+ // if notification already received by observer, test is done
+ callbacksFired = true;
+ if (loginStateChanged && identityAuthComplete) {
+ do_test_finished();
+ run_next_test();
+ }
+ }
+ ));
+
+ makeObserver("identity-auth-complete", function(aSubject, aTopic, aData) {
+ identityAuthComplete = true;
+ do_test_finished();
+ run_next_test();
+ });
+
+ makeObserver("identity-login-state-changed", function (aSubject, aTopic, aData) {
+ do_check_neq(aSubject, null);
+
+ do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+ do_check_eq(aData, id);
+
+ // if callbacks in caller doc already fired, test is done.
+ loginStateChanged = true;
+ if (callbacksFired && identityAuthComplete) {
+ do_test_finished();
+ run_next_test();
+ }
+ });
+
+ IDService.RP.watch(mockedDoc);
+
+ // Create a provisioning flow for our auth flow to attach to
+ setup_provisioning(
+ TEST_USER,
+ function(provFlow) {
+ _provId = provFlow.id;
+
+ IDService.IDP.beginProvisioning(provFlow);
+ }, function() {},
+ {
+ beginProvisioningCallback: function(email, duration_s) {
+ // let's say this user needs to authenticate
+ IDService.IDP._doAuthentication(_provId, {idpParams:TEST_IDPPARAMS});
+
+ // test_begin_authentication_flow verifies that the right
+ // message is sent to the UI. So that works. Moving on,
+ // the UI calls setAuthenticationFlow ...
+ _authId = uuid();
+ IDService.IDP.setAuthenticationFlow(_authId, _provId);
+
+ // ... then the UI calls beginAuthentication ...
+ authCaller.id = _authId;
+ IDService.IDP._provisionFlows[_provId].caller = authCaller;
+ IDService.IDP.beginAuthentication(authCaller);
+ }
+ }
+ );
+ });
+
+ // A mock calling context
+ let authCaller = {
+ doBeginAuthenticationCallback: function doBeginAuthenticationCallback(identity) {
+ do_check_eq(identity, TEST_USER);
+ // completeAuthentication will emit "identity-auth-complete"
+ IDService.IDP.completeAuthentication(_authId);
+ },
+
+ doError: function(err) {
+ log("OW! My doError callback hurts!", err);
+ },
+ };
+
+}
+
+var TESTS = [];
+
+TESTS.push(test_begin_authentication_flow);
+TESTS.push(test_complete_authentication_flow);
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_crypto_service.js b/toolkit/identity/tests/unit/test_crypto_service.js
new file mode 100644
index 000000000..561c3804a
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_crypto_service.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import('resource://gre/modules/identity/LogUtils.jsm');
+
+const idService = Cc["@mozilla.org/identity/crypto-service;1"]
+ .getService(Ci.nsIIdentityCryptoService);
+
+const ALG_DSA = "DS160";
+const ALG_RSA = "RS256";
+
+const BASE64_URL_ENCODINGS = [
+ // The vectors from RFC 4648 are very silly, but we may as well include them.
+ ["", ""],
+ ["f", "Zg=="],
+ ["fo", "Zm8="],
+ ["foo", "Zm9v"],
+ ["foob", "Zm9vYg=="],
+ ["fooba", "Zm9vYmE="],
+ ["foobar", "Zm9vYmFy"],
+
+ // It's quite likely you could get a string like this in an assertion audience
+ ["i-like-pie.com", "aS1saWtlLXBpZS5jb20="],
+
+ // A few extra to be really sure
+ ["andré@example.com", "YW5kcsOpQGV4YW1wbGUuY29t"],
+ ["πόλλ' οἶδ' ἀλώπηξ, ἀλλ' ἐχῖνος ἓν μέγα",
+ "z4DPjM67zrsnIM6_4by2zrQnIOG8gM67z47PgM63zr4sIOG8gM67zrsnIOG8kM-H4b-Wzr3Ov8-CIOG8k869IM68zq3Os86x"],
+];
+
+// When the output of an operation is a
+function do_check_eq_or_slightly_less(x, y) {
+ do_check_true(x >= y - (3 * 8));
+}
+
+function test_base64_roundtrip() {
+ let message = "Attack at dawn!";
+ let encoded = idService.base64UrlEncode(message);
+ let decoded = base64UrlDecode(encoded);
+ do_check_neq(message, encoded);
+ do_check_eq(decoded, message);
+ run_next_test();
+}
+
+function test_dsa() {
+ idService.generateKeyPair(ALG_DSA, function (rv, keyPair) {
+ log("DSA generateKeyPair finished ", rv);
+ do_check_true(Components.isSuccessCode(rv));
+ do_check_eq(typeof keyPair.sign, "function");
+ do_check_eq(keyPair.keyType, ALG_DSA);
+ do_check_eq_or_slightly_less(keyPair.hexDSAGenerator.length, 1024 / 8 * 2);
+ do_check_eq_or_slightly_less(keyPair.hexDSAPrime.length, 1024 / 8 * 2);
+ do_check_eq_or_slightly_less(keyPair.hexDSASubPrime.length, 160 / 8 * 2);
+ do_check_eq_or_slightly_less(keyPair.hexDSAPublicValue.length, 1024 / 8 * 2);
+ // XXX: test that RSA parameters throw the correct error
+
+ log("about to sign with DSA key");
+ keyPair.sign("foo", function (rv2, signature) {
+ log("DSA sign finished ", rv2, signature);
+ do_check_true(Components.isSuccessCode(rv2));
+ do_check_true(signature.length > 1);
+ // TODO: verify the signature with the public key
+ run_next_test();
+ });
+ });
+}
+
+function test_rsa() {
+ idService.generateKeyPair(ALG_RSA, function (rv, keyPair) {
+ log("RSA generateKeyPair finished ", rv);
+ do_check_true(Components.isSuccessCode(rv));
+ do_check_eq(typeof keyPair.sign, "function");
+ do_check_eq(keyPair.keyType, ALG_RSA);
+ do_check_eq_or_slightly_less(keyPair.hexRSAPublicKeyModulus.length,
+ 2048 / 8);
+ do_check_true(keyPair.hexRSAPublicKeyExponent.length > 1);
+
+ log("about to sign with RSA key");
+ keyPair.sign("foo", function (rv2, signature) {
+ log("RSA sign finished ", rv2, signature);
+ do_check_true(Components.isSuccessCode(rv2));
+ do_check_true(signature.length > 1);
+ run_next_test();
+ });
+ });
+}
+
+function test_base64UrlEncode() {
+ for (let [source, target] of BASE64_URL_ENCODINGS) {
+ do_check_eq(target, idService.base64UrlEncode(source));
+ }
+ run_next_test();
+}
+
+function test_base64UrlDecode() {
+ let utf8Converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ utf8Converter.charset = "UTF-8";
+
+ // We know the encoding of our inputs - on conversion back out again, make
+ // sure they're the same.
+ for (let [source, target] of BASE64_URL_ENCODINGS) {
+ let result = utf8Converter.ConvertToUnicode(base64UrlDecode(target));
+ result += utf8Converter.Finish();
+ do_check_eq(source, result);
+ }
+ run_next_test();
+}
+
+add_test(test_base64_roundtrip);
+add_test(test_dsa);
+add_test(test_rsa);
+add_test(test_base64UrlEncode);
+add_test(test_base64UrlDecode);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_firefox_accounts.js b/toolkit/identity/tests/unit/test_firefox_accounts.js
new file mode 100644
index 000000000..c0c63deb6
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_firefox_accounts.js
@@ -0,0 +1,270 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/DOMIdentity.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FirefoxAccounts",
+ "resource://gre/modules/identity/FirefoxAccounts.jsm");
+
+// Make the profile dir available; this is necessary so that
+// services/fxaccounts/FxAccounts.jsm can read and write its signed-in user
+// data.
+do_get_profile();
+
+function MockFXAManager() {
+ this.signedInUser = true;
+}
+MockFXAManager.prototype = {
+ getAssertion: function(audience) {
+ let result = this.signedInUser ? TEST_ASSERTION : null;
+ return Promise.resolve(result);
+ },
+
+ signOut: function() {
+ this.signedInUser = false;
+ return Promise.resolve(null);
+ },
+
+ signIn: function(user) {
+ this.signedInUser = user;
+ return Promise.resolve(user);
+ },
+}
+
+var originalManager = FirefoxAccounts.fxAccountsManager;
+FirefoxAccounts.fxAccountsManager = new MockFXAManager();
+do_register_cleanup(() => {
+ log("restoring fxaccountsmanager");
+ FirefoxAccounts.fxAccountsManager = originalManager;
+});
+
+function withNobodySignedIn() {
+ return FirefoxAccounts.fxAccountsManager.signOut();
+}
+
+function withSomebodySignedIn() {
+ return FirefoxAccounts.fxAccountsManager.signIn('Pertelote');
+}
+
+function test_overall() {
+ do_check_neq(FirefoxAccounts, null);
+ run_next_test();
+}
+
+function test_mock() {
+ do_test_pending();
+
+ withSomebodySignedIn().then(() => {
+ FirefoxAccounts.fxAccountsManager.getAssertion().then(assertion => {
+ do_check_eq(assertion, TEST_ASSERTION);
+ do_test_finished();
+ run_next_test();
+ });
+ });
+}
+
+function test_watch_signed_in() {
+ do_test_pending();
+
+ let received = [];
+
+ let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, data) {
+ received.push([method, data]);
+
+ if (method == "ready") {
+ // confirm that we were signed in and then ready was called
+ do_check_eq(received.length, 2);
+ do_check_eq(received[0][0], "login");
+ do_check_eq(received[0][1], TEST_ASSERTION);
+ do_check_eq(received[1][0], "ready");
+ do_test_finished();
+ run_next_test();
+ }
+ });
+
+ withSomebodySignedIn().then(() => {
+ FirefoxAccounts.RP.watch(mockedRP);
+ });
+}
+
+function test_watch_signed_out() {
+ do_test_pending();
+
+ let received = [];
+
+ let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) {
+ received.push(method);
+
+ if (method == "ready") {
+ // confirm that we were signed out and then ready was called
+ do_check_eq(received.length, 2);
+ do_check_eq(received[0], "logout");
+ do_check_eq(received[1], "ready");
+
+ do_test_finished();
+ run_next_test();
+ }
+ });
+
+ withNobodySignedIn().then(() => {
+ FirefoxAccounts.RP.watch(mockedRP);
+ });
+}
+
+function test_request() {
+ do_test_pending();
+
+ let received = [];
+
+ let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, data) {
+ received.push([method, data]);
+
+ // On watch(), we are signed out. Then we call request().
+ if (received.length === 2) {
+ do_check_eq(received[0][0], "logout");
+ do_check_eq(received[1][0], "ready");
+
+ // Pretend request() showed ux and the user signed in
+ withSomebodySignedIn().then(() => {
+ FirefoxAccounts.RP.request(mockedRP.id);
+ });
+ }
+
+ if (received.length === 3) {
+ do_check_eq(received[2][0], "login");
+ do_check_eq(received[2][1], TEST_ASSERTION);
+
+ do_test_finished();
+ run_next_test();
+ }
+ });
+
+ // First, call watch() with nobody signed in
+ withNobodySignedIn().then(() => {
+ FirefoxAccounts.RP.watch(mockedRP);
+ });
+}
+
+function test_logout() {
+ do_test_pending();
+
+ let received = [];
+
+ let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) {
+ received.push(method);
+
+ // At first, watch() signs us in automatically. Then we sign out.
+ if (received.length === 2) {
+ do_check_eq(received[0], "login");
+ do_check_eq(received[1], "ready");
+
+ FirefoxAccounts.RP.logout(mockedRP.id);
+ }
+
+ if (received.length === 3) {
+ do_check_eq(received[2], "logout");
+ do_test_finished();
+ run_next_test();
+ }
+ });
+
+ // First, call watch()
+ withSomebodySignedIn().then(() => {
+ FirefoxAccounts.RP.watch(mockedRP);
+ });
+}
+
+function test_error() {
+ do_test_pending();
+
+ let received = [];
+
+ // Mock the fxAccountsManager so that getAssertion rejects its promise and
+ // triggers our onerror handler. (This is the method that's used internally
+ // by FirefoxAccounts.RP.request().)
+ let originalGetAssertion = FirefoxAccounts.fxAccountsManager.getAssertion;
+ FirefoxAccounts.fxAccountsManager.getAssertion = function(audience) {
+ return Promise.reject(new Error("barf!"));
+ };
+
+ let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, message) {
+ // We will immediately receive an error, due to watch()'s attempt
+ // to getAssertion().
+ do_check_eq(method, "error");
+ do_check_true(/barf/.test(message));
+
+ // Put things back the way they were
+ FirefoxAccounts.fxAccountsManager.getAssertion = originalGetAssertion;
+
+ do_test_finished();
+ run_next_test();
+ });
+
+ // First, call watch()
+ withSomebodySignedIn().then(() => {
+ FirefoxAccounts.RP.watch(mockedRP);
+ });
+}
+
+function test_child_process_shutdown() {
+ do_test_pending();
+ let rpCount = FirefoxAccounts.RP._rpFlows.size;
+
+ makeObserver("identity-child-process-shutdown", (aTopic, aSubject, aData) => {
+ // Last of all, the shutdown observer message will be fired.
+ // This takes place after the RP has a chance to delete flows
+ // and clean up.
+ do_check_eq(FirefoxAccounts.RP._rpFlows.size, rpCount);
+ do_test_finished();
+ run_next_test();
+ });
+
+ let mockedRP = mock_fxa_rp(null, TEST_URL, (method) => {
+ // We should enter this function for 'ready' and 'child-process-shutdown'.
+ // After we have a chance to do our thing, the shutdown observer message
+ // will fire and be caught by the function above.
+ do_check_eq(FirefoxAccounts.RP._rpFlows.size, rpCount + 1);
+ switch (method) {
+ case "ready":
+ DOMIdentity._childProcessShutdown("my message manager");
+ break;
+
+ case "child-process-shutdown":
+ // We have to call this explicitly because there's no real
+ // dom window here.
+ FirefoxAccounts.RP.childProcessShutdown(mockedRP._mm);
+ break;
+
+ default:
+ break;
+ }
+ });
+
+ mockedRP._mm = "my message manager";
+ withSomebodySignedIn().then(() => {
+ FirefoxAccounts.RP.watch(mockedRP);
+ });
+
+ // fake a dom window context
+ DOMIdentity.newContext(mockedRP, mockedRP._mm);
+}
+
+var TESTS = [
+ test_overall,
+ test_mock,
+ test_watch_signed_in,
+ test_watch_signed_out,
+ test_request,
+ test_logout,
+ test_error,
+ test_child_process_shutdown,
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_identity.js b/toolkit/identity/tests/unit/test_identity.js
new file mode 100644
index 000000000..5e2206c2a
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_identity.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "IDService",
+ "resource://gre/modules/identity/Identity.jsm",
+ "IdentityService");
+
+function test_overall() {
+ do_check_neq(IDService, null);
+ run_next_test();
+}
+
+function test_mock_doc() {
+ do_test_pending();
+ let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {
+ do_check_eq(action, 'coffee');
+ do_test_finished();
+ run_next_test();
+ });
+
+ mockedDoc.doCoffee();
+}
+
+function test_add_identity() {
+ IDService.reset();
+
+ IDService.addIdentity(TEST_USER);
+
+ let identities = IDService.RP.getIdentitiesForSite(TEST_URL);
+ do_check_eq(identities.result.length, 1);
+ do_check_eq(identities.result[0], TEST_USER);
+
+ run_next_test();
+}
+
+function test_select_identity() {
+ do_test_pending();
+
+ IDService.reset();
+
+ let id = "ishtar@mockmyid.com";
+ setup_test_identity(id, TEST_CERT, function() {
+ let gotAssertion = false;
+ let mockedDoc = mock_doc(null, TEST_URL, call_sequentially(
+ function(action, params) {
+ // ready emitted from first watch() call
+ do_check_eq(action, 'ready');
+ do_check_null(params);
+ },
+ // first the login call
+ function(action, params) {
+ do_check_eq(action, 'login');
+ do_check_neq(params, null);
+
+ // XXX - check that the assertion is for the right email
+
+ gotAssertion = true;
+ },
+ // then the ready call
+ function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_null(params);
+
+ // we should have gotten the assertion already
+ do_check_true(gotAssertion);
+
+ do_test_finished();
+ run_next_test();
+ }));
+
+ // register the callbacks
+ IDService.RP.watch(mockedDoc);
+
+ // register the request UX observer
+ makeObserver("identity-request", function (aSubject, aTopic, aData) {
+ // do the select identity
+ // we expect this to succeed right away because of test_identity
+ // so we don't mock network requests or otherwise
+ IDService.selectIdentity(aSubject.wrappedJSObject.rpId, id);
+ });
+
+ // do the request
+ IDService.RP.request(mockedDoc.id, {});
+ });
+}
+
+function test_parse_good_email() {
+ var parsed = IDService.parseEmail('prime-minister@jed.gov');
+ do_check_eq(parsed.username, 'prime-minister');
+ do_check_eq(parsed.domain, 'jed.gov');
+ run_next_test();
+}
+
+function test_parse_bogus_emails() {
+ do_check_eq(null, IDService.parseEmail('@evil.org'));
+ do_check_eq(null, IDService.parseEmail('foo@bar@baz.com'));
+ do_check_eq(null, IDService.parseEmail('you@wellsfargo.com/accounts/transfer?to=dolske&amt=all'));
+ run_next_test();
+}
+
+var TESTS = [test_overall, test_mock_doc];
+
+TESTS.push(test_add_identity);
+TESTS.push(test_select_identity);
+TESTS.push(test_parse_good_email);
+TESTS.push(test_parse_bogus_emails);
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_identity_utils.js b/toolkit/identity/tests/unit/test_identity_utils.js
new file mode 100644
index 000000000..6ccc4e311
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_identity_utils.js
@@ -0,0 +1,46 @@
+
+"use strict";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/identity/IdentityUtils.jsm');
+
+function test_check_deprecated() {
+ let options = {
+ id: 123,
+ loggedInEmail: "jed@foo.com",
+ pies: 42
+ };
+
+ do_check_true(checkDeprecated(options, "loggedInEmail"));
+ do_check_false(checkDeprecated(options, "flans"));
+
+ run_next_test();
+}
+
+function test_check_renamed() {
+ let options = {
+ id: 123,
+ loggedInEmail: "jed@foo.com",
+ pies: 42
+ };
+
+ checkRenamed(options, "loggedInEmail", "loggedInUser");
+
+ // It moves loggedInEmail to loggedInUser
+ do_check_false(!!options.loggedInEmail);
+ do_check_eq(options.loggedInUser, "jed@foo.com");
+
+ run_next_test();
+}
+
+var TESTS = [
+ test_check_deprecated,
+ test_check_renamed
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_jwcrypto.js b/toolkit/identity/tests/unit/test_jwcrypto.js
new file mode 100644
index 000000000..f8fe82453
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_jwcrypto.js
@@ -0,0 +1,281 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict"
+
+Cu.import('resource://gre/modules/identity/LogUtils.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "IDService",
+ "resource://gre/modules/identity/Identity.jsm",
+ "IdentityService");
+
+XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this,
+ "CryptoService",
+ "@mozilla.org/identity/crypto-service;1",
+ "nsIIdentityCryptoService");
+
+const RP_ORIGIN = "http://123done.org";
+const INTERNAL_ORIGIN = "browserid://";
+
+const SECOND_MS = 1000;
+const MINUTE_MS = SECOND_MS * 60;
+const HOUR_MS = MINUTE_MS * 60;
+
+function test_sanity() {
+ do_test_pending();
+
+ jwcrypto.generateKeyPair("DS160", function(err, kp) {
+ do_check_null(err);
+
+ do_test_finished();
+ run_next_test();
+ });
+}
+
+function test_generate() {
+ do_test_pending();
+ jwcrypto.generateKeyPair("DS160", function(err, kp) {
+ do_check_null(err);
+ do_check_neq(kp, null);
+
+ do_test_finished();
+ run_next_test();
+ });
+}
+
+function test_get_assertion() {
+ do_test_pending();
+
+ jwcrypto.generateKeyPair(
+ "DS160",
+ function(err, kp) {
+ jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN, (err2, backedAssertion) => {
+ do_check_null(err2);
+
+ do_check_eq(backedAssertion.split("~").length, 2);
+ do_check_eq(backedAssertion.split(".").length, 3);
+
+ do_test_finished();
+ run_next_test();
+ });
+ });
+}
+
+function test_rsa() {
+ do_test_pending();
+ function checkRSA(err, kpo) {
+ do_check_neq(kpo, undefined);
+ log(kpo.serializedPublicKey);
+ let pk = JSON.parse(kpo.serializedPublicKey);
+ do_check_eq(pk.algorithm, "RS");
+/* TODO
+ do_check_neq(kpo.sign, null);
+ do_check_eq(typeof kpo.sign, "function");
+ do_check_neq(kpo.userID, null);
+ do_check_neq(kpo.url, null);
+ do_check_eq(kpo.url, INTERNAL_ORIGIN);
+ do_check_neq(kpo.exponent, null);
+ do_check_neq(kpo.modulus, null);
+
+ // TODO: should sign be async?
+ let sig = kpo.sign("This is a message to sign");
+
+ do_check_neq(sig, null);
+ do_check_eq(typeof sig, "string");
+ do_check_true(sig.length > 1);
+*/
+ do_test_finished();
+ run_next_test();
+ }
+
+ jwcrypto.generateKeyPair("RS256", checkRSA);
+}
+
+function test_dsa() {
+ do_test_pending();
+ function checkDSA(err, kpo) {
+ do_check_neq(kpo, undefined);
+ log(kpo.serializedPublicKey);
+ let pk = JSON.parse(kpo.serializedPublicKey);
+ do_check_eq(pk.algorithm, "DS");
+/* TODO
+ do_check_neq(kpo.sign, null);
+ do_check_eq(typeof kpo.sign, "function");
+ do_check_neq(kpo.userID, null);
+ do_check_neq(kpo.url, null);
+ do_check_eq(kpo.url, INTERNAL_ORIGIN);
+ do_check_neq(kpo.generator, null);
+ do_check_neq(kpo.prime, null);
+ do_check_neq(kpo.subPrime, null);
+ do_check_neq(kpo.publicValue, null);
+
+ let sig = kpo.sign("This is a message to sign");
+
+ do_check_neq(sig, null);
+ do_check_eq(typeof sig, "string");
+ do_check_true(sig.length > 1);
+*/
+ do_test_finished();
+ run_next_test();
+ }
+
+ jwcrypto.generateKeyPair("DS160", checkDSA);
+}
+
+function test_get_assertion_with_offset() {
+ do_test_pending();
+
+
+ // Use an arbitrary date in the past to ensure we don't accidentally pass
+ // this test with current dates, missing offsets, etc.
+ let serverMsec = Date.parse("Tue Oct 31 2000 00:00:00 GMT-0800");
+
+ // local clock skew
+ // clock is 12 hours fast; -12 hours offset must be applied
+ let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
+ let localMsec = serverMsec - localtimeOffsetMsec;
+
+ jwcrypto.generateKeyPair(
+ "DS160",
+ function(err, kp) {
+ jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN,
+ { duration: MINUTE_MS,
+ localtimeOffsetMsec: localtimeOffsetMsec,
+ now: localMsec},
+ function(err2, backedAssertion) {
+ do_check_null(err2);
+
+ // properly formed
+ let cert;
+ let assertion;
+ [cert, assertion] = backedAssertion.split("~");
+
+ do_check_eq(cert, "fake-cert");
+ do_check_eq(assertion.split(".").length, 3);
+
+ let components = extractComponents(assertion);
+
+ // Expiry is within two minutes, corrected for skew
+ let exp = parseInt(components.payload.exp, 10);
+ do_check_true(exp - serverMsec === MINUTE_MS);
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+ }
+ );
+}
+
+function test_assertion_lifetime() {
+ do_test_pending();
+
+ jwcrypto.generateKeyPair(
+ "DS160",
+ function(err, kp) {
+ jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN,
+ {duration: MINUTE_MS},
+ function(err2, backedAssertion) {
+ do_check_null(err2);
+
+ // properly formed
+ let cert;
+ let assertion;
+ [cert, assertion] = backedAssertion.split("~");
+
+ do_check_eq(cert, "fake-cert");
+ do_check_eq(assertion.split(".").length, 3);
+
+ let components = extractComponents(assertion);
+
+ // Expiry is within one minute, as we specified above
+ let exp = parseInt(components.payload.exp, 10);
+ do_check_true(Math.abs(Date.now() - exp) > 50 * SECOND_MS);
+ do_check_true(Math.abs(Date.now() - exp) <= MINUTE_MS);
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+ }
+ );
+}
+
+function test_audience_encoding_bug972582() {
+ let audience = "i-like-pie.com";
+
+ jwcrypto.generateKeyPair(
+ "DS160",
+ function(err, kp) {
+ do_check_null(err);
+ jwcrypto.generateAssertion("fake-cert", kp, audience,
+ function(err2, backedAssertion) {
+ do_check_null(err2);
+
+ let [cert, assertion] = backedAssertion.split("~");
+ let components = extractComponents(assertion);
+ do_check_eq(components.payload.aud, audience);
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+ }
+ );
+}
+
+// End of tests
+// Helper function follow
+
+function extractComponents(signedObject) {
+ if (typeof(signedObject) != 'string') {
+ throw new Error("malformed signature " + typeof(signedObject));
+ }
+
+ let parts = signedObject.split(".");
+ if (parts.length != 3) {
+ throw new Error("signed object must have three parts, this one has " + parts.length);
+ }
+
+ let headerSegment = parts[0];
+ let payloadSegment = parts[1];
+ let cryptoSegment = parts[2];
+
+ let header = JSON.parse(base64UrlDecode(headerSegment));
+ let payload = JSON.parse(base64UrlDecode(payloadSegment));
+
+ // Ensure well-formed header
+ do_check_eq(Object.keys(header).length, 1);
+ do_check_true(!!header.alg);
+
+ // Ensure well-formed payload
+ for (let field of ["exp", "aud"]) {
+ do_check_true(!!payload[field]);
+ }
+
+ return {header: header,
+ payload: payload,
+ headerSegment: headerSegment,
+ payloadSegment: payloadSegment,
+ cryptoSegment: cryptoSegment};
+}
+
+var TESTS = [
+ test_sanity,
+ test_generate,
+ test_get_assertion,
+ test_get_assertion_with_offset,
+ test_assertion_lifetime,
+ test_audience_encoding_bug972582,
+];
+
+TESTS = TESTS.concat([test_rsa, test_dsa]);
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_load_modules.js b/toolkit/identity/tests/unit/test_load_modules.js
new file mode 100644
index 000000000..4c531312c
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_load_modules.js
@@ -0,0 +1,20 @@
+/* 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/. */
+
+const modules = [
+ "Identity.jsm",
+ "IdentityProvider.jsm",
+ "IdentityStore.jsm",
+ "jwcrypto.jsm",
+ "RelyingParty.jsm",
+ "Sandbox.jsm",
+];
+
+function run_test() {
+ for (let m of modules) {
+ let resource = "resource://gre/modules/identity/" + m;
+ Components.utils.import(resource, {});
+ do_print("loaded " + resource);
+ }
+}
diff --git a/toolkit/identity/tests/unit/test_log_utils.js b/toolkit/identity/tests/unit/test_log_utils.js
new file mode 100644
index 000000000..ac43c297d
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_log_utils.js
@@ -0,0 +1,74 @@
+
+"use strict";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/identity/LogUtils.jsm');
+
+function toggle_debug() {
+ do_test_pending();
+
+ function Wrapper() {
+ this.init();
+ }
+ Wrapper.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ if (aTopic === "nsPref:changed") {
+ // race condition?
+ do_check_eq(Logger._debug, true);
+ do_test_finished();
+ run_next_test();
+ }
+ },
+
+ init: function() {
+ Services.prefs.addObserver('toolkit.identity.debug', this, false);
+ }
+ };
+
+ var wrapper = new Wrapper();
+ Services.prefs.setBoolPref('toolkit.identity.debug', true);
+}
+
+// test that things don't break
+
+function logAlias(...args) {
+ Logger.log.apply(Logger, ["log alias"].concat(args));
+}
+function reportErrorAlias(...args) {
+ Logger.reportError.apply(Logger, ["report error alias"].concat(args));
+}
+
+function test_log() {
+ Logger.log("log test", "I like pie");
+ do_test_finished();
+ run_next_test();
+}
+
+function test_reportError() {
+ Logger.reportError("log test", "We are out of pies!!!");
+ do_test_finished();
+ run_next_test();
+}
+
+function test_wrappers() {
+ logAlias("I like potatoes");
+ do_test_finished();
+ reportErrorAlias("Too much red bull");
+}
+
+var TESTS = [
+// XXX fix me
+// toggle_debug,
+ test_log,
+ test_reportError,
+ test_wrappers,
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_minimalidentity.js b/toolkit/identity/tests/unit/test_minimalidentity.js
new file mode 100644
index 000000000..77c30c84f
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_minimalidentity.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "MinimalIDService",
+ "resource://gre/modules/identity/MinimalIdentity.jsm",
+ "IdentityService");
+
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/DOMIdentity.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["test_minimalidentity"].concat(aMessageArgs));
+}
+
+function test_overall() {
+ do_check_neq(MinimalIDService, null);
+ run_next_test();
+}
+
+function test_mock_doc() {
+ do_test_pending();
+ let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {
+ do_check_eq(action, 'coffee');
+ do_test_finished();
+ run_next_test();
+ });
+
+ mockedDoc.doCoffee();
+}
+
+/*
+ * Test that the "identity-controller-watch" signal is emitted correctly
+ */
+function test_watch() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL);
+ makeObserver("identity-controller-watch", function (aSubject, aTopic, aData) {
+ do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id);
+ do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL);
+ do_test_finished();
+ run_next_test();
+ });
+
+ MinimalIDService.RP.watch(mockedDoc);
+}
+
+/*
+ * Test that the "identity-controller-request" signal is emitted correctly
+ */
+function test_request() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL);
+ makeObserver("identity-controller-request", function (aSubject, aTopic, aData) {
+ do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id);
+ do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL);
+ do_test_finished();
+ run_next_test();
+ });
+
+ MinimalIDService.RP.watch(mockedDoc);
+ MinimalIDService.RP.request(mockedDoc.id, {});
+}
+
+/*
+ * Test that the forceAuthentication flag can be sent
+ */
+function test_request_forceAuthentication() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL);
+ makeObserver("identity-controller-request", function (aSubject, aTopic, aData) {
+ do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id);
+ do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL);
+ do_check_eq(aSubject.wrappedJSObject.forceAuthentication, true);
+ do_test_finished();
+ run_next_test();
+ });
+
+ MinimalIDService.RP.watch(mockedDoc);
+ MinimalIDService.RP.request(mockedDoc.id, {forceAuthentication: true});
+}
+
+/*
+ * Test that the issuer can be forced
+ */
+function test_request_forceIssuer() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL);
+ makeObserver("identity-controller-request", function (aSubject, aTopic, aData) {
+ do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id);
+ do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL);
+ do_check_eq(aSubject.wrappedJSObject.issuer, "https://jed.gov");
+ do_test_finished();
+ run_next_test();
+ });
+
+ MinimalIDService.RP.watch(mockedDoc);
+ MinimalIDService.RP.request(mockedDoc.id, {issuer: "https://jed.gov"});
+}
+
+/*
+ * Test that the "identity-controller-logout" signal is emitted correctly
+ */
+function test_logout() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL);
+ makeObserver("identity-controller-logout", function (aSubject, aTopic, aData) {
+ do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id);
+ do_test_finished();
+ run_next_test();
+ });
+
+ MinimalIDService.RP.watch(mockedDoc);
+ MinimalIDService.RP.logout(mockedDoc.id, {});
+}
+
+/*
+ * Test that logout() before watch() fails gently
+ */
+
+function test_logoutBeforeWatch() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL);
+ makeObserver("identity-controller-logout", function() {
+ do_throw("How can we logout when watch was not called?");
+ });
+
+ MinimalIDService.RP.logout(mockedDoc.id, {});
+ do_test_finished();
+ run_next_test();
+}
+
+/*
+ * Test that request() before watch() fails gently
+ */
+
+function test_requestBeforeWatch() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL);
+ makeObserver("identity-controller-request", function() {
+ do_throw("How can we request when watch was not called?");
+ });
+
+ MinimalIDService.RP.request(mockedDoc.id, {});
+ do_test_finished();
+ run_next_test();
+}
+
+/*
+ * Test that internal unwatch() before watch() fails gently
+ */
+
+function test_unwatchBeforeWatch() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL);
+
+ MinimalIDService.RP.unwatch(mockedDoc.id, {});
+ do_test_finished();
+ run_next_test();
+}
+
+/*
+ * Test that the RP flow is cleaned up on child process shutdown
+ */
+
+function test_childProcessShutdown() {
+ do_test_pending();
+ let UNIQUE_MESSAGE_MANAGER = "i am a beautiful snowflake";
+ let initialRPCount = Object.keys(MinimalIDService.RP._rpFlows).length;
+
+ let mockedDoc = mock_doc(null, TEST_URL, (action, params) => {
+ if (action == "child-process-shutdown") {
+ // since there's no actual dom window connection, we have to
+ // do this bit manually here.
+ MinimalIDService.RP.childProcessShutdown(UNIQUE_MESSAGE_MANAGER);
+ }
+ });
+ mockedDoc._mm = UNIQUE_MESSAGE_MANAGER;
+
+ makeObserver("identity-controller-watch", function (aSubject, aTopic, aData) {
+ DOMIdentity._childProcessShutdown(UNIQUE_MESSAGE_MANAGER);
+ });
+
+ makeObserver("identity-child-process-shutdown", (aTopic, aSubject, aData) => {
+ do_check_eq(Object.keys(MinimalIDService.RP._rpFlows).length, initialRPCount);
+ do_test_finished();
+ run_next_test();
+ });
+
+ // fake a dom window context
+ DOMIdentity.newContext(mockedDoc, UNIQUE_MESSAGE_MANAGER);
+
+ MinimalIDService.RP.watch(mockedDoc);
+}
+
+var TESTS = [
+ test_overall,
+ test_mock_doc,
+ test_watch,
+ test_request,
+ test_request_forceAuthentication,
+ test_request_forceIssuer,
+ test_logout,
+ test_logoutBeforeWatch,
+ test_requestBeforeWatch,
+ test_unwatchBeforeWatch,
+ test_childProcessShutdown,
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_observer_topics.js b/toolkit/identity/tests/unit/test_observer_topics.js
new file mode 100644
index 000000000..8e5a89c91
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_observer_topics.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * By their nature, these tests duplicate some of the functionality of
+ * other tests for Identity, RelyingParty, and IdentityProvider.
+ *
+ * In particular, "identity-auth-complete" and
+ * "identity-login-state-changed" are tested in test_authentication.js
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "IDService",
+ "resource://gre/modules/identity/Identity.jsm",
+ "IdentityService");
+
+function test_smoke() {
+ do_check_neq(IDService, null);
+ run_next_test();
+}
+
+function test_identity_request() {
+ // In response to navigator.id.request(), initiate a login with user
+ // interaction by notifying observers of 'identity-request'
+
+ do_test_pending();
+
+ IDService.reset();
+
+ let id = "landru@mockmyid.com";
+ setup_test_identity(id, TEST_CERT, function() {
+ // deliberately adding a trailing final slash on the domain
+ // to test path composition
+ let mockedDoc = mock_doc(null, "http://jed.gov/", function() {});
+
+ // by calling watch() we create an rp flow.
+ IDService.RP.watch(mockedDoc);
+
+ // register the request UX observer
+ makeObserver("identity-request", function (aSubject, aTopic, aData) {
+ do_check_eq(aTopic, "identity-request");
+ do_check_eq(aData, null);
+
+ // check that all the URLs are properly resolved
+ let subj = aSubject.wrappedJSObject;
+ do_check_eq(subj.privacyPolicy, "http://jed.gov/pp.html");
+ do_check_eq(subj.termsOfService, "http://jed.gov/tos.html");
+
+ do_test_finished();
+ run_next_test();
+ });
+
+ let requestOptions = {
+ privacyPolicy: "/pp.html",
+ termsOfService: "/tos.html"
+ };
+ IDService.RP.request(mockedDoc.id, requestOptions);
+ });
+
+}
+
+function test_identity_auth() {
+ // see test_authentication.js for "identity-auth-complete"
+ // and "identity-login-state-changed"
+
+ do_test_pending();
+ let _provId = "bogus";
+
+ // Simulate what would be returned by IDService._fetchWellKnownFile
+ // for a given domain.
+ let idpParams = {
+ domain: "myfavoriteflan.com",
+ idpParams: {
+ authentication: "/foo/authenticate.html",
+ provisioning: "/foo/provision.html"
+ }
+ };
+
+ // Create an RP flow
+ let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {});
+ IDService.RP.watch(mockedDoc);
+
+ // The identity-auth notification is sent up to the UX from the
+ // _doAuthentication function. Be ready to receive it and call
+ // beginAuthentication
+ makeObserver("identity-auth", function (aSubject, aTopic, aData) {
+ do_check_neq(aSubject, null);
+ do_check_eq(aTopic, "identity-auth");
+ do_check_eq(aData, "https://myfavoriteflan.com/foo/authenticate.html");
+
+ do_check_eq(aSubject.wrappedJSObject.provId, _provId);
+ do_test_finished();
+ run_next_test();
+ });
+
+ // Even though our provisioning flow id is bogus, IdentityProvider
+ // won't look at it until farther along in the authentication
+ // process. So this test can pass with a fake provId.
+ IDService.IDP._doAuthentication(_provId, idpParams);
+}
+
+var TESTS = [
+ test_smoke,
+ test_identity_request,
+ test_identity_auth,
+ ];
+
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_provisioning.js b/toolkit/identity/tests/unit/test_provisioning.js
new file mode 100644
index 000000000..c05805bef
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_provisioning.js
@@ -0,0 +1,242 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/identity/IdentityProvider.jsm");
+
+function check_provision_flow_done(provId) {
+ do_check_null(IdentityProvider._provisionFlows[provId]);
+}
+
+function test_begin_provisioning() {
+ do_test_pending();
+
+ setup_provisioning(
+ TEST_USER,
+ function(caller) {
+ // call .beginProvisioning()
+ IdentityProvider.beginProvisioning(caller);
+ }, function() {},
+ {
+ beginProvisioningCallback: function(email, duration_s) {
+ do_check_eq(email, TEST_USER);
+ do_check_true(duration_s > 0);
+ do_check_true(duration_s <= (24 * 3600));
+
+ do_test_finished();
+ run_next_test();
+ }
+ });
+}
+
+function test_raise_provisioning_failure() {
+ do_test_pending();
+ let _callerId = null;
+
+ setup_provisioning(
+ TEST_USER,
+ function(caller) {
+ // call .beginProvisioning()
+ _callerId = caller.id;
+ IdentityProvider.beginProvisioning(caller);
+ }, function(err) {
+ // this should be invoked with a populated error
+ do_check_neq(err, null);
+ do_check_true(err.indexOf("can't authenticate this email") > -1);
+
+ do_test_finished();
+ run_next_test();
+ },
+ {
+ beginProvisioningCallback: function(email, duration_s) {
+ // raise the failure as if we can't provision this email
+ IdentityProvider.raiseProvisioningFailure(_callerId, "can't authenticate this email");
+ }
+ });
+}
+
+function test_genkeypair_before_begin_provisioning() {
+ do_test_pending();
+
+ setup_provisioning(
+ TEST_USER,
+ function(caller) {
+ // call genKeyPair without beginProvisioning
+ IdentityProvider.genKeyPair(caller.id);
+ },
+ // expect this to be called with an error
+ function(err) {
+ do_check_neq(err, null);
+
+ do_test_finished();
+ run_next_test();
+ },
+ {
+ // this should not be called at all!
+ genKeyPairCallback: function(pk) {
+ // a test that will surely fail because we shouldn't be here.
+ do_check_true(false);
+
+ do_test_finished();
+ run_next_test();
+ }
+ }
+ );
+}
+
+function test_genkeypair() {
+ do_test_pending();
+ let _callerId = null;
+
+ setup_provisioning(
+ TEST_USER,
+ function(caller) {
+ _callerId = caller.id;
+ IdentityProvider.beginProvisioning(caller);
+ },
+ function(err) {
+ // should not be called!
+ do_check_true(false);
+
+ do_test_finished();
+ run_next_test();
+ },
+ {
+ beginProvisioningCallback: function(email, time_s) {
+ IdentityProvider.genKeyPair(_callerId);
+ },
+ genKeyPairCallback: function(kp) {
+ do_check_neq(kp, null);
+
+ // yay!
+ do_test_finished();
+ run_next_test();
+ }
+ }
+ );
+}
+
+// we've already ensured that genkeypair can't be called
+// before beginProvisioning, so this test should be enough
+// to ensure full sequential call of the 3 APIs.
+function test_register_certificate_before_genkeypair() {
+ do_test_pending();
+ let _callerID = null;
+
+ setup_provisioning(
+ TEST_USER,
+ function(caller) {
+ // do the right thing for beginProvisioning
+ _callerID = caller.id;
+ IdentityProvider.beginProvisioning(caller);
+ },
+ // expect this to be called with an error
+ function(err) {
+ do_check_neq(err, null);
+
+ do_test_finished();
+ run_next_test();
+ },
+ {
+ beginProvisioningCallback: function(email, duration_s) {
+ // now we try to register cert but no keygen has been done
+ IdentityProvider.registerCertificate(_callerID, "fake-cert");
+ }
+ }
+ );
+}
+
+function test_register_certificate() {
+ do_test_pending();
+ let _callerId = null;
+
+ setup_provisioning(
+ TEST_USER,
+ function(caller) {
+ _callerId = caller.id;
+ IdentityProvider.beginProvisioning(caller);
+ },
+ function(err) {
+ // we should be cool!
+ do_check_null(err);
+
+ // check that the cert is there
+ let identity = get_idstore().fetchIdentity(TEST_USER);
+ do_check_neq(identity, null);
+ do_check_eq(identity.cert, "fake-cert-42");
+
+ do_execute_soon(function check_done() {
+ // cleanup will happen after the callback is called
+ check_provision_flow_done(_callerId);
+
+ do_test_finished();
+ run_next_test();
+ });
+ },
+ {
+ beginProvisioningCallback: function(email, duration_s) {
+ IdentityProvider.genKeyPair(_callerId);
+ },
+ genKeyPairCallback: function(pk) {
+ IdentityProvider.registerCertificate(_callerId, "fake-cert-42");
+ }
+ }
+ );
+}
+
+
+function test_get_assertion_after_provision() {
+ do_test_pending();
+ let _callerId = null;
+
+ setup_provisioning(
+ TEST_USER,
+ function(caller) {
+ _callerId = caller.id;
+ IdentityProvider.beginProvisioning(caller);
+ },
+ function(err) {
+ // we should be cool!
+ do_check_null(err);
+
+ // check that the cert is there
+ let identity = get_idstore().fetchIdentity(TEST_USER);
+ do_check_neq(identity, null);
+ do_check_eq(identity.cert, "fake-cert-42");
+
+ do_execute_soon(function check_done() {
+ // cleanup will happen after the callback is called
+ check_provision_flow_done(_callerId);
+
+ do_test_finished();
+ run_next_test();
+ });
+ },
+ {
+ beginProvisioningCallback: function(email, duration_s) {
+ IdentityProvider.genKeyPair(_callerId);
+ },
+ genKeyPairCallback: function(pk) {
+ IdentityProvider.registerCertificate(_callerId, "fake-cert-42");
+ }
+ }
+ );
+
+}
+
+var TESTS = [];
+
+TESTS.push(test_begin_provisioning);
+TESTS.push(test_raise_provisioning_failure);
+TESTS.push(test_genkeypair_before_begin_provisioning);
+TESTS.push(test_genkeypair);
+TESTS.push(test_register_certificate_before_genkeypair);
+TESTS.push(test_register_certificate);
+TESTS.push(test_get_assertion_after_provision);
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_relying_party.js b/toolkit/identity/tests/unit/test_relying_party.js
new file mode 100644
index 000000000..e78d22779
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_relying_party.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "RelyingParty",
+ "resource://gre/modules/identity/RelyingParty.jsm");
+
+function resetState() {
+ get_idstore().reset();
+ RelyingParty.reset();
+}
+
+function test_watch_loggedin_ready() {
+ do_test_pending();
+
+ resetState();
+
+ let id = TEST_USER;
+ setup_test_identity(id, TEST_CERT, function() {
+ let store = get_idstore();
+
+ // set it up so we're supposed to be logged in to TEST_URL
+ store.setLoginState(TEST_URL, true, id);
+ RelyingParty.watch(mock_doc(id, TEST_URL, function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_eq(params, undefined);
+
+ do_test_finished();
+ run_next_test();
+ }));
+ });
+}
+
+function test_watch_loggedin_login() {
+ do_test_pending();
+
+ resetState();
+
+ let id = TEST_USER;
+ setup_test_identity(id, TEST_CERT, function() {
+ let store = get_idstore();
+
+ // set it up so we're supposed to be logged in to TEST_URL
+ store.setLoginState(TEST_URL, true, id);
+
+ // check for first a login() call, then a ready() call
+ RelyingParty.watch(mock_doc(null, TEST_URL, call_sequentially(
+ function(action, params) {
+ do_check_eq(action, 'login');
+ do_check_neq(params, null);
+ },
+ function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_null(params);
+
+ do_test_finished();
+ run_next_test();
+ }
+ )));
+ });
+}
+
+function test_watch_loggedin_logout() {
+ do_test_pending();
+
+ resetState();
+
+ let id = TEST_USER;
+ let other_id = "otherid@foo.com";
+ setup_test_identity(other_id, TEST_CERT, function() {
+ setup_test_identity(id, TEST_CERT, function() {
+ let store = get_idstore();
+
+ // set it up so we're supposed to be logged in to TEST_URL
+ // with id, not other_id
+ store.setLoginState(TEST_URL, true, id);
+
+ // this should cause a login with an assertion for id,
+ // not for other_id
+ RelyingParty.watch(mock_doc(other_id, TEST_URL, call_sequentially(
+ function(action, params) {
+ do_check_eq(action, 'login');
+ do_check_neq(params, null);
+ },
+ function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_null(params);
+
+ do_test_finished();
+ run_next_test();
+ }
+ )));
+ });
+ });
+}
+
+function test_watch_notloggedin_ready() {
+ do_test_pending();
+
+ resetState();
+
+ RelyingParty.watch(mock_doc(null, TEST_URL, function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_eq(params, undefined);
+
+ do_test_finished();
+ run_next_test();
+ }));
+}
+
+function test_watch_notloggedin_logout() {
+ do_test_pending();
+
+ resetState();
+
+ RelyingParty.watch(mock_doc(TEST_USER, TEST_URL, call_sequentially(
+ function(action, params) {
+ do_check_eq(action, 'logout');
+ do_check_eq(params, undefined);
+
+ let store = get_idstore();
+ do_check_null(store.getLoginState(TEST_URL));
+ },
+ function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_eq(params, undefined);
+ do_test_finished();
+ run_next_test();
+ }
+ )));
+}
+
+function test_request() {
+ do_test_pending();
+
+ // set up a watch, to be consistent
+ let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {
+ // this isn't going to be called for now
+ // XXX but it is called - is that bad?
+ });
+
+ RelyingParty.watch(mockedDoc);
+
+ // be ready for the UX identity-request notification
+ makeObserver("identity-request", function (aSubject, aTopic, aData) {
+ do_check_neq(aSubject, null);
+
+ do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+
+ do_test_finished();
+ run_next_test();
+ });
+
+ RelyingParty.request(mockedDoc.id, {});
+}
+
+/*
+ * ensure the forceAuthentication param can be passed through
+ */
+function test_request_forceAuthentication() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {});
+
+ RelyingParty.watch(mockedDoc);
+
+ makeObserver("identity-request", function(aSubject, aTopic, aData) {
+ do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+ do_check_eq(aSubject.wrappedJSObject.forceAuthentication, true);
+ do_test_finished();
+ run_next_test();
+ });
+
+ RelyingParty.request(mockedDoc.id, {forceAuthentication: true});
+}
+
+/*
+ * ensure the issuer can be forced
+ */
+function test_request_forceIssuer() {
+ do_test_pending();
+
+ let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {});
+
+ RelyingParty.watch(mockedDoc);
+
+ makeObserver("identity-request", function(aSubject, aTopic, aData) {
+ do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+ do_check_eq(aSubject.wrappedJSObject.issuer, "https://ozten.co.uk");
+ do_test_finished();
+ run_next_test();
+ });
+
+ RelyingParty.request(mockedDoc.id, {issuer: "https://ozten.co.uk"});
+}
+function test_logout() {
+ do_test_pending();
+
+ resetState();
+
+ let id = TEST_USER;
+ setup_test_identity(id, TEST_CERT, function() {
+ let store = get_idstore();
+
+ // set it up so we're supposed to be logged in to TEST_URL
+ store.setLoginState(TEST_URL, true, id);
+
+ let doLogout;
+ let mockedDoc = mock_doc(id, TEST_URL, call_sequentially(
+ function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_eq(params, undefined);
+
+ do_timeout(100, doLogout);
+ },
+ function(action, params) {
+ do_check_eq(action, 'logout');
+ do_check_eq(params, undefined);
+ },
+ function(action, params) {
+ do_check_eq(action, 'ready');
+ do_check_eq(params, undefined);
+
+ do_test_finished();
+ run_next_test();
+ }));
+
+ doLogout = function() {
+ RelyingParty.logout(mockedDoc.id);
+ do_check_false(store.getLoginState(TEST_URL).isLoggedIn);
+ do_check_eq(store.getLoginState(TEST_URL).email, TEST_USER);
+ };
+
+ RelyingParty.watch(mockedDoc);
+ });
+}
+
+var TESTS = [
+ test_watch_loggedin_ready,
+ test_watch_loggedin_login,
+ test_watch_loggedin_logout,
+ test_watch_notloggedin_ready,
+ test_watch_notloggedin_logout,
+ test_request,
+ test_request_forceAuthentication,
+ test_request_forceIssuer,
+ test_logout,
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_store.js b/toolkit/identity/tests/unit/test_store.js
new file mode 100644
index 000000000..1cd9cc4dd
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_store.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "IDService",
+ "resource://gre/modules/identity/Identity.jsm",
+ "IdentityService");
+
+function test_id_store() {
+ // XXX - this is ugly, peaking in like this into IDService
+ // probably should instantiate our own.
+ var store = get_idstore();
+
+ // try adding an identity
+ store.addIdentity(TEST_USER, TEST_PRIVKEY, TEST_CERT);
+ do_check_neq(store.getIdentities()[TEST_USER], null);
+ do_check_eq(store.getIdentities()[TEST_USER].cert, TEST_CERT);
+
+ // does fetch identity work?
+ do_check_neq(store.fetchIdentity(TEST_USER), null);
+ do_check_eq(store.fetchIdentity(TEST_USER).cert, TEST_CERT);
+
+ // clear the cert should keep the identity but not the cert
+ store.clearCert(TEST_USER);
+ do_check_neq(store.getIdentities()[TEST_USER], null);
+ do_check_null(store.getIdentities()[TEST_USER].cert);
+
+ // remove it should remove everything
+ store.removeIdentity(TEST_USER);
+ do_check_eq(store.getIdentities()[TEST_USER], undefined);
+
+ // act like we're logged in to TEST_URL
+ store.setLoginState(TEST_URL, true, TEST_USER);
+ do_check_neq(store.getLoginState(TEST_URL), null);
+ do_check_true(store.getLoginState(TEST_URL).isLoggedIn);
+ do_check_eq(store.getLoginState(TEST_URL).email, TEST_USER);
+
+ // log out
+ store.setLoginState(TEST_URL, false, TEST_USER);
+ do_check_neq(store.getLoginState(TEST_URL), null);
+ do_check_false(store.getLoginState(TEST_URL).isLoggedIn);
+
+ // email is still set
+ do_check_eq(store.getLoginState(TEST_URL).email, TEST_USER);
+
+ // not logged into other site
+ do_check_null(store.getLoginState(TEST_URL2));
+
+ // clear login state
+ store.clearLoginState(TEST_URL);
+ do_check_null(store.getLoginState(TEST_URL));
+ do_check_null(store.getLoginState(TEST_URL2));
+
+ run_next_test();
+}
+
+var TESTS = [test_id_store, ];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/test_well-known.js b/toolkit/identity/tests/unit/test_well-known.js
new file mode 100644
index 000000000..5e86f5ae4
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_well-known.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "IDService",
+ "resource://gre/modules/identity/Identity.jsm",
+ "IdentityService");
+
+const WELL_KNOWN_PATH = "/.well-known/browserid";
+
+var SERVER_PORT = 8080;
+
+// valid IDP
+function test_well_known_1() {
+ do_test_pending();
+
+ let server = new HttpServer();
+ server.registerFile(WELL_KNOWN_PATH, do_get_file("data/idp_1" + WELL_KNOWN_PATH));
+ server.start(SERVER_PORT);
+ let hostPort = "localhost:" + SERVER_PORT;
+
+ function check_well_known(aErr, aCallbackObj) {
+ do_check_null(aErr);
+ do_check_eq(aCallbackObj.domain, hostPort);
+ let idpParams = aCallbackObj.idpParams;
+ do_check_eq(idpParams['public-key'].algorithm, "RS");
+ do_check_eq(idpParams.authentication, "/browserid/sign_in.html");
+ do_check_eq(idpParams.provisioning, "/browserid/provision.html");
+
+ do_test_finished();
+ server.stop(run_next_test);
+ }
+
+ IDService._fetchWellKnownFile(hostPort, check_well_known, "http");
+}
+
+// valid domain, non-exixtent browserid file
+function test_well_known_404() {
+ do_test_pending();
+
+ let server = new HttpServer();
+ // Don't register the well-known file
+ // Change ports to avoid HTTP caching
+ SERVER_PORT++;
+ server.start(SERVER_PORT);
+
+ let hostPort = "localhost:" + SERVER_PORT;
+
+ function check_well_known_404(aErr, aCallbackObj) {
+ do_check_eq("Error", aErr);
+ do_check_eq(undefined, aCallbackObj);
+ do_test_finished();
+ server.stop(run_next_test);
+ }
+
+ IDService._fetchWellKnownFile(hostPort, check_well_known_404, "http");
+}
+
+// valid domain, invalid browserid file (no "provisioning" member)
+function test_well_known_invalid_1() {
+ do_test_pending();
+
+ let server = new HttpServer();
+ server.registerFile(WELL_KNOWN_PATH, do_get_file("data/idp_invalid_1" + WELL_KNOWN_PATH));
+ // Change ports to avoid HTTP caching
+ SERVER_PORT++;
+ server.start(SERVER_PORT);
+
+ let hostPort = "localhost:" + SERVER_PORT;
+
+ function check_well_known_invalid_1(aErr, aCallbackObj) {
+ // check for an error message
+ do_check_true(aErr && aErr.length > 0);
+ do_check_eq(undefined, aCallbackObj);
+ do_test_finished();
+ server.stop(run_next_test);
+ }
+
+ IDService._fetchWellKnownFile(hostPort, check_well_known_invalid_1, "http");
+}
+
+var TESTS = [test_well_known_1, test_well_known_404, test_well_known_invalid_1];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/identity/tests/unit/xpcshell.ini b/toolkit/identity/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..8ef9b79bc
--- /dev/null
+++ b/toolkit/identity/tests/unit/xpcshell.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+head = head_identity.js
+tail = tail_identity.js
+skip-if = (appname != "b2g" || toolkit == 'gonk')
+support-files =
+ data/idp_1/.well-known/browserid
+ data/idp_invalid_1/.well-known/browserid
+
+# Test load modules first so syntax failures are caught early.
+[test_load_modules.js]
+[test_minimalidentity.js]
+[test_firefox_accounts.js]
+
+[test_identity_utils.js]
+[test_log_utils.js]
+[test_authentication.js]
+[test_crypto_service.js]
+[test_identity.js]
+[test_jwcrypto.js]
+[test_observer_topics.js]
+[test_provisioning.js]
+[test_relying_party.js]
+[test_store.js]
+[test_well-known.js]