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 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
24 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
26 const fxAccounts = getFxAccountsSingleton();
27 import { RESTRequest } from "resource://services-common/rest.sys.mjs";
30 * Create a new FxAccountsProfileClient to be able to fetch Firefox Account profile information.
32 * @param {Object} options Options
33 * @param {String} options.serverURL
34 * The URL of the profile server to query.
35 * Example: https://profile.accounts.firefox.com/v1
36 * @param {String} options.token
37 * The bearer token to access the profile server
40 export var FxAccountsProfileClient = function (options) {
41 if (!options || !options.serverURL) {
42 throw new Error("Missing 'serverURL' configuration option");
45 this.fxai = options.fxai || fxAccounts._internal;
48 this.serverURL = new URL(options.serverURL);
50 throw new Error("Invalid 'serverURL'");
52 log.debug("FxAccountsProfileClient: Initialized");
55 FxAccountsProfileClient.prototype = {
58 * The server to fetch profile information from.
63 * Interface for making remote requests.
65 _Request: RESTRequest,
68 * Remote request helper which abstracts authentication away.
70 * @param {String} path
71 * Profile server path, i.e "/profile".
72 * @param {String} [method]
73 * Type of request, e.g. "GET".
74 * @param {String} [etag]
75 * Optional ETag used for caching purposes.
76 * @param {Object} [body]
77 * Optional request body, to be sent as application/json.
79 * Resolves: {body: Object, etag: Object} Successful response from the Profile server.
80 * Rejects: {FxAccountsProfileClientError} Profile client error.
83 async _createRequest(path, method = "GET", etag = null, body = null) {
84 method = method.toUpperCase();
85 let token = await this._getTokenForRequest(method);
87 return await this._rawRequest(path, method, token, etag, body);
89 if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
92 // it's an auth error - assume our token expired and retry.
94 "Fetching the profile returned a 401 - revoking our token and retrying"
96 await this.fxai.removeCachedOAuthToken({ token });
97 token = await this._getTokenForRequest(method);
98 // and try with the new token - if that also fails then we fail after
99 // revoking the token.
101 return await this._rawRequest(path, method, token, etag, body);
103 if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
107 "Retry fetching the profile still returned a 401 - revoking our token and failing"
109 await this.fxai.removeCachedOAuthToken({ token });
116 * Helper to get an OAuth token for a request.
118 * OAuth tokens are cached, so it's fine to call this for each request.
120 * @param {String} [method]
121 * Type of request, i.e "GET".
123 * Resolves: Object containing "scope", "token" and "key" properties
124 * Rejects: {FxAccountsProfileClientError} Profile client error.
127 async _getTokenForRequest(method) {
128 let scope = SCOPE_PROFILE;
129 if (method === "POST") {
130 scope = SCOPE_PROFILE_WRITE;
132 return this.fxai.getOAuthToken({ scope });
136 * Remote "raw" request helper - doesn't handle auth errors and tokens.
138 * @param {String} path
139 * Profile server path, i.e "/profile".
140 * @param {String} method
141 * Type of request, i.e "GET".
142 * @param {String} token
143 * @param {String} etag
144 * @param {Object} payload
145 * The payload of the request, if any.
147 * Resolves: {body: Object, etag: Object} Successful response from the Profile server
148 or null if 304 is hit (same ETag).
149 * Rejects: {FxAccountsProfileClientError} Profile client error.
152 async _rawRequest(path, method, token, etag = null, payload = null) {
153 let profileDataUrl = this.serverURL + path;
154 let request = new this._Request(profileDataUrl);
156 request.setHeader("Authorization", "Bearer " + token);
157 request.setHeader("Accept", "application/json");
159 request.setHeader("If-None-Match", etag);
162 if (method != "GET" && method != "POST") {
163 // method not supported
164 throw new FxAccountsProfileClientError({
165 error: ERROR_NETWORK,
166 errno: ERRNO_NETWORK,
167 code: ERROR_CODE_METHOD_NOT_ALLOWED,
168 message: ERROR_MSG_METHOD_NOT_ALLOWED,
172 await request.dispatch(method, payload);
174 throw new FxAccountsProfileClientError({
175 error: ERROR_NETWORK,
176 errno: ERRNO_NETWORK,
177 message: error.toString(),
183 if (request.response.status == 304) {
186 body = JSON.parse(request.response.body);
188 throw new FxAccountsProfileClientError({
191 code: request.response.status,
192 message: request.response.body,
196 // "response.success" means status code is 200
197 if (!request.response.success) {
198 throw new FxAccountsProfileClientError({
199 error: body.error || ERROR_UNKNOWN,
200 errno: body.errno || ERRNO_UNKNOWN_ERROR,
201 code: request.response.status,
202 message: body.message || body,
207 etag: request.response.headers.etag,
212 * Retrieve user's profile from the server
214 * @param {String} [etag]
215 * Optional ETag used for caching purposes. (may generate a 304 exception)
217 * Resolves: {body: Object, etag: Object} Successful response from the '/profile' endpoint.
218 * Rejects: {FxAccountsProfileClientError} profile client error.
221 log.debug("FxAccountsProfileClient: Requested profile");
222 return this._createRequest("/profile", "GET", etag);
227 * Normalized profile client errors
228 * @param {Object} [details]
229 * Error details object
230 * @param {number} [details.code]
232 * @param {number} [details.errno]
234 * @param {String} [details.error]
236 * @param {String|null} [details.message]
240 export var FxAccountsProfileClientError = function (details) {
241 details = details || {};
243 this.name = "FxAccountsProfileClientError";
244 this.code = details.code || null;
245 this.errno = details.errno || ERRNO_UNKNOWN_ERROR;
246 this.error = details.error || ERROR_UNKNOWN;
247 this.message = details.message || null;
251 * Returns error object properties
253 * @returns {{name: *, code: *, errno: *, error: *, message: *}}
256 FxAccountsProfileClientError.prototype._toStringFields = function () {
262 message: this.message,
267 * String representation of a profile client error
271 FxAccountsProfileClientError.prototype.toString = function () {
272 return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";