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