|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
8 |
|
9 const PREF_DEBUG = "toolkit.identity.debug"; |
|
10 const PREF_ENABLED = "dom.identity.enabled"; |
|
11 |
|
12 // Bug 822450: Workaround for Bug 821740. When testing with marionette, |
|
13 // relax navigator.id.request's requirement that it be handling native |
|
14 // events. Synthetic marionette events are ok. |
|
15 const PREF_SYNTHETIC_EVENTS_OK = "dom.identity.syntheticEventsOk"; |
|
16 |
|
17 // Maximum length of a string that will go through IPC |
|
18 const MAX_STRING_LENGTH = 2048; |
|
19 // Maximum number of times navigator.id.request can be called for a document |
|
20 const MAX_RP_CALLS = 100; |
|
21 |
|
22 Cu.import("resource://gre/modules/Services.jsm"); |
|
23 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
24 |
|
25 XPCOMUtils.defineLazyModuleGetter(this, "checkDeprecated", |
|
26 "resource://gre/modules/identity/IdentityUtils.jsm"); |
|
27 XPCOMUtils.defineLazyModuleGetter(this, "checkRenamed", |
|
28 "resource://gre/modules/identity/IdentityUtils.jsm"); |
|
29 XPCOMUtils.defineLazyModuleGetter(this, "objectCopy", |
|
30 "resource://gre/modules/identity/IdentityUtils.jsm"); |
|
31 |
|
32 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", |
|
33 "@mozilla.org/uuid-generator;1", |
|
34 "nsIUUIDGenerator"); |
|
35 |
|
36 // This is the child process corresponding to nsIDOMIdentity |
|
37 XPCOMUtils.defineLazyServiceGetter(this, "cpmm", |
|
38 "@mozilla.org/childprocessmessagemanager;1", |
|
39 "nsIMessageSender"); |
|
40 |
|
41 |
|
42 const ERRORS = { |
|
43 "ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS": |
|
44 "Only privileged and certified apps may use Firefox Accounts", |
|
45 "ERROR_INVALID_ASSERTION_AUDIENCE": |
|
46 "Assertion audience may not differ from origin", |
|
47 "ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT": |
|
48 "The request() method may only be invoked when handling user input", |
|
49 }; |
|
50 |
|
51 function nsDOMIdentity(aIdentityInternal) { |
|
52 this._identityInternal = aIdentityInternal; |
|
53 } |
|
54 nsDOMIdentity.prototype = { |
|
55 __exposedProps__: { |
|
56 // Relying Party (RP) |
|
57 watch: 'r', |
|
58 request: 'r', |
|
59 logout: 'r', |
|
60 get: 'r', |
|
61 getVerifiedEmail: 'r', |
|
62 |
|
63 // Provisioning |
|
64 beginProvisioning: 'r', |
|
65 genKeyPair: 'r', |
|
66 registerCertificate: 'r', |
|
67 raiseProvisioningFailure: 'r', |
|
68 |
|
69 // Authentication |
|
70 beginAuthentication: 'r', |
|
71 completeAuthentication: 'r', |
|
72 raiseAuthenticationFailure: 'r' |
|
73 }, |
|
74 |
|
75 // require native events unless syntheticEventsOk is set |
|
76 get nativeEventsRequired() { |
|
77 if (Services.prefs.prefHasUserValue(PREF_SYNTHETIC_EVENTS_OK) && |
|
78 (Services.prefs.getPrefType(PREF_SYNTHETIC_EVENTS_OK) === |
|
79 Ci.nsIPrefBranch.PREF_BOOL)) { |
|
80 return !Services.prefs.getBoolPref(PREF_SYNTHETIC_EVENTS_OK); |
|
81 } |
|
82 return true; |
|
83 }, |
|
84 |
|
85 reportErrors: function(message) { |
|
86 let onerror = function() {}; |
|
87 if (this._rpWatcher && this._rpWatcher.onerror) { |
|
88 onerror = this._rpWatcher.onerror; |
|
89 } |
|
90 |
|
91 message.errors.forEach((error) => { |
|
92 // Report an error string to content |
|
93 Cu.reportError(ERRORS[error]); |
|
94 |
|
95 // Report error code to RP callback, if available |
|
96 onerror(error); |
|
97 }); |
|
98 }, |
|
99 |
|
100 /** |
|
101 * Relying Party (RP) APIs |
|
102 */ |
|
103 |
|
104 watch: function nsDOMIdentity_watch(aOptions = {}) { |
|
105 if (this._rpWatcher) { |
|
106 // For the initial release of Firefox Accounts, we support callers who |
|
107 // invoke watch() either for Firefox Accounts, or Persona, but not both. |
|
108 // In the future, we may wish to support the dual invocation (say, for |
|
109 // packaged apps so they can sign users in who reject the app's request |
|
110 // to sign in with their Firefox Accounts identity). |
|
111 throw new Error("navigator.id.watch was already called"); |
|
112 } |
|
113 |
|
114 assertCorrectCallbacks(aOptions); |
|
115 |
|
116 let message = this.DOMIdentityMessage(aOptions); |
|
117 |
|
118 // loggedInUser vs loggedInEmail |
|
119 // https://developer.mozilla.org/en-US/docs/DOM/navigator.id.watch |
|
120 // This parameter, loggedInUser, was renamed from loggedInEmail in early |
|
121 // September, 2012. Both names will continue to work for the time being, |
|
122 // but code should be changed to use loggedInUser instead. |
|
123 checkRenamed(aOptions, "loggedInEmail", "loggedInUser"); |
|
124 message["loggedInUser"] = aOptions["loggedInUser"]; |
|
125 |
|
126 let emailType = typeof(aOptions["loggedInUser"]); |
|
127 if (aOptions["loggedInUser"] && aOptions["loggedInUser"] !== "undefined") { |
|
128 if (emailType !== "string") { |
|
129 throw new Error("loggedInUser must be a String or null"); |
|
130 } |
|
131 |
|
132 // TODO: Bug 767610 - check email format. |
|
133 // See HTMLInputElement::IsValidEmailAddress |
|
134 if (aOptions["loggedInUser"].indexOf("@") == -1 |
|
135 || aOptions["loggedInUser"].length > MAX_STRING_LENGTH) { |
|
136 throw new Error("loggedInUser is not valid"); |
|
137 } |
|
138 // Set loggedInUser in this block that "undefined" doesn't get through. |
|
139 message.loggedInUser = aOptions.loggedInUser; |
|
140 } |
|
141 this._log("loggedInUser: " + message.loggedInUser); |
|
142 |
|
143 this._rpWatcher = aOptions; |
|
144 this._rpWatcher.audience = message.audience; |
|
145 |
|
146 if (message.errors.length) { |
|
147 this.reportErrors(message); |
|
148 // We don't delete the rpWatcher object, because we don't want the |
|
149 // broken client to be able to call watch() any more. It's broken. |
|
150 return; |
|
151 } |
|
152 this._identityInternal._mm.sendAsyncMessage("Identity:RP:Watch", message); |
|
153 }, |
|
154 |
|
155 request: function nsDOMIdentity_request(aOptions = {}) { |
|
156 this._log("request: " + JSON.stringify(aOptions)); |
|
157 |
|
158 // Has the caller called watch() before this? |
|
159 if (!this._rpWatcher) { |
|
160 throw new Error("navigator.id.request called before navigator.id.watch"); |
|
161 } |
|
162 if (this._rpCalls > MAX_RP_CALLS) { |
|
163 throw new Error("navigator.id.request called too many times"); |
|
164 } |
|
165 |
|
166 let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor) |
|
167 .getInterface(Ci.nsIDOMWindowUtils); |
|
168 |
|
169 let message = this.DOMIdentityMessage(aOptions); |
|
170 |
|
171 // We permit calling of request() outside of a user input handler only when |
|
172 // a certified or privileged app is calling, or when we are handling the |
|
173 // (deprecated) get() or getVerifiedEmail() calls, which make use of an RP |
|
174 // context marked as _internal. |
|
175 |
|
176 if (!aOptions._internal && |
|
177 this._appStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED && |
|
178 this._appStatus !== Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { |
|
179 |
|
180 // If the caller is not special in one of those ways, see if the user has |
|
181 // preffed on 'syntheticEventsOk' (useful for testing); otherwise, if |
|
182 // this is a non-native event, reject it. |
|
183 let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor) |
|
184 .getInterface(Ci.nsIDOMWindowUtils); |
|
185 |
|
186 if (!util.isHandlingUserInput && this.nativeEventsRequired) { |
|
187 message.errors.push("ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT"); |
|
188 } |
|
189 } |
|
190 |
|
191 // Report and fail hard on any errors. |
|
192 if (message.errors.length) { |
|
193 this.reportErrors(message); |
|
194 return; |
|
195 } |
|
196 |
|
197 if (aOptions) { |
|
198 // Optional string properties |
|
199 let optionalStringProps = ["privacyPolicy", "termsOfService"]; |
|
200 for (let propName of optionalStringProps) { |
|
201 if (!aOptions[propName] || aOptions[propName] === "undefined") |
|
202 continue; |
|
203 if (typeof(aOptions[propName]) !== "string") { |
|
204 throw new Error(propName + " must be a string representing a URL."); |
|
205 } |
|
206 if (aOptions[propName].length > MAX_STRING_LENGTH) { |
|
207 throw new Error(propName + " is invalid."); |
|
208 } |
|
209 message[propName] = aOptions[propName]; |
|
210 } |
|
211 |
|
212 if (aOptions["oncancel"] |
|
213 && typeof(aOptions["oncancel"]) !== "function") { |
|
214 throw new Error("oncancel is not a function"); |
|
215 } else { |
|
216 // Store optional cancel callback for later. |
|
217 this._onCancelRequestCallback = aOptions.oncancel; |
|
218 } |
|
219 } |
|
220 |
|
221 this._rpCalls++; |
|
222 this._identityInternal._mm.sendAsyncMessage("Identity:RP:Request", message); |
|
223 }, |
|
224 |
|
225 logout: function nsDOMIdentity_logout() { |
|
226 if (!this._rpWatcher) { |
|
227 throw new Error("navigator.id.logout called before navigator.id.watch"); |
|
228 } |
|
229 if (this._rpCalls > MAX_RP_CALLS) { |
|
230 throw new Error("navigator.id.logout called too many times"); |
|
231 } |
|
232 |
|
233 this._rpCalls++; |
|
234 let message = this.DOMIdentityMessage(); |
|
235 |
|
236 // Report and fail hard on any errors. |
|
237 if (message.errors.length) { |
|
238 this.reportErrors(message); |
|
239 return; |
|
240 } |
|
241 |
|
242 this._identityInternal._mm.sendAsyncMessage("Identity:RP:Logout", message); |
|
243 }, |
|
244 |
|
245 /* |
|
246 * Get an assertion. This function is deprecated. RPs are |
|
247 * encouraged to use the observer API instead (watch + request). |
|
248 */ |
|
249 get: function nsDOMIdentity_get(aCallback, aOptions) { |
|
250 var opts = {}; |
|
251 aOptions = aOptions || {}; |
|
252 |
|
253 // We use the observer API (watch + request) to implement get(). |
|
254 // Because the caller can call get() and getVerifiedEmail() as |
|
255 // many times as they want, we lift the restriction that watch() can |
|
256 // only be called once. |
|
257 this._rpWatcher = null; |
|
258 |
|
259 // This flag tells internal_api.js (in the shim) to record in the |
|
260 // login parameters whether the assertion was acquired silently or |
|
261 // with user interaction. |
|
262 opts._internal = true; |
|
263 |
|
264 opts.privacyPolicy = aOptions.privacyPolicy || undefined; |
|
265 opts.termsOfService = aOptions.termsOfService || undefined; |
|
266 opts.privacyURL = aOptions.privacyURL || undefined; |
|
267 opts.tosURL = aOptions.tosURL || undefined; |
|
268 opts.siteName = aOptions.siteName || undefined; |
|
269 opts.siteLogo = aOptions.siteLogo || undefined; |
|
270 |
|
271 opts.oncancel = function get_oncancel() { |
|
272 if (aCallback) { |
|
273 aCallback(null); |
|
274 aCallback = null; |
|
275 } |
|
276 }; |
|
277 |
|
278 if (checkDeprecated(aOptions, "silent")) { |
|
279 // Silent has been deprecated, do nothing. Placing the check here |
|
280 // prevents the callback from being called twice, once with null and |
|
281 // once after internalWatch has been called. See issue #1532: |
|
282 // https://github.com/mozilla/browserid/issues/1532 |
|
283 if (aCallback) { |
|
284 setTimeout(function() { aCallback(null); }, 0); |
|
285 } |
|
286 return; |
|
287 } |
|
288 |
|
289 // Get an assertion by using our observer api: watch + request. |
|
290 var self = this; |
|
291 this.watch({ |
|
292 _internal: true, |
|
293 onlogin: function get_onlogin(assertion, internalParams) { |
|
294 if (assertion && aCallback && internalParams && !internalParams.silent) { |
|
295 aCallback(assertion); |
|
296 aCallback = null; |
|
297 } |
|
298 }, |
|
299 onlogout: function get_onlogout() {}, |
|
300 onready: function get_onready() { |
|
301 self.request(opts); |
|
302 } |
|
303 }); |
|
304 }, |
|
305 |
|
306 getVerifiedEmail: function nsDOMIdentity_getVerifiedEmail(aCallback) { |
|
307 Cu.reportError("WARNING: getVerifiedEmail has been deprecated"); |
|
308 this.get(aCallback, {}); |
|
309 }, |
|
310 |
|
311 /** |
|
312 * Identity Provider (IDP) Provisioning APIs |
|
313 */ |
|
314 |
|
315 beginProvisioning: function nsDOMIdentity_beginProvisioning(aCallback) { |
|
316 this._log("beginProvisioning"); |
|
317 if (this._beginProvisioningCallback) { |
|
318 throw new Error("navigator.id.beginProvisioning already called."); |
|
319 } |
|
320 if (!aCallback || typeof(aCallback) !== "function") { |
|
321 throw new Error("beginProvisioning callback is required."); |
|
322 } |
|
323 |
|
324 this._beginProvisioningCallback = aCallback; |
|
325 this._identityInternal._mm.sendAsyncMessage("Identity:IDP:BeginProvisioning", |
|
326 this.DOMIdentityMessage()); |
|
327 }, |
|
328 |
|
329 genKeyPair: function nsDOMIdentity_genKeyPair(aCallback) { |
|
330 this._log("genKeyPair"); |
|
331 if (!this._beginProvisioningCallback) { |
|
332 throw new Error("navigator.id.genKeyPair called outside of provisioning"); |
|
333 } |
|
334 if (this._genKeyPairCallback) { |
|
335 throw new Error("navigator.id.genKeyPair already called."); |
|
336 } |
|
337 if (!aCallback || typeof(aCallback) !== "function") { |
|
338 throw new Error("genKeyPair callback is required."); |
|
339 } |
|
340 |
|
341 this._genKeyPairCallback = aCallback; |
|
342 this._identityInternal._mm.sendAsyncMessage("Identity:IDP:GenKeyPair", |
|
343 this.DOMIdentityMessage()); |
|
344 }, |
|
345 |
|
346 registerCertificate: function nsDOMIdentity_registerCertificate(aCertificate) { |
|
347 this._log("registerCertificate"); |
|
348 if (!this._genKeyPairCallback) { |
|
349 throw new Error("navigator.id.registerCertificate called outside of provisioning"); |
|
350 } |
|
351 if (this._provisioningEnded) { |
|
352 throw new Error("Provisioning already ended"); |
|
353 } |
|
354 this._provisioningEnded = true; |
|
355 |
|
356 let message = this.DOMIdentityMessage(); |
|
357 message.cert = aCertificate; |
|
358 this._identityInternal._mm.sendAsyncMessage("Identity:IDP:RegisterCertificate", message); |
|
359 }, |
|
360 |
|
361 raiseProvisioningFailure: function nsDOMIdentity_raiseProvisioningFailure(aReason) { |
|
362 this._log("raiseProvisioningFailure '" + aReason + "'"); |
|
363 if (this._provisioningEnded) { |
|
364 throw new Error("Provisioning already ended"); |
|
365 } |
|
366 if (!aReason || typeof(aReason) != "string") { |
|
367 throw new Error("raiseProvisioningFailure reason is required"); |
|
368 } |
|
369 this._provisioningEnded = true; |
|
370 |
|
371 let message = this.DOMIdentityMessage(); |
|
372 message.reason = aReason; |
|
373 this._identityInternal._mm.sendAsyncMessage("Identity:IDP:ProvisioningFailure", message); |
|
374 }, |
|
375 |
|
376 /** |
|
377 * Identity Provider (IDP) Authentication APIs |
|
378 */ |
|
379 |
|
380 beginAuthentication: function nsDOMIdentity_beginAuthentication(aCallback) { |
|
381 this._log("beginAuthentication"); |
|
382 if (this._beginAuthenticationCallback) { |
|
383 throw new Error("navigator.id.beginAuthentication already called."); |
|
384 } |
|
385 if (typeof(aCallback) !== "function") { |
|
386 throw new Error("beginAuthentication callback is required."); |
|
387 } |
|
388 if (!aCallback || typeof(aCallback) !== "function") { |
|
389 throw new Error("beginAuthentication callback is required."); |
|
390 } |
|
391 |
|
392 this._beginAuthenticationCallback = aCallback; |
|
393 this._identityInternal._mm.sendAsyncMessage("Identity:IDP:BeginAuthentication", |
|
394 this.DOMIdentityMessage()); |
|
395 }, |
|
396 |
|
397 completeAuthentication: function nsDOMIdentity_completeAuthentication() { |
|
398 if (this._authenticationEnded) { |
|
399 throw new Error("Authentication already ended"); |
|
400 } |
|
401 if (!this._beginAuthenticationCallback) { |
|
402 throw new Error("navigator.id.completeAuthentication called outside of authentication"); |
|
403 } |
|
404 this._authenticationEnded = true; |
|
405 |
|
406 this._identityInternal._mm.sendAsyncMessage("Identity:IDP:CompleteAuthentication", |
|
407 this.DOMIdentityMessage()); |
|
408 }, |
|
409 |
|
410 raiseAuthenticationFailure: function nsDOMIdentity_raiseAuthenticationFailure(aReason) { |
|
411 if (this._authenticationEnded) { |
|
412 throw new Error("Authentication already ended"); |
|
413 } |
|
414 if (!aReason || typeof(aReason) != "string") { |
|
415 throw new Error("raiseProvisioningFailure reason is required"); |
|
416 } |
|
417 |
|
418 let message = this.DOMIdentityMessage(); |
|
419 message.reason = aReason; |
|
420 this._identityInternal._mm.sendAsyncMessage("Identity:IDP:AuthenticationFailure", message); |
|
421 }, |
|
422 |
|
423 // Private. |
|
424 _init: function nsDOMIdentity__init(aWindow) { |
|
425 |
|
426 this._initializeState(); |
|
427 |
|
428 // Store window and origin URI. |
|
429 this._window = aWindow; |
|
430 this._origin = aWindow.document.nodePrincipal.origin; |
|
431 this._appStatus = aWindow.document.nodePrincipal.appStatus; |
|
432 this._appId = aWindow.document.nodePrincipal.appId; |
|
433 |
|
434 // Setup identifiers for current window. |
|
435 let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
436 .getInterface(Ci.nsIDOMWindowUtils); |
|
437 |
|
438 // We need to inherit the id from the internalIdentity service. |
|
439 // See comments below in that service's init. |
|
440 this._id = this._identityInternal._id; |
|
441 }, |
|
442 |
|
443 /** |
|
444 * Called during init and shutdown. |
|
445 */ |
|
446 _initializeState: function nsDOMIdentity__initializeState() { |
|
447 // Some state to prevent abuse |
|
448 // Limit the number of calls to .request |
|
449 this._rpCalls = 0; |
|
450 this._provisioningEnded = false; |
|
451 this._authenticationEnded = false; |
|
452 |
|
453 this._rpWatcher = null; |
|
454 this._onCancelRequestCallback = null; |
|
455 this._beginProvisioningCallback = null; |
|
456 this._genKeyPairCallback = null; |
|
457 this._beginAuthenticationCallback = null; |
|
458 }, |
|
459 |
|
460 _receiveMessage: function nsDOMIdentity_receiveMessage(aMessage) { |
|
461 let msg = aMessage.json; |
|
462 |
|
463 switch (aMessage.name) { |
|
464 case "Identity:ResetState": |
|
465 if (!this._identityInternal._debug) { |
|
466 return; |
|
467 } |
|
468 this._initializeState(); |
|
469 Services.obs.notifyObservers(null, "identity-DOM-state-reset", this._id); |
|
470 break; |
|
471 case "Identity:RP:Watch:OnLogin": |
|
472 // Do we have a watcher? |
|
473 if (!this._rpWatcher) { |
|
474 this._log("WARNING: Received OnLogin message, but there is no RP watcher"); |
|
475 return; |
|
476 } |
|
477 |
|
478 if (this._rpWatcher.onlogin) { |
|
479 if (this._rpWatcher._internal) { |
|
480 this._rpWatcher.onlogin(msg.assertion, msg._internalParams); |
|
481 } else { |
|
482 this._rpWatcher.onlogin(msg.assertion); |
|
483 } |
|
484 } |
|
485 break; |
|
486 case "Identity:RP:Watch:OnLogout": |
|
487 // Do we have a watcher? |
|
488 if (!this._rpWatcher) { |
|
489 this._log("WARNING: Received OnLogout message, but there is no RP watcher"); |
|
490 return; |
|
491 } |
|
492 |
|
493 if (this._rpWatcher.onlogout) { |
|
494 this._rpWatcher.onlogout(); |
|
495 } |
|
496 break; |
|
497 case "Identity:RP:Watch:OnReady": |
|
498 // Do we have a watcher? |
|
499 if (!this._rpWatcher) { |
|
500 this._log("WARNING: Received OnReady message, but there is no RP watcher"); |
|
501 return; |
|
502 } |
|
503 |
|
504 if (this._rpWatcher.onready) { |
|
505 this._rpWatcher.onready(); |
|
506 } |
|
507 break; |
|
508 case "Identity:RP:Watch:OnCancel": |
|
509 // Do we have a watcher? |
|
510 if (!this._rpWatcher) { |
|
511 this._log("WARNING: Received OnCancel message, but there is no RP watcher"); |
|
512 return; |
|
513 } |
|
514 |
|
515 if (this._onCancelRequestCallback) { |
|
516 this._onCancelRequestCallback(); |
|
517 } |
|
518 break; |
|
519 case "Identity:RP:Watch:OnError": |
|
520 if (!this._rpWatcher) { |
|
521 this._log("WARNING: Received OnError message, but there is no RP watcher"); |
|
522 return; |
|
523 } |
|
524 |
|
525 if (this._rpWatcher.onerror) { |
|
526 this._rpWatcher.onerror(JSON.stringify({name: msg.message.error})); |
|
527 } |
|
528 break; |
|
529 case "Identity:IDP:CallBeginProvisioningCallback": |
|
530 this._callBeginProvisioningCallback(msg); |
|
531 break; |
|
532 case "Identity:IDP:CallGenKeyPairCallback": |
|
533 this._callGenKeyPairCallback(msg); |
|
534 break; |
|
535 case "Identity:IDP:CallBeginAuthenticationCallback": |
|
536 this._callBeginAuthenticationCallback(msg); |
|
537 break; |
|
538 } |
|
539 }, |
|
540 |
|
541 _log: function nsDOMIdentity__log(msg) { |
|
542 this._identityInternal._log(msg); |
|
543 }, |
|
544 |
|
545 _callGenKeyPairCallback: function nsDOMIdentity__callGenKeyPairCallback(message) { |
|
546 // create a pubkey object that works |
|
547 let chrome_pubkey = JSON.parse(message.publicKey); |
|
548 |
|
549 // bunch of stuff to create a proper object in window context |
|
550 function genPropDesc(value) { |
|
551 return { |
|
552 enumerable: true, configurable: true, writable: true, value: value |
|
553 }; |
|
554 } |
|
555 |
|
556 let propList = {}; |
|
557 for (let k in chrome_pubkey) { |
|
558 propList[k] = genPropDesc(chrome_pubkey[k]); |
|
559 } |
|
560 |
|
561 let pubkey = Cu.createObjectIn(this._window); |
|
562 Object.defineProperties(pubkey, propList); |
|
563 Cu.makeObjectPropsNormal(pubkey); |
|
564 |
|
565 // do the callback |
|
566 this._genKeyPairCallback(pubkey); |
|
567 }, |
|
568 |
|
569 _callBeginProvisioningCallback: |
|
570 function nsDOMIdentity__callBeginProvisioningCallback(message) { |
|
571 let identity = message.identity; |
|
572 let certValidityDuration = message.certDuration; |
|
573 this._beginProvisioningCallback(identity, |
|
574 certValidityDuration); |
|
575 }, |
|
576 |
|
577 _callBeginAuthenticationCallback: |
|
578 function nsDOMIdentity__callBeginAuthenticationCallback(message) { |
|
579 let identity = message.identity; |
|
580 this._beginAuthenticationCallback(identity); |
|
581 }, |
|
582 |
|
583 /** |
|
584 * Helper to create messages to send using a message manager. |
|
585 * Pass through user options if they are not functions. Always |
|
586 * overwrite id, origin, audience, and appStatus. The caller |
|
587 * does not get to set those. |
|
588 */ |
|
589 DOMIdentityMessage: function DOMIdentityMessage(aOptions) { |
|
590 aOptions = aOptions || {}; |
|
591 let message = { |
|
592 errors: [] |
|
593 }; |
|
594 let principal = Ci.nsIPrincipal; |
|
595 |
|
596 objectCopy(aOptions, message); |
|
597 |
|
598 // outer window id |
|
599 message.id = this._id; |
|
600 |
|
601 // window origin |
|
602 message.origin = this._origin; |
|
603 |
|
604 // On b2g, an app's status can be NOT_INSTALLED, INSTALLED, PRIVILEGED, or |
|
605 // CERTIFIED. Compare the appStatus value to the constants enumerated in |
|
606 // Ci.nsIPrincipal.APP_STATUS_*. |
|
607 message.appStatus = this._appStatus; |
|
608 |
|
609 // Currently, we only permit certified and privileged apps to use |
|
610 // Firefox Accounts. |
|
611 if (aOptions.wantIssuer == "firefox-accounts" && |
|
612 this._appStatus !== principal.APP_STATUS_PRIVILEGED && |
|
613 this._appStatus !== principal.APP_STATUS_CERTIFIED) { |
|
614 message.errors.push("ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS"); |
|
615 } |
|
616 |
|
617 // Normally the window origin will be the audience in assertions. On b2g, |
|
618 // certified apps have the power to override this and declare any audience |
|
619 // the want. Privileged apps can also declare a different audience, as |
|
620 // long as it is the same as the origin specified in their manifest files. |
|
621 // All other apps are stuck with b2g origins of the form app://{guid}. |
|
622 // Since such an origin is meaningless for the purposes of verification, |
|
623 // they will have to jump through some hoops to sign in: Specifically, they |
|
624 // will have to host their sign-in flows and DOM API requests in an iframe, |
|
625 // have the iframe xhr post assertions up to their server for verification, |
|
626 // and then post-message the results down to their app. |
|
627 let _audience = message.origin; |
|
628 if (message.audience && message.audience != message.origin) { |
|
629 if (this._appStatus === principal.APP_STATUS_CERTIFIED) { |
|
630 _audience = message.audience; |
|
631 this._log("Certified app setting assertion audience: " + _audience); |
|
632 } else { |
|
633 message.errors.push("ERROR_INVALID_ASSERTION_AUDIENCE"); |
|
634 } |
|
635 } |
|
636 |
|
637 // Replace any audience supplied by the RP with one that has been sanitised |
|
638 message.audience = _audience; |
|
639 |
|
640 this._log("DOMIdentityMessage: " + JSON.stringify(message)); |
|
641 |
|
642 return message; |
|
643 }, |
|
644 |
|
645 uninit: function DOMIdentity_uninit() { |
|
646 this._log("nsDOMIdentity uninit() " + this._id); |
|
647 this._identityInternal._mm.sendAsyncMessage( |
|
648 "Identity:RP:Unwatch", |
|
649 { id: this._id } |
|
650 ); |
|
651 } |
|
652 |
|
653 }; |
|
654 |
|
655 /** |
|
656 * Internal functions that shouldn't be exposed to content. |
|
657 */ |
|
658 function nsDOMIdentityInternal() { |
|
659 } |
|
660 nsDOMIdentityInternal.prototype = { |
|
661 |
|
662 // nsIMessageListener |
|
663 receiveMessage: function nsDOMIdentityInternal_receiveMessage(aMessage) { |
|
664 let msg = aMessage.json; |
|
665 // Is this message intended for this window? |
|
666 if (msg.id != this._id) { |
|
667 return; |
|
668 } |
|
669 this._identity._receiveMessage(aMessage); |
|
670 }, |
|
671 |
|
672 // nsIObserver |
|
673 observe: function nsDOMIdentityInternal_observe(aSubject, aTopic, aData) { |
|
674 let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; |
|
675 if (wId != this._innerWindowID) { |
|
676 return; |
|
677 } |
|
678 |
|
679 this._identity.uninit(); |
|
680 |
|
681 Services.obs.removeObserver(this, "inner-window-destroyed"); |
|
682 this._identity._initializeState(); |
|
683 this._identity = null; |
|
684 |
|
685 // TODO: Also send message to DOMIdentity notifiying window is no longer valid |
|
686 // ie. in the case that the user closes the auth. window and we need to know. |
|
687 |
|
688 try { |
|
689 for (let msgName of this._messages) { |
|
690 this._mm.removeMessageListener(msgName, this); |
|
691 } |
|
692 } catch (ex) { |
|
693 // Avoid errors when removing more than once. |
|
694 } |
|
695 |
|
696 this._mm = null; |
|
697 }, |
|
698 |
|
699 // nsIDOMGlobalPropertyInitializer |
|
700 init: function nsDOMIdentityInternal_init(aWindow) { |
|
701 if (Services.prefs.getPrefType(PREF_ENABLED) != Ci.nsIPrefBranch.PREF_BOOL |
|
702 || !Services.prefs.getBoolPref(PREF_ENABLED)) { |
|
703 return null; |
|
704 } |
|
705 |
|
706 this._debug = |
|
707 Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL |
|
708 && Services.prefs.getBoolPref(PREF_DEBUG); |
|
709 |
|
710 let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
711 .getInterface(Ci.nsIDOMWindowUtils); |
|
712 |
|
713 // To avoid cross-process windowId collisions, use a uuid as an |
|
714 // almost certainly unique identifier. |
|
715 // |
|
716 // XXX Bug 869182 - use a combination of child process id and |
|
717 // innerwindow id to construct the unique id. |
|
718 this._id = uuidgen.generateUUID().toString(); |
|
719 this._innerWindowID = util.currentInnerWindowID; |
|
720 |
|
721 // nsDOMIdentity needs to know our _id, so this goes after |
|
722 // its creation. |
|
723 this._identity = new nsDOMIdentity(this); |
|
724 this._identity._init(aWindow); |
|
725 |
|
726 this._log("init was called from " + aWindow.document.location); |
|
727 |
|
728 this._mm = cpmm; |
|
729 |
|
730 // Setup listeners for messages from parent process. |
|
731 this._messages = [ |
|
732 "Identity:ResetState", |
|
733 "Identity:RP:Watch:OnLogin", |
|
734 "Identity:RP:Watch:OnLogout", |
|
735 "Identity:RP:Watch:OnReady", |
|
736 "Identity:RP:Watch:OnCancel", |
|
737 "Identity:RP:Watch:OnError", |
|
738 "Identity:IDP:CallBeginProvisioningCallback", |
|
739 "Identity:IDP:CallGenKeyPairCallback", |
|
740 "Identity:IDP:CallBeginAuthenticationCallback" |
|
741 ]; |
|
742 this._messages.forEach(function(msgName) { |
|
743 this._mm.addMessageListener(msgName, this); |
|
744 }, this); |
|
745 |
|
746 // Setup observers so we can remove message listeners. |
|
747 Services.obs.addObserver(this, "inner-window-destroyed", false); |
|
748 |
|
749 return this._identity; |
|
750 }, |
|
751 |
|
752 // Private. |
|
753 _log: function nsDOMIdentityInternal__log(msg) { |
|
754 if (!this._debug) { |
|
755 return; |
|
756 } |
|
757 dump("nsDOMIdentity (" + this._id + "): " + msg + "\n"); |
|
758 }, |
|
759 |
|
760 // Component setup. |
|
761 classID: Components.ID("{210853d9-2c97-4669-9761-b1ab9cbf57ef}"), |
|
762 |
|
763 QueryInterface: XPCOMUtils.generateQI( |
|
764 [Ci.nsIDOMGlobalPropertyInitializer, Ci.nsIMessageListener] |
|
765 ), |
|
766 |
|
767 classInfo: XPCOMUtils.generateCI({ |
|
768 classID: Components.ID("{210853d9-2c97-4669-9761-b1ab9cbf57ef}"), |
|
769 contractID: "@mozilla.org/dom/identity;1", |
|
770 interfaces: [], |
|
771 classDescription: "Identity DOM Implementation" |
|
772 }) |
|
773 }; |
|
774 |
|
775 function assertCorrectCallbacks(aOptions) { |
|
776 // The relying party (RP) provides callbacks on watch(). |
|
777 // |
|
778 // In the future, BrowserID will probably only require an onlogin() |
|
779 // callback, lifting the requirement that BrowserID handle logged-in |
|
780 // state management for RPs. See |
|
781 // https://github.com/mozilla/id-specs/blob/greenfield/browserid/api-rp.md |
|
782 // |
|
783 // However, Firefox Accounts requires callers to provide onlogout(), |
|
784 // onready(), and also supports an onerror() callback. |
|
785 |
|
786 let requiredCallbacks = ["onlogin"]; |
|
787 let optionalCallbacks = ["onlogout", "onready", "onerror"]; |
|
788 |
|
789 if (aOptions.wantIssuer == "firefox-accounts") { |
|
790 requiredCallbacks = ["onlogin", "onlogout", "onready"]; |
|
791 optionalCallbacks = ["onerror"]; |
|
792 } |
|
793 |
|
794 for (let cbName of requiredCallbacks) { |
|
795 if (typeof(aOptions[cbName]) != "function") { |
|
796 throw new Error(cbName + " callback is required."); |
|
797 } |
|
798 } |
|
799 |
|
800 for (let cbName of optionalCallbacks) { |
|
801 if (aOptions[cbName] && typeof(aOptions[cbName]) != "function") { |
|
802 throw new Error(cbName + " must be a function"); |
|
803 } |
|
804 } |
|
805 } |
|
806 |
|
807 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsDOMIdentityInternal]); |