Bug 1581709 - Use sessionTokens for OAuth requests. r=vladikoff

Differential Revision: https://phabricator.services.mozilla.com/D46517

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Edouard Oger 2019-10-08 15:45:06 +00:00
parent 9c27546918
commit da3f0d0753
11 changed files with 161 additions and 926 deletions

View File

@ -67,12 +67,6 @@ ChromeUtils.defineModuleGetter(
"resource://services-crypto/jwcrypto.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FxAccountsOAuthGrantClient",
"resource://gre/modules/FxAccountsOAuthGrantClient.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FxAccountsCommands",
@ -251,8 +245,8 @@ AccountState.prototype = {
},
// Set a cached token. |tokenData| must have a 'token' element, but may also
// have additional fields (eg, it probably specifies the server to revoke
// from). The 'get' functions below return the entire |tokenData| value.
// have additional fields.
// The 'get' functions below return the entire |tokenData| value.
setCachedToken(scopeArray, tokenData) {
this._cachePreamble();
if (!tokenData.token) {
@ -461,7 +455,7 @@ class FxAccounts {
}
/**
* Retrieves an OAuth authorization code
* Retrieves an OAuth authorization code.
*
* @param {Object} options
* @param options.client_id
@ -475,8 +469,7 @@ class FxAccounts {
*/
authorizeOAuthCode(options) {
return this._withVerifiedAccountState(async state => {
const client = this._internal.oauthClient;
const oAuthURL = client.serverURL.href;
const { sessionToken } = await state.getUserAccountData(["sessionToken"]);
const params = { ...options };
if (params.keys_jwk) {
const jwk = JSON.parse(
@ -492,8 +485,10 @@ class FxAccounts {
delete params.keys_jwk;
}
try {
const assertion = await this._internal.getAssertion(oAuthURL);
return await client.authorizeCodeFromAssertion(assertion, params);
return await this._internal.fxAccountsClient.oauthAuthorize(
sessionToken,
params
);
} catch (err) {
throw this._internal._errorToErrorClass(err);
}
@ -862,20 +857,6 @@ FxAccountsInternal.prototype = {
return this._device;
},
_oauthClient: null,
get oauthClient() {
if (!this._oauthClient) {
const serverURL = Services.urlFormatter.formatURLPref(
"identity.fxaccounts.remote.oauth.uri"
);
this._oauthClient = new FxAccountsOAuthGrantClient({
serverURL,
client_id: FX_OAUTH_CLIENT_ID,
});
}
return this._oauthClient;
},
// A hook-point for tests who may want a mocked AccountState or mocked storage.
newAccountState(credentials) {
let storage = new FxAccountsStorageManager();
@ -1147,11 +1128,10 @@ FxAccountsInternal.prototype = {
},
_destroyOAuthToken(tokenData) {
let client = new FxAccountsOAuthGrantClient({
serverURL: tokenData.server,
client_id: FX_OAUTH_CLIENT_ID,
});
return client.destroyToken(tokenData.token);
return this.fxAccountsClient.oauthDestroy(
FX_OAUTH_CLIENT_ID,
tokenData.token
);
},
_destroyAllOAuthTokens(tokenInfos) {
@ -1566,17 +1546,16 @@ FxAccountsInternal.prototype = {
},
// Does the actual fetch of an oauth token for getOAuthToken()
async _doTokenFetch(client, scopeString) {
let oAuthURL = client.serverURL.href;
try {
log.debug("getOAuthToken fetching new token from", oAuthURL);
let assertion = await this.getAssertion(oAuthURL);
let result = await client.getTokenFromAssertion(assertion, scopeString);
let token = result.access_token;
return token;
} catch (err) {
throw this._errorToErrorClass(err);
}
async _doTokenFetch(scopeString) {
return this.withVerifiedAccountState(async state => {
const { sessionToken } = await state.getUserAccountData(["sessionToken"]);
const result = await this.fxAccountsClient.oauthToken(
sessionToken,
FX_OAUTH_CLIENT_ID,
scopeString
);
return result.access_token;
});
},
getOAuthToken(options = {}) {
@ -1606,8 +1585,6 @@ FxAccountsInternal.prototype = {
// Build the string we use in our "inflight" map and that we send to the
// server. Because it's used as a key in the map we sort the scopes.
let scopeString = scope.sort().join(" ");
let client = options.client || this.oauthClient;
let oAuthURL = client.serverURL.href;
// We keep a map of in-flight requests to avoid multiple promise-based
// consumers concurrently requesting the same token.
@ -1619,7 +1596,7 @@ FxAccountsInternal.prototype = {
// We need to start a new fetch and stick the promise in our in-flight map
// and remove it when it resolves.
let promise = this._doTokenFetch(client, scopeString)
let promise = this._doTokenFetch(scopeString)
.then(token => {
// As a sanity check, ensure something else hasn't raced getting a token
// of the same scope. If something has we just make noise rather than
@ -1629,7 +1606,7 @@ FxAccountsInternal.prototype = {
}
// If we got one, cache it.
if (token) {
let entry = { token, server: oAuthURL };
let entry = { token };
currentState.setCachedToken(scope, entry);
}
return token;

View File

@ -250,6 +250,78 @@ this.FxAccountsClient.prototype = {
return this._request("/account/attached_clients", "GET", credentials);
},
/**
* Retrieves an OAuth authorization code.
*
* @param String sessionTokenHex
* The session token encoded in hex
* @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`.
*/
async oauthAuthorize(sessionTokenHex, options) {
const credentials = await deriveHawkCredentials(
sessionTokenHex,
"sessionToken"
);
const body = {
client_id: options.client_id,
response_type: "code",
state: options.state,
scope: options.scope,
access_type: options.access_type,
code_challenge: options.code_challenge,
code_challenge_method: options.code_challenge_method,
};
if (options.keys_jwe) {
body.keys_jwe = options.keys_jwe;
}
return this._request("/oauth/authorization", "POST", credentials, body);
},
/**
* Obtain an OAuth access token by authenticating using a session token.
*
* @param String sessionTokenHex
* The session token encoded in hex
* @param String clientId
* @param String scopeString
* List of space-separated scopes.
* @return {Promise<Object>} Object containing an `access_token`.
*/
async oauthToken(sessionTokenHex, clientId, scopeString) {
const credentials = await deriveHawkCredentials(
sessionTokenHex,
"sessionToken"
);
const body = {
client_id: clientId,
grant_type: "fxa-credentials",
scope: scopeString,
};
return this._request("/oauth/token", "POST", credentials, body);
},
/**
* Destroy an OAuth access token or refresh token.
*
* @param String clientId
* @param String token The token to be revoked.
*/
async oauthDestroy(clientId, token) {
const body = {
client_id: clientId,
token,
};
return this._request("/oauth/destroy", "POST", null, body);
},
/**
* Query for the information required to derive
* scoped encryption keys requested by the specified OAuth client.

View File

@ -40,7 +40,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
XPCOMUtils.defineLazyPreferenceGetter(
this,
"REQUIRES_HTTPS",
// Also used in FxAccountsOAuthGrantClient.jsm.
"identity.fxaccounts.allowHttp",
false,
null,

View File

@ -1,298 +0,0 @@
/* 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()) + ")";
};

View File

@ -24,7 +24,6 @@ EXTRA_JS_MODULES += [
'FxAccountsConfig.jsm',
'FxAccountsDevice.jsm',
'FxAccountsKeys.jsm',
'FxAccountsOAuthGrantClient.jsm',
'FxAccountsPairing.jsm',
'FxAccountsPairingChannel.js',
'FxAccountsProfile.jsm',

View File

@ -23,10 +23,6 @@ const {
ONVERIFIED_NOTIFICATION,
SCOPE_OLD_SYNC,
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
const {
FxAccountsOAuthGrantClient,
FxAccountsOAuthGrantClientError,
} = ChromeUtils.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
const { PromiseUtils } = ChromeUtils.import(
"resource://gre/modules/PromiseUtils.jsm"
);
@ -1322,23 +1318,17 @@ add_test(function test_getOAuthToken() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let getTokenFromAssertionCalled = false;
let oauthTokenCalled = false;
fxa._internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://example.com/v1",
client_id: "abc123",
});
client.getTokenFromAssertion = function() {
getTokenFromAssertionCalled = true;
let client = fxa._internal.fxAccountsClient;
client.oauthToken = () => {
oauthTokenCalled = true;
return Promise.resolve({ access_token: "token" });
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile", client }).then(result => {
Assert.ok(getTokenFromAssertionCalled);
fxa.getOAuthToken({ scope: "profile" }).then(result => {
Assert.ok(oauthTokenCalled);
Assert.equal(result, "token");
run_next_test();
});
@ -1349,24 +1339,18 @@ add_test(function test_getOAuthTokenScoped() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let getTokenFromAssertionCalled = false;
let oauthTokenCalled = false;
fxa._internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://example.com/v1",
client_id: "abc123",
});
client.getTokenFromAssertion = function(assertion, scopeString) {
let client = fxa._internal.fxAccountsClient;
client.oauthToken = (_1, _2, scopeString) => {
equal(scopeString, "bar foo"); // scopes are sorted locally before request.
getTokenFromAssertionCalled = true;
oauthTokenCalled = true;
return Promise.resolve({ access_token: "token" });
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: ["foo", "bar"], client }).then(result => {
Assert.ok(getTokenFromAssertionCalled);
fxa.getOAuthToken({ scope: ["foo", "bar"] }).then(result => {
Assert.ok(oauthTokenCalled);
Assert.equal(result, "token");
run_next_test();
});
@ -1377,44 +1361,35 @@ add_task(async function test_getOAuthTokenCached() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let numTokenFromAssertionCalls = 0;
let numOauthTokenCalls = 0;
fxa._internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://example.com/v1",
client_id: "abc123",
});
client.getTokenFromAssertion = function() {
numTokenFromAssertionCalls += 1;
let client = fxa._internal.fxAccountsClient;
client.oauthToken = () => {
numOauthTokenCalls += 1;
return Promise.resolve({ access_token: "token" });
};
await fxa.setSignedInUser(alice);
let result = await fxa.getOAuthToken({
scope: "profile",
client,
service: "test-service",
});
Assert.equal(numTokenFromAssertionCalls, 1);
Assert.equal(numOauthTokenCalls, 1);
Assert.equal(result, "token");
// requesting it again should not re-fetch the token.
result = await fxa.getOAuthToken({
scope: "profile",
client,
service: "test-service",
});
Assert.equal(numTokenFromAssertionCalls, 1);
Assert.equal(numOauthTokenCalls, 1);
Assert.equal(result, "token");
// But requesting the same service and a different scope *will* get a new one.
result = await fxa.getOAuthToken({
scope: "something-else",
client,
service: "test-service",
});
Assert.equal(numTokenFromAssertionCalls, 2);
Assert.equal(numOauthTokenCalls, 2);
Assert.equal(result, "token");
});
@ -1422,52 +1397,42 @@ add_task(async function test_getOAuthTokenCachedScopeNormalization() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let numTokenFromAssertionCalls = 0;
let numOAuthTokenCalls = 0;
fxa._internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://example.com/v1",
client_id: "abc123",
});
client.getTokenFromAssertion = function() {
numTokenFromAssertionCalls += 1;
let client = fxa._internal.fxAccountsClient;
client.oauthToken = () => {
numOAuthTokenCalls += 1;
return Promise.resolve({ access_token: "token" });
};
await fxa.setSignedInUser(alice);
let result = await fxa.getOAuthToken({
scope: ["foo", "bar"],
client,
service: "test-service",
});
Assert.equal(numTokenFromAssertionCalls, 1);
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
// requesting it again with the scope array in a different order not re-fetch the token.
result = await fxa.getOAuthToken({
scope: ["bar", "foo"],
client,
service: "test-service",
});
Assert.equal(numTokenFromAssertionCalls, 1);
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
// requesting it again with the scope array in different case not re-fetch the token.
result = await fxa.getOAuthToken({
scope: ["Bar", "Foo"],
client,
service: "test-service",
});
Assert.equal(numTokenFromAssertionCalls, 1);
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
// But requesting with a new entry in the array does fetch one.
result = await fxa.getOAuthToken({
scope: ["foo", "bar", "etc"],
client,
service: "test-service",
});
Assert.equal(numTokenFromAssertionCalls, 2);
Assert.equal(numOAuthTokenCalls, 2);
Assert.equal(result, "token");
});
@ -1534,85 +1499,18 @@ add_test(function test_getOAuthToken_unverified() {
});
});
add_test(function test_getOAuthToken_network_error() {
add_test(function test_getOAuthToken_error() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
fxa._internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://example.com/v1",
client_id: "abc123",
});
client.getTokenFromAssertion = function() {
return Promise.reject(
new FxAccountsOAuthGrantClientError({
error: ERROR_NETWORK,
errno: ERRNO_NETWORK,
})
);
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile", client }).catch(err => {
Assert.equal(err.message, "NETWORK_ERROR");
Assert.equal(err.details.errno, ERRNO_NETWORK);
run_next_test();
});
});
});
add_test(function test_getOAuthToken_auth_error() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
fxa._internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://example.com/v1",
client_id: "abc123",
});
client.getTokenFromAssertion = function() {
return Promise.reject(
new FxAccountsOAuthGrantClientError({
error: ERROR_INVALID_FXA_ASSERTION,
errno: ERRNO_INVALID_FXA_ASSERTION,
})
);
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile", client }).catch(err => {
Assert.equal(err.message, "AUTH_ERROR");
Assert.equal(err.details.errno, ERRNO_INVALID_FXA_ASSERTION);
run_next_test();
});
});
});
add_test(function test_getOAuthToken_unknown_error() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
fxa._internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://example.com/v1",
client_id: "abc123",
});
client.getTokenFromAssertion = function() {
let client = fxa._internal.fxAccountsClient;
client.oauthToken = () => {
return Promise.reject("boom");
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile", client }).catch(err => {
Assert.equal(err.message, "UNKNOWN_ERROR");
fxa.getOAuthToken({ scope: "profile" }).catch(err => {
run_next_test();
});
});

View File

@ -1,319 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {
ERRNO_INVALID_FXA_ASSERTION,
ERRNO_NETWORK,
ERRNO_PARSE,
ERRNO_UNKNOWN_ERROR,
ERROR_CODE_METHOD_NOT_ALLOWED,
ERROR_MSG_METHOD_NOT_ALLOWED,
ERROR_NETWORK,
ERROR_PARSE,
ERROR_UNKNOWN,
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
const {
FxAccountsOAuthGrantClient,
FxAccountsOAuthGrantClientError,
} = ChromeUtils.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
const CLIENT_OPTIONS = {
serverURL: "https://127.0.0.1:9010/v1",
client_id: "abc123",
};
const STATUS_SUCCESS = 200;
/**
* Mock request responder
* @param {String} response
* Mocked raw response from the server
* @returns {Function}
*/
var mockResponse = function(response) {
return function() {
return {
setHeader() {},
async post() {
this.response = response;
return response;
},
};
};
};
/**
* Mock request error responder
* @param {Error} error
* Error object
* @returns {Function}
*/
var mockResponseError = function(error) {
return function() {
return {
setHeader() {},
async post() {
throw error;
},
};
};
};
add_test(function missingParams() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
try {
client.getTokenFromAssertion();
} catch (e) {
Assert.equal(e.message, "Missing 'assertion' parameter");
}
try {
client.getTokenFromAssertion("assertion");
} catch (e) {
Assert.equal(e.message, "Missing 'scope' parameter");
}
run_next_test();
});
add_test(function successfulResponse() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
success: true,
status: STATUS_SUCCESS,
body: JSON.stringify({
access_token: "http://example.com/image.jpeg",
id: "0d5c1a89b8c54580b8e3e8adadae864a",
}),
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("assertion", "scope").then(function(result) {
Assert.equal(result.access_token, "http://example.com/image.jpeg");
run_next_test();
});
});
add_test(function successfulDestroy() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
success: true,
status: STATUS_SUCCESS,
body: JSON.stringify({}),
};
client._Request = new mockResponse(response);
client.destroyToken("deadbeef").then(run_next_test);
});
add_test(function parseErrorResponse() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
success: true,
status: STATUS_SUCCESS,
body: "unexpected",
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("assertion", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, STATUS_SUCCESS);
Assert.equal(e.errno, ERRNO_PARSE);
Assert.equal(e.error, ERROR_PARSE);
Assert.equal(e.message, "unexpected");
run_next_test();
});
});
add_task(async function serverErrorResponse() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
status: 400,
body: JSON.stringify({
code: 400,
errno: 104,
error: "Bad Request",
message: "Unauthorized",
reason: "Invalid fxa assertion",
}),
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("blah", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, 400);
Assert.equal(e.errno, ERRNO_INVALID_FXA_ASSERTION);
Assert.equal(e.error, "Bad Request");
Assert.equal(e.message, "Unauthorized");
run_next_test();
});
});
add_task(async function networkErrorResponse() {
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://domain.dummy",
client_id: "abc123",
});
client.getTokenFromAssertion("assertion", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, null);
Assert.equal(e.errno, ERRNO_NETWORK);
Assert.equal(e.error, ERROR_NETWORK);
run_next_test();
});
});
add_test(function unsupportedMethod() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
return client._createRequest("/", "PUT").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, ERROR_CODE_METHOD_NOT_ALLOWED);
Assert.equal(e.errno, ERRNO_NETWORK);
Assert.equal(e.error, ERROR_NETWORK);
Assert.equal(e.message, ERROR_MSG_METHOD_NOT_ALLOWED);
run_next_test();
});
});
add_test(function onCompleteRequestError() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
client._Request = new mockResponseError(new Error("onComplete error"));
client.getTokenFromAssertion("assertion", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, null);
Assert.equal(e.errno, ERRNO_NETWORK);
Assert.equal(e.error, ERROR_NETWORK);
Assert.equal(e.message, "Error: onComplete error");
run_next_test();
});
});
add_test(function incorrectErrno() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
status: 400,
body: JSON.stringify({
code: 400,
errno: "bad errno",
error: "Bad Request",
message: "Unauthorized",
reason: "Invalid fxa assertion",
}),
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("blah", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, 400);
Assert.equal(e.errno, ERRNO_UNKNOWN_ERROR);
Assert.equal(e.error, "Bad Request");
Assert.equal(e.message, "Unauthorized");
run_next_test();
});
});
add_test(function constructorTests() {
validationHelper(undefined, "Error: Missing configuration options");
validationHelper({}, "Error: Missing 'serverURL' parameter");
validationHelper(
{ serverURL: "https://example.com" },
"Error: Missing 'client_id' parameter"
);
validationHelper(
{ serverURL: "https://example.com" },
"Error: Missing 'client_id' parameter"
);
validationHelper(
{ client_id: "123ABC" },
"Error: Missing 'serverURL' parameter"
);
validationHelper(
{ client_id: "123ABC", serverURL: "http://example.com" },
"Error: 'serverURL' must be HTTPS"
);
try {
Services.prefs.setBoolPref("identity.fxaccounts.allowHttp", true);
validationHelper(
{ client_id: "123ABC", serverURL: "http://example.com" },
null
);
} finally {
Services.prefs.clearUserPref("identity.fxaccounts.allowHttp");
}
run_next_test();
});
add_test(function errorTests() {
let error1 = new FxAccountsOAuthGrantClientError();
Assert.equal(error1.name, "FxAccountsOAuthGrantClientError");
Assert.equal(error1.code, null);
Assert.equal(error1.errno, ERRNO_UNKNOWN_ERROR);
Assert.equal(error1.error, ERROR_UNKNOWN);
Assert.equal(error1.message, null);
let error2 = new FxAccountsOAuthGrantClientError({
code: STATUS_SUCCESS,
errno: 1,
error: "Error",
message: "Something",
});
let fields2 = error2._toStringFields();
let statusCode = 1;
Assert.equal(error2.name, "FxAccountsOAuthGrantClientError");
Assert.equal(error2.code, STATUS_SUCCESS);
Assert.equal(error2.errno, statusCode);
Assert.equal(error2.error, "Error");
Assert.equal(error2.message, "Something");
Assert.equal(fields2.name, "FxAccountsOAuthGrantClientError");
Assert.equal(fields2.code, STATUS_SUCCESS);
Assert.equal(fields2.errno, statusCode);
Assert.equal(fields2.error, "Error");
Assert.equal(fields2.message, "Something");
Assert.ok(error2.toString().includes("Something"));
run_next_test();
});
add_test(function networkErrorResponse() {
let client = new FxAccountsOAuthGrantClient({
serverURL: "https://domain.dummy",
client_id: "abc123",
});
client.getTokenFromAssertion("assertion", "scope").catch(function(e) {
Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
Assert.equal(e.code, null);
Assert.equal(e.errno, ERRNO_NETWORK);
Assert.equal(e.error, ERROR_NETWORK);
run_next_test();
});
});
/**
* Quick way to test the "FxAccountsOAuthGrantClient" constructor.
*
* @param {Object} options
* FxAccountsOAuthGrantClient constructor options
* @param {String} expected
* Expected error message, or null if it's expected to pass.
* @returns {*}
*/
function validationHelper(options, expected) {
try {
new FxAccountsOAuthGrantClient(options);
} catch (e) {
return Assert.equal(e.toString(), expected);
}
return Assert.equal(expected, null);
}

