summaryrefslogtreecommitdiffstats
path: root/toolkit/identity/Identity.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/identity/Identity.jsm')
-rw-r--r--toolkit/identity/Identity.jsm309
1 files changed, 309 insertions, 0 deletions
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();