/* -*- Mode: js2; js2-basic-offset: 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, "jwcrypto", "resource://gre/modules/identity/jwcrypto.jsm"); function log(...aMessageArgs) { Logger.log.apply(Logger, ["minimal core"].concat(aMessageArgs)); } function reportError(...aMessageArgs) { Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs)); } function makeMessageObject(aRpCaller) { let options = {}; options.id = aRpCaller.id; options.origin = aRpCaller.origin; // loggedInUser can be undefined, null, or a string options.loggedInUser = aRpCaller.loggedInUser; // Special flag for internal calls 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; } function IDService() { Services.obs.addObserver(this, "quit-application-granted", false); // Services.obs.addObserver(this, "identity-auth-complete", 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": Services.obs.removeObserver(this, "quit-application-granted"); // Services.obs.removeObserver(this, "identity-auth-complete"); break; } }, /** * 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) { let options = makeMessageObject({messageManager: messageManager, id: null, origin: null}); Services.obs.notifyObservers({wrappedJSObject: options}, "identity-child-process-shutdown", null); 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) { dump("WARNING: doLogin found no rp to go with callerId " + aRpCallerId + "\n"); return; } rp.doLogin(aAssertion, aInternalParams); }, doLogout: function doLogout(aRpCallerId) { let rp = this._rpFlows[aRpCallerId]; if (!rp) { dump("WARNING: doLogout found no rp to go with callerId " + aRpCallerId + "\n"); 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) { dump("WARNING: doReady found no rp to go with callerId " + aRpCallerId + "\n"); return; } rp.doReady(); }, doCancel: function doCancel(aRpCallerId) { let rp = this._rpFlows[aRpCallerId]; if (!rp) { dump("WARNING: doCancel found no rp to go with callerId " + aRpCallerId + "\n"); return; } rp.doCancel(); }, /* * XXX Bug 804229: Implement Identity Provider Functions * * Stubs for Identity Provider functions follow */ /** * 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) { }, /** * 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); }, /** * 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) { }, /** * 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) { }, /** * 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) { }, /** * 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) { }, /** * The auth frame has called navigator.id.cancelAuthentication * * @param aAuthId * (int) the identifier of the authentication caller * */ cancelAuthentication: function cancelAuthentication(aAuthId) { }, // 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();