Bumping manifests a=b2g-bump
[gecko.git] / toolkit / identity / Identity.jsm
blob4690b1eefdd7eb2a3caa2bcdf5f8d83f90225e85
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 this.EXPORTED_SYMBOLS = ["IdentityService"];
11 const Cu = Components.utils;
12 const Ci = Components.interfaces;
13 const Cc = Components.classes;
14 const Cr = Components.results;
16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
17 Cu.import("resource://gre/modules/Services.jsm");
18 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
19 Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
20 Cu.import("resource://gre/modules/identity/RelyingParty.jsm");
21 Cu.import("resource://gre/modules/identity/IdentityProvider.jsm");
23 XPCOMUtils.defineLazyModuleGetter(this,
24                                   "jwcrypto",
25                                   "resource://gre/modules/identity/jwcrypto.jsm");
27 function log(...aMessageArgs) {
28   Logger.log.apply(Logger, ["core"].concat(aMessageArgs));
30 function reportError(...aMessageArgs) {
31   Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs));
34 function IDService() {
35   Services.obs.addObserver(this, "quit-application-granted", false);
36   Services.obs.addObserver(this, "identity-auth-complete", false);
38   this._store = IdentityStore;
39   this.RP = RelyingParty;
40   this.IDP = IdentityProvider;
43 IDService.prototype = {
44   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
46   observe: function observe(aSubject, aTopic, aData) {
47     switch (aTopic) {
48       case "quit-application-granted":
49         Services.obs.removeObserver(this, "quit-application-granted");
50         this.shutdown();
51         break;
52       case "identity-auth-complete":
53         if (!aSubject || !aSubject.wrappedJSObject)
54           break;
55         let subject = aSubject.wrappedJSObject;
56         log("Auth complete:", aSubject.wrappedJSObject);
57         // We have authenticated in order to provision an identity.
58         // So try again.
59         this.selectIdentity(subject.rpId, subject.identity);
60         break;
61     }
62   },
64   reset: function reset() {
65     // Explicitly call reset() on our RP and IDP classes.
66     // This is here to make testing easier.  When the
67     // quit-application-granted signal is emitted, reset() will be
68     // called here, on RP, on IDP, and on the store.  So you don't
69     // need to use this :)
70     this._store.reset();
71     this.RP.reset();
72     this.IDP.reset();
73   },
75   shutdown: function shutdown() {
76     log("shutdown");
77     Services.obs.removeObserver(this, "identity-auth-complete");
78     // try to prevent abort/crash during shutdown of mochitest-browser2...
79     try {
80       Services.obs.removeObserver(this, "quit-application-granted");
81     } catch(e) {}
82   },
84   /**
85    * Parse an email into username and domain if it is valid, else return null
86    */
87   parseEmail: function parseEmail(email) {
88     var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/);
89     if (match) {
90       return {
91         username: match[1],
92         domain: match[2]
93       };
94     }
95     return null;
96   },
98   /**
99    * The UX wants to add a new identity
100    * often followed by selectIdentity()
101    *
102    * @param aIdentity
103    *        (string) the email chosen for login
104    */
105   addIdentity: function addIdentity(aIdentity) {
106     if (this._store.fetchIdentity(aIdentity) === null) {
107       this._store.addIdentity(aIdentity, null, null);
108     }
109   },
111   /**
112    * The UX comes back and calls selectIdentity once the user has picked
113    * an identity.
114    *
115    * @param aRPId
116    *        (integer) the id of the doc object obtained in .watch() and
117    *                  passed to the UX component.
118    *
119    * @param aIdentity
120    *        (string) the email chosen for login
121    */
122   selectIdentity: function selectIdentity(aRPId, aIdentity) {
123     log("selectIdentity: RP id:", aRPId, "identity:", aIdentity);
125     // Get the RP that was stored when watch() was invoked.
126     let rp = this.RP._rpFlows[aRPId];
127     if (!rp) {
128       reportError("selectIdentity", "Invalid RP id: ", aRPId);
129       return;
130     }
132     // It's possible that we are in the process of provisioning an
133     // identity.
134     let provId = rp.provId;
136     let rpLoginOptions = {
137       loggedInUser: aIdentity,
138       origin: rp.origin
139     };
140     log("selectIdentity: provId:", provId, "origin:", rp.origin);
142     // Once we have a cert, and once the user is authenticated with the
143     // IdP, we can generate an assertion and deliver it to the doc.
144     let self = this;
145     this.RP._generateAssertion(rp.origin, aIdentity, function hadReadyAssertion(err, assertion) {
146       if (!err && assertion) {
147         self.RP._doLogin(rp, rpLoginOptions, assertion);
148         return;
150       }
151       // Need to provision an identity first.  Begin by discovering
152       // the user's IdP.
153       self._discoverIdentityProvider(aIdentity, function gotIDP(err, idpParams) {
154         if (err) {
155           rp.doError(err);
156           return;
157         }
159         // The idpParams tell us where to go to provision and authenticate
160         // the identity.
161         self.IDP._provisionIdentity(aIdentity, idpParams, provId, function gotID(err, aProvId) {
163           // Provision identity may have created a new provision flow
164           // for us.  To make it easier to relate provision flows with
165           // RP callers, we cross index the two here.
166           rp.provId = aProvId;
167           self.IDP._provisionFlows[aProvId].rpId = aRPId;
169           // At this point, we already have a cert.  If the user is also
170           // already authenticated with the IdP, then we can try again
171           // to generate an assertion and login.
172           if (err) {
173             // We are not authenticated.  If we have already tried to
174             // authenticate and failed, then this is a "hard fail" and
175             // we give up.  Otherwise we try to authenticate with the
176             // IdP.
178             if (self.IDP._provisionFlows[aProvId].didAuthentication) {
179               self.IDP._cleanUpProvisionFlow(aProvId);
180               self.RP._cleanUpProvisionFlow(aRPId, aProvId);
181               log("ERROR: selectIdentity: authentication hard fail");
182               rp.doError("Authentication fail.");
183               return;
184             }
185             // Try to authenticate with the IdP.  Note that we do
186             // not clean up the provision flow here.  We will continue
187             // to use it.
188             self.IDP._doAuthentication(aProvId, idpParams);
189             return;
190           }
192           // Provisioning flows end when a certificate has been registered.
193           // Thus IdentityProvider's registerCertificate() cleans up the
194           // current provisioning flow.  We only do this here on error.
195           self.RP._generateAssertion(rp.origin, aIdentity, function gotAssertion(err, assertion) {
196             if (err) {
197               rp.doError(err);
198               return;
199             }
200             self.RP._doLogin(rp, rpLoginOptions, assertion);
201             self.RP._cleanUpProvisionFlow(aRPId, aProvId);
202             return;
203           });
204         });
205       });
206     });
207   },
209   // methods for chrome and add-ons
211   /**
212    * Discover the IdP for an identity
213    *
214    * @param aIdentity
215    *        (string) the email we're logging in with
216    *
217    * @param aCallback
218    *        (function) callback to invoke on completion
219    *                   with first-positional parameter the error.
220    */
221   _discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) {
222     // XXX bug 767610 - validate email address call
223     // When that is available, we can remove this custom parser
224     var parsedEmail = this.parseEmail(aIdentity);
225     if (parsedEmail === null) {
226       return aCallback("Could not parse email: " + aIdentity);
227     }
228     log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain);
230     this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) {
231       // idpParams includes the pk, authorization url, and
232       // provisioning url.
234       // XXX bug 769861 follow any authority delegations
235       // if no well-known at any point in the delegation
236       // fall back to browserid.org as IdP
237       return aCallback(err, idpParams);
238     });
239   },
241   /**
242    * Fetch the well-known file from the domain.
243    *
244    * @param aDomain
245    *
246    * @param aScheme
247    *        (string) (optional) Protocol to use.  Default is https.
248    *                 This is necessary because we are unable to test
249    *                 https.
250    *
251    * @param aCallback
252    *
253    */
254   _fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme='https') {
255     // XXX bug 769854 make tests https and remove aScheme option
256     let url = aScheme + '://' + aDomain + "/.well-known/browserid";
257     log("_fetchWellKnownFile:", url);
259     // this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window
260     let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
261                 .createInstance(Ci.nsIXMLHttpRequest);
263     // XXX bug 769865 gracefully handle being off-line
264     // XXX bug 769866 decide on how to handle redirects
265     req.open("GET", url, true);
266     req.responseType = "json";
267     req.mozBackgroundRequest = true;
268     req.onload = function _fetchWellKnownFile_onload() {
269       if (req.status < 200 || req.status >= 400) {
270         log("_fetchWellKnownFile", url, ": server returned status:", req.status);
271         return aCallback("Error");
272       }
273       try {
274         let idpParams = req.response;
276         // Verify that the IdP returned a valid configuration
277         if (! (idpParams.provisioning &&
278             idpParams.authentication &&
279             idpParams['public-key'])) {
280           let errStr= "Invalid well-known file from: " + aDomain;
281           log("_fetchWellKnownFile:", errStr);
282           return aCallback(errStr);
283         }
285         let callbackObj = {
286           domain: aDomain,
287           idpParams: idpParams,
288         };
289         log("_fetchWellKnownFile result: ", callbackObj);
290         // Yay.  Valid IdP configuration for the domain.
291         return aCallback(null, callbackObj);
293       } catch (err) {
294         reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err);
295         return aCallback(err.toString());
296       }
297     };
298     req.onerror = function _fetchWellKnownFile_onerror() {
299       log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText);
300       log("ERROR: _fetchWellKnownFile:", err);
301       return aCallback("Error");
302     };
303     req.send(null);
304   },
308 this.IdentityService = new IDService();