michael@0: /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["IdentityService"]; michael@0: michael@0: const Cu = Components.utils; michael@0: const Ci = Components.interfaces; michael@0: const Cc = Components.classes; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/identity/LogUtils.jsm"); michael@0: Cu.import("resource://gre/modules/identity/IdentityStore.jsm"); michael@0: Cu.import("resource://gre/modules/identity/RelyingParty.jsm"); michael@0: Cu.import("resource://gre/modules/identity/IdentityProvider.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, michael@0: "jwcrypto", michael@0: "resource://gre/modules/identity/jwcrypto.jsm"); michael@0: michael@0: function log(...aMessageArgs) { michael@0: Logger.log.apply(Logger, ["core"].concat(aMessageArgs)); michael@0: } michael@0: function reportError(...aMessageArgs) { michael@0: Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs)); michael@0: } michael@0: michael@0: function IDService() { michael@0: Services.obs.addObserver(this, "quit-application-granted", false); michael@0: Services.obs.addObserver(this, "identity-auth-complete", false); michael@0: michael@0: this._store = IdentityStore; michael@0: this.RP = RelyingParty; michael@0: this.IDP = IdentityProvider; michael@0: } michael@0: michael@0: IDService.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), michael@0: michael@0: observe: function observe(aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "quit-application-granted": michael@0: Services.obs.removeObserver(this, "quit-application-granted"); michael@0: this.shutdown(); michael@0: break; michael@0: case "identity-auth-complete": michael@0: if (!aSubject || !aSubject.wrappedJSObject) michael@0: break; michael@0: let subject = aSubject.wrappedJSObject; michael@0: log("Auth complete:", aSubject.wrappedJSObject); michael@0: // We have authenticated in order to provision an identity. michael@0: // So try again. michael@0: this.selectIdentity(subject.rpId, subject.identity); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: reset: function reset() { michael@0: // Explicitly call reset() on our RP and IDP classes. michael@0: // This is here to make testing easier. When the michael@0: // quit-application-granted signal is emitted, reset() will be michael@0: // called here, on RP, on IDP, and on the store. So you don't michael@0: // need to use this :) michael@0: this._store.reset(); michael@0: this.RP.reset(); michael@0: this.IDP.reset(); michael@0: }, michael@0: michael@0: shutdown: function shutdown() { michael@0: log("shutdown"); michael@0: Services.obs.removeObserver(this, "identity-auth-complete"); michael@0: Services.obs.removeObserver(this, "quit-application-granted"); michael@0: }, michael@0: michael@0: /** michael@0: * Parse an email into username and domain if it is valid, else return null michael@0: */ michael@0: parseEmail: function parseEmail(email) { michael@0: var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/); michael@0: if (match) { michael@0: return { michael@0: username: match[1], michael@0: domain: match[2] michael@0: }; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * The UX wants to add a new identity michael@0: * often followed by selectIdentity() michael@0: * michael@0: * @param aIdentity michael@0: * (string) the email chosen for login michael@0: */ michael@0: addIdentity: function addIdentity(aIdentity) { michael@0: if (this._store.fetchIdentity(aIdentity) === null) { michael@0: this._store.addIdentity(aIdentity, null, null); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The UX comes back and calls selectIdentity once the user has picked michael@0: * an identity. michael@0: * michael@0: * @param aRPId michael@0: * (integer) the id of the doc object obtained in .watch() and michael@0: * passed to the UX component. michael@0: * michael@0: * @param aIdentity michael@0: * (string) the email chosen for login michael@0: */ michael@0: selectIdentity: function selectIdentity(aRPId, aIdentity) { michael@0: log("selectIdentity: RP id:", aRPId, "identity:", aIdentity); michael@0: michael@0: // Get the RP that was stored when watch() was invoked. michael@0: let rp = this.RP._rpFlows[aRPId]; michael@0: if (!rp) { michael@0: reportError("selectIdentity", "Invalid RP id: ", aRPId); michael@0: return; michael@0: } michael@0: michael@0: // It's possible that we are in the process of provisioning an michael@0: // identity. michael@0: let provId = rp.provId; michael@0: michael@0: let rpLoginOptions = { michael@0: loggedInUser: aIdentity, michael@0: origin: rp.origin michael@0: }; michael@0: log("selectIdentity: provId:", provId, "origin:", rp.origin); michael@0: michael@0: // Once we have a cert, and once the user is authenticated with the michael@0: // IdP, we can generate an assertion and deliver it to the doc. michael@0: let self = this; michael@0: this.RP._generateAssertion(rp.origin, aIdentity, function hadReadyAssertion(err, assertion) { michael@0: if (!err && assertion) { michael@0: self.RP._doLogin(rp, rpLoginOptions, assertion); michael@0: return; michael@0: michael@0: } michael@0: // Need to provision an identity first. Begin by discovering michael@0: // the user's IdP. michael@0: self._discoverIdentityProvider(aIdentity, function gotIDP(err, idpParams) { michael@0: if (err) { michael@0: rp.doError(err); michael@0: return; michael@0: } michael@0: michael@0: // The idpParams tell us where to go to provision and authenticate michael@0: // the identity. michael@0: self.IDP._provisionIdentity(aIdentity, idpParams, provId, function gotID(err, aProvId) { michael@0: michael@0: // Provision identity may have created a new provision flow michael@0: // for us. To make it easier to relate provision flows with michael@0: // RP callers, we cross index the two here. michael@0: rp.provId = aProvId; michael@0: self.IDP._provisionFlows[aProvId].rpId = aRPId; michael@0: michael@0: // At this point, we already have a cert. If the user is also michael@0: // already authenticated with the IdP, then we can try again michael@0: // to generate an assertion and login. michael@0: if (err) { michael@0: // We are not authenticated. If we have already tried to michael@0: // authenticate and failed, then this is a "hard fail" and michael@0: // we give up. Otherwise we try to authenticate with the michael@0: // IdP. michael@0: michael@0: if (self.IDP._provisionFlows[aProvId].didAuthentication) { michael@0: self.IDP._cleanUpProvisionFlow(aProvId); michael@0: self.RP._cleanUpProvisionFlow(aRPId, aProvId); michael@0: log("ERROR: selectIdentity: authentication hard fail"); michael@0: rp.doError("Authentication fail."); michael@0: return; michael@0: } michael@0: // Try to authenticate with the IdP. Note that we do michael@0: // not clean up the provision flow here. We will continue michael@0: // to use it. michael@0: self.IDP._doAuthentication(aProvId, idpParams); michael@0: return; michael@0: } michael@0: michael@0: // Provisioning flows end when a certificate has been registered. michael@0: // Thus IdentityProvider's registerCertificate() cleans up the michael@0: // current provisioning flow. We only do this here on error. michael@0: self.RP._generateAssertion(rp.origin, aIdentity, function gotAssertion(err, assertion) { michael@0: if (err) { michael@0: rp.doError(err); michael@0: return; michael@0: } michael@0: self.RP._doLogin(rp, rpLoginOptions, assertion); michael@0: self.RP._cleanUpProvisionFlow(aRPId, aProvId); michael@0: return; michael@0: }); michael@0: }); michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: // methods for chrome and add-ons michael@0: michael@0: /** michael@0: * Discover the IdP for an identity michael@0: * michael@0: * @param aIdentity michael@0: * (string) the email we're logging in with michael@0: * michael@0: * @param aCallback michael@0: * (function) callback to invoke on completion michael@0: * with first-positional parameter the error. michael@0: */ michael@0: _discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) { michael@0: // XXX bug 767610 - validate email address call michael@0: // When that is available, we can remove this custom parser michael@0: var parsedEmail = this.parseEmail(aIdentity); michael@0: if (parsedEmail === null) { michael@0: return aCallback("Could not parse email: " + aIdentity); michael@0: } michael@0: log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain); michael@0: michael@0: this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) { michael@0: // idpParams includes the pk, authorization url, and michael@0: // provisioning url. michael@0: michael@0: // XXX bug 769861 follow any authority delegations michael@0: // if no well-known at any point in the delegation michael@0: // fall back to browserid.org as IdP michael@0: return aCallback(err, idpParams); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Fetch the well-known file from the domain. michael@0: * michael@0: * @param aDomain michael@0: * michael@0: * @param aScheme michael@0: * (string) (optional) Protocol to use. Default is https. michael@0: * This is necessary because we are unable to test michael@0: * https. michael@0: * michael@0: * @param aCallback michael@0: * michael@0: */ michael@0: _fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme='https') { michael@0: // XXX bug 769854 make tests https and remove aScheme option michael@0: let url = aScheme + '://' + aDomain + "/.well-known/browserid"; michael@0: log("_fetchWellKnownFile:", url); michael@0: michael@0: // this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window michael@0: let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: michael@0: // XXX bug 769865 gracefully handle being off-line michael@0: // XXX bug 769866 decide on how to handle redirects michael@0: req.open("GET", url, true); michael@0: req.responseType = "json"; michael@0: req.mozBackgroundRequest = true; michael@0: req.onload = function _fetchWellKnownFile_onload() { michael@0: if (req.status < 200 || req.status >= 400) { michael@0: log("_fetchWellKnownFile", url, ": server returned status:", req.status); michael@0: return aCallback("Error"); michael@0: } michael@0: try { michael@0: let idpParams = req.response; michael@0: michael@0: // Verify that the IdP returned a valid configuration michael@0: if (! (idpParams.provisioning && michael@0: idpParams.authentication && michael@0: idpParams['public-key'])) { michael@0: let errStr= "Invalid well-known file from: " + aDomain; michael@0: log("_fetchWellKnownFile:", errStr); michael@0: return aCallback(errStr); michael@0: } michael@0: michael@0: let callbackObj = { michael@0: domain: aDomain, michael@0: idpParams: idpParams, michael@0: }; michael@0: log("_fetchWellKnownFile result: ", callbackObj); michael@0: // Yay. Valid IdP configuration for the domain. michael@0: return aCallback(null, callbackObj); michael@0: michael@0: } catch (err) { michael@0: reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err); michael@0: return aCallback(err.toString()); michael@0: } michael@0: }; michael@0: req.onerror = function _fetchWellKnownFile_onerror() { michael@0: log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText); michael@0: log("ERROR: _fetchWellKnownFile:", err); michael@0: return aCallback("Error"); michael@0: }; michael@0: req.send(null); michael@0: }, michael@0: michael@0: }; michael@0: michael@0: this.IdentityService = new IDService();