Bumping manifests a=b2g-bump
[gecko.git] / toolkit / identity / RelyingParty.jsm
blob3e9f25f8ee4cd21cab66e5831a8d8e8d78598d57
1 /* -*- js-indent-level: 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));
31 function reportError(...aMessageArgs) {
32   Logger.reportError.apply(Logger, ["RP"].concat(aMessageArgs));
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();
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   },
372 this.RelyingParty = new IdentityRelyingParty();