summaryrefslogtreecommitdiff
path: root/toolkit/identity/IdentityProvider.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/identity/IdentityProvider.jsm')
-rw-r--r--toolkit/identity/IdentityProvider.jsm496
1 files changed, 496 insertions, 0 deletions
diff --git a/toolkit/identity/IdentityProvider.jsm b/toolkit/identity/IdentityProvider.jsm
new file mode 100644
index 0000000000..11529bfbaa
--- /dev/null
+++ b/toolkit/identity/IdentityProvider.jsm
@@ -0,0 +1,496 @@
+/* -*- 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";
+
+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");
+
+this.EXPORTED_SYMBOLS = ["IdentityProvider"];
+const FALLBACK_PROVIDER = "browserid.org";
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["IDP"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError.apply(Logger, ["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]),
+ _sandboxConfigured: false,
+
+ 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);
+ }
+
+ return undefined;
+ },
+
+ shutdown: function RP_shutdown() {
+ this.reset();
+
+ if (this._sandboxConfigured) {
+ // Tear down message manager listening on the hidden window
+ Cu.import("resource://gre/modules/DOMIdentity.jsm");
+ DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, false);
+ this._sandboxConfigured = false;
+ }
+
+ 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);
+
+ if (!this._sandboxConfigured) {
+ // Configure message manager listening on the hidden window
+ Cu.import("resource://gre/modules/DOMIdentity.jsm");
+ DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, true);
+ this._sandboxConfigured = true;
+ }
+
+ 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];
+ }
+
+};
+
+this.IdentityProvider = new IdentityProviderService();