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: 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/Sandbox.jsm"); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["IdentityProvider"]; michael@0: const FALLBACK_PROVIDER = "browserid.org"; 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, ["IDP"].concat(aMessageArgs)); michael@0: } michael@0: function reportError(...aMessageArgs) { michael@0: Logger.reportError.apply(Logger, ["IDP"].concat(aMessageArgs)); michael@0: } michael@0: michael@0: michael@0: function IdentityProviderService() { michael@0: XPCOMUtils.defineLazyModuleGetter(this, michael@0: "_store", michael@0: "resource://gre/modules/identity/IdentityStore.jsm", michael@0: "IdentityStore"); michael@0: michael@0: this.reset(); michael@0: } michael@0: michael@0: IdentityProviderService.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), michael@0: _sandboxConfigured: false, 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: } michael@0: }, michael@0: michael@0: reset: function IDP_reset() { michael@0: // Clear the provisioning flows. Provision flows contain an michael@0: // identity, idpParams (how to reach the IdP to provision and michael@0: // authenticate), a callback (a completion callback for when things michael@0: // are done), and a provisioningFrame (which is the provisioning michael@0: // sandbox). Additionally, two callbacks will be attached: michael@0: // beginProvisioningCallback and genKeyPairCallback. michael@0: this._provisionFlows = {}; michael@0: michael@0: // Clear the authentication flows. Authentication flows attach michael@0: // to provision flows. In the process of provisioning an id, it michael@0: // may be necessary to authenticate with an IdP. The authentication michael@0: // flow maintains the state of that authentication process. michael@0: this._authenticationFlows = {}; michael@0: }, michael@0: michael@0: getProvisionFlow: function getProvisionFlow(aProvId, aErrBack) { michael@0: let provFlow = this._provisionFlows[aProvId]; michael@0: if (provFlow) { michael@0: return provFlow; michael@0: } michael@0: michael@0: let err = "No provisioning flow found with id " + aProvId; michael@0: log("ERROR:", err); michael@0: if (typeof aErrBack === 'function') { michael@0: aErrBack(err); michael@0: } michael@0: }, michael@0: michael@0: shutdown: function RP_shutdown() { michael@0: this.reset(); michael@0: michael@0: if (this._sandboxConfigured) { michael@0: // Tear down message manager listening on the hidden window michael@0: Cu.import("resource://gre/modules/DOMIdentity.jsm"); michael@0: DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, false); michael@0: this._sandboxConfigured = false; michael@0: } michael@0: michael@0: Services.obs.removeObserver(this, "quit-application-granted"); michael@0: }, michael@0: michael@0: get securityLevel() { michael@0: return 1; michael@0: }, michael@0: michael@0: get certDuration() { michael@0: switch(this.securityLevel) { michael@0: default: michael@0: return 3600; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Provision an Identity michael@0: * michael@0: * @param aIdentity michael@0: * (string) the email we're logging in with michael@0: * michael@0: * @param aIDPParams michael@0: * (object) parameters of the IdP 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: _provisionIdentity: function _provisionIdentity(aIdentity, aIDPParams, aProvId, aCallback) { michael@0: let provPath = aIDPParams.idpParams.provisioning; michael@0: let url = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(provPath); michael@0: log("_provisionIdentity: identity:", aIdentity, "url:", url); michael@0: michael@0: // If aProvId is not null, then we already have a flow michael@0: // with a sandbox. Otherwise, get a sandbox and create a michael@0: // new provision flow. michael@0: michael@0: if (aProvId) { michael@0: // Re-use an existing sandbox michael@0: log("_provisionIdentity: re-using sandbox in provisioning flow with id:", aProvId); michael@0: this._provisionFlows[aProvId].provisioningSandbox.reload(); michael@0: michael@0: } else { michael@0: this._createProvisioningSandbox(url, function createdSandbox(aSandbox) { michael@0: // create a provisioning flow, using the sandbox id, and michael@0: // stash callback associated with this provisioning workflow. michael@0: michael@0: let provId = aSandbox.id; michael@0: this._provisionFlows[provId] = { michael@0: identity: aIdentity, michael@0: idpParams: aIDPParams, michael@0: securityLevel: this.securityLevel, michael@0: provisioningSandbox: aSandbox, michael@0: callback: function doCallback(aErr) { michael@0: aCallback(aErr, provId); michael@0: }, michael@0: }; michael@0: michael@0: log("_provisionIdentity: Created sandbox and provisioning flow with id:", provId); michael@0: // XXX bug 769862 - provisioning flow should timeout after N seconds michael@0: michael@0: }.bind(this)); michael@0: } michael@0: }, michael@0: michael@0: // DOM Methods michael@0: /** michael@0: * the provisioning iframe sandbox has called navigator.id.beginProvisioning() michael@0: * michael@0: * @param aCaller michael@0: * (object) the iframe sandbox caller with all callbacks and michael@0: * other information. Callbacks include: michael@0: * - doBeginProvisioningCallback(id, duration_s) michael@0: * - doGenKeyPairCallback(pk) michael@0: */ michael@0: beginProvisioning: function beginProvisioning(aCaller) { michael@0: log("beginProvisioning:", aCaller.id); michael@0: michael@0: // Expect a flow for this caller already to be underway. michael@0: let provFlow = this.getProvisionFlow(aCaller.id, aCaller.doError); michael@0: michael@0: // keep the caller object around michael@0: provFlow.caller = aCaller; michael@0: michael@0: let identity = provFlow.identity; michael@0: let frame = provFlow.provisioningFrame; michael@0: michael@0: // Determine recommended length of cert. michael@0: let duration = this.certDuration; michael@0: michael@0: // Make a record that we have begun provisioning. This is required michael@0: // for genKeyPair. michael@0: provFlow.didBeginProvisioning = true; michael@0: michael@0: // Let the sandbox know to invoke the callback to beginProvisioning with michael@0: // the identity and cert length. michael@0: return aCaller.doBeginProvisioningCallback(identity, duration); michael@0: }, michael@0: michael@0: /** michael@0: * the provisioning iframe sandbox has called michael@0: * navigator.id.raiseProvisioningFailure() michael@0: * michael@0: * @param aProvId michael@0: * (int) the identifier of the provisioning flow tied to that sandbox michael@0: * @param aReason michael@0: */ michael@0: raiseProvisioningFailure: function raiseProvisioningFailure(aProvId, aReason) { michael@0: reportError("Provisioning failure", aReason); michael@0: michael@0: // look up the provisioning caller and its callback michael@0: let provFlow = this.getProvisionFlow(aProvId); michael@0: michael@0: // Sandbox is deleted in _cleanUpProvisionFlow in case we re-use it. michael@0: michael@0: // This may be either a "soft" or "hard" fail. If it's a michael@0: // soft fail, we'll flow through setAuthenticationFlow, where michael@0: // the provision flow data will be copied into a new auth michael@0: // flow. If it's a hard fail, then the callback will be michael@0: // responsible for cleaning up the now defunct provision flow. michael@0: michael@0: // invoke the callback with an error. michael@0: provFlow.callback(aReason); michael@0: }, michael@0: michael@0: /** michael@0: * When navigator.id.genKeyPair is called from provisioning iframe sandbox. michael@0: * Generates a keypair for the current user being provisioned. michael@0: * michael@0: * @param aProvId michael@0: * (int) the identifier of the provisioning caller tied to that sandbox michael@0: * michael@0: * It is an error to call genKeypair without receiving the callback for michael@0: * the beginProvisioning() call first. michael@0: */ michael@0: genKeyPair: function genKeyPair(aProvId) { michael@0: // Look up the provisioning caller and make sure it's valid. michael@0: let provFlow = this.getProvisionFlow(aProvId); michael@0: michael@0: if (!provFlow.didBeginProvisioning) { michael@0: let errStr = "ERROR: genKeyPair called before beginProvisioning"; michael@0: log(errStr); michael@0: provFlow.callback(errStr); michael@0: return; michael@0: } michael@0: michael@0: // Ok generate a keypair michael@0: jwcrypto.generateKeyPair(jwcrypto.ALGORITHMS.DS160, function gkpCb(err, kp) { michael@0: log("in gkp callback"); michael@0: if (err) { michael@0: log("ERROR: genKeyPair:", err); michael@0: provFlow.callback(err); michael@0: return; michael@0: } michael@0: michael@0: provFlow.kp = kp; michael@0: michael@0: // Serialize the publicKey of the keypair and send it back to the michael@0: // sandbox. michael@0: log("genKeyPair: generated keypair for provisioning flow with id:", aProvId); michael@0: provFlow.caller.doGenKeyPairCallback(provFlow.kp.serializedPublicKey); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * When navigator.id.registerCertificate is called from provisioning iframe michael@0: * sandbox. michael@0: * michael@0: * Sets the certificate for the user for which a certificate was requested michael@0: * via a preceding call to beginProvisioning (and genKeypair). michael@0: * michael@0: * @param aProvId michael@0: * (integer) the identifier of the provisioning caller tied to that michael@0: * sandbox michael@0: * michael@0: * @param aCert michael@0: * (String) A JWT representing the signed certificate for the user michael@0: * being provisioned, provided by the IdP. michael@0: */ michael@0: registerCertificate: function registerCertificate(aProvId, aCert) { michael@0: log("registerCertificate:", aProvId, aCert); michael@0: michael@0: // look up provisioning caller, make sure it's valid. michael@0: let provFlow = this.getProvisionFlow(aProvId); michael@0: michael@0: if (!provFlow.caller) { michael@0: reportError("registerCertificate", "No provision flow or caller"); michael@0: return; michael@0: } michael@0: if (!provFlow.kp) { michael@0: let errStr = "Cannot register a certificate without a keypair"; michael@0: reportError("registerCertificate", errStr); michael@0: provFlow.callback(errStr); michael@0: return; michael@0: } michael@0: michael@0: // store the keypair and certificate just provided in IDStore. michael@0: this._store.addIdentity(provFlow.identity, provFlow.kp, aCert); michael@0: michael@0: // Great success! michael@0: provFlow.callback(null); michael@0: michael@0: // Clean up the flow. michael@0: this._cleanUpProvisionFlow(aProvId); michael@0: }, michael@0: michael@0: /** michael@0: * Begin the authentication process with an IdP michael@0: * michael@0: * @param aProvId michael@0: * (int) the identifier of the provisioning flow which failed michael@0: * michael@0: * @param aCallback michael@0: * (function) to invoke upon completion, with michael@0: * first-positional-param error. michael@0: */ michael@0: _doAuthentication: function _doAuthentication(aProvId, aIDPParams) { michael@0: log("_doAuthentication: provId:", aProvId, "idpParams:", aIDPParams); michael@0: // create an authentication caller and its identifier AuthId michael@0: // stash aIdentity, idpparams, and callback in it. michael@0: michael@0: // extract authentication URL from idpParams michael@0: let authPath = aIDPParams.idpParams.authentication; michael@0: let authURI = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(authPath); michael@0: michael@0: // beginAuthenticationFlow causes the "identity-auth" topic to be michael@0: // observed. Since it's sending a notification to the DOM, there's michael@0: // no callback. We wait for the DOM to trigger the next phase of michael@0: // provisioning. michael@0: this._beginAuthenticationFlow(aProvId, authURI); michael@0: michael@0: // either we bind the AuthID to the sandbox ourselves, or UX does that, michael@0: // in which case we need to tell UX the AuthId. michael@0: // Currently, the UX creates the UI and gets the AuthId from the window michael@0: // and sets is with setAuthenticationFlow michael@0: }, michael@0: michael@0: /** michael@0: * The authentication frame has called navigator.id.beginAuthentication michael@0: * michael@0: * IMPORTANT: the aCaller is *always* non-null, even if this is called from michael@0: * a regular content page. We have to make sure, on every DOM call, that michael@0: * aCaller is an expected authentication-flow identifier. If not, we throw michael@0: * an error or something. michael@0: * michael@0: * @param aCaller michael@0: * (object) the authentication caller michael@0: * michael@0: */ michael@0: beginAuthentication: function beginAuthentication(aCaller) { michael@0: log("beginAuthentication: caller id:", aCaller.id); michael@0: michael@0: // Begin the authentication flow after having concluded a provisioning michael@0: // flow. The aCaller that the DOM gives us will have the same ID as michael@0: // the provisioning flow we just concluded. (see setAuthenticationFlow) michael@0: let authFlow = this._authenticationFlows[aCaller.id]; michael@0: if (!authFlow) { michael@0: return aCaller.doError("beginAuthentication: no flow for caller id", aCaller.id); michael@0: } michael@0: michael@0: authFlow.caller = aCaller; michael@0: michael@0: let identity = this._provisionFlows[authFlow.provId].identity; michael@0: michael@0: // tell the UI to start the authentication process michael@0: log("beginAuthentication: authFlow:", aCaller.id, "identity:", identity); michael@0: return authFlow.caller.doBeginAuthenticationCallback(identity); michael@0: }, michael@0: michael@0: /** michael@0: * The auth frame has called navigator.id.completeAuthentication michael@0: * michael@0: * @param aAuthId michael@0: * (int) the identifier of the authentication caller tied to that sandbox michael@0: * michael@0: */ michael@0: completeAuthentication: function completeAuthentication(aAuthId) { michael@0: log("completeAuthentication:", aAuthId); michael@0: michael@0: // look up the AuthId caller, and get its callback. michael@0: let authFlow = this._authenticationFlows[aAuthId]; michael@0: if (!authFlow) { michael@0: reportError("completeAuthentication", "No auth flow with id", aAuthId); michael@0: return; michael@0: } michael@0: let provId = authFlow.provId; michael@0: michael@0: // delete caller michael@0: delete authFlow['caller']; michael@0: delete this._authenticationFlows[aAuthId]; michael@0: michael@0: let provFlow = this.getProvisionFlow(provId); michael@0: provFlow.didAuthentication = true; michael@0: let subject = { michael@0: rpId: provFlow.rpId, michael@0: identity: provFlow.identity, michael@0: }; michael@0: Services.obs.notifyObservers({ wrappedJSObject: subject }, "identity-auth-complete", aAuthId); michael@0: }, michael@0: michael@0: /** michael@0: * The auth frame has called navigator.id.cancelAuthentication michael@0: * michael@0: * @param aAuthId michael@0: * (int) the identifier of the authentication caller michael@0: * michael@0: */ michael@0: cancelAuthentication: function cancelAuthentication(aAuthId) { michael@0: log("cancelAuthentication:", aAuthId); michael@0: michael@0: // look up the AuthId caller, and get its callback. michael@0: let authFlow = this._authenticationFlows[aAuthId]; michael@0: if (!authFlow) { michael@0: reportError("cancelAuthentication", "No auth flow with id:", aAuthId); michael@0: return; michael@0: } michael@0: let provId = authFlow.provId; michael@0: michael@0: // delete caller michael@0: delete authFlow['caller']; michael@0: delete this._authenticationFlows[aAuthId]; michael@0: michael@0: let provFlow = this.getProvisionFlow(provId); michael@0: provFlow.didAuthentication = true; michael@0: Services.obs.notifyObservers(null, "identity-auth-complete", aAuthId); michael@0: michael@0: // invoke callback with ERROR. michael@0: let errStr = "Authentication canceled by IDP"; michael@0: log("ERROR: cancelAuthentication:", errStr); michael@0: provFlow.callback(errStr); michael@0: }, michael@0: michael@0: /** michael@0: * Called by the UI to set the ID and caller for the authentication flow after it gets its ID michael@0: */ michael@0: setAuthenticationFlow: function(aAuthId, aProvId) { michael@0: // this is the transition point between the two flows, michael@0: // provision and authenticate. We tell the auth flow which michael@0: // provisioning flow it is started from. michael@0: log("setAuthenticationFlow: authId:", aAuthId, "provId:", aProvId); michael@0: this._authenticationFlows[aAuthId] = { provId: aProvId }; michael@0: this._provisionFlows[aProvId].authId = aAuthId; michael@0: }, michael@0: michael@0: /** michael@0: * Load the provisioning URL in a hidden frame to start the provisioning michael@0: * process. michael@0: */ michael@0: _createProvisioningSandbox: function _createProvisioningSandbox(aURL, aCallback) { michael@0: log("_createProvisioningSandbox:", aURL); michael@0: michael@0: if (!this._sandboxConfigured) { michael@0: // Configure message manager listening on the hidden window michael@0: Cu.import("resource://gre/modules/DOMIdentity.jsm"); michael@0: DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, true); michael@0: this._sandboxConfigured = true; michael@0: } michael@0: michael@0: new Sandbox(aURL, aCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Load the authentication UI to start the authentication process. michael@0: */ michael@0: _beginAuthenticationFlow: function _beginAuthenticationFlow(aProvId, aURL) { michael@0: log("_beginAuthenticationFlow:", aProvId, aURL); michael@0: let propBag = {provId: aProvId}; michael@0: michael@0: Services.obs.notifyObservers({wrappedJSObject:propBag}, "identity-auth", aURL); michael@0: }, michael@0: michael@0: /** michael@0: * Clean up a provision flow and the authentication flow and sandbox michael@0: * that may be attached to it. michael@0: */ michael@0: _cleanUpProvisionFlow: function _cleanUpProvisionFlow(aProvId) { michael@0: log('_cleanUpProvisionFlow:', aProvId); michael@0: let prov = this._provisionFlows[aProvId]; michael@0: michael@0: // Clean up the sandbox, if there is one. michael@0: if (prov.provisioningSandbox) { michael@0: let sandbox = this._provisionFlows[aProvId]['provisioningSandbox']; michael@0: if (sandbox.free) { michael@0: log('_cleanUpProvisionFlow: freeing sandbox'); michael@0: sandbox.free(); michael@0: } michael@0: delete this._provisionFlows[aProvId]['provisioningSandbox']; michael@0: } michael@0: michael@0: // Clean up a related authentication flow, if there is one. michael@0: if (this._authenticationFlows[prov.authId]) { michael@0: delete this._authenticationFlows[prov.authId]; michael@0: } michael@0: michael@0: // Finally delete the provision flow michael@0: delete this._provisionFlows[aProvId]; michael@0: } michael@0: michael@0: }; michael@0: michael@0: this.IdentityProvider = new IdentityProviderService();