/* -*- 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"); 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; }, /** * 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) { return aCallback("Could not parse email: " + aIdentity); } 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();