View File

@ -1,70 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// A test of FxAccountsOAuthGrantClient but using a real server it can
// hit.
"use strict";
const { FxAccountsOAuthGrantClient } = ChromeUtils.import(
"resource://gre/modules/FxAccountsOAuthGrantClient.jsm"
);
// handlers for our server.
var numTokenFetches;
var activeTokens;
function authorize(request, response) {
response.setStatusLine("1.1", 200, "OK");
let token = "token" + numTokenFetches;
numTokenFetches += 1;
activeTokens.add(token);
response.write(JSON.stringify({ access_token: token }));
}
function destroy(request, response) {
// Getting the body seems harder than it should be!
let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
Ci.nsIScriptableInputStream
);
sis.init(request.bodyInputStream);
let body = JSON.parse(sis.read(sis.available()));
sis.close();
let token = body.token;
ok(activeTokens.delete(token));
print("after destroy have", activeTokens.size, "tokens left.");
response.setStatusLine("1.1", 200, "OK");
response.write("{}");
}
function startServer() {
numTokenFetches = 0;
activeTokens = new Set();
let srv = new HttpServer();
srv.registerPathHandler("/v1/authorization", authorize);
srv.registerPathHandler("/v1/destroy", destroy);
srv.start(-1);
return srv;
}
add_task(async function getAndRevokeToken() {
Services.prefs.setBoolPref("identity.fxaccounts.allowHttp", true);
let server = startServer();
try {
let clientOptions = {
serverURL: "http://localhost:" + server.identity.primaryPort + "/v1",
client_id: "abc123",
};
let client = new FxAccountsOAuthGrantClient(clientOptions);
let result = await client.getTokenFromAssertion("assertion", "scope");
equal(result.access_token, "token0");
equal(numTokenFetches, 1, "we hit the server to fetch a token");
await client.destroyToken("token0");
equal(activeTokens.size, 0, "We hit the server to revoke it");
} finally {
await promiseStopServer(server);
Services.prefs.clearUserPref("identity.fxaccounts.allowHttp");
}
});
// XXX - TODO - we should probably add more tests for unexpected responses etc.

