1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * A client to fetch profile information for a Firefox Account.
14 ERROR_CODE_METHOD_NOT_ALLOWED,
15 ERROR_MSG_METHOD_NOT_ALLOWED,
22 } = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
23 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
25 const fxAccounts = getFxAccountsSingleton();
26 import { RESTRequest } from "resource://services-common/rest.sys.mjs";
29 * Create a new FxAccountsProfileClient to be able to fetch Firefox Account profile information.
31 * @param {Object} options Options
32 * @param {String} options.serverURL
33 * The URL of the profile server to query.
34 * Example: https://profile.accounts.firefox.com/v1
35 * @param {String} options.token
36 * The bearer token to access the profile server
39 export var FxAccountsProfileClient = function(options) {
40 if (!options || !options.serverURL) {
41 throw new Error("Missing 'serverURL' configuration option");
44 this.fxai = options.fxai || fxAccounts._internal;
47 this.serverURL = new URL(options.serverURL);
49 throw new Error("Invalid 'serverURL'");
51 log.debug("FxAccountsProfileClient: Initialized");
54 FxAccountsProfileClient.prototype = {
57 * The server to fetch profile information from.
62 * Interface for making remote requests.
64 _Request: RESTRequest,
67 * Remote request helper which abstracts authentication away.
69 * @param {String} path
70 * Profile server path, i.e "/profile".
71 * @param {String} [method]
72 * Type of request, e.g. "GET".
73 * @param {String} [etag]
74 * Optional ETag used for caching purposes.
75 * @param {Object} [body]
76 * Optional request body, to be sent as application/json.
78 * Resolves: {body: Object, etag: Object} Successful response from the Profile server.
79 * Rejects: {FxAccountsProfileClientError} Profile client error.
82 async _createRequest(path, method = "GET", etag = null, body = null) {
83 method = method.toUpperCase();
84 let token = await this._getTokenForRequest(method);
86 return await this._rawRequest(path, method, token, etag, body);
88 if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
91 // it's an auth error - assume our token expired and retry.
93 "Fetching the profile returned a 401 - revoking our token and retrying"
95 await this.fxai.removeCachedOAuthToken({ token });
96 token = await this._getTokenForRequest(method);
97 // and try with the new token - if that also fails then we fail after
98 // revoking the token.
100 return await this._rawRequest(path, method, token, etag, body);
102 if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
106 "Retry fetching the profile still returned a 401 - revoking our token and failing"
108 await this.fxai.removeCachedOAuthToken({ token });
115 * Helper to get an OAuth token for a request.
117 * OAuth tokens are cached, so it's fine to call this for each request.
119 * @param {String} [method]
120 * Type of request, i.e "GET".
122 * Resolves: Object containing "scope", "token" and "key" properties
123 * Rejects: {FxAccountsProfileClientError} Profile client error.
126 async _getTokenForRequest(method) {
127 let scope = SCOPE_PROFILE;
128 if (method === "POST") {
129 scope = SCOPE_PROFILE_WRITE;
131 return this.fxai.getOAuthToken({ scope });
135 * Remote "raw" request helper - doesn't handle auth errors and tokens.
137 * @param {String} path
138 * Profile server path, i.e "/profile".
139 * @param {String} method
140 * Type of request, i.e "GET".
141 * @param {String} token
142 * @param {String} etag
143 * @param {Object} payload
144 * The payload of the request, if any.
146 * Resolves: {body: Object, etag: Object} Successful response from the Profile server
147 or null if 304 is hit (same ETag).
148 * Rejects: {FxAccountsProfileClientError} Profile client error.
151 async _rawRequest(path, method, token, etag = null, payload = null) {
152 let profileDataUrl = this.serverURL + path;
153 let request = new this._Request(profileDataUrl);
155 request.setHeader("Authorization", "Bearer " + token);
156 request.setHeader("Accept", "application/json");
158 request.setHeader("If-None-Match", etag);
161 if (method != "GET" && method != "POST") {
162 // method not supported
163 throw new FxAccountsProfileClientError({
164 error: ERROR_NETWORK,
165 errno: ERRNO_NETWORK,
166 code: ERROR_CODE_METHOD_NOT_ALLOWED,
167 message: ERROR_MSG_METHOD_NOT_ALLOWED,
171 await request.dispatch(method, payload);
173 throw new FxAccountsProfileClientError({
174 error: ERROR_NETWORK,
175 errno: ERRNO_NETWORK,
176 message: error.toString(),
182 if (request.response.status == 304) {
185 body = JSON.parse(request.response.body);
187 throw new FxAccountsProfileClientError({
190 code: request.response.status,
191 message: request.response.body,
195 // "response.success" means status code is 200
196 if (!request.response.success) {
197 throw new FxAccountsProfileClientError({
198 error: body.error || ERROR_UNKNOWN,
199 errno: body.errno || ERRNO_UNKNOWN_ERROR,
200 code: request.response.status,
201 message: body.message || body,
206 etag: request.response.headers.etag,
211 * Retrieve user's profile from the server
213 * @param {String} [etag]
214 * Optional ETag used for caching purposes. (may generate a 304 exception)
216 * Resolves: {body: Object, etag: Object} Successful response from the '/profile' endpoint.
217 * Rejects: {FxAccountsProfileClientError} profile client error.
220 log.debug("FxAccountsProfileClient: Requested profile");
221 return this._createRequest("/profile", "GET", etag);
226 * Normalized profile client errors
227 * @param {Object} [details]
228 * Error details object
229 * @param {number} [details.code]
231 * @param {number} [details.errno]
233 * @param {String} [details.error]
235 * @param {String|null} [details.message]
239 export var FxAccountsProfileClientError = function(details) {
240 details = details || {};
242 this.name = "FxAccountsProfileClientError";
243 this.code = details.code || null;
244 this.errno = details.errno || ERRNO_UNKNOWN_ERROR;
245 this.error = details.error || ERROR_UNKNOWN;
246 this.message = details.message || null;
250 * Returns error object properties
252 * @returns {{name: *, code: *, errno: *, error: *, message: *}}
255 FxAccountsProfileClientError.prototype._toStringFields = function() {
261 message: this.message,
266 * String representation of a profile client error
270 FxAccountsProfileClientError.prototype.toString = function() {
271 return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";