summaryrefslogtreecommitdiffstats
path: root/toolkit/identity/RelyingParty.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/identity/RelyingParty.jsm')
-rw-r--r--toolkit/identity/RelyingParty.jsm367
1 files changed, 367 insertions, 0 deletions
diff --git a/toolkit/identity/RelyingParty.jsm b/toolkit/identity/RelyingParty.jsm
new file mode 100644
index 000000000..5996383ca
--- /dev/null
+++ b/toolkit/identity/RelyingParty.jsm
@@ -0,0 +1,367 @@
+/* -*- 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/IdentityStore.jsm");
+
+this.EXPORTED_SYMBOLS = ["RelyingParty"];
+
+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, ["RP"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError.apply(Logger, ["RP"].concat(aMessageArgs));
+}
+
+function IdentityRelyingParty() {
+ // The store is a singleton shared among Identity, RelyingParty, and
+ // IdentityProvider. The Identity module takes care of resetting
+ // state in the _store on shutdown.
+ this._store = IdentityStore;
+
+ this.reset();
+}
+
+IdentityRelyingParty.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 RP_reset() {
+ // Forget all documents that call in. (These are sometimes
+ // referred to as callers.)
+ this._rpFlows = {};
+ },
+
+ shutdown: function RP_shutdown() {
+ this.reset();
+ Services.obs.removeObserver(this, "quit-application-granted");
+ },
+
+ /**
+ * 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) {
+ this._rpFlows[aRpCaller.id] = aRpCaller;
+ let origin = aRpCaller.origin;
+ let state = this._store.getLoginState(origin) || { isLoggedIn: false, email: null };
+
+ log("watch: rpId:", aRpCaller.id,
+ "origin:", origin,
+ "loggedInUser:", aRpCaller.loggedInUser,
+ "loggedIn:", state.isLoggedIn,
+ "email:", state.email);
+
+ // If the user is already logged in, then there are three cases
+ // to deal with:
+ //
+ // 1. the email is valid and unchanged: 'ready'
+ // 2. the email is null: 'login'; 'ready'
+ // 3. the email has changed: 'login'; 'ready'
+ if (state.isLoggedIn) {
+ if (state.email && aRpCaller.loggedInUser === state.email) {
+ this._notifyLoginStateChanged(aRpCaller.id, state.email);
+ return aRpCaller.doReady();
+
+ } else if (aRpCaller.loggedInUser === null) {
+ // Generate assertion for existing login
+ let options = {loggedInUser: state.email, origin: origin};
+ return this._doLogin(aRpCaller, options);
+ }
+ // A loggedInUser different from state.email has been specified.
+ // Change login identity.
+
+ let options = {loggedInUser: state.email, origin: origin};
+ return this._doLogin(aRpCaller, options);
+
+ // If the user is not logged in, there are two cases:
+ //
+ // 1. a logged in email was provided: 'ready'; 'logout'
+ // 2. not logged in, no email given: 'ready';
+
+ }
+ if (aRpCaller.loggedInUser) {
+ return this._doLogout(aRpCaller, {origin: origin});
+ }
+ return aRpCaller.doReady();
+ },
+
+ /**
+ * A utility for watch() to set state and notify the dom
+ * on login
+ *
+ * Note that this calls _getAssertion
+ */
+ _doLogin: function _doLogin(aRpCaller, aOptions, aAssertion) {
+ log("_doLogin: rpId:", aRpCaller.id, "origin:", aOptions.origin);
+
+ let loginWithAssertion = function loginWithAssertion(assertion) {
+ this._store.setLoginState(aOptions.origin, true, aOptions.loggedInUser);
+ this._notifyLoginStateChanged(aRpCaller.id, aOptions.loggedInUser);
+ aRpCaller.doLogin(assertion);
+ aRpCaller.doReady();
+ }.bind(this);
+
+ if (aAssertion) {
+ loginWithAssertion(aAssertion);
+ } else {
+ this._getAssertion(aOptions, function gotAssertion(err, assertion) {
+ if (err) {
+ reportError("_doLogin:", "Failed to get assertion on login attempt:", err);
+ this._doLogout(aRpCaller);
+ } else {
+ loginWithAssertion(assertion);
+ }
+ }.bind(this));
+ }
+ },
+
+ /**
+ * A utility for watch() to set state and notify the dom
+ * on logout.
+ */
+ _doLogout: function _doLogout(aRpCaller, aOptions) {
+ log("_doLogout: rpId:", aRpCaller.id, "origin:", aOptions.origin);
+
+ let state = this._store.getLoginState(aOptions.origin) || {};
+
+ state.isLoggedIn = false;
+ this._notifyLoginStateChanged(aRpCaller.id, null);
+
+ aRpCaller.doLogout();
+ aRpCaller.doReady();
+ },
+
+ /**
+ * For use with login or logout, emit 'identity-login-state-changed'
+ *
+ * The notification will send the rp caller id in the properties,
+ * and the email of the user in the message.
+ *
+ * @param aRpCallerId
+ * (integer) The id of the RP caller
+ *
+ * @param aIdentity
+ * (string) The email of the user whose login state has changed
+ */
+ _notifyLoginStateChanged: function _notifyLoginStateChanged(aRpCallerId, aIdentity) {
+ log("_notifyLoginStateChanged: rpId:", aRpCallerId, "identity:", aIdentity);
+
+ let options = {rpId: aRpCallerId};
+ Services.obs.notifyObservers({wrappedJSObject: options},
+ "identity-login-state-changed",
+ aIdentity);
+ },
+
+ /**
+ * 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) {
+ log("request: rpId:", aRPId);
+ let rp = this._rpFlows[aRPId];
+
+ // Notify UX to display identity picker.
+ // Pass the doc id to UX so it can pass it back to us later.
+ let options = {rpId: aRPId, origin: rp.origin};
+ objectCopy(aOptions, options);
+
+ // Append URLs after resolving
+ let baseURI = Services.io.newURI(rp.origin, null, null);
+ for (let optionName of ["privacyPolicy", "termsOfService"]) {
+ if (aOptions[optionName]) {
+ options[optionName] = baseURI.resolve(aOptions[optionName]);
+ }
+ }
+
+ Services.obs.notifyObservers({wrappedJSObject: options}, "identity-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) {
+ log("logout: RP caller id:", aRpCallerId);
+ let rp = this._rpFlows[aRpCallerId];
+ if (rp && rp.origin) {
+ let origin = rp.origin;
+ log("logout: origin:", origin);
+ this._doLogout(rp, {origin: origin});
+ } else {
+ log("logout: no RP found with id:", aRpCallerId);
+ }
+ // We don't delete this._rpFlows[aRpCallerId], because
+ // the user might log back in again.
+ },
+
+ getDefaultEmailForOrigin: function getDefaultEmailForOrigin(aOrigin) {
+ let identities = this.getIdentitiesForSite(aOrigin);
+ let result = identities.lastUsed || null;
+ log("getDefaultEmailForOrigin:", aOrigin, "->", result);
+ return result;
+ },
+
+ /**
+ * Return the list of identities a user may want to use to login to aOrigin.
+ */
+ getIdentitiesForSite: function getIdentitiesForSite(aOrigin) {
+ let rv = { result: [] };
+ for (let id in this._store.getIdentities()) {
+ rv.result.push(id);
+ }
+ let loginState = this._store.getLoginState(aOrigin);
+ if (loginState && loginState.email)
+ rv.lastUsed = loginState.email;
+ return rv;
+ },
+
+ /**
+ * Obtain a BrowserID assertion with the specified characteristics.
+ *
+ * @param aCallback
+ * (Function) Callback to be called with (err, assertion) where 'err'
+ * can be an Error or NULL, and 'assertion' can be NULL or a valid
+ * BrowserID assertion. If no callback is provided, an exception is
+ * thrown.
+ *
+ * @param aOptions
+ * (Object) An object that may contain the following properties:
+ *
+ * "audience" : The audience for which the assertion is to be
+ * issued. If this property is not set an exception
+ * will be thrown.
+ *
+ * Any properties not listed above will be ignored.
+ */
+ _getAssertion: function _getAssertion(aOptions, aCallback) {
+ let audience = aOptions.origin;
+ let email = aOptions.loggedInUser || this.getDefaultEmailForOrigin(audience);
+ log("_getAssertion: audience:", audience, "email:", email);
+ if (!audience) {
+ throw "audience required for _getAssertion";
+ }
+
+ // We might not have any identity info for this email
+ if (!this._store.fetchIdentity(email)) {
+ this._store.addIdentity(email, null, null);
+ }
+
+ let cert = this._store.fetchIdentity(email)['cert'];
+ if (cert) {
+ this._generateAssertion(audience, email, function generatedAssertion(err, assertion) {
+ if (err) {
+ log("ERROR: _getAssertion:", err);
+ }
+ log("_getAssertion: generated assertion:", assertion);
+ return aCallback(err, assertion);
+ });
+ }
+ },
+
+ /**
+ * Generate an assertion, including provisioning via IdP if necessary,
+ * but no user interaction, so if provisioning fails, aCallback is invoked
+ * with an error.
+ *
+ * @param aAudience
+ * (string) web origin
+ *
+ * @param aIdentity
+ * (string) the email we're logging in with
+ *
+ * @param aCallback
+ * (function) callback to invoke on completion
+ * with first-positional parameter the error.
+ */
+ _generateAssertion: function _generateAssertion(aAudience, aIdentity, aCallback) {
+ log("_generateAssertion: audience:", aAudience, "identity:", aIdentity);
+
+ let id = this._store.fetchIdentity(aIdentity);
+ if (! (id && id.cert)) {
+ let errStr = "Cannot generate an assertion without a certificate";
+ log("ERROR: _generateAssertion:", errStr);
+ aCallback(errStr);
+ return;
+ }
+
+ let kp = id.keyPair;
+
+ if (!kp) {
+ let errStr = "Cannot generate an assertion without a keypair";
+ log("ERROR: _generateAssertion:", errStr);
+ aCallback(errStr);
+ return;
+ }
+
+ jwcrypto.generateAssertion(id.cert, kp, aAudience, aCallback);
+ },
+
+ /**
+ * Clean up references to the provisioning flow for the specified RP.
+ */
+ _cleanUpProvisionFlow: function RP_cleanUpProvisionFlow(aRPId, aProvId) {
+ let rp = this._rpFlows[aRPId];
+ if (rp) {
+ delete rp['provId'];
+ } else {
+ log("Error: Couldn't delete provision flow ", aProvId, " for RP ", aRPId);
+ }
+ },
+
+};
+
+this.RelyingParty = new IdentityRelyingParty();