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