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