gecko/services/fxaccounts/FxAccountsProfileClient.jsm

255 lines
7.9 KiB
JavaScript

/* 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/. */
/**
* A client to fetch profile information for a Firefox Account.
*/
"use strict;"
this.EXPORTED_SYMBOLS = ["FxAccountsProfileClient", "FxAccountsProfileClientError"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://services-common/rest.js");
Cu.importGlobalProperties(["URL"]);
/**
* Create a new FxAccountsProfileClient to be able to fetch Firefox Account profile information.
*
* @param {Object} options Options
* @param {String} options.serverURL
* The URL of the profile server to query.
* Example: https://profile.accounts.firefox.com/v1
* @param {String} options.token
* The bearer token to access the profile server
* @constructor
*/
this.FxAccountsProfileClient = function(options) {
if (!options || !options.serverURL) {
throw new Error("Missing 'serverURL' configuration option");
}
this.fxa = options.fxa || fxAccounts;
// This is a work-around for loop that manages its own oauth tokens.
// * If |token| is in options we use it and don't attempt any token refresh
// on 401. This is for loop.
// * If |token| doesn't exist we will fetch our own token. This is for the
// normal FxAccounts methods for obtaining the profile.
// We should nuke all |this.token| support once loop moves closer to FxAccounts.
this.token = options.token;
try {
this.serverURL = new URL(options.serverURL);
} catch (e) {
throw new Error("Invalid 'serverURL'");
}
this.oauthOptions = {
scope: "profile",
};
log.debug("FxAccountsProfileClient: Initialized");
};
this.FxAccountsProfileClient.prototype = {
/**
* {nsIURI}
* The server to fetch profile information from.
*/
serverURL: null,
/**
* Interface for making remote requests.
*/
_Request: RESTRequest,
/**
* Remote request helper which abstracts authentication away.
*
* @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: {FxAccountsProfileClientError} Profile client error.
* @private
*/
_createRequest: Task.async(function* (path, method = "GET") {
let token = this.token;
if (!token) {
// tokens are cached, so getting them each request is cheap.
token = yield this.fxa.getOAuthToken(this.oauthOptions);
}
try {
return (yield this._rawRequest(path, method, token));
} catch (ex if ex instanceof FxAccountsProfileClientError && ex.code == 401) {
// If this object was instantiated with a token then we don't refresh it.
if (this.token) {
throw ex;
}
// it's an auth error - assume our token expired and retry.
log.info("Fetching the profile returned a 401 - revoking our token and retrying");
yield this.fxa.removeCachedOAuthToken({token});
token = yield this.fxa.getOAuthToken(this.oauthOptions);
// and try with the new token - if that also fails then we fail after
// revoking the token.
try {
return (yield this._rawRequest(path, method, token));
} catch (ex if ex instanceof FxAccountsProfileClientError && ex.code == 401) {
log.info("Retry fetching the profile still returned a 401 - revoking our token and failing");
yield this.fxa.removeCachedOAuthToken({token});
throw ex;
}
}
}),
/**
* Remote "raw" request helper - doesn't handle auth errors and tokens.
*
* @param {String} path
* Profile server path, i.e "/profile".
* @param {String} method
* Type of request, i.e "GET".
* @param {String} token
* @return Promise
* Resolves: {Object} Successful response from the Profile server.
* Rejects: {FxAccountsProfileClientError} Profile client error.
* @private
*/
_rawRequest: function(path, method, token) {
return new Promise((resolve, reject) => {
let profileDataUrl = this.serverURL + path;
let request = new this._Request(profileDataUrl);
method = method.toUpperCase();
request.setHeader("Authorization", "Bearer " + token);
request.setHeader("Accept", "application/json");
request.onComplete = function (error) {
if (error) {
return reject(new FxAccountsProfileClientError({
error: ERROR_NETWORK,
errno: ERRNO_NETWORK,
message: error.toString(),
}));
}
let body = null;
try {
body = JSON.parse(request.response.body);
} catch (e) {
return reject(new FxAccountsProfileClientError({
error: ERROR_PARSE,
errno: ERRNO_PARSE,
code: request.response.status,
message: request.response.body,
}));
}
// "response.success" means status code is 200
if (request.response.success) {
return resolve(body);
} else {
return reject(new FxAccountsProfileClientError({
error: body.error || ERROR_UNKNOWN,
errno: body.errno || ERRNO_UNKNOWN_ERROR,
code: request.response.status,
message: body.message || body,
}));
}
};
if (method === "GET") {
request.get();
} else {
// method not supported
return reject(new FxAccountsProfileClientError({
error: ERROR_NETWORK,
errno: ERRNO_NETWORK,
code: ERROR_CODE_METHOD_NOT_ALLOWED,
message: ERROR_MSG_METHOD_NOT_ALLOWED,
}));
}
});
},
/**
* Retrieve user's profile from the server
*
* @return Promise
* Resolves: {Object} Successful response from the '/profile' endpoint.
* Rejects: {FxAccountsProfileClientError} profile client error.
*/
fetchProfile: function () {
log.debug("FxAccountsProfileClient: Requested profile");
return this._createRequest("/profile", "GET");
},
/**
* Retrieve user's profile from the server
*
* @return Promise
* Resolves: {Object} Successful response from the '/avatar' endpoint.
* Rejects: {FxAccountsProfileClientError} profile client error.
*/
fetchProfileImage: function () {
log.debug("FxAccountsProfileClient: Requested avatar");
return this._createRequest("/avatar", "GET");
}
};
/**
* 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
*/
this.FxAccountsProfileClientError = function(details) {
details = details || {};
this.name = "FxAccountsProfileClientError";
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
*/
FxAccountsProfileClientError.prototype._toStringFields = function() {
return {
name: this.name,
code: this.code,
errno: this.errno,
error: this.error,
message: this.message,
};
};
/**
* String representation of a profile client error
*
* @returns {String}
*/
FxAccountsProfileClientError.prototype.toString = function() {
return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";
};