2014-08-28 14:10:00 +00:00
|
|
|
/* 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.
|
|
|
|
*/
|
2017-10-15 18:50:30 +00:00
|
|
|
"use strict;";
|
2014-08-28 14:10:00 +00:00
|
|
|
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
|
|
"FxAccountsProfileClient",
|
|
|
|
"FxAccountsProfileClientError",
|
|
|
|
];
|
|
|
|
|
2019-02-21 20:58:04 +00:00
|
|
|
const {
|
|
|
|
ERRNO_NETWORK,
|
|
|
|
ERRNO_PARSE,
|
|
|
|
ERRNO_UNKNOWN_ERROR,
|
|
|
|
ERROR_CODE_METHOD_NOT_ALLOWED,
|
|
|
|
ERROR_MSG_METHOD_NOT_ALLOWED,
|
|
|
|
ERROR_NETWORK,
|
|
|
|
ERROR_PARSE,
|
|
|
|
ERROR_UNKNOWN,
|
|
|
|
log,
|
|
|
|
SCOPE_PROFILE,
|
2020-07-27 01:46:50 +00:00
|
|
|
SCOPE_PROFILE_WRITE,
|
2019-02-21 20:58:04 +00:00
|
|
|
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
2019-01-17 18:18:31 +00:00
|
|
|
const { fxAccounts } = ChromeUtils.import(
|
|
|
|
"resource://gre/modules/FxAccounts.jsm"
|
|
|
|
);
|
|
|
|
const { RESTRequest } = ChromeUtils.import(
|
|
|
|
"resource://services-common/rest.js"
|
|
|
|
);
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
2019-07-05 08:58:22 +00:00
|
|
|
);
|
2014-08-28 14:10:00 +00:00
|
|
|
|
2018-05-26 00:02:29 +00:00
|
|
|
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
|
2014-08-28 14:10:00 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
var FxAccountsProfileClient = function(options) {
|
2015-06-18 09:28:11 +00:00
|
|
|
if (!options || !options.serverURL) {
|
|
|
|
throw new Error("Missing 'serverURL' configuration option");
|
2014-08-28 14:10:00 +00:00
|
|
|
}
|
|
|
|
|
2019-09-12 02:08:50 +00:00
|
|
|
this.fxai = options.fxai || fxAccounts._internal;
|
2015-06-18 09:28:11 +00:00
|
|
|
|
2014-08-28 14:10:00 +00:00
|
|
|
try {
|
|
|
|
this.serverURL = new URL(options.serverURL);
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error("Invalid 'serverURL'");
|
|
|
|
}
|
|
|
|
log.debug("FxAccountsProfileClient: Initialized");
|
|
|
|
};
|
|
|
|
|
2020-01-29 21:50:04 +00:00
|
|
|
FxAccountsProfileClient.prototype = {
|
2014-08-28 14:10:00 +00:00
|
|
|
/**
|
|
|
|
* {nsIURI}
|
|
|
|
* The server to fetch profile information from.
|
|
|
|
*/
|
|
|
|
serverURL: null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Interface for making remote requests.
|
|
|
|
*/
|
|
|
|
_Request: RESTRequest,
|
|
|
|
|
|
|
|
/**
|
2015-06-18 09:28:11 +00:00
|
|
|
* Remote request helper which abstracts authentication away.
|
2014-08-28 14:10:00 +00:00
|
|
|
*
|
|
|
|
* @param {String} path
|
|
|
|
* Profile server path, i.e "/profile".
|
|
|
|
* @param {String} [method]
|
2020-07-27 01:46:50 +00:00
|
|
|
* Type of request, e.g. "GET".
|
2017-01-09 20:56:28 +00:00
|
|
|
* @param {String} [etag]
|
|
|
|
* Optional ETag used for caching purposes.
|
2020-07-27 01:46:50 +00:00
|
|
|
* @param {Object} [body]
|
|
|
|
* Optional request body, to be sent as application/json.
|
2014-08-28 14:10:00 +00:00
|
|
|
* @return Promise
|
2017-01-09 20:56:28 +00:00
|
|
|
* Resolves: {body: Object, etag: Object} Successful response from the Profile server.
|
2014-08-28 14:10:00 +00:00
|
|
|
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
|
|
|
* @private
|
|
|
|
*/
|
2020-07-27 01:46:50 +00:00
|
|
|
async _createRequest(path, method = "GET", etag = null, body = null) {
|
|
|
|
method = method.toUpperCase();
|
|
|
|
let token = await this._getTokenForRequest(method);
|
2015-06-18 09:28:11 +00:00
|
|
|
try {
|
2020-07-27 01:46:50 +00:00
|
|
|
return await this._rawRequest(path, method, token, etag, body);
|
2016-01-26 15:07:56 +00:00
|
|
|
} catch (ex) {
|
2016-12-12 21:50:10 +00:00
|
|
|
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
2016-01-26 15:07:56 +00:00
|
|
|
throw ex;
|
|
|
|
}
|
2015-06-18 09:28:11 +00:00
|
|
|
// it's an auth error - assume our token expired and retry.
|
|
|
|
log.info(
|
|
|
|
"Fetching the profile returned a 401 - revoking our token and retrying"
|
|
|
|
);
|
2019-09-12 02:08:50 +00:00
|
|
|
await this.fxai.removeCachedOAuthToken({ token });
|
2020-07-27 01:46:50 +00:00
|
|
|
token = await this._getTokenForRequest(method);
|
2015-06-18 09:28:11 +00:00
|
|
|
// and try with the new token - if that also fails then we fail after
|
|
|
|
// revoking the token.
|
|
|
|
try {
|
2020-07-27 01:46:50 +00:00
|
|
|
return await this._rawRequest(path, method, token, etag, body);
|
2016-01-26 15:07:56 +00:00
|
|
|
} catch (ex) {
|
2016-12-12 21:50:10 +00:00
|
|
|
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
2016-01-26 15:07:56 +00:00
|
|
|
throw ex;
|
|
|
|
}
|
2015-06-18 09:28:11 +00:00
|
|
|
log.info(
|
|
|
|
"Retry fetching the profile still returned a 401 - revoking our token and failing"
|
|
|
|
);
|
2019-09-12 02:08:50 +00:00
|
|
|
await this.fxai.removeCachedOAuthToken({ token });
|
2015-06-18 09:28:11 +00:00
|
|
|
throw ex;
|
|
|
|
}
|
|
|
|
}
|
2017-05-02 23:29:33 +00:00
|
|
|
},
|
2015-06-18 09:28:11 +00:00
|
|
|
|
2020-07-27 01:46:50 +00:00
|
|
|
/**
|
|
|
|
* Helper to get an OAuth token for a request.
|
|
|
|
*
|
|
|
|
* OAuth tokens are cached, so it's fine to call this for each request.
|
|
|
|
*
|
|
|
|
* @param {String} [method]
|
|
|
|
* Type of request, i.e "GET".
|
|
|
|
* @return Promise
|
|
|
|
* Resolves: Object containing "scope", "token" and "key" properties
|
|
|
|
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
async _getTokenForRequest(method) {
|
|
|
|
let scope = SCOPE_PROFILE;
|
|
|
|
if (method === "POST") {
|
|
|
|
scope = SCOPE_PROFILE_WRITE;
|
|
|
|
}
|
|
|
|
return this.fxai.getOAuthToken({ scope });
|
|
|
|
},
|
|
|
|
|
2015-06-18 09:28:11 +00:00
|
|
|
/**
|
|
|
|
* 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
|
2017-01-09 20:56:28 +00:00
|
|
|
* @param {String} etag
|
2020-07-27 01:46:50 +00:00
|
|
|
* @param {Object} payload
|
|
|
|
* The payload of the request, if any.
|
2015-06-18 09:28:11 +00:00
|
|
|
* @return Promise
|
2017-02-07 17:55:01 +00:00
|
|
|
* Resolves: {body: Object, etag: Object} Successful response from the Profile server
|
|
|
|
or null if 304 is hit (same ETag).
|
2015-06-18 09:28:11 +00:00
|
|
|
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
|
|
|
* @private
|
|
|
|
*/
|
2020-07-27 01:46:50 +00:00
|
|
|
async _rawRequest(path, method, token, etag = null, payload = null) {
|
2018-03-15 03:34:50 +00:00
|
|
|
let profileDataUrl = this.serverURL + path;
|
|
|
|
let request = new this._Request(profileDataUrl);
|
|
|
|
|
|
|
|
request.setHeader("Authorization", "Bearer " + token);
|
|
|
|
request.setHeader("Accept", "application/json");
|
|
|
|
if (etag) {
|
|
|
|
request.setHeader("If-None-Match", etag);
|
|
|
|
}
|
2014-08-28 14:10:00 +00:00
|
|
|
|
2020-07-27 01:46:50 +00:00
|
|
|
if (method != "GET" && method != "POST") {
|
2018-03-15 03:34:50 +00:00
|
|
|
// method not supported
|
|
|
|
throw new FxAccountsProfileClientError({
|
|
|
|
error: ERROR_NETWORK,
|
|
|
|
errno: ERRNO_NETWORK,
|
|
|
|
code: ERROR_CODE_METHOD_NOT_ALLOWED,
|
|
|
|
message: ERROR_MSG_METHOD_NOT_ALLOWED,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
try {
|
2020-07-27 01:46:50 +00:00
|
|
|
await request.dispatch(method, payload);
|
2018-03-15 03:34:50 +00:00
|
|
|
} catch (error) {
|
|
|
|
throw new FxAccountsProfileClientError({
|
|
|
|
error: ERROR_NETWORK,
|
|
|
|
errno: ERRNO_NETWORK,
|
|
|
|
message: error.toString(),
|
|
|
|
});
|
|
|
|
}
|
2014-08-28 14:10:00 +00:00
|
|
|
|
2018-03-15 03:34:50 +00:00
|
|
|
let body = null;
|
|
|
|
try {
|
|
|
|
if (request.response.status == 304) {
|
|
|
|
return null;
|
2014-08-28 14:10:00 +00:00
|
|
|
}
|
2018-03-15 03:34:50 +00:00
|
|
|
body = JSON.parse(request.response.body);
|
|
|
|
} catch (e) {
|
|
|
|
throw 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) {
|
|
|
|
throw new FxAccountsProfileClientError({
|
|
|
|
error: body.error || ERROR_UNKNOWN,
|
|
|
|
errno: body.errno || ERRNO_UNKNOWN_ERROR,
|
|
|
|
code: request.response.status,
|
|
|
|
message: body.message || body,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
body,
|
|
|
|
etag: request.response.headers.etag,
|
|
|
|
};
|
2014-08-28 14:10:00 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve user's profile from the server
|
|
|
|
*
|
2017-01-09 20:56:28 +00:00
|
|
|
* @param {String} [etag]
|
|
|
|
* Optional ETag used for caching purposes. (may generate a 304 exception)
|
2014-08-28 14:10:00 +00:00
|
|
|
* @return Promise
|
2017-01-09 20:56:28 +00:00
|
|
|
* Resolves: {body: Object, etag: Object} Successful response from the '/profile' endpoint.
|
2014-08-28 14:10:00 +00:00
|
|
|
* Rejects: {FxAccountsProfileClientError} profile client error.
|
|
|
|
*/
|
2017-01-09 20:56:28 +00:00
|
|
|
fetchProfile(etag) {
|
2014-08-28 14:10:00 +00:00
|
|
|
log.debug("FxAccountsProfileClient: Requested profile");
|
2017-01-09 20:56:28 +00:00
|
|
|
return this._createRequest("/profile", "GET", etag);
|
2014-08-28 14:10:00 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2014-09-11 23:02:20 +00:00
|
|
|
var FxAccountsProfileClientError = function(details) {
|
2014-08-28 14:10:00 +00:00
|
|
|
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()) + ")";
|
|
|
|
};
|