/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * Firefox Accounts OAuth browser login helper. * Uses the WebChannel component to receive OAuth messages and complete login flows. */ this.EXPORTED_SYMBOLS = ["FxAccountsOAuthClient"]; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FxAccountsCommon.js"); XPCOMUtils.defineLazyModuleGetter(this, "WebChannel", "resource://gre/modules/WebChannel.jsm"); Cu.importGlobalProperties(["URL"]); /** * Create a new FxAccountsOAuthClient for browser some service. * * @param {Object} options Options * @param {Object} options.parameters * Opaque alphanumeric token to be included in verification links * @param {String} options.parameters.client_id * OAuth id returned from client registration * @param {String} options.parameters.state * A value that will be returned to the client as-is upon redirection * @param {String} options.parameters.oauth_uri * The FxA OAuth server uri * @param {String} options.parameters.content_uri * The FxA Content server uri * @param {String} [options.parameters.scope] * Optional. A colon-separated list of scopes that the user has authorized * @param {String} [options.parameters.action] * Optional. If provided, should be either signup, signin or force_auth. * @param {String} [options.parameters.email] * Optional. Required if options.paramters.action is 'force_auth'. * @param {Boolean} [options.parameters.keys] * Optional. If true then relier-specific encryption keys will be * available in the second argument to onComplete. * @param [authorizationEndpoint] {String} * Optional authorization endpoint for the OAuth server * @constructor */ this.FxAccountsOAuthClient = function(options) { this._validateOptions(options); this.parameters = options.parameters; this._configureChannel(); let authorizationEndpoint = options.authorizationEndpoint || "/authorization"; try { this._fxaOAuthStartUrl = new URL(this.parameters.oauth_uri + authorizationEndpoint + "?"); } catch (e) { throw new Error("Invalid OAuth Url"); } let params = this._fxaOAuthStartUrl.searchParams; params.append("client_id", this.parameters.client_id); params.append("state", this.parameters.state); params.append("scope", this.parameters.scope || ""); params.append("action", this.parameters.action || "signin"); params.append("webChannelId", this._webChannelId); if (this.parameters.keys) { params.append("keys", "true"); } // Only append if we actually have a value. if (this.parameters.email) { params.append("email", this.parameters.email); } }; this.FxAccountsOAuthClient.prototype = { /** * Function that gets called once the OAuth flow is complete. * The callback will receive an object with code and state properties. * If the keys parameter was specified and true, the callback will receive * a second argument with kAr and kBr properties. */ onComplete: null, /** * Function that gets called if there is an error during the OAuth flow, * for example due to a state mismatch. * The callback will receive an Error object as its argument. */ onError: null, /** * Configuration object that stores all OAuth parameters. */ parameters: null, /** * WebChannel that is used to communicate with content page. */ _channel: null, /** * Boolean to indicate if this client has completed an OAuth flow. */ _complete: false, /** * The url that opens the Firefox Accounts OAuth flow. */ _fxaOAuthStartUrl: null, /** * WebChannel id. */ _webChannelId: null, /** * WebChannel origin, used to validate origin of messages. */ _webChannelOrigin: null, /** * Opens a tab at "this._fxaOAuthStartUrl". * Registers a WebChannel listener and sets up a callback if needed. */ launchWebFlow: function () { if (!this._channelCallback) { this._registerChannel(); } if (this._complete) { throw new Error("This client already completed the OAuth flow"); } else { let opener = Services.wm.getMostRecentWindow("navigator:browser").gBrowser; opener.selectedTab = opener.addTab(this._fxaOAuthStartUrl.href); } }, /** * Release all resources that are in use. */ tearDown: function() { this.onComplete = null; this.onError = null; this._complete = true; this._channel.stopListening(); this._channel = null; }, /** * Configures WebChannel id and origin * * @private */ _configureChannel: function() { this._webChannelId = "oauth_" + this.parameters.client_id; // if this.parameters.content_uri is present but not a valid URI, then this will throw an error. try { this._webChannelOrigin = Services.io.newURI(this.parameters.content_uri, null, null); } catch (e) { throw e; } }, /** * Create a new channel with the WebChannelBroker, setup a callback listener * @private */ _registerChannel: function() { /** * Processes messages that are called back from the FxAccountsChannel * * @param webChannelId {String} * Command webChannelId * @param message {Object} * Command message * @param sendingContext {Object} * Channel message event sendingContext * @private */ let listener = function (webChannelId, message, sendingContext) { if (message) { let command = message.command; let data = message.data; let target = sendingContext && sendingContext.browser; switch (command) { case "oauth_complete": // validate the returned state and call onComplete or onError let result = null; let err = null; if (this.parameters.state !== data.state) { err = new Error("OAuth flow failed. State doesn't match"); } else if (this.parameters.keys && !data.keys) { err = new Error("OAuth flow failed. Keys were not returned"); } else { result = { code: data.code, state: data.state }; } // if the message asked to close the tab if (data.closeWindow && target) { // for e10s reasons the best way is to use the TabBrowser to close the tab. let tabbrowser = target.getTabBrowser(); if (tabbrowser) { let tab = tabbrowser.getTabForBrowser(target); if (tab) { tabbrowser.removeTab(tab); log.debug("OAuth flow closed the tab."); } else { log.debug("OAuth flow failed to close the tab. Tab not found in TabBrowser."); } } else { log.debug("OAuth flow failed to close the tab. TabBrowser not found."); } } if (err) { log.debug(err.message); if (this.onError) { this.onError(err); } } else { log.debug("OAuth flow completed."); if (this.onComplete) { if (this.parameters.keys) { this.onComplete(result, data.keys); } else { this.onComplete(result); } } } // onComplete will be called for this client only once // calling onComplete again will result in a failure of the OAuth flow this.tearDown(); break; } } }; this._channelCallback = listener.bind(this); this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin); this._channel.listen(this._channelCallback); log.debug("Channel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath); }, /** * Validates the required FxA OAuth parameters * * @param options {Object} * OAuth client options * @private */ _validateOptions: function (options) { if (!options || !options.parameters) { throw new Error("Missing 'parameters' configuration option"); } ["oauth_uri", "client_id", "content_uri", "state"].forEach(option => { if (!options.parameters[option]) { throw new Error("Missing 'parameters." + option + "' parameter"); } }); if (options.parameters.action == "force_auth" && !options.parameters.email) { throw new Error("parameters.email is required for action 'force_auth'"); } }, };