services/fxaccounts/FxAccountsOAuthGrantClient.jsm
author Ryan Hunt <rhunt@eqrion.net>
Mon, 25 Feb 2019 16:11:11 -0600
changeset 522780 76dd23f024d5cac080443bf7872ef7103c429542
parent 521194 9dea142f2cc02912cc8bdaf2bddc5debed25a6d6
child 544264 854373e1ab9e7a438d9a666d76d4cfd227a7e0ae
permissions -rw-r--r--
Bug 1523969 part 19 - Move method definition inline comments to new line in 'netwerk/'. r=dragana Differential Revision: https://phabricator.services.mozilla.com/D21120

/* 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 Grant Client allows clients to obtain
 * an OAuth token from a BrowserID assertion. Only certain client
 * IDs support this privilage.
 */

var EXPORTED_SYMBOLS = ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"];

const {ERRNO_NETWORK, ERRNO_PARSE, ERRNO_UNKNOWN_ERROR, ERROR_CODE_METHOD_NOT_ALLOWED, ERROR_MSG_METHOD_NOT_ALLOWED, ERROR_NETWORK, ERROR_PARSE, ERROR_UNKNOWN, OAUTH_SERVER_ERRNO_OFFSET, log} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
const {RESTRequest} = ChromeUtils.import("resource://services-common/rest.js");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);

const AUTH_ENDPOINT = "/authorization";
const DESTROY_ENDPOINT = "/destroy";
// This is the same pref that's used by FxAccounts.jsm.
const ALLOW_HTTP_PREF = "identity.fxaccounts.allowHttp";

/**
 * Create a new FxAccountsOAuthClient for browser some service.
 *
 * @param {Object} options Options
 *   @param {Object} options.parameters
 *     @param {String} options.parameters.client_id
 *     OAuth id returned from client registration
 *     @param {String} options.parameters.serverURL
 *     The FxA OAuth server URL
 *   @param [authorizationEndpoint] {String}
 *   Optional authorization endpoint for the OAuth server
 * @constructor
 */
var FxAccountsOAuthGrantClient = function(options) {
  this._validateOptions(options);
  this.parameters = options;

  try {
    this.serverURL = new URL(this.parameters.serverURL);
  } catch (e) {
    throw new Error("Invalid 'serverURL'");
  }

  let forceHTTPS = !Services.prefs.getBoolPref(ALLOW_HTTP_PREF, false);
  if (forceHTTPS && this.serverURL.protocol != "https:") {
    throw new Error("'serverURL' must be HTTPS");
  }

  log.debug("FxAccountsOAuthGrantClient Initialized");
};

