toolkit/identity/RelyingParty.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
     2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     5  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 "use strict";
     9 const Cu = Components.utils;
    10 const Ci = Components.interfaces;
    11 const Cc = Components.classes;
    12 const Cr = Components.results;
    14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    15 Cu.import("resource://gre/modules/Services.jsm");
    16 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
    17 Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
    19 this.EXPORTED_SYMBOLS = ["RelyingParty"];
    21 XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
    22                                   "resource://gre/modules/identity/IdentityUtils.jsm");
    24 XPCOMUtils.defineLazyModuleGetter(this,
    25                                   "jwcrypto",
    26                                   "resource://gre/modules/identity/jwcrypto.jsm");
    28 function log(...aMessageArgs) {
    29   Logger.log.apply(Logger, ["RP"].concat(aMessageArgs));
    30 }
    31 function reportError(...aMessageArgs) {
    32   Logger.reportError.apply(Logger, ["RP"].concat(aMessageArgs));
    33 }
    35 function IdentityRelyingParty() {
    36   // The store is a singleton shared among Identity, RelyingParty, and
    37   // IdentityProvider.  The Identity module takes care of resetting
    38   // state in the _store on shutdown.
    39   this._store = IdentityStore;
    41   this.reset();
    42 }
    44 IdentityRelyingParty.prototype = {
    45   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
    47   observe: function observe(aSubject, aTopic, aData) {
    48     switch (aTopic) {
    49       case "quit-application-granted":
    50         Services.obs.removeObserver(this, "quit-application-granted");
    51         this.shutdown();
    52         break;
    54     }
    55   },
    57   reset: function RP_reset() {
    58     // Forget all documents that call in.  (These are sometimes
    59     // referred to as callers.)
    60     this._rpFlows = {};
    61   },
    63   shutdown: function RP_shutdown() {
    64     this.reset();
    65     Services.obs.removeObserver(this, "quit-application-granted");
    66   },
    68   /**
    69    * Register a listener for a given windowID as a result of a call to
    70    * navigator.id.watch().
    71    *
    72    * @param aCaller
    73    *        (Object)  an object that represents the caller document, and
    74    *                  is expected to have properties:
    75    *                  - id (unique, e.g. uuid)
    76    *                  - loggedInUser (string or null)
    77    *                  - origin (string)
    78    *
    79    *                  and a bunch of callbacks
    80    *                  - doReady()
    81    *                  - doLogin()
    82    *                  - doLogout()
    83    *                  - doError()
    84    *                  - doCancel()
    85    *
    86    */
    87   watch: function watch(aRpCaller) {
    88     this._rpFlows[aRpCaller.id] = aRpCaller;
    89     let origin = aRpCaller.origin;
    90     let state = this._store.getLoginState(origin) || { isLoggedIn: false, email: null };
    92     log("watch: rpId:", aRpCaller.id,
    93         "origin:", origin,
    94         "loggedInUser:", aRpCaller.loggedInUser,
    95         "loggedIn:", state.isLoggedIn,
    96         "email:", state.email);
    98     // If the user is already logged in, then there are three cases
    99     // to deal with:
   100     //
   101     //   1. the email is valid and unchanged:  'ready'
   102     //   2. the email is null:                 'login'; 'ready'
   103     //   3. the email has changed:             'login'; 'ready'
   104     if (state.isLoggedIn) {
   105       if (state.email && aRpCaller.loggedInUser === state.email) {
   106         this._notifyLoginStateChanged(aRpCaller.id, state.email);
   107         return aRpCaller.doReady();
   109       } else if (aRpCaller.loggedInUser === null) {
   110         // Generate assertion for existing login
   111         let options = {loggedInUser: state.email, origin: origin};
   112         return this._doLogin(aRpCaller, options);
   114       } else {
   115         // A loggedInUser different from state.email has been specified.
   116         // Change login identity.
   118         let options = {loggedInUser: state.email, origin: origin};
   119         return this._doLogin(aRpCaller, options);
   120       }
   122     // If the user is not logged in, there are two cases:
   123     //
   124     //   1. a logged in email was provided: 'ready'; 'logout'
   125     //   2. not logged in, no email given:  'ready';
   127     } else {
   128       if (aRpCaller.loggedInUser) {
   129         return this._doLogout(aRpCaller, {origin: origin});
   131       } else {
   132         return aRpCaller.doReady();
   133       }
   134     }
   135   },
   137   /**
   138    * A utility for watch() to set state and notify the dom
   139    * on login
   140    *
   141    * Note that this calls _getAssertion
   142    */
   143   _doLogin: function _doLogin(aRpCaller, aOptions, aAssertion) {
   144     log("_doLogin: rpId:", aRpCaller.id, "origin:", aOptions.origin);
   146     let loginWithAssertion = function loginWithAssertion(assertion) {
   147       this._store.setLoginState(aOptions.origin, true, aOptions.loggedInUser);
   148       this._notifyLoginStateChanged(aRpCaller.id, aOptions.loggedInUser);
   149       aRpCaller.doLogin(assertion);
   150       aRpCaller.doReady();
   151     }.bind(this);
   153     if (aAssertion) {
   154       loginWithAssertion(aAssertion);
   155     } else {
   156       this._getAssertion(aOptions, function gotAssertion(err, assertion) {
   157         if (err) {
   158           reportError("_doLogin:", "Failed to get assertion on login attempt:", err);
   159           this._doLogout(aRpCaller);
   160         } else {
   161           loginWithAssertion(assertion);
   162         }
   163       }.bind(this));
   164     }
   165   },
   167   /**
   168    * A utility for watch() to set state and notify the dom
   169    * on logout.
   170    */
   171   _doLogout: function _doLogout(aRpCaller, aOptions) {
   172     log("_doLogout: rpId:", aRpCaller.id, "origin:", aOptions.origin);
   174     let state = this._store.getLoginState(aOptions.origin) || {};
   176     state.isLoggedIn = false;
   177     this._notifyLoginStateChanged(aRpCaller.id, null);
   179     aRpCaller.doLogout();
   180     aRpCaller.doReady();
   181   },
   183   /**
   184    * For use with login or logout, emit 'identity-login-state-changed'
   185    *
   186    * The notification will send the rp caller id in the properties,
   187    * and the email of the user in the message.
   188    *
   189    * @param aRpCallerId
   190    *        (integer) The id of the RP caller
   191    *
   192    * @param aIdentity
   193    *        (string) The email of the user whose login state has changed
   194    */
   195   _notifyLoginStateChanged: function _notifyLoginStateChanged(aRpCallerId, aIdentity) {
   196     log("_notifyLoginStateChanged: rpId:", aRpCallerId, "identity:", aIdentity);
   198     let options = {rpId: aRpCallerId};
   199     Services.obs.notifyObservers({wrappedJSObject: options},
   200                                  "identity-login-state-changed",
   201                                  aIdentity);
   202   },
   204   /**
   205    * Initiate a login with user interaction as a result of a call to
   206    * navigator.id.request().
   207    *
   208    * @param aRPId
   209    *        (integer)  the id of the doc object obtained in .watch()
   210    *
   211    * @param aOptions
   212    *        (Object)  options including privacyPolicy, termsOfService
   213    */
   214   request: function request(aRPId, aOptions) {
   215     log("request: rpId:", aRPId);
   216     let rp = this._rpFlows[aRPId];
   218     // Notify UX to display identity picker.
   219     // Pass the doc id to UX so it can pass it back to us later.
   220     let options = {rpId: aRPId, origin: rp.origin};
   221     objectCopy(aOptions, options);
   223     // Append URLs after resolving
   224     let baseURI = Services.io.newURI(rp.origin, null, null);
   225     for (let optionName of ["privacyPolicy", "termsOfService"]) {
   226       if (aOptions[optionName]) {
   227         options[optionName] = baseURI.resolve(aOptions[optionName]);
   228       }
   229     }
   231     Services.obs.notifyObservers({wrappedJSObject: options}, "identity-request", null);
   232   },
   234   /**
   235    * Invoked when a user wishes to logout of a site (for instance, when clicking
   236    * on an in-content logout button).
   237    *
   238    * @param aRpCallerId
   239    *        (integer)  the id of the doc object obtained in .watch()
   240    *
   241    */
   242   logout: function logout(aRpCallerId) {
   243     log("logout: RP caller id:", aRpCallerId);
   244     let rp = this._rpFlows[aRpCallerId];
   245     if (rp && rp.origin) {
   246       let origin = rp.origin;
   247       log("logout: origin:", origin);
   248       this._doLogout(rp, {origin: origin});
   249     } else {
   250       log("logout: no RP found with id:", aRpCallerId);
   251     }
   252     // We don't delete this._rpFlows[aRpCallerId], because
   253     // the user might log back in again.
   254   },
   256   getDefaultEmailForOrigin: function getDefaultEmailForOrigin(aOrigin) {
   257     let identities = this.getIdentitiesForSite(aOrigin);
   258     let result = identities.lastUsed || null;
   259     log("getDefaultEmailForOrigin:", aOrigin, "->", result);
   260     return result;
   261   },
   263   /**
   264    * Return the list of identities a user may want to use to login to aOrigin.
   265    */
   266   getIdentitiesForSite: function getIdentitiesForSite(aOrigin) {
   267     let rv = { result: [] };
   268     for (let id in this._store.getIdentities()) {
   269       rv.result.push(id);
   270     }
   271     let loginState = this._store.getLoginState(aOrigin);
   272     if (loginState && loginState.email)
   273       rv.lastUsed = loginState.email;
   274     return rv;
   275   },
   277   /**
   278    * Obtain a BrowserID assertion with the specified characteristics.
   279    *
   280    * @param aCallback
   281    *        (Function) Callback to be called with (err, assertion) where 'err'
   282    *        can be an Error or NULL, and 'assertion' can be NULL or a valid
   283    *        BrowserID assertion. If no callback is provided, an exception is
   284    *        thrown.
   285    *
   286    * @param aOptions
   287    *        (Object) An object that may contain the following properties:
   288    *
   289    *          "audience"      : The audience for which the assertion is to be
   290    *                            issued. If this property is not set an exception
   291    *                            will be thrown.
   292    *
   293    *        Any properties not listed above will be ignored.
   294    */
   295   _getAssertion: function _getAssertion(aOptions, aCallback) {
   296     let audience = aOptions.origin;
   297     let email = aOptions.loggedInUser || this.getDefaultEmailForOrigin(audience);
   298     log("_getAssertion: audience:", audience, "email:", email);
   299     if (!audience) {
   300       throw "audience required for _getAssertion";
   301     }
   303     // We might not have any identity info for this email
   304     if (!this._store.fetchIdentity(email)) {
   305       this._store.addIdentity(email, null, null);
   306     }
   308     let cert = this._store.fetchIdentity(email)['cert'];
   309     if (cert) {
   310       this._generateAssertion(audience, email, function generatedAssertion(err, assertion) {
   311         if (err) {
   312           log("ERROR: _getAssertion:", err);
   313         }
   314         log("_getAssertion: generated assertion:", assertion);
   315         return aCallback(err, assertion);
   316       });
   317     }
   318   },
   320   /**
   321    * Generate an assertion, including provisioning via IdP if necessary,
   322    * but no user interaction, so if provisioning fails, aCallback is invoked
   323    * with an error.
   324    *
   325    * @param aAudience
   326    *        (string) web origin
   327    *
   328    * @param aIdentity
   329    *        (string) the email we're logging in with
   330    *
   331    * @param aCallback
   332    *        (function) callback to invoke on completion
   333    *                   with first-positional parameter the error.
   334    */
   335   _generateAssertion: function _generateAssertion(aAudience, aIdentity, aCallback) {
   336     log("_generateAssertion: audience:", aAudience, "identity:", aIdentity);
   338     let id = this._store.fetchIdentity(aIdentity);
   339     if (! (id && id.cert)) {
   340       let errStr = "Cannot generate an assertion without a certificate";
   341       log("ERROR: _generateAssertion:", errStr);
   342       aCallback(errStr);
   343       return;
   344     }
   346     let kp = id.keyPair;
   348     if (!kp) {
   349       let errStr = "Cannot generate an assertion without a keypair";
   350       log("ERROR: _generateAssertion:", errStr);
   351       aCallback(errStr);
   352       return;
   353     }
   355     jwcrypto.generateAssertion(id.cert, kp, aAudience, aCallback);
   356   },
   358   /**
   359    * Clean up references to the provisioning flow for the specified RP.
   360    */
   361   _cleanUpProvisionFlow: function RP_cleanUpProvisionFlow(aRPId, aProvId) {
   362     let rp = this._rpFlows[aRPId];
   363     if (rp) {
   364       delete rp['provId'];
   365     } else {
   366       log("Error: Couldn't delete provision flow ", aProvId, " for RP ", aRPId);
   367     }
   368   },
   370 };
   372 this.RelyingParty = new IdentityRelyingParty();

mercurial