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