View File

@ -82,6 +82,21 @@ function MockFxAccountsClient() {
this.getDeviceList = function() {
return Promise.resolve();
};
this.oauthDestroy = sinon.stub().callsFake((_clientId, token) => {
this.activeTokens.delete(token);
return Promise.resolve();
});
this.oauthToken = function(_sessionToken, _clientId, _scopeString) {
let token = "token" + this.numTokenFetches;
this.numTokenFetches += 1;
this.activeTokens.add(token);
print("oauthToken returning token", token);
return Promise.resolve({ access_token: token });
};
// Test only stuff.
this.numTokenFetches = 0;
this.activeTokens = new Set();
FxAccountsClient.apply(this);
}
@ -90,7 +105,7 @@ MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype,
};
function MockFxAccounts(mockGrantClient) {
function MockFxAccounts() {
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(),
getAssertion: () => Promise.resolve("assertion"),
@ -100,12 +115,6 @@ function MockFxAccounts(mockGrantClient) {
storage.initialize(credentials);
return new AccountState(storage);
},
_destroyOAuthToken(tokenData) {
// somewhat sad duplication of _destroyOAuthToken, but hard to avoid.
return mockGrantClient.destroyToken(tokenData.token).then(() => {
Services.obs.notifyObservers(null, "testhelper-fxa-revoke-complete");
});
},
_getDeviceName() {
return "mock device name";
},
@ -121,8 +130,8 @@ function MockFxAccounts(mockGrantClient) {
});
}
async function createMockFxA(mockGrantClient) {
let fxa = new MockFxAccounts(mockGrantClient);
async function createMockFxA() {
let fxa = new MockFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
@ -141,33 +150,10 @@ async function createMockFxA(mockGrantClient) {
// The tests.
function MockFxAccountsOAuthGrantClient() {
this.activeTokens = new Set();
}
MockFxAccountsOAuthGrantClient.prototype = {
serverURL: { href: "http://localhost" },
getTokenFromAssertion(assertion, scope) {
let token = "token" + this.numTokenFetches;
this.numTokenFetches += 1;
this.activeTokens.add(token);
print("getTokenFromAssertion returning token", token);
return Promise.resolve({ access_token: token });
},
destroyToken(token) {
ok(this.activeTokens.delete(token));
print("after destroy have", this.activeTokens.size, "tokens left.");
return Promise.resolve({});
},
// and some stuff used only for tests.
numTokenFetches: 0,
activeTokens: null,
};
add_task(async function testRevoke() {
let client = new MockFxAccountsOAuthGrantClient();
let tokenOptions = { scope: "test-scope", client };
let fxa = await createMockFxA(client);
let tokenOptions = { scope: "test-scope" };
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
// get our first token and check we hit the mock.
let token1 = await fxa.getOAuthToken(tokenOptions);
@ -176,11 +162,9 @@ add_task(async function testRevoke() {
ok(token1, "got a token");
equal(token1, "token0");
// FxA fires an observer when the "background" revoke is complete.
let revokeComplete = promiseNotification("testhelper-fxa-revoke-complete");
// drop the new token from our cache.
await fxa.removeCachedOAuthToken({ token: token1 });
await revokeComplete;
ok(client.oauthDestroy.calledOnce);
// the revoke should have been successful.
equal(client.activeTokens.size, 0);
@ -193,17 +177,17 @@ add_task(async function testRevoke() {
});
add_task(async function testSignOutDestroysTokens() {
let client = new MockFxAccountsOAuthGrantClient();
let fxa = await createMockFxA(client);
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
// get our first token and check we hit the mock.
let token1 = await fxa.getOAuthToken({ scope: "test-scope", client });
let token1 = await fxa.getOAuthToken({ scope: "test-scope" });
equal(client.numTokenFetches, 1);
equal(client.activeTokens.size, 1);
ok(token1, "got a token");
// get another
let token2 = await fxa.getOAuthToken({ scope: "test-scope-2", client });
let token2 = await fxa.getOAuthToken({ scope: "test-scope-2" });
equal(client.numTokenFetches, 2);
equal(client.activeTokens.size, 2);
ok(token2, "got a token");
@ -214,6 +198,7 @@ add_task(async function testSignOutDestroysTokens() {
// now sign out - they should be removed.
await fxa.signOut();
await signoutComplete;
ok(client.oauthDestroy.calledTwice);
// No active tokens left.
equal(client.activeTokens.size, 0);
});
@ -224,14 +209,14 @@ add_task(async function testTokenRaces() {
// This should provoke a potential race in the token fetching but we use
// a map of in-flight token fetches, so we should still only perform 2
// fetches, but each of the 4 calls should resolve with the correct values.
let client = new MockFxAccountsOAuthGrantClient();
let fxa = await createMockFxA(client);
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
let results = await Promise.all([
fxa.getOAuthToken({ scope: "test-scope", client }),
fxa.getOAuthToken({ scope: "test-scope", client }),
fxa.getOAuthToken({ scope: "test-scope-2", client }),
fxa.getOAuthToken({ scope: "test-scope-2", client }),
fxa.getOAuthToken({ scope: "test-scope" }),
fxa.getOAuthToken({ scope: "test-scope" }),
fxa.getOAuthToken({ scope: "test-scope-2" }),
fxa.getOAuthToken({ scope: "test-scope-2" }),
]);
equal(client.numTokenFetches, 2, "should have fetched 2 tokens.");
@ -242,14 +227,9 @@ add_task(async function testTokenRaces() {
equal(results[2], results[3]);
// should be 2 active.
equal(client.activeTokens.size, 2);
// Which can each be revoked, which will trigger a notification.
let notifications = Promise.all([
promiseNotification("testhelper-fxa-revoke-complete"),
promiseNotification("testhelper-fxa-revoke-complete"),
]);
await fxa.removeCachedOAuthToken({ token: results[0] });
equal(client.activeTokens.size, 1);
await fxa.removeCachedOAuthToken({ token: results[2] });
equal(client.activeTokens.size, 0);
await notifications;
ok(client.oauthDestroy.calledTwice);
});

View File

@ -13,8 +13,6 @@ support-files =
[test_credentials.js]
[test_device.js]
[test_loginmgr_storage.js]
[test_oauth_grant_client.js]
[test_oauth_grant_client_server.js]
[test_oauth_tokens.js]
[test_oauth_token_storage.js]
[test_pairing.js]

View File

@ -84,7 +84,6 @@
"FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
"FxAccountsCommands.js": ["SendTab", "FxAccountsCommands"],
"FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_COMMAND_RECEIVED_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "ON_NEW_DEVICE_ID", "COMMAND_SENDTAB", "SCOPE_PROFILE", "SCOPE_OLD_SYNC", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "COMMAND_PAIR_HEARTBEAT", "COMMAND_PAIR_SUPP_METADATA", "COMMAND_PAIR_AUTHORIZE", "COMMAND_PAIR_DECLINE", "COMMAND_PAIR_COMPLETE", "COMMAND_PAIR_PREFERENCES", "COMMAND_PROFILE_CHANGE", "COMMAND_CAN_LINK_ACCOUNT", "COMMAND_LOGIN", "COMMAND_LOGOUT", "COMMAND_DELETE", "COMMAND_SYNC_PREFERENCES", "COMMAND_CHANGE_PASSWORD", "COMMAND_FXA_STATUS", "PREF_LAST_FXA_USER", "PREF_REMOTE_PAIRING_URI", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
"FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],
"FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"],
"FxAccountsPush.js": ["FxAccountsPushService"],
"FxAccountsStorage.jsm": ["FxAccountsStorageManagerCanStoreField", "FxAccountsStorageManager"],