this.FxAccountsOAuthGrantClient.prototype = {

  /**
   * Retrieves an OAuth access token for the signed in user
   *
   * @param {Object} assertion BrowserID assertion
   * @param {String} scope OAuth scope
   * @return Promise
   *        Resolves: {Object} Object with access_token property
   */
  getTokenFromAssertion(assertion, scope) {
    if (!assertion) {
      throw new Error("Missing 'assertion' parameter");
    }
    if (!scope) {
      throw new Error("Missing 'scope' parameter");
    }
    let params = {
      scope,
      client_id: this.parameters.client_id,
      assertion,
      response_type: "token",
    };

    return this._createRequest(AUTH_ENDPOINT, "POST", params);
  },

  /**
   * Retrieves an OAuth authorization code using an assertion
   *
   * @param {Object} assertion BrowserID assertion
   * @param {Object} options
   * @param options.client_id
   * @param options.state
   * @param options.scope
   * @param options.access_type
   * @param options.code_challenge_method
   * @param options.code_challenge
   * @param [options.keys_jwe]
   * @returns {Promise<Object>} Object containing "code" and "state" properties.
   */
  authorizeCodeFromAssertion(assertion, options) {
    if (!assertion) {
      throw new Error("Missing 'assertion' parameter");
    }
    const {client_id, state, scope, access_type, code_challenge, code_challenge_method, keys_jwe} = options;
    const params = {
      assertion,
      client_id,
      response_type: "code",
      state,
      scope,
      access_type,
      code_challenge,
      code_challenge_method,
    };
    if (keys_jwe) {
      params.keys_jwe = keys_jwe;
    }
    return this._createRequest(AUTH_ENDPOINT, "POST", params);
  },

  /**
   * Destroys a previously fetched OAuth access token.
   *
   * @param {String} token The previously fetched token
   * @return Promise
   *        Resolves: {Object} with the server response, which is typically
   *        ignored.
   */
  destroyToken(token) {
    if (!token) {
      throw new Error("Missing 'token' parameter");
    }
    let params = {
      token,
    };

    return this._createRequest(DESTROY_ENDPOINT, "POST", params);
  },

  /**
   * Validates the required FxA OAuth parameters
   *
   * @param options {Object}
   *        OAuth client options
   * @private
   */
  _validateOptions(options) {
    if (!options) {
      throw new Error("Missing configuration options");
    }

    ["serverURL", "client_id"].forEach(option => {
      if (!options[option]) {
        throw new Error("Missing '" + option + "' parameter");
      }
    });
  },

  /**
   * Interface for making remote requests.
   */
  _Request: RESTRequest,

  /**
   * Remote request helper
   *
   * @param {String} path
   *        Profile server path, i.e "/profile".
   * @param {String} [method]
   *        Type of request, i.e "GET".
   * @return Promise
   *         Resolves: {Object} Successful response from the Profile server.
   *         Rejects: {FxAccountsOAuthGrantClientError} Profile client error.
   * @private
   */
  async _createRequest(path, method = "POST", params) {
    let profileDataUrl = this.serverURL + path;
    let request = new this._Request(profileDataUrl);
    method = method.toUpperCase();

    request.setHeader("Accept", "application/json");
    request.setHeader("Content-Type", "application/json");

    if (method != "POST") {
      throw new FxAccountsOAuthGrantClientError({
        error: ERROR_NETWORK,
        errno: ERRNO_NETWORK,
        code: ERROR_CODE_METHOD_NOT_ALLOWED,
        message: ERROR_MSG_METHOD_NOT_ALLOWED,
      });
    }

    try {
      await request.post(params);
    } catch (error) {
      throw new FxAccountsOAuthGrantClientError({
        error: ERROR_NETWORK,
        errno: ERRNO_NETWORK,
        message: error.toString(),
      });
    }

    let body = null;
    try {
      body = JSON.parse(request.response.body);
    } catch (e) {
      throw new FxAccountsOAuthGrantClientError({
        error: ERROR_PARSE,
        errno: ERRNO_PARSE,
        code: request.response.status,
        message: request.response.body,
      });
    }

    if (request.response.success) {
      return body;
    }

    if (typeof body.errno === "number") {
      // Offset oauth server errnos to avoid conflict with other FxA server errnos
      body.errno += OAUTH_SERVER_ERRNO_OFFSET;
    } else if (body.errno) {
      body.errno = ERRNO_UNKNOWN_ERROR;
    }
    throw new FxAccountsOAuthGrantClientError(body);
  },

};

/**
 * Normalized profile client errors
 * @param {Object} [details]
 *        Error details object
 *   @param {number} [details.code]
 *          Error code
 *   @param {number} [details.errno]
 *          Error number
 *   @param {String} [details.error]
 *          Error description
 *   @param {String|null} [details.message]
 *          Error message
 * @constructor
 */
var FxAccountsOAuthGrantClientError = function(details) {
  details = details || {};

  this.name = "FxAccountsOAuthGrantClientError";
  this.code = details.code || null;
  this.errno = details.errno || ERRNO_UNKNOWN_ERROR;
  this.error = details.error || ERROR_UNKNOWN;
  this.message = details.message || null;
};

/**
 * Returns error object properties
 *
 * @returns {{name: *, code: *, errno: *, error: *, message: *}}
 * @private
 */
FxAccountsOAuthGrantClientError.prototype._toStringFields = function() {
  return {
    name: this.name,
    code: this.code,
    errno: this.errno,
    error: this.error,
    message: this.message,
  };
};

/**
 * String representation of a oauth grant client error
 *
 * @returns {String}
 */
FxAccountsOAuthGrantClientError.prototype.toString = function() {
  return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";
};