michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: const PREF_DEBUG = "toolkit.identity.debug"; michael@0: const PREF_ENABLED = "dom.identity.enabled"; michael@0: michael@0: // Bug 822450: Workaround for Bug 821740. When testing with marionette, michael@0: // relax navigator.id.request's requirement that it be handling native michael@0: // events. Synthetic marionette events are ok. michael@0: const PREF_SYNTHETIC_EVENTS_OK = "dom.identity.syntheticEventsOk"; michael@0: michael@0: // Maximum length of a string that will go through IPC michael@0: const MAX_STRING_LENGTH = 2048; michael@0: // Maximum number of times navigator.id.request can be called for a document michael@0: const MAX_RP_CALLS = 100; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "checkDeprecated", michael@0: "resource://gre/modules/identity/IdentityUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "checkRenamed", michael@0: "resource://gre/modules/identity/IdentityUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "objectCopy", michael@0: "resource://gre/modules/identity/IdentityUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", michael@0: "@mozilla.org/uuid-generator;1", michael@0: "nsIUUIDGenerator"); michael@0: michael@0: // This is the child process corresponding to nsIDOMIdentity michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: michael@0: michael@0: const ERRORS = { michael@0: "ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS": michael@0: "Only privileged and certified apps may use Firefox Accounts", michael@0: "ERROR_INVALID_ASSERTION_AUDIENCE": michael@0: "Assertion audience may not differ from origin", michael@0: "ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT": michael@0: "The request() method may only be invoked when handling user input", michael@0: }; michael@0: michael@0: function nsDOMIdentity(aIdentityInternal) { michael@0: this._identityInternal = aIdentityInternal; michael@0: } michael@0: nsDOMIdentity.prototype = { michael@0: __exposedProps__: { michael@0: // Relying Party (RP) michael@0: watch: 'r', michael@0: request: 'r', michael@0: logout: 'r', michael@0: get: 'r', michael@0: getVerifiedEmail: 'r', michael@0: michael@0: // Provisioning michael@0: beginProvisioning: 'r', michael@0: genKeyPair: 'r', michael@0: registerCertificate: 'r', michael@0: raiseProvisioningFailure: 'r', michael@0: michael@0: // Authentication michael@0: beginAuthentication: 'r', michael@0: completeAuthentication: 'r', michael@0: raiseAuthenticationFailure: 'r' michael@0: }, michael@0: michael@0: // require native events unless syntheticEventsOk is set michael@0: get nativeEventsRequired() { michael@0: if (Services.prefs.prefHasUserValue(PREF_SYNTHETIC_EVENTS_OK) && michael@0: (Services.prefs.getPrefType(PREF_SYNTHETIC_EVENTS_OK) === michael@0: Ci.nsIPrefBranch.PREF_BOOL)) { michael@0: return !Services.prefs.getBoolPref(PREF_SYNTHETIC_EVENTS_OK); michael@0: } michael@0: return true; michael@0: }, michael@0: michael@0: reportErrors: function(message) { michael@0: let onerror = function() {}; michael@0: if (this._rpWatcher && this._rpWatcher.onerror) { michael@0: onerror = this._rpWatcher.onerror; michael@0: } michael@0: michael@0: message.errors.forEach((error) => { michael@0: // Report an error string to content michael@0: Cu.reportError(ERRORS[error]); michael@0: michael@0: // Report error code to RP callback, if available michael@0: onerror(error); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Relying Party (RP) APIs michael@0: */ michael@0: michael@0: watch: function nsDOMIdentity_watch(aOptions = {}) { michael@0: if (this._rpWatcher) { michael@0: // For the initial release of Firefox Accounts, we support callers who michael@0: // invoke watch() either for Firefox Accounts, or Persona, but not both. michael@0: // In the future, we may wish to support the dual invocation (say, for michael@0: // packaged apps so they can sign users in who reject the app's request michael@0: // to sign in with their Firefox Accounts identity). michael@0: throw new Error("navigator.id.watch was already called"); michael@0: } michael@0: michael@0: assertCorrectCallbacks(aOptions); michael@0: michael@0: let message = this.DOMIdentityMessage(aOptions); michael@0: michael@0: // loggedInUser vs loggedInEmail michael@0: // https://developer.mozilla.org/en-US/docs/DOM/navigator.id.watch michael@0: // This parameter, loggedInUser, was renamed from loggedInEmail in early michael@0: // September, 2012. Both names will continue to work for the time being, michael@0: // but code should be changed to use loggedInUser instead. michael@0: checkRenamed(aOptions, "loggedInEmail", "loggedInUser"); michael@0: message["loggedInUser"] = aOptions["loggedInUser"]; michael@0: michael@0: let emailType = typeof(aOptions["loggedInUser"]); michael@0: if (aOptions["loggedInUser"] && aOptions["loggedInUser"] !== "undefined") { michael@0: if (emailType !== "string") { michael@0: throw new Error("loggedInUser must be a String or null"); michael@0: } michael@0: michael@0: // TODO: Bug 767610 - check email format. michael@0: // See HTMLInputElement::IsValidEmailAddress michael@0: if (aOptions["loggedInUser"].indexOf("@") == -1 michael@0: || aOptions["loggedInUser"].length > MAX_STRING_LENGTH) { michael@0: throw new Error("loggedInUser is not valid"); michael@0: } michael@0: // Set loggedInUser in this block that "undefined" doesn't get through. michael@0: message.loggedInUser = aOptions.loggedInUser; michael@0: } michael@0: this._log("loggedInUser: " + message.loggedInUser); michael@0: michael@0: this._rpWatcher = aOptions; michael@0: this._rpWatcher.audience = message.audience; michael@0: michael@0: if (message.errors.length) { michael@0: this.reportErrors(message); michael@0: // We don't delete the rpWatcher object, because we don't want the michael@0: // broken client to be able to call watch() any more. It's broken. michael@0: return; michael@0: } michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:RP:Watch", message); michael@0: }, michael@0: michael@0: request: function nsDOMIdentity_request(aOptions = {}) { michael@0: this._log("request: " + JSON.stringify(aOptions)); michael@0: michael@0: // Has the caller called watch() before this? michael@0: if (!this._rpWatcher) { michael@0: throw new Error("navigator.id.request called before navigator.id.watch"); michael@0: } michael@0: if (this._rpCalls > MAX_RP_CALLS) { michael@0: throw new Error("navigator.id.request called too many times"); michael@0: } michael@0: michael@0: let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: michael@0: let message = this.DOMIdentityMessage(aOptions); michael@0: michael@0: // We permit calling of request() outside of a user input handler only when michael@0: // a certified or privileged app is calling, or when we are handling the michael@0: // (deprecated) get() or getVerifiedEmail() calls, which make use of an RP michael@0: // context marked as _internal. michael@0: michael@0: if (!aOptions._internal && michael@0: this._appStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED && michael@0: this._appStatus !== Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) { michael@0: michael@0: // If the caller is not special in one of those ways, see if the user has michael@0: // preffed on 'syntheticEventsOk' (useful for testing); otherwise, if michael@0: // this is a non-native event, reject it. michael@0: let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: michael@0: if (!util.isHandlingUserInput && this.nativeEventsRequired) { michael@0: message.errors.push("ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT"); michael@0: } michael@0: } michael@0: michael@0: // Report and fail hard on any errors. michael@0: if (message.errors.length) { michael@0: this.reportErrors(message); michael@0: return; michael@0: } michael@0: michael@0: if (aOptions) { michael@0: // Optional string properties michael@0: let optionalStringProps = ["privacyPolicy", "termsOfService"]; michael@0: for (let propName of optionalStringProps) { michael@0: if (!aOptions[propName] || aOptions[propName] === "undefined") michael@0: continue; michael@0: if (typeof(aOptions[propName]) !== "string") { michael@0: throw new Error(propName + " must be a string representing a URL."); michael@0: } michael@0: if (aOptions[propName].length > MAX_STRING_LENGTH) { michael@0: throw new Error(propName + " is invalid."); michael@0: } michael@0: message[propName] = aOptions[propName]; michael@0: } michael@0: michael@0: if (aOptions["oncancel"] michael@0: && typeof(aOptions["oncancel"]) !== "function") { michael@0: throw new Error("oncancel is not a function"); michael@0: } else { michael@0: // Store optional cancel callback for later. michael@0: this._onCancelRequestCallback = aOptions.oncancel; michael@0: } michael@0: } michael@0: michael@0: this._rpCalls++; michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:RP:Request", message); michael@0: }, michael@0: michael@0: logout: function nsDOMIdentity_logout() { michael@0: if (!this._rpWatcher) { michael@0: throw new Error("navigator.id.logout called before navigator.id.watch"); michael@0: } michael@0: if (this._rpCalls > MAX_RP_CALLS) { michael@0: throw new Error("navigator.id.logout called too many times"); michael@0: } michael@0: michael@0: this._rpCalls++; michael@0: let message = this.DOMIdentityMessage(); michael@0: michael@0: // Report and fail hard on any errors. michael@0: if (message.errors.length) { michael@0: this.reportErrors(message); michael@0: return; michael@0: } michael@0: michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:RP:Logout", message); michael@0: }, michael@0: michael@0: /* michael@0: * Get an assertion. This function is deprecated. RPs are michael@0: * encouraged to use the observer API instead (watch + request). michael@0: */ michael@0: get: function nsDOMIdentity_get(aCallback, aOptions) { michael@0: var opts = {}; michael@0: aOptions = aOptions || {}; michael@0: michael@0: // We use the observer API (watch + request) to implement get(). michael@0: // Because the caller can call get() and getVerifiedEmail() as michael@0: // many times as they want, we lift the restriction that watch() can michael@0: // only be called once. michael@0: this._rpWatcher = null; michael@0: michael@0: // This flag tells internal_api.js (in the shim) to record in the michael@0: // login parameters whether the assertion was acquired silently or michael@0: // with user interaction. michael@0: opts._internal = true; michael@0: michael@0: opts.privacyPolicy = aOptions.privacyPolicy || undefined; michael@0: opts.termsOfService = aOptions.termsOfService || undefined; michael@0: opts.privacyURL = aOptions.privacyURL || undefined; michael@0: opts.tosURL = aOptions.tosURL || undefined; michael@0: opts.siteName = aOptions.siteName || undefined; michael@0: opts.siteLogo = aOptions.siteLogo || undefined; michael@0: michael@0: opts.oncancel = function get_oncancel() { michael@0: if (aCallback) { michael@0: aCallback(null); michael@0: aCallback = null; michael@0: } michael@0: }; michael@0: michael@0: if (checkDeprecated(aOptions, "silent")) { michael@0: // Silent has been deprecated, do nothing. Placing the check here michael@0: // prevents the callback from being called twice, once with null and michael@0: // once after internalWatch has been called. See issue #1532: michael@0: // https://github.com/mozilla/browserid/issues/1532 michael@0: if (aCallback) { michael@0: setTimeout(function() { aCallback(null); }, 0); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // Get an assertion by using our observer api: watch + request. michael@0: var self = this; michael@0: this.watch({ michael@0: _internal: true, michael@0: onlogin: function get_onlogin(assertion, internalParams) { michael@0: if (assertion && aCallback && internalParams && !internalParams.silent) { michael@0: aCallback(assertion); michael@0: aCallback = null; michael@0: } michael@0: }, michael@0: onlogout: function get_onlogout() {}, michael@0: onready: function get_onready() { michael@0: self.request(opts); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: getVerifiedEmail: function nsDOMIdentity_getVerifiedEmail(aCallback) { michael@0: Cu.reportError("WARNING: getVerifiedEmail has been deprecated"); michael@0: this.get(aCallback, {}); michael@0: }, michael@0: michael@0: /** michael@0: * Identity Provider (IDP) Provisioning APIs michael@0: */ michael@0: michael@0: beginProvisioning: function nsDOMIdentity_beginProvisioning(aCallback) { michael@0: this._log("beginProvisioning"); michael@0: if (this._beginProvisioningCallback) { michael@0: throw new Error("navigator.id.beginProvisioning already called."); michael@0: } michael@0: if (!aCallback || typeof(aCallback) !== "function") { michael@0: throw new Error("beginProvisioning callback is required."); michael@0: } michael@0: michael@0: this._beginProvisioningCallback = aCallback; michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:IDP:BeginProvisioning", michael@0: this.DOMIdentityMessage()); michael@0: }, michael@0: michael@0: genKeyPair: function nsDOMIdentity_genKeyPair(aCallback) { michael@0: this._log("genKeyPair"); michael@0: if (!this._beginProvisioningCallback) { michael@0: throw new Error("navigator.id.genKeyPair called outside of provisioning"); michael@0: } michael@0: if (this._genKeyPairCallback) { michael@0: throw new Error("navigator.id.genKeyPair already called."); michael@0: } michael@0: if (!aCallback || typeof(aCallback) !== "function") { michael@0: throw new Error("genKeyPair callback is required."); michael@0: } michael@0: michael@0: this._genKeyPairCallback = aCallback; michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:IDP:GenKeyPair", michael@0: this.DOMIdentityMessage()); michael@0: }, michael@0: michael@0: registerCertificate: function nsDOMIdentity_registerCertificate(aCertificate) { michael@0: this._log("registerCertificate"); michael@0: if (!this._genKeyPairCallback) { michael@0: throw new Error("navigator.id.registerCertificate called outside of provisioning"); michael@0: } michael@0: if (this._provisioningEnded) { michael@0: throw new Error("Provisioning already ended"); michael@0: } michael@0: this._provisioningEnded = true; michael@0: michael@0: let message = this.DOMIdentityMessage(); michael@0: message.cert = aCertificate; michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:IDP:RegisterCertificate", message); michael@0: }, michael@0: michael@0: raiseProvisioningFailure: function nsDOMIdentity_raiseProvisioningFailure(aReason) { michael@0: this._log("raiseProvisioningFailure '" + aReason + "'"); michael@0: if (this._provisioningEnded) { michael@0: throw new Error("Provisioning already ended"); michael@0: } michael@0: if (!aReason || typeof(aReason) != "string") { michael@0: throw new Error("raiseProvisioningFailure reason is required"); michael@0: } michael@0: this._provisioningEnded = true; michael@0: michael@0: let message = this.DOMIdentityMessage(); michael@0: message.reason = aReason; michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:IDP:ProvisioningFailure", message); michael@0: }, michael@0: michael@0: /** michael@0: * Identity Provider (IDP) Authentication APIs michael@0: */ michael@0: michael@0: beginAuthentication: function nsDOMIdentity_beginAuthentication(aCallback) { michael@0: this._log("beginAuthentication"); michael@0: if (this._beginAuthenticationCallback) { michael@0: throw new Error("navigator.id.beginAuthentication already called."); michael@0: } michael@0: if (typeof(aCallback) !== "function") { michael@0: throw new Error("beginAuthentication callback is required."); michael@0: } michael@0: if (!aCallback || typeof(aCallback) !== "function") { michael@0: throw new Error("beginAuthentication callback is required."); michael@0: } michael@0: michael@0: this._beginAuthenticationCallback = aCallback; michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:IDP:BeginAuthentication", michael@0: this.DOMIdentityMessage()); michael@0: }, michael@0: michael@0: completeAuthentication: function nsDOMIdentity_completeAuthentication() { michael@0: if (this._authenticationEnded) { michael@0: throw new Error("Authentication already ended"); michael@0: } michael@0: if (!this._beginAuthenticationCallback) { michael@0: throw new Error("navigator.id.completeAuthentication called outside of authentication"); michael@0: } michael@0: this._authenticationEnded = true; michael@0: michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:IDP:CompleteAuthentication", michael@0: this.DOMIdentityMessage()); michael@0: }, michael@0: michael@0: raiseAuthenticationFailure: function nsDOMIdentity_raiseAuthenticationFailure(aReason) { michael@0: if (this._authenticationEnded) { michael@0: throw new Error("Authentication already ended"); michael@0: } michael@0: if (!aReason || typeof(aReason) != "string") { michael@0: throw new Error("raiseProvisioningFailure reason is required"); michael@0: } michael@0: michael@0: let message = this.DOMIdentityMessage(); michael@0: message.reason = aReason; michael@0: this._identityInternal._mm.sendAsyncMessage("Identity:IDP:AuthenticationFailure", message); michael@0: }, michael@0: michael@0: // Private. michael@0: _init: function nsDOMIdentity__init(aWindow) { michael@0: michael@0: this._initializeState(); michael@0: michael@0: // Store window and origin URI. michael@0: this._window = aWindow; michael@0: this._origin = aWindow.document.nodePrincipal.origin; michael@0: this._appStatus = aWindow.document.nodePrincipal.appStatus; michael@0: this._appId = aWindow.document.nodePrincipal.appId; michael@0: michael@0: // Setup identifiers for current window. michael@0: let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: michael@0: // We need to inherit the id from the internalIdentity service. michael@0: // See comments below in that service's init. michael@0: this._id = this._identityInternal._id; michael@0: }, michael@0: michael@0: /** michael@0: * Called during init and shutdown. michael@0: */ michael@0: _initializeState: function nsDOMIdentity__initializeState() { michael@0: // Some state to prevent abuse michael@0: // Limit the number of calls to .request michael@0: this._rpCalls = 0; michael@0: this._provisioningEnded = false; michael@0: this._authenticationEnded = false; michael@0: michael@0: this._rpWatcher = null; michael@0: this._onCancelRequestCallback = null; michael@0: this._beginProvisioningCallback = null; michael@0: this._genKeyPairCallback = null; michael@0: this._beginAuthenticationCallback = null; michael@0: }, michael@0: michael@0: _receiveMessage: function nsDOMIdentity_receiveMessage(aMessage) { michael@0: let msg = aMessage.json; michael@0: michael@0: switch (aMessage.name) { michael@0: case "Identity:ResetState": michael@0: if (!this._identityInternal._debug) { michael@0: return; michael@0: } michael@0: this._initializeState(); michael@0: Services.obs.notifyObservers(null, "identity-DOM-state-reset", this._id); michael@0: break; michael@0: case "Identity:RP:Watch:OnLogin": michael@0: // Do we have a watcher? michael@0: if (!this._rpWatcher) { michael@0: this._log("WARNING: Received OnLogin message, but there is no RP watcher"); michael@0: return; michael@0: } michael@0: michael@0: if (this._rpWatcher.onlogin) { michael@0: if (this._rpWatcher._internal) { michael@0: this._rpWatcher.onlogin(msg.assertion, msg._internalParams); michael@0: } else { michael@0: this._rpWatcher.onlogin(msg.assertion); michael@0: } michael@0: } michael@0: break; michael@0: case "Identity:RP:Watch:OnLogout": michael@0: // Do we have a watcher? michael@0: if (!this._rpWatcher) { michael@0: this._log("WARNING: Received OnLogout message, but there is no RP watcher"); michael@0: return; michael@0: } michael@0: michael@0: if (this._rpWatcher.onlogout) { michael@0: this._rpWatcher.onlogout(); michael@0: } michael@0: break; michael@0: case "Identity:RP:Watch:OnReady": michael@0: // Do we have a watcher? michael@0: if (!this._rpWatcher) { michael@0: this._log("WARNING: Received OnReady message, but there is no RP watcher"); michael@0: return; michael@0: } michael@0: michael@0: if (this._rpWatcher.onready) { michael@0: this._rpWatcher.onready(); michael@0: } michael@0: break; michael@0: case "Identity:RP:Watch:OnCancel": michael@0: // Do we have a watcher? michael@0: if (!this._rpWatcher) { michael@0: this._log("WARNING: Received OnCancel message, but there is no RP watcher"); michael@0: return; michael@0: } michael@0: michael@0: if (this._onCancelRequestCallback) { michael@0: this._onCancelRequestCallback(); michael@0: } michael@0: break; michael@0: case "Identity:RP:Watch:OnError": michael@0: if (!this._rpWatcher) { michael@0: this._log("WARNING: Received OnError message, but there is no RP watcher"); michael@0: return; michael@0: } michael@0: michael@0: if (this._rpWatcher.onerror) { michael@0: this._rpWatcher.onerror(JSON.stringify({name: msg.message.error})); michael@0: } michael@0: break; michael@0: case "Identity:IDP:CallBeginProvisioningCallback": michael@0: this._callBeginProvisioningCallback(msg); michael@0: break; michael@0: case "Identity:IDP:CallGenKeyPairCallback": michael@0: this._callGenKeyPairCallback(msg); michael@0: break; michael@0: case "Identity:IDP:CallBeginAuthenticationCallback": michael@0: this._callBeginAuthenticationCallback(msg); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _log: function nsDOMIdentity__log(msg) { michael@0: this._identityInternal._log(msg); michael@0: }, michael@0: michael@0: _callGenKeyPairCallback: function nsDOMIdentity__callGenKeyPairCallback(message) { michael@0: // create a pubkey object that works michael@0: let chrome_pubkey = JSON.parse(message.publicKey); michael@0: michael@0: // bunch of stuff to create a proper object in window context michael@0: function genPropDesc(value) { michael@0: return { michael@0: enumerable: true, configurable: true, writable: true, value: value michael@0: }; michael@0: } michael@0: michael@0: let propList = {}; michael@0: for (let k in chrome_pubkey) { michael@0: propList[k] = genPropDesc(chrome_pubkey[k]); michael@0: } michael@0: michael@0: let pubkey = Cu.createObjectIn(this._window); michael@0: Object.defineProperties(pubkey, propList); michael@0: Cu.makeObjectPropsNormal(pubkey); michael@0: michael@0: // do the callback michael@0: this._genKeyPairCallback(pubkey); michael@0: }, michael@0: michael@0: _callBeginProvisioningCallback: michael@0: function nsDOMIdentity__callBeginProvisioningCallback(message) { michael@0: let identity = message.identity; michael@0: let certValidityDuration = message.certDuration; michael@0: this._beginProvisioningCallback(identity, michael@0: certValidityDuration); michael@0: }, michael@0: michael@0: _callBeginAuthenticationCallback: michael@0: function nsDOMIdentity__callBeginAuthenticationCallback(message) { michael@0: let identity = message.identity; michael@0: this._beginAuthenticationCallback(identity); michael@0: }, michael@0: michael@0: /** michael@0: * Helper to create messages to send using a message manager. michael@0: * Pass through user options if they are not functions. Always michael@0: * overwrite id, origin, audience, and appStatus. The caller michael@0: * does not get to set those. michael@0: */ michael@0: DOMIdentityMessage: function DOMIdentityMessage(aOptions) { michael@0: aOptions = aOptions || {}; michael@0: let message = { michael@0: errors: [] michael@0: }; michael@0: let principal = Ci.nsIPrincipal; michael@0: michael@0: objectCopy(aOptions, message); michael@0: michael@0: // outer window id michael@0: message.id = this._id; michael@0: michael@0: // window origin michael@0: message.origin = this._origin; michael@0: michael@0: // On b2g, an app's status can be NOT_INSTALLED, INSTALLED, PRIVILEGED, or michael@0: // CERTIFIED. Compare the appStatus value to the constants enumerated in michael@0: // Ci.nsIPrincipal.APP_STATUS_*. michael@0: message.appStatus = this._appStatus; michael@0: michael@0: // Currently, we only permit certified and privileged apps to use michael@0: // Firefox Accounts. michael@0: if (aOptions.wantIssuer == "firefox-accounts" && michael@0: this._appStatus !== principal.APP_STATUS_PRIVILEGED && michael@0: this._appStatus !== principal.APP_STATUS_CERTIFIED) { michael@0: message.errors.push("ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS"); michael@0: } michael@0: michael@0: // Normally the window origin will be the audience in assertions. On b2g, michael@0: // certified apps have the power to override this and declare any audience michael@0: // the want. Privileged apps can also declare a different audience, as michael@0: // long as it is the same as the origin specified in their manifest files. michael@0: // All other apps are stuck with b2g origins of the form app://{guid}. michael@0: // Since such an origin is meaningless for the purposes of verification, michael@0: // they will have to jump through some hoops to sign in: Specifically, they michael@0: // will have to host their sign-in flows and DOM API requests in an iframe, michael@0: // have the iframe xhr post assertions up to their server for verification, michael@0: // and then post-message the results down to their app. michael@0: let _audience = message.origin; michael@0: if (message.audience && message.audience != message.origin) { michael@0: if (this._appStatus === principal.APP_STATUS_CERTIFIED) { michael@0: _audience = message.audience; michael@0: this._log("Certified app setting assertion audience: " + _audience); michael@0: } else { michael@0: message.errors.push("ERROR_INVALID_ASSERTION_AUDIENCE"); michael@0: } michael@0: } michael@0: michael@0: // Replace any audience supplied by the RP with one that has been sanitised michael@0: message.audience = _audience; michael@0: michael@0: this._log("DOMIdentityMessage: " + JSON.stringify(message)); michael@0: michael@0: return message; michael@0: }, michael@0: michael@0: uninit: function DOMIdentity_uninit() { michael@0: this._log("nsDOMIdentity uninit() " + this._id); michael@0: this._identityInternal._mm.sendAsyncMessage( michael@0: "Identity:RP:Unwatch", michael@0: { id: this._id } michael@0: ); michael@0: } michael@0: michael@0: }; michael@0: michael@0: /** michael@0: * Internal functions that shouldn't be exposed to content. michael@0: */ michael@0: function nsDOMIdentityInternal() { michael@0: } michael@0: nsDOMIdentityInternal.prototype = { michael@0: michael@0: // nsIMessageListener michael@0: receiveMessage: function nsDOMIdentityInternal_receiveMessage(aMessage) { michael@0: let msg = aMessage.json; michael@0: // Is this message intended for this window? michael@0: if (msg.id != this._id) { michael@0: return; michael@0: } michael@0: this._identity._receiveMessage(aMessage); michael@0: }, michael@0: michael@0: // nsIObserver michael@0: observe: function nsDOMIdentityInternal_observe(aSubject, aTopic, aData) { michael@0: let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; michael@0: if (wId != this._innerWindowID) { michael@0: return; michael@0: } michael@0: michael@0: this._identity.uninit(); michael@0: michael@0: Services.obs.removeObserver(this, "inner-window-destroyed"); michael@0: this._identity._initializeState(); michael@0: this._identity = null; michael@0: michael@0: // TODO: Also send message to DOMIdentity notifiying window is no longer valid michael@0: // ie. in the case that the user closes the auth. window and we need to know. michael@0: michael@0: try { michael@0: for (let msgName of this._messages) { michael@0: this._mm.removeMessageListener(msgName, this); michael@0: } michael@0: } catch (ex) { michael@0: // Avoid errors when removing more than once. michael@0: } michael@0: michael@0: this._mm = null; michael@0: }, michael@0: michael@0: // nsIDOMGlobalPropertyInitializer michael@0: init: function nsDOMIdentityInternal_init(aWindow) { michael@0: if (Services.prefs.getPrefType(PREF_ENABLED) != Ci.nsIPrefBranch.PREF_BOOL michael@0: || !Services.prefs.getBoolPref(PREF_ENABLED)) { michael@0: return null; michael@0: } michael@0: michael@0: this._debug = michael@0: Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL michael@0: && Services.prefs.getBoolPref(PREF_DEBUG); michael@0: michael@0: let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: michael@0: // To avoid cross-process windowId collisions, use a uuid as an michael@0: // almost certainly unique identifier. michael@0: // michael@0: // XXX Bug 869182 - use a combination of child process id and michael@0: // innerwindow id to construct the unique id. michael@0: this._id = uuidgen.generateUUID().toString(); michael@0: this._innerWindowID = util.currentInnerWindowID; michael@0: michael@0: // nsDOMIdentity needs to know our _id, so this goes after michael@0: // its creation. michael@0: this._identity = new nsDOMIdentity(this); michael@0: this._identity._init(aWindow); michael@0: michael@0: this._log("init was called from " + aWindow.document.location); michael@0: michael@0: this._mm = cpmm; michael@0: michael@0: // Setup listeners for messages from parent process. michael@0: this._messages = [ michael@0: "Identity:ResetState", michael@0: "Identity:RP:Watch:OnLogin", michael@0: "Identity:RP:Watch:OnLogout", michael@0: "Identity:RP:Watch:OnReady", michael@0: "Identity:RP:Watch:OnCancel", michael@0: "Identity:RP:Watch:OnError", michael@0: "Identity:IDP:CallBeginProvisioningCallback", michael@0: "Identity:IDP:CallGenKeyPairCallback", michael@0: "Identity:IDP:CallBeginAuthenticationCallback" michael@0: ]; michael@0: this._messages.forEach(function(msgName) { michael@0: this._mm.addMessageListener(msgName, this); michael@0: }, this); michael@0: michael@0: // Setup observers so we can remove message listeners. michael@0: Services.obs.addObserver(this, "inner-window-destroyed", false); michael@0: michael@0: return this._identity; michael@0: }, michael@0: michael@0: // Private. michael@0: _log: function nsDOMIdentityInternal__log(msg) { michael@0: if (!this._debug) { michael@0: return; michael@0: } michael@0: dump("nsDOMIdentity (" + this._id + "): " + msg + "\n"); michael@0: }, michael@0: michael@0: // Component setup. michael@0: classID: Components.ID("{210853d9-2c97-4669-9761-b1ab9cbf57ef}"), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI( michael@0: [Ci.nsIDOMGlobalPropertyInitializer, Ci.nsIMessageListener] michael@0: ), michael@0: michael@0: classInfo: XPCOMUtils.generateCI({ michael@0: classID: Components.ID("{210853d9-2c97-4669-9761-b1ab9cbf57ef}"), michael@0: contractID: "@mozilla.org/dom/identity;1", michael@0: interfaces: [], michael@0: classDescription: "Identity DOM Implementation" michael@0: }) michael@0: }; michael@0: michael@0: function assertCorrectCallbacks(aOptions) { michael@0: // The relying party (RP) provides callbacks on watch(). michael@0: // michael@0: // In the future, BrowserID will probably only require an onlogin() michael@0: // callback, lifting the requirement that BrowserID handle logged-in michael@0: // state management for RPs. See michael@0: // https://github.com/mozilla/id-specs/blob/greenfield/browserid/api-rp.md michael@0: // michael@0: // However, Firefox Accounts requires callers to provide onlogout(), michael@0: // onready(), and also supports an onerror() callback. michael@0: michael@0: let requiredCallbacks = ["onlogin"]; michael@0: let optionalCallbacks = ["onlogout", "onready", "onerror"]; michael@0: michael@0: if (aOptions.wantIssuer == "firefox-accounts") { michael@0: requiredCallbacks = ["onlogin", "onlogout", "onready"]; michael@0: optionalCallbacks = ["onerror"]; michael@0: } michael@0: michael@0: for (let cbName of requiredCallbacks) { michael@0: if (typeof(aOptions[cbName]) != "function") { michael@0: throw new Error(cbName + " callback is required."); michael@0: } michael@0: } michael@0: michael@0: for (let cbName of optionalCallbacks) { michael@0: if (aOptions[cbName] && typeof(aOptions[cbName]) != "function") { michael@0: throw new Error(cbName + " must be a function"); michael@0: } michael@0: } michael@0: } michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsDOMIdentityInternal]);