Remove Firefox Accounts service and tie-ins.

See previous commit for removal of browser identity module.
This commit is contained in:
wolfbeast 2019-04-19 02:02:56 +02:00 • committed by Roy Tam
commit c64dc935f9
420 changed files with 9 additions and 51958 deletions

View file

@ -1,136 +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/. */
/**
* This module implements client-side key stretching for use in Firefox
* Accounts account creation and login.
*
* See https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
*/
"use strict";
this.EXPORTED_SYMBOLS = ["Credentials"];
const {utils: Cu, interfaces: Ci} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://services-common/utils.js");
const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/";
const PBKDF2_ROUNDS = 1000;
const STRETCHED_PW_LENGTH_BYTES = 32;
const HKDF_SALT = CommonUtils.hexToBytes("00");
const HKDF_LENGTH = 32;
const HMAC_ALGORITHM = Ci.nsICryptoHMAC.SHA256;
const HMAC_LENGTH = 32;
// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
// default.
const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
try {
this.LOG_LEVEL =
Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
&& Services.prefs.getCharPref(PREF_LOG_LEVEL);
} catch (e) {
this.LOG_LEVEL = Log.Level.Error;
}
var log = Log.repository.getLogger("Identity.FxAccounts");
log.level = LOG_LEVEL;
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
this.Credentials = Object.freeze({
/**
* Make constants accessible to tests
*/
constants: {
PROTOCOL_VERSION: PROTOCOL_VERSION,
PBKDF2_ROUNDS: PBKDF2_ROUNDS,
STRETCHED_PW_LENGTH_BYTES: STRETCHED_PW_LENGTH_BYTES,
HKDF_SALT: HKDF_SALT,
HKDF_LENGTH: HKDF_LENGTH,
HMAC_ALGORITHM: HMAC_ALGORITHM,
HMAC_LENGTH: HMAC_LENGTH,
},
/**
* KW function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
*
* keyWord derivation for use as a salt.
*
*
* @param {String} context String for use in generating salt
*
* @return {bitArray} the salt
*
* Note that PROTOCOL_VERSION does not refer in any way to the version of the
* Firefox Accounts API.
*/
keyWord: function(context) {
return CommonUtils.stringToBytes(PROTOCOL_VERSION + context);
},
/**
* KWE function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
*
* keyWord extended with a name and an email.
*
* @param {String} name The name of the salt
* @param {String} email The email of the user.
*
* @return {bitArray} the salt combination with the namespace
*
* Note that PROTOCOL_VERSION does not refer in any way to the version of the
* Firefox Accounts API.
*/
keyWordExtended: function(name, email) {
return CommonUtils.stringToBytes(PROTOCOL_VERSION + name + ':' + email);
},
setup: function(emailInput, passwordInput, options={}) {
let deferred = Promise.defer();
log.debug("setup credentials for " + emailInput);
let hkdfSalt = options.hkdfSalt || HKDF_SALT;
let hkdfLength = options.hkdfLength || HKDF_LENGTH;
let hmacLength = options.hmacLength || HMAC_LENGTH;
let hmacAlgorithm = options.hmacAlgorithm || HMAC_ALGORITHM;
let stretchedPWLength = options.stretchedPassLength || STRETCHED_PW_LENGTH_BYTES;
let pbkdf2Rounds = options.pbkdf2Rounds || PBKDF2_ROUNDS;
let result = {};
let password = CommonUtils.encodeUTF8(passwordInput);
let salt = this.keyWordExtended("quickStretch", emailInput);
let runnable = () => {
let start = Date.now();
let quickStretchedPW = CryptoUtils.pbkdf2Generate(
password, salt, pbkdf2Rounds, stretchedPWLength, hmacAlgorithm, hmacLength);
result.quickStretchedPW = quickStretchedPW;
result.authPW =
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength);
result.unwrapBKey =
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength);
log.debug("Credentials set up after " + (Date.now() - start) + " ms");
deferred.resolve(result);
}
Services.tm.currentThread.dispatch(runnable,
Ci.nsIThread.DISPATCH_NORMAL);
log.debug("Dispatched thread for credentials setup crypto work");
return deferred.promise;
}
});

File diff suppressed because it is too large Load diff

View file

@ -1,623 +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/. */
this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/hawkclient.js");
Cu.import("resource://services-common/hawkrequest.js");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/Credentials.jsm");
const HOST_PREF = "identity.fxaccounts.auth.uri";
const SIGNIN = "/account/login";
const SIGNUP = "/account/create";
this.FxAccountsClient = function(host = Services.prefs.getCharPref(HOST_PREF)) {
this.host = host;
// The FxA auth server expects requests to certain endpoints to be authorized
// using Hawk.
this.hawk = new HawkClient(host);
this.hawk.observerPrefix = "FxA:hawk";
// Manage server backoff state. C.f.
// https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol
this.backoffError = null;
};
this.FxAccountsClient.prototype = {
/**
* Return client clock offset, in milliseconds, as determined by hawk client.
* Provided because callers should not have to know about hawk
* implementation.
*
* The offset is the number of milliseconds that must be added to the client
* clock to make it equal to the server clock. For example, if the client is
* five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
*/
get localtimeOffsetMsec() {
return this.hawk.localtimeOffsetMsec;
},
/*
* Return current time in milliseconds
*
* Not used by this module, but made available to the FxAccounts.jsm
* that uses this client.
*/
now: function() {
return this.hawk.now();
},
/**
* Common code from signIn and signUp.
*
* @param path
* Request URL path. Can be /account/create or /account/login
* @param email
* The email address for the account (utf8)
* @param password
* The user's password
* @param [getKeys=false]
* If set to true the keyFetchToken will be retrieved
* @param [retryOK=true]
* If capitalization of the email is wrong and retryOK is set to true,
* we will retry with the suggested capitalization from the server
* @return Promise
* Returns a promise that resolves to an object:
* {
* authAt: authentication time for the session (seconds since epoch)
* email: the primary email for this account
* keyFetchToken: a key fetch token (hex)
* sessionToken: a session token (hex)
* uid: the user's unique ID (hex)
* unwrapBKey: used to unwrap kB, derived locally from the
* password (not revealed to the FxA server)
* verified (optional): flag indicating verification status of the
* email
* }
*/
_createSession: function(path, email, password, getKeys=false,
retryOK=true) {
return Credentials.setup(email, password).then((creds) => {
let data = {
authPW: CommonUtils.bytesAsHex(creds.authPW),
email: email,
};
let keys = getKeys ? "?keys=true" : "";
return this._request(path + keys, "POST", null, data).then(
// Include the canonical capitalization of the email in the response so
// the caller can set its signed-in user state accordingly.
result => {
result.email = data.email;
result.unwrapBKey = CommonUtils.bytesAsHex(creds.unwrapBKey);
return result;
},
error => {
log.debug("Session creation failed", error);
// If the user entered an email with different capitalization from
// what's stored in the database (e.g., Greta.Garbo@gmail.COM as
// opposed to greta.garbo@gmail.com), the server will respond with a
// errno 120 (code 400) and the expected capitalization of the email.
// We retry with this email exactly once. If successful, we use the
// server's version of the email as the signed-in-user's email. This
// is necessary because the email also serves as salt; so we must be
// in agreement with the server on capitalization.
//
// API reference:
// https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md
if (ERRNO_INCORRECT_EMAIL_CASE === error.errno && retryOK) {
if (!error.email) {
log.error("Server returned errno 120 but did not provide email");
throw error;
}
return this._createSession(path, error.email, password, getKeys,
false);
}
throw error;
}
);
});
},
/**
* Create a new Firefox Account and authenticate
*
* @param email
* The email address for the account (utf8)
* @param password
* The user's password
* @param [getKeys=false]
* If set to true the keyFetchToken will be retrieved
* @return Promise
* Returns a promise that resolves to an object:
* {
* uid: the user's unique ID (hex)
* sessionToken: a session token (hex)
* keyFetchToken: a key fetch token (hex),
* unwrapBKey: used to unwrap kB, derived locally from the
* password (not revealed to the FxA server)
* }
*/
signUp: function(email, password, getKeys=false) {
return this._createSession(SIGNUP, email, password, getKeys,
false /* no retry */);
},
/**
* Authenticate and create a new session with the Firefox Account API server
*
* @param email
* The email address for the account (utf8)
* @param password
* The user's password
* @param [getKeys=false]
* If set to true the keyFetchToken will be retrieved
* @return Promise
* Returns a promise that resolves to an object:
* {
* authAt: authentication time for the session (seconds since epoch)
* email: the primary email for this account
* keyFetchToken: a key fetch token (hex)
* sessionToken: a session token (hex)
* uid: the user's unique ID (hex)
* unwrapBKey: used to unwrap kB, derived locally from the
* password (not revealed to the FxA server)
* verified: flag indicating verification status of the email
* }
*/
signIn: function signIn(email, password, getKeys=false) {
return this._createSession(SIGNIN, email, password, getKeys,
true /* retry */);
},
/**
* Check the status of a session given a session token
*
* @param sessionTokenHex
* The session token encoded in hex
* @return Promise
* Resolves with a boolean indicating if the session is still valid
*/
sessionStatus: function (sessionTokenHex) {
return this._request("/session/status", "GET",
deriveHawkCredentials(sessionTokenHex, "sessionToken")).then(
() => Promise.resolve(true),
error => {
if (isInvalidTokenError(error)) {
return Promise.resolve(false);
}
throw error;
}
);
},
/**
* Destroy the current session with the Firefox Account API server
*
* @param sessionTokenHex
* The session token encoded in hex
* @return Promise
*/
signOut: function (sessionTokenHex, options = {}) {
let path = "/session/destroy";
if (options.service) {
path += "?service=" + encodeURIComponent(options.service);
}
return this._request(path, "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
},
/**
* Check the verification status of the user's FxA email address
*
* @param sessionTokenHex
* The current session token encoded in hex
* @return Promise
*/
recoveryEmailStatus: function (sessionTokenHex, options = {}) {
let path = "/recovery_email/status";
if (options.reason) {
path += "?reason=" + encodeURIComponent(options.reason);
}
return this._request(path, "GET",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
},
/**
* Resend the verification email for the user
*
* @param sessionTokenHex
* The current token encoded in hex
* @return Promise
*/
resendVerificationEmail: function(sessionTokenHex) {
return this._request("/recovery_email/resend_code", "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
},
/**
* Retrieve encryption keys
*
* @param keyFetchTokenHex
* A one-time use key fetch token encoded in hex
* @return Promise
* Returns a promise that resolves to an object:
* {
* kA: an encryption key for recevorable data (bytes)
* wrapKB: an encryption key that requires knowledge of the
* user's password (bytes)
* }
*/
accountKeys: function (keyFetchTokenHex) {
let creds = deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
let keyRequestKey = creds.extra.slice(0, 32);
let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
Credentials.keyWord("account/keys"), 3 * 32);
let respHMACKey = morecreds.slice(0, 32);
let respXORKey = morecreds.slice(32, 96);
return this._request("/account/keys", "GET", creds).then(resp => {
if (!resp.bundle) {
throw new Error("failed to retrieve keys");
}
let bundle = CommonUtils.hexToBytes(resp.bundle);
let mac = bundle.slice(-32);
let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
CryptoUtils.makeHMACKey(respHMACKey));
let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher);
if (mac !== bundleMAC) {
throw new Error("error unbundling encryption keys");
}
let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
return {
kA: keyAWrapB.slice(0, 32),
wrapKB: keyAWrapB.slice(32)
};
});
},
/**
* Sends a public key to the FxA API server and returns a signed certificate
*
* @param sessionTokenHex
* The current session token encoded in hex
* @param serializedPublicKey
* A public key (usually generated by jwcrypto)
* @param lifetime
* The lifetime of the certificate
* @return Promise
* Returns a promise that resolves to the signed certificate.
* The certificate can be used to generate a Persona assertion.
* @throws a new Error
* wrapping any of these HTTP code/errno pairs:
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12
*/
signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) {
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { publicKey: serializedPublicKey,
duration: lifetime };
return Promise.resolve()
.then(_ => this._request("/certificate/sign", "POST", creds, body))
.then(resp => resp.cert,
err => {
log.error("HAWK.signCertificate error: " + JSON.stringify(err));
throw err;
});
},
/**
* Determine if an account exists
*
* @param email
* The email address to check
* @return Promise
* The promise resolves to true if the account exists, or false
* if it doesn't. The promise is rejected on other errors.
*/
accountExists: function (email) {
return this.signIn(email, "").then(
(cantHappen) => {
throw new Error("How did I sign in with an empty password?");
},
(expectedError) => {
switch (expectedError.errno) {
case ERRNO_ACCOUNT_DOES_NOT_EXIST:
return false;
break;
case ERRNO_INCORRECT_PASSWORD:
return true;
break;
default:
// not so expected, any more ...
throw expectedError;
break;
}
}
);
},
/**
* Given the uid of an existing account (not an arbitrary email), ask
* the server if it still exists via /account/status.
*
* Used for differentiating between password change and account deletion.
*/
accountStatus: function(uid) {
return this._request("/account/status?uid="+uid, "GET").then(
(result) => {
return result.exists;
},
(error) => {
log.error("accountStatus failed with: " + error);
return Promise.reject(error);
}
);
},
/**
* Register a new device
*
* @method registerDevice
* @param sessionTokenHex
* Session token obtained from signIn
* @param name
* Device name
* @param type
* Device type (mobile|desktop)
* @param [options]
* Extra device options
* @param [options.pushCallback]
* `pushCallback` push endpoint callback
* @param [options.pushPublicKey]
* `pushPublicKey` push public key (URLSafe Base64 string)
* @param [options.pushAuthKey]
* `pushAuthKey` push auth secret (URLSafe Base64 string)
* @return Promise
* Resolves to an object:
* {
* id: Device identifier
* createdAt: Creation time (milliseconds since epoch)
* name: Name of device
* type: Type of device (mobile|desktop)
* }
*/
registerDevice(sessionTokenHex, name, type, options = {}) {
let path = "/account/device";
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { name, type };
if (options.pushCallback) {
body.pushCallback = options.pushCallback;
}
if (options.pushPublicKey && options.pushAuthKey) {
body.pushPublicKey = options.pushPublicKey;
body.pushAuthKey = options.pushAuthKey;
}
return this._request(path, "POST", creds, body);
},
/**
* Sends a message to other devices. Must conform with the push payload schema:
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json
*
* @method notifyDevice
* @param sessionTokenHex
* Session token obtained from signIn
* @param deviceIds
* Devices to send the message to
* @param payload
* Data to send with the message
* @return Promise
* Resolves to an empty object:
* {}
*/
notifyDevices(sessionTokenHex, deviceIds, payload, TTL = 0) {
const body = {
to: deviceIds,
payload,
TTL
};
return this._request("/account/devices/notify", "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"), body);
},
/**
* Update the session or name for an existing device
*
* @method updateDevice
* @param sessionTokenHex
* Session token obtained from signIn
* @param id
* Device identifier
* @param name
* Device name
* @param [options]
* Extra device options
* @param [options.pushCallback]
* `pushCallback` push endpoint callback
* @param [options.pushPublicKey]
* `pushPublicKey` push public key (URLSafe Base64 string)
* @param [options.pushAuthKey]
* `pushAuthKey` push auth secret (URLSafe Base64 string)
* @return Promise
* Resolves to an object:
* {
* id: Device identifier
* name: Device name
* }
*/
updateDevice(sessionTokenHex, id, name, options = {}) {
let path = "/account/device";
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { id, name };
if (options.pushCallback) {
body.pushCallback = options.pushCallback;
}
if (options.pushPublicKey && options.pushAuthKey) {
body.pushPublicKey = options.pushPublicKey;
body.pushAuthKey = options.pushAuthKey;
}
return this._request(path, "POST", creds, body);
},
/**
* Delete a device and its associated session token, signing the user
* out of the server.
*
* @method signOutAndDestroyDevice
* @param sessionTokenHex
* Session token obtained from signIn
* @param id
* Device identifier
* @param [options]
* Options object
* @param [options.service]
* `service` query parameter
* @return Promise
* Resolves to an empty object:
* {}
*/
signOutAndDestroyDevice(sessionTokenHex, id, options = {}) {
let path = "/account/device/destroy";
if (options.service) {
path += "?service=" + encodeURIComponent(options.service);
}
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { id };
return this._request(path, "POST", creds, body);
},
/**
* Get a list of currently registered devices
*
* @method getDeviceList
* @param sessionTokenHex
* Session token obtained from signIn
* @return Promise
* Resolves to an array of objects:
* [
* {
* id: Device id
* isCurrentDevice: Boolean indicating whether the item
* represents the current device
* name: Device name
* type: Device type (mobile|desktop)
* },
* ...
* ]
*/
getDeviceList(sessionTokenHex) {
let path = "/account/devices";
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
return this._request(path, "GET", creds, {});
},
_clearBackoff: function() {
this.backoffError = null;
},
/**
* A general method for sending raw API calls to the FxA auth server.
* All request bodies and responses are JSON.
*
* @param path
* API endpoint path
* @param method
* The HTTP request method
* @param credentials
* Hawk credentials
* @param jsonPayload
* A JSON payload
* @return Promise
* Returns a promise that resolves to the JSON response of the API call,
* or is rejected with an error. Error responses have the following properties:
* {
* "code": 400, // matches the HTTP status code
* "errno": 107, // stable application-level error number
* "error": "Bad Request", // string description of the error type
* "message": "the value of salt is not allowed to be undefined",
* "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
* }
*/
_request: function hawkRequest(path, method, credentials, jsonPayload) {
let deferred = Promise.defer();
// We were asked to back off.
if (this.backoffError) {
log.debug("Received new request during backoff, re-rejecting.");
deferred.reject(this.backoffError);
return deferred.promise;
}
this.hawk.request(path, method, credentials, jsonPayload).then(
(response) => {
try {
let responseObj = JSON.parse(response.body);
deferred.resolve(responseObj);
} catch (err) {
log.error("json parse error on response: " + response.body);
deferred.reject({error: err});
}
},
(error) => {
log.error("error " + method + "ing " + path + ": " + JSON.stringify(error));
if (error.retryAfter) {
log.debug("Received backoff response; caching error as flag.");
this.backoffError = error;
// Schedule clearing of cached-error-as-flag.
CommonUtils.namedTimer(
this._clearBackoff,
error.retryAfter * 1000,
this,
"fxaBackoffTimer"
);
}
deferred.reject(error);
}
);
return deferred.promise;
},
};
function isInvalidTokenError(error) {
if (error.code != 401) {
return false;
}
switch (error.errno) {
case ERRNO_INVALID_AUTH_TOKEN:
case ERRNO_INVALID_AUTH_TIMESTAMP:
case ERRNO_INVALID_AUTH_NONCE:
return true;
}
return false;
}

View file

@ -1,368 +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/. */
var { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Log.jsm");
// loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config",
// "Debug", "Trace" or "All". If none is specified, "Debug" will be used by
// default. Note "Debug" is usually appropriate so that when this log is
// included in the Sync file logs we get verbose output.
const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
// The level of messages that will be dumped to the console. If not specified,
// "Error" will be used.
const PREF_LOG_LEVEL_DUMP = "identity.fxaccounts.log.appender.dump";
// A pref that can be set so "sensitive" information (eg, personally
// identifiable info, credentials, etc) will be logged.
const PREF_LOG_SENSITIVE_DETAILS = "identity.fxaccounts.log.sensitive";
var exports = Object.create(null);
XPCOMUtils.defineLazyGetter(exports, 'log', function() {
let log = Log.repository.getLogger("FirefoxAccounts");
// We set the log level to debug, but the default dump appender is set to
// the level reflected in the pref. Other code that consumes FxA may then
// choose to add another appender at a different level.
log.level = Log.Level.Debug;
let appender = new Log.DumpAppender();
appender.level = Log.Level.Error;
log.addAppender(appender);
try {
// The log itself.
let level =
Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
&& Services.prefs.getCharPref(PREF_LOG_LEVEL);
log.level = Log.Level[level] || Log.Level.Debug;
// The appender.
level =
Services.prefs.getPrefType(PREF_LOG_LEVEL_DUMP) == Ci.nsIPrefBranch.PREF_STRING
&& Services.prefs.getCharPref(PREF_LOG_LEVEL_DUMP);
appender.level = Log.Level[level] || Log.Level.Error;
} catch (e) {
log.error(e);
}
return log;
});
// A boolean to indicate if personally identifiable information (or anything
// else sensitive, such as credentials) should be logged.
XPCOMUtils.defineLazyGetter(exports, 'logPII', function() {
try {
return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS);
} catch (_) {
return false;
}
});
exports.FXACCOUNTS_PERMISSION = "firefox-accounts";
exports.DATA_FORMAT_VERSION = 1;
exports.DEFAULT_STORAGE_FILENAME = "signedInUser.json";
// Token life times.
// Having this parameter be short has limited security value and can cause
// spurious authentication values if the client's clock is skewed and
// we fail to adjust. See Bug 983256.
exports.ASSERTION_LIFETIME = 1000 * 3600 * 24 * 365 * 25; // 25 years
// This is a time period we want to guarantee that the assertion will be
// valid after we generate it (e.g., the signed cert won't expire in this
// period).
exports.ASSERTION_USE_PERIOD = 1000 * 60 * 5; // 5 minutes
exports.CERT_LIFETIME = 1000 * 3600 * 6; // 6 hours
exports.KEY_LIFETIME = 1000 * 3600 * 12; // 12 hours
// After we start polling for account verification, we stop polling when this
// many milliseconds have elapsed.
exports.POLL_SESSION = 1000 * 60 * 20; // 20 minutes
// Observer notifications.
exports.ONLOGIN_NOTIFICATION = "fxaccounts:onlogin";
exports.ONVERIFIED_NOTIFICATION = "fxaccounts:onverified";
exports.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout";
// Internal to services/fxaccounts only
exports.ON_FXA_UPDATE_NOTIFICATION = "fxaccounts:update";
exports.ON_DEVICE_DISCONNECTED_NOTIFICATION = "fxaccounts:device_disconnected";
exports.ON_PASSWORD_CHANGED_NOTIFICATION = "fxaccounts:password_changed";
exports.ON_PASSWORD_RESET_NOTIFICATION = "fxaccounts:password_reset";
exports.ON_COLLECTION_CHANGED_NOTIFICATION = "sync:collection_changed";
exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update";
exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange";
exports.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION = "fxaccounts:statechange";
// UI Requests.
exports.UI_REQUEST_SIGN_IN_FLOW = "signInFlow";
exports.UI_REQUEST_REFRESH_AUTH = "refreshAuthentication";
// The OAuth client ID for Firefox Desktop
exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
// Firefox Accounts WebChannel ID
exports.WEBCHANNEL_ID = "account_updates";
// Server errno.
// From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format
exports.ERRNO_ACCOUNT_ALREADY_EXISTS = 101;
exports.ERRNO_ACCOUNT_DOES_NOT_EXIST = 102;
exports.ERRNO_INCORRECT_PASSWORD = 103;
exports.ERRNO_UNVERIFIED_ACCOUNT = 104;
exports.ERRNO_INVALID_VERIFICATION_CODE = 105;
exports.ERRNO_NOT_VALID_JSON_BODY = 106;
exports.ERRNO_INVALID_BODY_PARAMETERS = 107;
exports.ERRNO_MISSING_BODY_PARAMETERS = 108;
exports.ERRNO_INVALID_REQUEST_SIGNATURE = 109;
exports.ERRNO_INVALID_AUTH_TOKEN = 110;
exports.ERRNO_INVALID_AUTH_TIMESTAMP = 111;
exports.ERRNO_MISSING_CONTENT_LENGTH = 112;
exports.ERRNO_REQUEST_BODY_TOO_LARGE = 113;
exports.ERRNO_TOO_MANY_CLIENT_REQUESTS = 114;
exports.ERRNO_INVALID_AUTH_NONCE = 115;
exports.ERRNO_ENDPOINT_NO_LONGER_SUPPORTED = 116;
exports.ERRNO_INCORRECT_LOGIN_METHOD = 117;
exports.ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD = 118;
exports.ERRNO_INCORRECT_API_VERSION = 119;
exports.ERRNO_INCORRECT_EMAIL_CASE = 120;
exports.ERRNO_ACCOUNT_LOCKED = 121;
exports.ERRNO_ACCOUNT_UNLOCKED = 122;
exports.ERRNO_UNKNOWN_DEVICE = 123;
exports.ERRNO_DEVICE_SESSION_CONFLICT = 124;
exports.ERRNO_SERVICE_TEMP_UNAVAILABLE = 201;
exports.ERRNO_PARSE = 997;
exports.ERRNO_NETWORK = 998;
exports.ERRNO_UNKNOWN_ERROR = 999;
// Offset oauth server errnos so they don't conflict with auth server errnos
exports.OAUTH_SERVER_ERRNO_OFFSET = 1000;
// OAuth Server errno.
exports.ERRNO_UNKNOWN_CLIENT_ID = 101 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INCORRECT_CLIENT_SECRET = 102 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INCORRECT_REDIRECT_URI = 103 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INVALID_FXA_ASSERTION = 104 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_UNKNOWN_CODE = 105 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INCORRECT_CODE = 106 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_EXPIRED_CODE = 107 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_OAUTH_INVALID_TOKEN = 108 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INVALID_REQUEST_PARAM = 109 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INVALID_RESPONSE_TYPE = 110 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_UNAUTHORIZED = 111 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_FORBIDDEN = 112 + exports.OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INVALID_CONTENT_TYPE = 113 + exports.OAUTH_SERVER_ERRNO_OFFSET;
// Errors.
exports.ERROR_ACCOUNT_ALREADY_EXISTS = "ACCOUNT_ALREADY_EXISTS";
exports.ERROR_ACCOUNT_DOES_NOT_EXIST = "ACCOUNT_DOES_NOT_EXIST ";
exports.ERROR_ACCOUNT_LOCKED = "ACCOUNT_LOCKED";
exports.ERROR_ACCOUNT_UNLOCKED = "ACCOUNT_UNLOCKED";
exports.ERROR_ALREADY_SIGNED_IN_USER = "ALREADY_SIGNED_IN_USER";
exports.ERROR_DEVICE_SESSION_CONFLICT = "DEVICE_SESSION_CONFLICT";
exports.ERROR_ENDPOINT_NO_LONGER_SUPPORTED = "ENDPOINT_NO_LONGER_SUPPORTED";
exports.ERROR_INCORRECT_API_VERSION = "INCORRECT_API_VERSION";
exports.ERROR_INCORRECT_EMAIL_CASE = "INCORRECT_EMAIL_CASE";
exports.ERROR_INCORRECT_KEY_RETRIEVAL_METHOD = "INCORRECT_KEY_RETRIEVAL_METHOD";
exports.ERROR_INCORRECT_LOGIN_METHOD = "INCORRECT_LOGIN_METHOD";
exports.ERROR_INVALID_EMAIL = "INVALID_EMAIL";
exports.ERROR_INVALID_AUDIENCE = "INVALID_AUDIENCE";
exports.ERROR_INVALID_AUTH_TOKEN = "INVALID_AUTH_TOKEN";
exports.ERROR_INVALID_AUTH_TIMESTAMP = "INVALID_AUTH_TIMESTAMP";
exports.ERROR_INVALID_AUTH_NONCE = "INVALID_AUTH_NONCE";
exports.ERROR_INVALID_BODY_PARAMETERS = "INVALID_BODY_PARAMETERS";
exports.ERROR_INVALID_PASSWORD = "INVALID_PASSWORD";
exports.ERROR_INVALID_VERIFICATION_CODE = "INVALID_VERIFICATION_CODE";
exports.ERROR_INVALID_REFRESH_AUTH_VALUE = "INVALID_REFRESH_AUTH_VALUE";
exports.ERROR_INVALID_REQUEST_SIGNATURE = "INVALID_REQUEST_SIGNATURE";
exports.ERROR_INTERNAL_INVALID_USER = "INTERNAL_ERROR_INVALID_USER";
exports.ERROR_MISSING_BODY_PARAMETERS = "MISSING_BODY_PARAMETERS";
exports.ERROR_MISSING_CONTENT_LENGTH = "MISSING_CONTENT_LENGTH";
exports.ERROR_NO_TOKEN_SESSION = "NO_TOKEN_SESSION";
exports.ERROR_NO_SILENT_REFRESH_AUTH = "NO_SILENT_REFRESH_AUTH";
exports.ERROR_NOT_VALID_JSON_BODY = "NOT_VALID_JSON_BODY";
exports.ERROR_OFFLINE = "OFFLINE";
exports.ERROR_PERMISSION_DENIED = "PERMISSION_DENIED";
exports.ERROR_REQUEST_BODY_TOO_LARGE = "REQUEST_BODY_TOO_LARGE";
exports.ERROR_SERVER_ERROR = "SERVER_ERROR";
exports.ERROR_SYNC_DISABLED = "SYNC_DISABLED";
exports.ERROR_TOO_MANY_CLIENT_REQUESTS = "TOO_MANY_CLIENT_REQUESTS";
exports.ERROR_SERVICE_TEMP_UNAVAILABLE = "SERVICE_TEMPORARY_UNAVAILABLE";
exports.ERROR_UI_ERROR = "UI_ERROR";
exports.ERROR_UI_REQUEST = "UI_REQUEST";
exports.ERROR_PARSE = "PARSE_ERROR";
exports.ERROR_NETWORK = "NETWORK_ERROR";
exports.ERROR_UNKNOWN = "UNKNOWN_ERROR";
exports.ERROR_UNKNOWN_DEVICE = "UNKNOWN_DEVICE";
exports.ERROR_UNVERIFIED_ACCOUNT = "UNVERIFIED_ACCOUNT";
// OAuth errors.
exports.ERROR_UNKNOWN_CLIENT_ID = "UNKNOWN_CLIENT_ID";
exports.ERROR_INCORRECT_CLIENT_SECRET = "INCORRECT_CLIENT_SECRET";
exports.ERROR_INCORRECT_REDIRECT_URI = "INCORRECT_REDIRECT_URI";
exports.ERROR_INVALID_FXA_ASSERTION = "INVALID_FXA_ASSERTION";
exports.ERROR_UNKNOWN_CODE = "UNKNOWN_CODE";
exports.ERROR_INCORRECT_CODE = "INCORRECT_CODE";
exports.ERROR_EXPIRED_CODE = "EXPIRED_CODE";
exports.ERROR_OAUTH_INVALID_TOKEN = "OAUTH_INVALID_TOKEN";
exports.ERROR_INVALID_REQUEST_PARAM = "INVALID_REQUEST_PARAM";
exports.ERROR_INVALID_RESPONSE_TYPE = "INVALID_RESPONSE_TYPE";
exports.ERROR_UNAUTHORIZED = "UNAUTHORIZED";
exports.ERROR_FORBIDDEN = "FORBIDDEN";
exports.ERROR_INVALID_CONTENT_TYPE = "INVALID_CONTENT_TYPE";
// Additional generic error classes for external consumers
exports.ERROR_NO_ACCOUNT = "NO_ACCOUNT";
exports.ERROR_AUTH_ERROR = "AUTH_ERROR";
exports.ERROR_INVALID_PARAMETER = "INVALID_PARAMETER";
// Status code errors
exports.ERROR_CODE_METHOD_NOT_ALLOWED = 405;
exports.ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
// FxAccounts has the ability to "split" the credentials between a plain-text
// JSON file in the profile dir and in the login manager.
// In order to prevent new fields accidentally ending up in the "wrong" place,
// all fields stored are listed here.
// The fields we save in the plaintext JSON.
// See bug 1013064 comments 23-25 for why the sessionToken is "safe"
exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set(
["email", "verified", "authAt", "sessionToken", "uid", "oauthTokens", "profile",
"deviceId", "deviceRegistrationVersion"]);
// Fields we store in secure storage if it exists.
exports.FXA_PWDMGR_SECURE_FIELDS = new Set(
["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]);
// Fields we keep in memory and don't persist anywhere.
exports.FXA_PWDMGR_MEMORY_FIELDS = new Set(
["cert", "keyPair"]);
// A whitelist of fields that remain in storage when the user needs to
// reauthenticate. All other fields will be removed.
exports.FXA_PWDMGR_REAUTH_WHITELIST = new Set(
["email", "uid", "profile", "deviceId", "deviceRegistrationVersion", "verified"]);
// The pseudo-host we use in the login manager
exports.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts";
// The realm we use in the login manager.
exports.FXA_PWDMGR_REALM = "Firefox Accounts credentials";
// Error matching.
exports.SERVER_ERRNO_TO_ERROR = {};
// Error mapping
exports.ERROR_TO_GENERAL_ERROR_CLASS = {};
for (let id in exports) {
this[id] = exports[id];
}
// Allow this file to be imported via Components.utils.import().
this.EXPORTED_SYMBOLS = Object.keys(exports);
// Set these up now that everything has been loaded into |this|.
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_ALREADY_EXISTS] = ERROR_ACCOUNT_ALREADY_EXISTS;
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_DOES_NOT_EXIST] = ERROR_ACCOUNT_DOES_NOT_EXIST;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_PASSWORD] = ERROR_INVALID_PASSWORD;
SERVER_ERRNO_TO_ERROR[ERRNO_UNVERIFIED_ACCOUNT] = ERROR_UNVERIFIED_ACCOUNT;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_VERIFICATION_CODE] = ERROR_INVALID_VERIFICATION_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_NOT_VALID_JSON_BODY] = ERROR_NOT_VALID_JSON_BODY;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_PARAMETERS] = ERROR_INVALID_BODY_PARAMETERS;
SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_BODY_PARAMETERS] = ERROR_MISSING_BODY_PARAMETERS;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_SIGNATURE] = ERROR_INVALID_REQUEST_SIGNATURE;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TOKEN] = ERROR_INVALID_AUTH_TOKEN;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TIMESTAMP] = ERROR_INVALID_AUTH_TIMESTAMP;
SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_CONTENT_LENGTH] = ERROR_MISSING_CONTENT_LENGTH;
SERVER_ERRNO_TO_ERROR[ERRNO_REQUEST_BODY_TOO_LARGE] = ERROR_REQUEST_BODY_TOO_LARGE;
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_CLIENT_REQUESTS] = ERROR_TOO_MANY_CLIENT_REQUESTS;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_NONCE] = ERROR_INVALID_AUTH_NONCE;
SERVER_ERRNO_TO_ERROR[ERRNO_ENDPOINT_NO_LONGER_SUPPORTED] = ERROR_ENDPOINT_NO_LONGER_SUPPORTED;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_LOGIN_METHOD] = ERROR_INCORRECT_LOGIN_METHOD;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD] = ERROR_INCORRECT_KEY_RETRIEVAL_METHOD;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_API_VERSION] = ERROR_INCORRECT_API_VERSION;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_EMAIL_CASE] = ERROR_INCORRECT_EMAIL_CASE;
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_LOCKED] = ERROR_ACCOUNT_LOCKED;
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_UNLOCKED] = ERROR_ACCOUNT_UNLOCKED;
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_DEVICE] = ERROR_UNKNOWN_DEVICE;
SERVER_ERRNO_TO_ERROR[ERRNO_DEVICE_SESSION_CONFLICT] = ERROR_DEVICE_SESSION_CONFLICT;
SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMP_UNAVAILABLE] = ERROR_SERVICE_TEMP_UNAVAILABLE;
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR] = ERROR_UNKNOWN;
SERVER_ERRNO_TO_ERROR[ERRNO_NETWORK] = ERROR_NETWORK;
// oauth
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_CLIENT_ID] = ERROR_UNKNOWN_CLIENT_ID;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_CLIENT_SECRET] = ERROR_INCORRECT_CLIENT_SECRET;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_REDIRECT_URI] = ERROR_INCORRECT_REDIRECT_URI;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_FXA_ASSERTION] = ERROR_INVALID_FXA_ASSERTION;
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_CODE] = ERROR_UNKNOWN_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_CODE] = ERROR_INCORRECT_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_EXPIRED_CODE] = ERROR_EXPIRED_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_OAUTH_INVALID_TOKEN] = ERROR_OAUTH_INVALID_TOKEN;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_PARAM] = ERROR_INVALID_REQUEST_PARAM;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_RESPONSE_TYPE] = ERROR_INVALID_RESPONSE_TYPE;
SERVER_ERRNO_TO_ERROR[ERRNO_UNAUTHORIZED] = ERROR_UNAUTHORIZED;
SERVER_ERRNO_TO_ERROR[ERRNO_FORBIDDEN] = ERROR_FORBIDDEN;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_CONTENT_TYPE] = ERROR_INVALID_CONTENT_TYPE;
// Map internal errors to more generic error classes for consumers
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_ALREADY_EXISTS] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_DOES_NOT_EXIST] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_LOCKED] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_UNLOCKED] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ALREADY_SIGNED_IN_USER] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_DEVICE_SESSION_CONFLICT] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ENDPOINT_NO_LONGER_SUPPORTED] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_API_VERSION] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_EMAIL_CASE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_KEY_RETRIEVAL_METHOD] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_LOGIN_METHOD] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_EMAIL] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUDIENCE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_TOKEN] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_TIMESTAMP] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_NONCE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_BODY_PARAMETERS] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_PASSWORD] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_VERIFICATION_CODE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REFRESH_AUTH_VALUE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REQUEST_SIGNATURE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INTERNAL_INVALID_USER] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_MISSING_BODY_PARAMETERS] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_MISSING_CONTENT_LENGTH] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NO_TOKEN_SESSION] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NO_SILENT_REFRESH_AUTH] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NOT_VALID_JSON_BODY] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_PERMISSION_DENIED] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_REQUEST_BODY_TOO_LARGE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNKNOWN_DEVICE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNVERIFIED_ACCOUNT] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UI_ERROR] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UI_REQUEST] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_OFFLINE] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_SERVER_ERROR] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_TOO_MANY_CLIENT_REQUESTS] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_SERVICE_TEMP_UNAVAILABLE] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_PARSE] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NETWORK] = ERROR_NETWORK;
// oauth
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_CLIENT_SECRET] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_REDIRECT_URI] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_FXA_ASSERTION] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNKNOWN_CODE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_CODE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_EXPIRED_CODE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_OAUTH_INVALID_TOKEN] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REQUEST_PARAM] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_RESPONSE_TYPE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNAUTHORIZED] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_FORBIDDEN] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_CONTENT_TYPE] = ERROR_AUTH_ERROR;

View file

@ -1,4 +0,0 @@
# FxAccountsPush.js
component {1b7db999-2ecd-4abf-bb95-a726896798ca} FxAccountsPush.js process=main
contract @mozilla.org/fxaccounts/push;1 {1b7db999-2ecd-4abf-bb95-a726896798ca}
category push chrome://fxa-device-update @mozilla.org/fxaccounts/push;1

View file

@ -1,179 +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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["FxAccountsConfig"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-common/rest.js");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
"resource://gre/modules/FxAccountsWebChannel.jsm");
const CONFIG_PREFS = [
"identity.fxaccounts.auth.uri",
"identity.fxaccounts.remote.oauth.uri",
"identity.fxaccounts.remote.profile.uri",
"identity.sync.tokenserver.uri",
"identity.fxaccounts.remote.webchannel.uri",
"identity.fxaccounts.settings.uri",
"identity.fxaccounts.remote.signup.uri",
"identity.fxaccounts.remote.signin.uri",
"identity.fxaccounts.remote.force_auth.uri",
];
this.FxAccountsConfig = {
// Returns a promise that resolves with the URI of the remote UI flows.
promiseAccountsSignUpURI: Task.async(function*() {
yield this.ensureConfigured();
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri");
if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
throw new Error("Firefox Accounts server must use HTTPS");
}
return url;
}),
// Returns a promise that resolves with the URI of the remote UI flows.
promiseAccountsSignInURI: Task.async(function*() {
yield this.ensureConfigured();
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri");
if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
throw new Error("Firefox Accounts server must use HTTPS");
}
return url;
}),
resetConfigURLs() {
let autoconfigURL = this.getAutoConfigURL();
if (!autoconfigURL) {
return;
}
// They have the autoconfig uri pref set, so we clear all the prefs that we
// will have initialized, which will leave them pointing at production.
for (let pref of CONFIG_PREFS) {
Services.prefs.clearUserPref(pref);
}
// Reset the webchannel.
EnsureFxAccountsWebChannel();
if (!Services.prefs.prefHasUserValue("webchannel.allowObject.urlWhitelist")) {
return;
}
let whitelistValue = Services.prefs.getCharPref("webchannel.allowObject.urlWhitelist");
if (whitelistValue.startsWith(autoconfigURL + " ")) {
whitelistValue = whitelistValue.slice(autoconfigURL.length + 1);
// Check and see if the value will be the default, and just clear the pref if it would
// to avoid it showing up as changed in about:config.
let defaultWhitelist;
try {
defaultWhitelist = Services.prefs.getDefaultBranch("webchannel.allowObject.").getCharPref("urlWhitelist");
} catch (e) {
// No default value ...
}
if (defaultWhitelist === whitelistValue) {
Services.prefs.clearUserPref("webchannel.allowObject.urlWhitelist");
} else {
Services.prefs.setCharPref("webchannel.allowObject.urlWhitelist", whitelistValue);
}
}
},
getAutoConfigURL() {
let pref;
try {
pref = Services.prefs.getCharPref("identity.fxaccounts.autoconfig.uri");
} catch (e) { /* no pref */ }
if (!pref) {
// no pref / empty pref means we don't bother here.
return "";
}
let rootURL = Services.urlFormatter.formatURL(pref);
if (rootURL.endsWith("/")) {
rootURL.slice(0, -1);
}
return rootURL;
},
ensureConfigured: Task.async(function*() {
let isSignedIn = !!(yield fxAccounts.getSignedInUser());
if (!isSignedIn) {
yield this.fetchConfigURLs();
}
}),
// Read expected client configuration from the fxa auth server
// (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration)
// and replace all the relevant our prefs with the information found there.
// This is only done before sign-in and sign-up, and even then only if the
// `identity.fxaccounts.autoconfig.uri` preference is set.
fetchConfigURLs: Task.async(function*() {
let rootURL = this.getAutoConfigURL();
if (!rootURL) {
return;
}
let configURL = rootURL + "/.well-known/fxa-client-configuration";
let jsonStr = yield new Promise((resolve, reject) => {
let request = new RESTRequest(configURL);
request.setHeader("Accept", "application/json");
request.get(error => {
if (error) {
log.error(`Failed to get configuration object from "${configURL}"`, error);
return reject(error);
}
if (!request.response.success) {
log.error(`Received HTTP response code ${request.response.status} from configuration object request`);
if (request.response && request.response.body) {
log.debug("Got error response", request.response.body);
}
return reject(request.response.status);
}
resolve(request.response.body);
});
});
log.debug("Got successful configuration response", jsonStr);
try {
// Update the prefs directly specified by the config.
let config = JSON.parse(jsonStr)
let authServerBase = config.auth_server_base_url;
if (!authServerBase.endsWith("/v1")) {
authServerBase += "/v1";
}
Services.prefs.setCharPref("identity.fxaccounts.auth.uri", authServerBase);
Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", config.oauth_server_base_url + "/v1");
Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", config.profile_server_base_url + "/v1");
Services.prefs.setCharPref("identity.sync.tokenserver.uri", config.sync_tokenserver_base_url + "/1.0/sync/1.5");
// Update the prefs that are based off of the autoconfig url
let contextParam = encodeURIComponent(
Services.prefs.getCharPref("identity.fxaccounts.contextParam"));
Services.prefs.setCharPref("identity.fxaccounts.remote.webchannel.uri", rootURL);
Services.prefs.setCharPref("identity.fxaccounts.settings.uri", rootURL + "/settings?service=sync&context=" + contextParam);
Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", rootURL + "/signup?service=sync&context=" + contextParam);
Services.prefs.setCharPref("identity.fxaccounts.remote.signin.uri", rootURL + "/signin?service=sync&context=" + contextParam);
Services.prefs.setCharPref("identity.fxaccounts.remote.force_auth.uri", rootURL + "/force_auth?service=sync&context=" + contextParam);
let whitelistValue = Services.prefs.getCharPref("webchannel.allowObject.urlWhitelist");
if (!whitelistValue.includes(rootURL)) {
whitelistValue = `${rootURL} ${whitelistValue}`;
Services.prefs.setCharPref("webchannel.allowObject.urlWhitelist", whitelistValue);
}
// Ensure the webchannel is pointed at the correct uri
EnsureFxAccountsWebChannel();
} catch (e) {
log.error("Failed to initialize configuration preferences from autoconfig object", e);
throw e;
}
}),
};

View file

@ -1,269 +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 browser login helper.
* Uses the WebChannel component to receive OAuth messages and complete login flows.
*/
this.EXPORTED_SYMBOLS = ["FxAccountsOAuthClient"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
"resource://gre/modules/WebChannel.jsm");
Cu.importGlobalProperties(["URL"]);
/**
* Create a new FxAccountsOAuthClient for browser some service.
*
* @param {Object} options Options
* @param {Object} options.parameters
* Opaque alphanumeric token to be included in verification links
* @param {String} options.parameters.client_id
* OAuth id returned from client registration
* @param {String} options.parameters.state
* A value that will be returned to the client as-is upon redirection
* @param {String} options.parameters.oauth_uri
* The FxA OAuth server uri
* @param {String} options.parameters.content_uri
* The FxA Content server uri
* @param {String} [options.parameters.scope]
* Optional. A colon-separated list of scopes that the user has authorized
* @param {String} [options.parameters.action]
* Optional. If provided, should be either signup, signin or force_auth.
* @param {String} [options.parameters.email]
* Optional. Required if options.paramters.action is 'force_auth'.
* @param {Boolean} [options.parameters.keys]
* Optional. If true then relier-specific encryption keys will be
* available in the second argument to onComplete.
* @param [authorizationEndpoint] {String}
* Optional authorization endpoint for the OAuth server
* @constructor
*/
this.FxAccountsOAuthClient = function(options) {
this._validateOptions(options);
this.parameters = options.parameters;
this._configureChannel();
let authorizationEndpoint = options.authorizationEndpoint || "/authorization";
try {
this._fxaOAuthStartUrl = new URL(this.parameters.oauth_uri + authorizationEndpoint + "?");
} catch (e) {
throw new Error("Invalid OAuth Url");
}
let params = this._fxaOAuthStartUrl.searchParams;
params.append("client_id", this.parameters.client_id);
params.append("state", this.parameters.state);
params.append("scope", this.parameters.scope || "");
params.append("action", this.parameters.action || "signin");
params.append("webChannelId", this._webChannelId);
if (this.parameters.keys) {
params.append("keys", "true");
}
// Only append if we actually have a value.
if (this.parameters.email) {
params.append("email", this.parameters.email);
}
};
this.FxAccountsOAuthClient.prototype = {
/**
* Function that gets called once the OAuth flow is complete.
* The callback will receive an object with code and state properties.
* If the keys parameter was specified and true, the callback will receive
* a second argument with kAr and kBr properties.
*/
onComplete: null,
/**
* Function that gets called if there is an error during the OAuth flow,
* for example due to a state mismatch.
* The callback will receive an Error object as its argument.
*/
onError: null,
/**
* Configuration object that stores all OAuth parameters.
*/
parameters: null,
/**
* WebChannel that is used to communicate with content page.
*/
_channel: null,
/**
* Boolean to indicate if this client has completed an OAuth flow.
*/
_complete: false,
/**
* The url that opens the Firefox Accounts OAuth flow.
*/
_fxaOAuthStartUrl: null,
/**
* WebChannel id.
*/
_webChannelId: null,
/**
* WebChannel origin, used to validate origin of messages.
*/
_webChannelOrigin: null,
/**
* Opens a tab at "this._fxaOAuthStartUrl".
* Registers a WebChannel listener and sets up a callback if needed.
*/
launchWebFlow: function () {
if (!this._channelCallback) {
this._registerChannel();
}
if (this._complete) {
throw new Error("This client already completed the OAuth flow");
} else {
let opener = Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
opener.selectedTab = opener.addTab(this._fxaOAuthStartUrl.href);
}
},
/**
* Release all resources that are in use.
*/
tearDown: function() {
this.onComplete = null;
this.onError = null;
this._complete = true;
this._channel.stopListening();
this._channel = null;
},
/**
* Configures WebChannel id and origin
*
* @private
*/
_configureChannel: function() {
this._webChannelId = "oauth_" + this.parameters.client_id;
// if this.parameters.content_uri is present but not a valid URI, then this will throw an error.
try {
this._webChannelOrigin = Services.io.newURI(this.parameters.content_uri, null, null);
} catch (e) {
throw e;
}
},
/**
* Create a new channel with the WebChannelBroker, setup a callback listener
* @private
*/
_registerChannel: function() {
/**
* Processes messages that are called back from the FxAccountsChannel
*
* @param webChannelId {String}
* Command webChannelId
* @param message {Object}
* Command message
* @param sendingContext {Object}
* Channel message event sendingContext
* @private
*/
let listener = function (webChannelId, message, sendingContext) {
if (message) {
let command = message.command;
let data = message.data;
let target = sendingContext && sendingContext.browser;
switch (command) {
case "oauth_complete":
// validate the returned state and call onComplete or onError
let result = null;
let err = null;
if (this.parameters.state !== data.state) {
err = new Error("OAuth flow failed. State doesn't match");
} else if (this.parameters.keys && !data.keys) {
err = new Error("OAuth flow failed. Keys were not returned");
} else {
result = {
code: data.code,
state: data.state
};
}
// if the message asked to close the tab
if (data.closeWindow && target) {
// for e10s reasons the best way is to use the TabBrowser to close the tab.
let tabbrowser = target.getTabBrowser();
if (tabbrowser) {
let tab = tabbrowser.getTabForBrowser(target);
if (tab) {
tabbrowser.removeTab(tab);
log.debug("OAuth flow closed the tab.");
} else {
log.debug("OAuth flow failed to close the tab. Tab not found in TabBrowser.");
}
} else {
log.debug("OAuth flow failed to close the tab. TabBrowser not found.");
}
}
if (err) {
log.debug(err.message);
if (this.onError) {
this.onError(err);
}
} else {
log.debug("OAuth flow completed.");
if (this.onComplete) {
if (this.parameters.keys) {
this.onComplete(result, data.keys);
} else {
this.onComplete(result);
}
}
}
// onComplete will be called for this client only once
// calling onComplete again will result in a failure of the OAuth flow
this.tearDown();
break;
}
}
};
this._channelCallback = listener.bind(this);
this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
this._channel.listen(this._channelCallback);
log.debug("Channel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
},
/**
* Validates the required FxA OAuth parameters
*
* @param options {Object}
* OAuth client options
* @private
*/
_validateOptions: function (options) {
if (!options || !options.parameters) {
throw new Error("Missing 'parameters' configuration option");
}
["oauth_uri", "client_id", "content_uri", "state"].forEach(option => {
if (!options.parameters[option]) {
throw new Error("Missing 'parameters." + option + "' parameter");
}
});
if (options.parameters.action == "force_auth" && !options.parameters.email) {
throw new Error("parameters.email is required for action 'force_auth'");
}
},
};

View file

@ -1,241 +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.
*/
this.EXPORTED_SYMBOLS = ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"];
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://services-common/rest.js");
Cu.importGlobalProperties(["URL"]);
const AUTH_ENDPOINT = "/authorization";
const DESTROY_ENDPOINT = "/destroy";
/**
* 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
*/
this.FxAccountsOAuthGrantClient = function(options) {
this._validateOptions(options);
this.parameters = options;
try {
this.serverURL = new URL(this.parameters.serverURL);
} catch (e) {
throw new Error("Invalid 'serverURL'");
}
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: function (assertion, scope) {
if (!assertion) {
throw new Error("Missing 'assertion' parameter");
}
if (!scope) {
throw new Error("Missing 'scope' parameter");
}
let params = {
scope: scope,
client_id: this.parameters.client_id,
assertion: assertion,
response_type: "token"
};
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: function (token) {
if (!token) {
throw new Error("Missing 'token' parameter");
}
let params = {
token: token,
};
return this._createRequest(DESTROY_ENDPOINT, "POST", params);
},
/**
* Validates the required FxA OAuth parameters
*
* @param options {Object}
* OAuth client options
* @private
*/
_validateOptions: function (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
*/
_createRequest: function(path, method = "POST", params) {
return new Promise((resolve, reject) => {
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");
request.onComplete = function (error) {
if (error) {
return reject(new FxAccountsOAuthGrantClientError({
error: ERROR_NETWORK,
errno: ERRNO_NETWORK,
message: error.toString(),
}));
}
let body = null;
try {
body = JSON.parse(request.response.body);
} catch (e) {
return reject(new FxAccountsOAuthGrantClientError({
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);
}
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;
}
return reject(new FxAccountsOAuthGrantClientError(body));
};
if (method === "POST") {
request.post(params);
} else {
// method not supported
return reject(new FxAccountsOAuthGrantClientError({
error: ERROR_NETWORK,
errno: ERRNO_NETWORK,
code: ERROR_CODE_METHOD_NOT_ALLOWED,
message: ERROR_MSG_METHOD_NOT_ALLOWED,
}));
}
});
},
};
/**
* 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.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

@ -1,191 +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/. */
"use strict";
/**
* Firefox Accounts Profile helper.
*
* This class abstracts interaction with the profile server for an account.
* It will handle things like fetching profile data, listening for updates to
* the user's profile in open browser tabs, and cacheing/invalidating profile data.
*/
this.EXPORTED_SYMBOLS = ["FxAccountsProfile"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient",
"resource://gre/modules/FxAccountsProfileClient.jsm");
// Based off of deepEqual from Assert.jsm
function deepEqual(actual, expected) {
if (actual === expected) {
return true;
} else if (typeof actual != "object" && typeof expected != "object") {
return actual == expected;
} else {
return objEquiv(actual, expected);
}
}
function isUndefinedOrNull(value) {
return value === null || value === undefined;
}
function objEquiv(a, b) {
if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
return false;
}
if (a.prototype !== b.prototype) {
return false;
}
let ka, kb, key, i;
try {
ka = Object.keys(a);
kb = Object.keys(b);
} catch (e) {
return false;
}
if (ka.length != kb.length) {
return false;
}
ka.sort();
kb.sort();
for (i = ka.length - 1; i >= 0; i--) {
key = ka[i];
if (!deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
function hasChanged(oldData, newData) {
return !deepEqual(oldData, newData);
}
this.FxAccountsProfile = function (options = {}) {
this._cachedProfile = null;
this._cachedAt = 0; // when we saved the cached version.
this._currentFetchPromise = null;
this._isNotifying = false; // are we sending a notification?
this.fxa = options.fxa || fxAccounts;
this.client = options.profileClient || new FxAccountsProfileClient({
fxa: this.fxa,
serverURL: options.profileServerUrl,
});
// An observer to invalidate our _cachedAt optimization. We use a weak-ref
// just incase this.tearDown isn't called in some cases.
Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true);
// for testing
if (options.channel) {
this.channel = options.channel;
}
}
this.FxAccountsProfile.prototype = {
// If we get subsequent requests for a profile within this period, don't bother
// making another request to determine if it is fresh or not.
PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes
observe(subject, topic, data) {
// If we get a profile change notification from our webchannel it means
// the user has just changed their profile via the web, so we want to
// ignore our "freshness threshold"
if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) {
log.debug("FxAccountsProfile observed profile change");
this._cachedAt = 0;
}
},
tearDown: function () {
this.fxa = null;
this.client = null;
this._cachedProfile = null;
Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION);
},
_getCachedProfile: function () {
// The cached profile will end up back in the generic accountData
// once bug 1157529 is fixed.
return Promise.resolve(this._cachedProfile);
},
_notifyProfileChange: function (uid) {
this._isNotifying = true;
Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
this._isNotifying = false;
},
// Cache fetched data if it is different from what's in the cache.
// Send out a notification if it has changed so that UI can update.
_cacheProfile: function (profileData) {
if (!hasChanged(this._cachedProfile, profileData)) {
log.debug("fetched profile matches cached copy");
return Promise.resolve(null); // indicates no change (but only tests care)
}
this._cachedProfile = profileData;
this._cachedAt = Date.now();
return this.fxa.getSignedInUser()
.then(userData => {
log.debug("notifying profile changed for user ${uid}", userData);
this._notifyProfileChange(userData.uid);
return profileData;
});
},
_fetchAndCacheProfile: function () {
if (!this._currentFetchPromise) {
this._currentFetchPromise = this.client.fetchProfile().then(profile => {
return this._cacheProfile(profile).then(() => {
return profile;
});
}).then(profile => {
this._currentFetchPromise = null;
return profile;
}, err => {
this._currentFetchPromise = null;
throw err;
});
}
return this._currentFetchPromise
},
// Returns cached data right away if available, then fetches the latest profile
// data in the background. After data is fetched a notification will be sent
// out if the profile has changed.
getProfile: function () {
return this._getCachedProfile()
.then(cachedProfile => {
if (cachedProfile) {
if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
// Note that _fetchAndCacheProfile isn't returned, so continues
// in the background.
this._fetchAndCacheProfile().catch(err => {
log.error("Background refresh of profile failed", err);
});
} else {
log.trace("not checking freshness of profile as it remains recent");
}
return cachedProfile;
}
return this._fetchAndCacheProfile();
})
.then(profile => {
return profile;
});
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIObserver,
Ci.nsISupportsWeakReference,
]),
};

View file

@ -1,260 +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/. */
/**
* 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) {
throw ex;
}
// 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) {
throw ex;
}
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()) + ")";
};

View file

@ -1,240 +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/. */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/Task.jsm");
/**
* FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
*
* @param [options]
* Object, custom options that used for testing
* @constructor
*/
function FxAccountsPushService(options = {}) {
this.log = log;
if (options.log) {
// allow custom log for testing purposes
this.log = options.log;
}
this.log.debug("FxAccountsPush loading service");
this.wrappedJSObject = this;
this.initialize(options);
}
FxAccountsPushService.prototype = {
/**
* Helps only initialize observers once.
*/
_initialized: false,
/**
* Instance of the nsIPushService or a mocked object.
*/
pushService: null,
/**
* Instance of FxAccounts or a mocked object.
*/
fxAccounts: null,
/**
* Component ID of this service, helps register this component.
*/
classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"),
/**
* Register used interfaces in this service
*/
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
/**
* Initialize the service and register all the required observers.
*
* @param [options]
*/
initialize(options) {
if (this._initialized) {
return false;
}
this._initialized = true;
if (options.pushService) {
this.pushService = options.pushService;
} else {
this.pushService = Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService);
}
if (options.fxAccounts) {
this.fxAccounts = options.fxAccounts;
} else {
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
}
// listen to new push messages, push changes and logout events
Services.obs.addObserver(this, this.pushService.pushTopic, false);
Services.obs.addObserver(this, this.pushService.subscriptionChangeTopic, false);
Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
this.log.debug("FxAccountsPush initialized");
},
/**
* Registers a new endpoint with the Push Server
*
* @returns {Promise}
* Promise always resolves with a subscription or a null if failed to subscribe.
*/
registerPushEndpoint() {
this.log.trace("FxAccountsPush registerPushEndpoint");
return new Promise((resolve) => {
this.pushService.subscribe(FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, subscription) => {
if (Components.isSuccessCode(result)) {
this.log.debug("FxAccountsPush got subscription");
resolve(subscription);
} else {
this.log.warn("FxAccountsPush failed to subscribe", result);
resolve(null);
}
});
});
},
/**
* Standard observer interface to listen to push messages, changes and logout.
*
* @param subject
* @param topic
* @param data
* @returns {Promise}
*/
_observe(subject, topic, data) {
this.log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`);
switch (topic) {
case this.pushService.pushTopic:
if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
let message = subject.QueryInterface(Ci.nsIPushMessage);
return this._onPushMessage(message);
}
break;
case this.pushService.subscriptionChangeTopic:
if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
return this._onPushSubscriptionChange();
}
break;
case ONLOGOUT_NOTIFICATION:
// user signed out, we need to stop polling the Push Server
return this.unsubscribe().catch(err => {
this.log.error("Error during unsubscribe", err);
});
break;
default:
break;
}
},
/**
* Wrapper around _observe that catches errors
*/
observe(subject, topic, data) {
Promise.resolve()
.then(() => this._observe(subject, topic, data))
.catch(err => this.log.error(err));
},
/**
* Fired when the Push server sends a notification.
*
* @private
* @returns {Promise}
*/
_onPushMessage(message) {
this.log.trace("FxAccountsPushService _onPushMessage");
if (!message.data) {
// Use the empty signal to check the verification state of the account right away
this.log.debug("empty push message - checking account status");
return this.fxAccounts.checkVerificationStatus();
}
let payload = message.data.json();
this.log.debug(`push command: ${payload.command}`);
switch (payload.command) {
case ON_DEVICE_DISCONNECTED_NOTIFICATION:
return this.fxAccounts.handleDeviceDisconnection(payload.data.id);
break;
case ON_PASSWORD_CHANGED_NOTIFICATION:
case ON_PASSWORD_RESET_NOTIFICATION:
return this._onPasswordChanged();
break;
case ON_COLLECTION_CHANGED_NOTIFICATION:
Services.obs.notifyObservers(null, ON_COLLECTION_CHANGED_NOTIFICATION, payload.data.collections);
default:
this.log.warn("FxA Push command unrecognized: " + payload.command);
}
},
/**
* Check the FxA session status after a password change/reset event.
* If the session is invalid, reset credentials and notify listeners of
* ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed
*
* @returns {Promise}
* @private
*/
_onPasswordChanged: Task.async(function* () {
if (!(yield this.fxAccounts.sessionStatus())) {
yield this.fxAccounts.resetCredentials();
Services.obs.notifyObservers(null, ON_ACCOUNT_STATE_CHANGE_NOTIFICATION, null);
}
}),
/**
* Fired when the Push server drops a subscription, or the subscription identifier changes.
*
* https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages
*
* @returns {Promise}
* @private
*/
_onPushSubscriptionChange() {
this.log.trace("FxAccountsPushService _onPushSubscriptionChange");
return this.fxAccounts.updateDeviceRegistration();
},
/**
* Unsubscribe from the Push server
*
* Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
*
* @returns {Promise}
* @private
*/
unsubscribe() {
this.log.trace("FxAccountsPushService unsubscribe");
return new Promise((resolve) => {
this.pushService.unsubscribe(FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, ok) => {
if (Components.isSuccessCode(result)) {
if (ok === true) {
this.log.debug("FxAccountsPushService unsubscribed");
} else {
this.log.debug("FxAccountsPushService had no subscription to unsubscribe");
}
} else {
this.log.warn("FxAccountsPushService failed to unsubscribe", result);
}
return resolve(ok);
});
});
},
};
// Service registration below registers with FxAccountsComponents.manifest
const components = [FxAccountsPushService];
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
// The following registration below helps with testing this service.
this.EXPORTED_SYMBOLS=["FxAccountsPushService"];

View file

@ -1,606 +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/. */
"use strict";
this.EXPORTED_SYMBOLS = [
"FxAccountsStorageManagerCanStoreField",
"FxAccountsStorageManager",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://services-common/utils.js");
var haveLoginManager = true;
// A helper function so code can check what fields are able to be stored by
// the storage manager without having a reference to a manager instance.
function FxAccountsStorageManagerCanStoreField(fieldName) {
return FXA_PWDMGR_MEMORY_FIELDS.has(fieldName) ||
FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName) ||
FXA_PWDMGR_SECURE_FIELDS.has(fieldName);
}
// The storage manager object.
this.FxAccountsStorageManager = function(options = {}) {
this.options = {
filename: options.filename || DEFAULT_STORAGE_FILENAME,
baseDir: options.baseDir || OS.Constants.Path.profileDir,
}
this.plainStorage = new JSONStorage(this.options);
// On b2g we have no loginManager for secure storage, and tests may want
// to pretend secure storage isn't available.
let useSecure = 'useSecure' in options ? options.useSecure : haveLoginManager;
if (useSecure) {
this.secureStorage = new LoginManagerStorage();
} else {
this.secureStorage = null;
}
this._clearCachedData();
// See .initialize() below - this protects against it not being called.
this._promiseInitialized = Promise.reject("initialize not called");
// A promise to avoid storage races - see _queueStorageOperation
this._promiseStorageComplete = Promise.resolve();
}
this.FxAccountsStorageManager.prototype = {
_initialized: false,
_needToReadSecure: true,
// An initialization routine that *looks* synchronous to the callers, but
// is actually async as everything else waits for it to complete.
initialize(accountData) {
if (this._initialized) {
throw new Error("already initialized");
}
this._initialized = true;
// If we just throw away our pre-rejected promise it is reported as an
// unhandled exception when it is GCd - so add an empty .catch handler here
// to prevent this.
this._promiseInitialized.catch(() => {});
this._promiseInitialized = this._initialize(accountData);
},
_initialize: Task.async(function* (accountData) {
log.trace("initializing new storage manager");
try {
if (accountData) {
// If accountData is passed we don't need to read any storage.
this._needToReadSecure = false;
// split it into the 2 parts, write it and we are done.
for (let [name, val] of Object.entries(accountData)) {
if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
this.cachedPlain[name] = val;
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
this.cachedSecure[name] = val;
} else {
// Hopefully it's an "in memory" field. If it's not we log a warning
// but still treat it as such (so it will still be available in this
// session but isn't persisted anywhere.)
if (!FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
log.warn("Unknown FxA field name in user data, treating as in-memory", name);
}
this.cachedMemory[name] = val;
}
}
// write it out and we are done.
yield this._write();
return;
}
// So we were initialized without account data - that means we need to
// read the state from storage. We try and read plain storage first and
// only attempt to read secure storage if the plain storage had a user.
this._needToReadSecure = yield this._readPlainStorage();
if (this._needToReadSecure && this.secureStorage) {
yield this._doReadAndUpdateSecure();
}
} finally {
log.trace("initializing of new storage manager done");
}
}),
finalize() {
// We can't throw this instance away while it is still writing or we may
// end up racing with the newly created one.
log.trace("StorageManager finalizing");
return this._promiseInitialized.then(() => {
return this._promiseStorageComplete;
}).then(() => {
this._promiseStorageComplete = null;
this._promiseInitialized = null;
this._clearCachedData();
log.trace("StorageManager finalized");
})
},
// We want to make sure we don't end up doing multiple storage requests
// concurrently - which has a small window for reads if the master-password
// is locked at initialization time and becomes unlocked later, and always
// has an opportunity for updates.
// We also want to make sure we finished writing when finalizing, so we
// can't accidentally end up with the previous user's write finishing after
// a signOut attempts to clear it.
// So all such operations "queue" themselves via this.
_queueStorageOperation(func) {
// |result| is the promise we return - it has no .catch handler, so callers
// of the storage operation still see failure as a normal rejection.
let result = this._promiseStorageComplete.then(func);
// But the promise we assign to _promiseStorageComplete *does* have a catch
// handler so that rejections in one storage operation does not prevent
// future operations from starting (ie, _promiseStorageComplete must never
// be in a rejected state)
this._promiseStorageComplete = result.catch(err => {
log.error("${func} failed: ${err}", {func, err});
});
return result;
},
// Get the account data by combining the plain and secure storage.
// If fieldNames is specified, it may be a string or an array of strings,
// and only those fields are returned. If not specified the entire account
// data is returned except for "in memory" fields. Note that not specifying
// field names will soon be deprecated/removed - we want all callers to
// specify the fields they care about.
getAccountData: Task.async(function* (fieldNames = null) {
yield this._promiseInitialized;
// We know we are initialized - this means our .cachedPlain is accurate
// and doesn't need to be read (it was read if necessary by initialize).
// So if there's no uid, there's no user signed in.
if (!('uid' in this.cachedPlain)) {
return null;
}
let result = {};
if (fieldNames === null) {
// The "old" deprecated way of fetching a logged in user.
for (let [name, value] of Object.entries(this.cachedPlain)) {
result[name] = value;
}
// But the secure data may not have been read, so try that now.
yield this._maybeReadAndUpdateSecure();
// .cachedSecure now has as much as it possibly can (which is possibly
// nothing if (a) secure storage remains locked and (b) we've never updated
// a field to be stored in secure storage.)
for (let [name, value] of Object.entries(this.cachedSecure)) {
result[name] = value;
}
// Note we don't return cachedMemory fields here - they must be explicitly
// requested.
return result;
}
// The new explicit way of getting attributes.
if (!Array.isArray(fieldNames)) {
fieldNames = [fieldNames];
}
let checkedSecure = false;
for (let fieldName of fieldNames) {
if (FXA_PWDMGR_MEMORY_FIELDS.has(fieldName)) {
if (this.cachedMemory[fieldName] !== undefined) {
result[fieldName] = this.cachedMemory[fieldName];
}
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) {
if (this.cachedPlain[fieldName] !== undefined) {
result[fieldName] = this.cachedPlain[fieldName];
}
} else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) {
// We may not have read secure storage yet.
if (!checkedSecure) {
yield this._maybeReadAndUpdateSecure();
checkedSecure = true;
}
if (this.cachedSecure[fieldName] !== undefined) {
result[fieldName] = this.cachedSecure[fieldName];
}
} else {
throw new Error("unexpected field '" + name + "'");
}
}
return result;
}),
// Update just the specified fields. This DOES NOT allow you to change to
// a different user, nor to set the user as signed-out.
updateAccountData: Task.async(function* (newFields) {
yield this._promiseInitialized;
if (!('uid' in this.cachedPlain)) {
// If this storage instance shows no logged in user, then you can't
// update fields.
throw new Error("No user is logged in");
}
if (!newFields || 'uid' in newFields || 'email' in newFields) {
// Once we support
// user changing email address this may need to change, but it's not
// clear how we would be told of such a change anyway...
throw new Error("Can't change uid or email address");
}
log.debug("_updateAccountData with items", Object.keys(newFields));
// work out what bucket.
for (let [name, value] of Object.entries(newFields)) {
if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
if (value == null) {
delete this.cachedMemory[name];
} else {
this.cachedMemory[name] = value;
}
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
if (value == null) {
delete this.cachedPlain[name];
} else {
this.cachedPlain[name] = value;
}
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
// don't do the "delete on null" thing here - we need to keep it until
// we have managed to read so we can nuke it on write.
this.cachedSecure[name] = value;
} else {
// Throwing seems reasonable here as some client code has explicitly
// specified the field name, so it's either confused or needs to update
// how this field is to be treated.
throw new Error("unexpected field '" + name + "'");
}
}
// If we haven't yet read the secure data, do so now, else we may write
// out partial data.
yield this._maybeReadAndUpdateSecure();
// Now save it - but don't wait on the _write promise - it's queued up as
// a storage operation, so .finalize() will wait for completion, but no need
// for us to.
this._write();
}),
_clearCachedData() {
this.cachedMemory = {};
this.cachedPlain = {};
// If we don't have secure storage available we have cachedPlain and
// cachedSecure be the same object.
this.cachedSecure = this.secureStorage == null ? this.cachedPlain : {};
},
/* Reads the plain storage and caches the read values in this.cachedPlain.
Only ever called once and unlike the "secure" storage, is expected to never
fail (ie, plain storage is considered always available, whereas secure
storage may be unavailable if it is locked).
Returns a promise that resolves with true if valid account data was found,
false otherwise.
Note: _readPlainStorage is only called during initialize, so isn't
protected via _queueStorageOperation() nor _promiseInitialized.
*/
_readPlainStorage: Task.async(function* () {
let got;
try {
got = yield this.plainStorage.get();
} catch(err) {
// File hasn't been created yet. That will be done
// when write is called.
if (!(err instanceof OS.File.Error) || !err.becauseNoSuchFile) {
log.error("Failed to read plain storage", err);
}
// either way, we return null.
got = null;
}
if (!got || !got.accountData || !got.accountData.uid ||
got.version != DATA_FORMAT_VERSION) {
return false;
}
// We need to update our .cachedPlain, but can't just assign to it as
// it may need to be the exact same object as .cachedSecure
// As a sanity check, .cachedPlain must be empty (as we are called by init)
// XXX - this would be a good use-case for a RuntimeAssert or similar, as
// being added in bug 1080457.
if (Object.keys(this.cachedPlain).length != 0) {
throw new Error("should be impossible to have cached data already.")
}
for (let [name, value] of Object.entries(got.accountData)) {
this.cachedPlain[name] = value;
}
return true;
}),
/* If we haven't managed to read the secure storage, try now, so
we can merge our cached data with the data that's already been set.
*/
_maybeReadAndUpdateSecure: Task.async(function* () {
if (this.secureStorage == null || !this._needToReadSecure) {
return;
}
return this._queueStorageOperation(() => {
if (this._needToReadSecure) { // we might have read it by now!
return this._doReadAndUpdateSecure();
}
});
}),
/* Unconditionally read the secure storage and merge our cached data (ie, data
which has already been set while the secure storage was locked) with
the read data
*/
_doReadAndUpdateSecure: Task.async(function* () {
let { uid, email } = this.cachedPlain;
try {
log.debug("reading secure storage with existing", Object.keys(this.cachedSecure));
// If we already have anything in .cachedSecure it means something has
// updated cachedSecure before we've read it. That means that after we do
// manage to read we must write back the merged data.
let needWrite = Object.keys(this.cachedSecure).length != 0;
let readSecure = yield this.secureStorage.get(uid, email);
// and update our cached data with it - anything already in .cachedSecure
// wins (including the fact it may be null or undefined, the latter
// which means it will be removed from storage.
if (readSecure && readSecure.version != DATA_FORMAT_VERSION) {
log.warn("got secure data but the data format version doesn't match");
readSecure = null;
}
if (readSecure && readSecure.accountData) {
log.debug("secure read fetched items", Object.keys(readSecure.accountData));
for (let [name, value] of Object.entries(readSecure.accountData)) {
if (!(name in this.cachedSecure)) {
this.cachedSecure[name] = value;
}
}
if (needWrite) {
log.debug("successfully read secure data; writing updated data back")
yield this._doWriteSecure();
}
}
this._needToReadSecure = false;
} catch (ex) {
if (ex instanceof this.secureStorage.STORAGE_LOCKED) {
log.debug("setAccountData: secure storage is locked trying to read");
} else {
log.error("failed to read secure storage", ex);
throw ex;
}
}
}),
_write() {
// We don't want multiple writes happening concurrently, and we also need to
// know when an "old" storage manager is done (this.finalize() waits for this)
return this._queueStorageOperation(() => this.__write());
},
__write: Task.async(function* () {
// Write everything back - later we could track what's actually dirty,
// but for now we write it all.
log.debug("writing plain storage", Object.keys(this.cachedPlain));
let toWritePlain = {
version: DATA_FORMAT_VERSION,
accountData: this.cachedPlain,
}
yield this.plainStorage.set(toWritePlain);
// If we have no secure storage manager we are done.
if (this.secureStorage == null) {
return;
}
// and only attempt to write to secure storage if we've managed to read it,
// otherwise we might clobber data that's already there.
if (!this._needToReadSecure) {
yield this._doWriteSecure();
}
}),
/* Do the actual write of secure data. Caller is expected to check if we actually
need to write and to ensure we are in a queued storage operation.
*/
_doWriteSecure: Task.async(function* () {
// We need to remove null items here.
for (let [name, value] of Object.entries(this.cachedSecure)) {
if (value == null) {
delete this.cachedSecure[name];
}
}
log.debug("writing secure storage", Object.keys(this.cachedSecure));
let toWriteSecure = {
version: DATA_FORMAT_VERSION,
accountData: this.cachedSecure,
}
try {
yield this.secureStorage.set(this.cachedPlain.uid, toWriteSecure);
} catch (ex) {
if (!(ex instanceof this.secureStorage.STORAGE_LOCKED)) {
throw ex;
}
// This shouldn't be possible as once it is unlocked it can't be
// re-locked, and we can only be here if we've previously managed to
// read.
log.error("setAccountData: secure storage is locked trying to write");
}
}),
// Delete the data for an account - ie, called on "sign out".
deleteAccountData() {
return this._queueStorageOperation(() => this._deleteAccountData());
},
_deleteAccountData: Task.async(function* () {
log.debug("removing account data");
yield this._promiseInitialized;
yield this.plainStorage.set(null);
if (this.secureStorage) {
yield this.secureStorage.set(null);
}
this._clearCachedData();
log.debug("account data reset");
}),
}
/**
* JSONStorage constructor that creates instances that may set/get
* to a specified file, in a directory that will be created if it
* doesn't exist.
*
* @param options {
* filename: of the file to write to
* baseDir: directory where the file resides
* }
* @return instance
*/
function JSONStorage(options) {
this.baseDir = options.baseDir;
this.path = OS.Path.join(options.baseDir, options.filename);
};
JSONStorage.prototype = {
set: function(contents) {
log.trace("starting write of json user data", contents ? Object.keys(contents.accountData) : "null");
let start = Date.now();
return OS.File.makeDir(this.baseDir, {ignoreExisting: true})
.then(CommonUtils.writeJSON.bind(null, contents, this.path))
.then(result => {
log.trace("finished write of json user data - took", Date.now()-start);
return result;
});
},
get: function() {
log.trace("starting fetch of json user data");
let start = Date.now();
return CommonUtils.readJSON(this.path).then(result => {
log.trace("finished fetch of json user data - took", Date.now()-start);
return result;
});
},
};
function StorageLockedError() {
}
/**
* LoginManagerStorage constructor that creates instances that set/get
* data stored securely in the nsILoginManager.
*
* @return instance
*/
function LoginManagerStorage() {
}
LoginManagerStorage.prototype = {
STORAGE_LOCKED: StorageLockedError,
// The fields in the credentials JSON object that are stored in plain-text
// in the profile directory. All other fields are stored in the login manager,
// and thus are only available when the master-password is unlocked.
// a hook point for testing.
get _isLoggedIn() {
return Services.logins.isLoggedIn;
},
// Clear any data from the login manager. Returns true if the login manager
// was unlocked (even if no existing logins existed) or false if it was
// locked (meaning we don't even know if it existed or not.)
_clearLoginMgrData: Task.async(function* () {
try { // Services.logins might be third-party and broken...
yield Services.logins.initializationPromise;
if (!this._isLoggedIn) {
return false;
}
let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
for (let login of logins) {
Services.logins.removeLogin(login);
}
return true;
} catch (ex) {
log.error("Failed to clear login data: ${}", ex);
return false;
}
}),
set: Task.async(function* (uid, contents) {
if (!contents) {
// Nuke it from the login manager.
let cleared = yield this._clearLoginMgrData();
if (!cleared) {
// just log a message - we verify that the uid matches when
// we reload it, so having a stale entry doesn't really hurt.
log.info("not removing credentials from login manager - not logged in");
}
log.trace("storage set finished clearing account data");
return;
}
// We are saving actual data.
log.trace("starting write of user data to the login manager");
try { // Services.logins might be third-party and broken...
// and the stuff into the login manager.
yield Services.logins.initializationPromise;
// If MP is locked we silently fail - the user may need to re-auth
// next startup.
if (!this._isLoggedIn) {
log.info("not saving credentials to login manager - not logged in");
throw new this.STORAGE_LOCKED();
}
// write the data to the login manager.
let loginInfo = new Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
let login = new loginInfo(FXA_PWDMGR_HOST,
null, // aFormSubmitURL,
FXA_PWDMGR_REALM, // aHttpRealm,
uid, // aUsername
JSON.stringify(contents), // aPassword
"", // aUsernameField
"");// aPasswordField
let existingLogins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null,
FXA_PWDMGR_REALM);
if (existingLogins.length) {
Services.logins.modifyLogin(existingLogins[0], login);
} else {
Services.logins.addLogin(login);
}
log.trace("finished write of user data to the login manager");
} catch (ex) {
if (ex instanceof this.STORAGE_LOCKED) {
throw ex;
}
// just log and consume the error here - it may be a 3rd party login
// manager replacement that's simply broken.
log.error("Failed to save data to the login manager", ex);
}
}),
get: Task.async(function* (uid, email) {
log.trace("starting fetch of user data from the login manager");
try { // Services.logins might be third-party and broken...
// read the data from the login manager and merge it for return.
yield Services.logins.initializationPromise;
if (!this._isLoggedIn) {
log.info("returning partial account data as the login manager is locked.");
throw new this.STORAGE_LOCKED();
}
let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
if (logins.length == 0) {
// This could happen if the MP was locked when we wrote the data.
log.info("Can't find any credentials in the login manager");
return null;
}
let login = logins[0];
// Support either the uid or the email as the username - as of bug 1183951
// we store the uid, but we support having either for b/w compat.
if (login.username == uid || login.username == email) {
return JSON.parse(login.password);
}
log.info("username in the login manager doesn't match - ignoring it");
yield this._clearLoginMgrData();
} catch (ex) {
if (ex instanceof this.STORAGE_LOCKED) {
throw ex;
}
// just log and consume the error here - it may be a 3rd party login
// manager replacement that's simply broken.
log.error("Failed to get data from the login manager", ex);
}
return null;
}),
}

View file

@ -1,474 +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 Web Channel.
*
* Uses the WebChannel component to receive messages
* about account state changes.
*/
this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
"resource://gre/modules/WebChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsStorageManagerCanStoreField",
"resource://gre/modules/FxAccountsStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
"resource://services-sync/main.js");
const COMMAND_PROFILE_CHANGE = "profile:change";
const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account";
const COMMAND_LOGIN = "fxaccounts:login";
const COMMAND_LOGOUT = "fxaccounts:logout";
const COMMAND_DELETE = "fxaccounts:delete";
const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences";
const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
/**
* A helper function that extracts the message and stack from an error object.
* Returns a `{ message, stack }` tuple. `stack` will be null if the error
* doesn't have a stack trace.
*/
function getErrorDetails(error) {
let details = { message: String(error), stack: null };
// Adapted from Console.jsm.
if (error.stack) {
let frames = [];
for (let frame = error.stack; frame; frame = frame.caller) {
frames.push(String(frame).padStart(4));
}
details.stack = frames.join("\n");
}
return details;
}
/**
* Create a new FxAccountsWebChannel to listen for account updates
*
* @param {Object} options Options
* @param {Object} options
* @param {String} options.content_uri
* The FxA Content server uri
* @param {String} options.channel_id
* The ID of the WebChannel
* @param {String} options.helpers
* Helpers functions. Should only be passed in for testing.
* @constructor
*/
this.FxAccountsWebChannel = function(options) {
if (!options) {
throw new Error("Missing configuration options");
}
if (!options["content_uri"]) {
throw new Error("Missing 'content_uri' option");
}
this._contentUri = options.content_uri;
if (!options["channel_id"]) {
throw new Error("Missing 'channel_id' option");
}
this._webChannelId = options.channel_id;
// options.helpers is only specified by tests.
this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
this._setupChannel();
};
this.FxAccountsWebChannel.prototype = {
/**
* WebChannel that is used to communicate with content page
*/
_channel: null,
/**
* Helpers interface that does the heavy lifting.
*/
_helpers: null,
/**
* WebChannel ID.
*/
_webChannelId: null,
/**
* WebChannel origin, used to validate origin of messages
*/
_webChannelOrigin: null,
/**
* Release all resources that are in use.
*/
tearDown() {
this._channel.stopListening();
this._channel = null;
this._channelCallback = null;
},
/**
* Configures and registers a new WebChannel
*
* @private
*/
_setupChannel() {
// if this.contentUri is present but not a valid URI, then this will throw an error.
try {
this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null);
this._registerChannel();
} catch (e) {
log.error(e);
throw e;
}
},
_receiveMessage(message, sendingContext) {
let command = message.command;
let data = message.data;
switch (command) {
case COMMAND_PROFILE_CHANGE:
Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, data.uid);
break;
case COMMAND_LOGIN:
this._helpers.login(data).catch(error =>
this._sendError(error, message, sendingContext));
break;
case COMMAND_LOGOUT:
case COMMAND_DELETE:
this._helpers.logout(data.uid).catch(error =>
this._sendError(error, message, sendingContext));
break;
case COMMAND_CAN_LINK_ACCOUNT:
let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
let response = {
command: command,
messageId: message.messageId,
data: { ok: canLinkAccount }
};
log.debug("FxAccountsWebChannel response", response);
this._channel.send(response, sendingContext);
break;
case COMMAND_SYNC_PREFERENCES:
this._helpers.openSyncPreferences(sendingContext.browser, data.entryPoint);
break;
case COMMAND_CHANGE_PASSWORD:
this._helpers.changePassword(data).catch(error =>
this._sendError(error, message, sendingContext));
break;
default:
log.warn("Unrecognized FxAccountsWebChannel command", command);
break;
}
},
_sendError(error, incomingMessage, sendingContext) {
log.error("Failed to handle FxAccountsWebChannel message", error);
this._channel.send({
command: incomingMessage.command,
messageId: incomingMessage.messageId,
data: {
error: getErrorDetails(error),
},
}, sendingContext);
},
/**
* Create a new channel with the WebChannelBroker, setup a callback listener
* @private
*/
_registerChannel() {
/**
* Processes messages that are called back from the FxAccountsChannel
*
* @param webChannelId {String}
* Command webChannelId
* @param message {Object}
* Command message
* @param sendingContext {Object}
* Message sending context.
* @param sendingContext.browser {browser}
* The <browser> object that captured the
* WebChannelMessageToChrome.
* @param sendingContext.eventTarget {EventTarget}
* The <EventTarget> where the message was sent.
* @param sendingContext.principal {Principal}
* The <Principal> of the EventTarget where the message was sent.
* @private
*
*/
let listener = (webChannelId, message, sendingContext) => {
if (message) {
log.debug("FxAccountsWebChannel message received", message.command);
if (logPII) {
log.debug("FxAccountsWebChannel message details", message);
}
try {
this._receiveMessage(message, sendingContext);
} catch (error) {
this._sendError(error, message, sendingContext);
}
}
};
this._channelCallback = listener;
this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
this._channel.listen(listener);
log.debug("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
}
};
this.FxAccountsWebChannelHelpers = function(options) {
options = options || {};
this._fxAccounts = options.fxAccounts || fxAccounts;
};
this.FxAccountsWebChannelHelpers.prototype = {
// If the last fxa account used for sync isn't this account, we display
// a modal dialog checking they really really want to do this...
// (This is sync-specific, so ideally would be in sync's identity module,
// but it's a little more seamless to do here, and sync is currently the
// only fxa consumer, so...
shouldAllowRelink(acctName) {
return !this._needRelinkWarning(acctName) ||
this._promptForRelink(acctName);
},
/**
* New users are asked in the content server whether they want to
* customize which data should be synced. The user is only shown
* the dialog listing the possible data types upon verification.
*
* Save a bit into prefs that is read on verification to see whether
* to show the list of data types that can be saved.
*/
setShowCustomizeSyncPref(showCustomizeSyncPref) {
Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, showCustomizeSyncPref);
},
getShowCustomizeSyncPref() {
return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
},
/**
* stores sync login info it in the fxaccounts service
*
* @param accountData the user's account data and credentials
*/
login(accountData) {
if (accountData.customizeSync) {
this.setShowCustomizeSyncPref(true);
delete accountData.customizeSync;
}
if (accountData.declinedSyncEngines) {
let declinedSyncEngines = accountData.declinedSyncEngines;
log.debug("Received declined engines", declinedSyncEngines);
Weave.Service.engineManager.setDeclined(declinedSyncEngines);
declinedSyncEngines.forEach(engine => {
Services.prefs.setBoolPref("services.sync.engine." + engine, false);
});
// if we got declinedSyncEngines that means we do not need to show the customize screen.
this.setShowCustomizeSyncPref(false);
delete accountData.declinedSyncEngines;
}
// the user has already been shown the "can link account"
// screen. No need to keep this data around.
delete accountData.verifiedCanLinkAccount;
// Remember who it was so we can log out next time.
this.setPreviousAccountNameHashPref(accountData.email);
// A sync-specific hack - we want to ensure sync has been initialized
// before we set the signed-in user.
let xps = Cc["@mozilla.org/weave/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
return xps.whenLoaded().then(() => {
return this._fxAccounts.setSignedInUser(accountData);
});
},
/**
* logout the fxaccounts service
*
* @param the uid of the account which have been logged out
*/
logout(uid) {
return fxAccounts.getSignedInUser().then(userData => {
if (userData.uid === uid) {
// true argument is `localOnly`, because server-side stuff
// has already been taken care of by the content server
return fxAccounts.signOut(true);
}
});
},
changePassword(credentials) {
// If |credentials| has fields that aren't handled by accounts storage,
// updateUserAccountData will throw - mainly to prevent errors in code
// that hard-codes field names.
// However, in this case the field names aren't really in our control.
// We *could* still insist the server know what fields names are valid,
// but that makes life difficult for the server when Firefox adds new
// features (ie, new fields) - forcing the server to track a map of
// versions to supported field names doesn't buy us much.
// So we just remove field names we know aren't handled.
let newCredentials = {
deviceId: null
};
for (let name of Object.keys(credentials)) {
if (name == "email" || name == "uid" || FxAccountsStorageManagerCanStoreField(name)) {
newCredentials[name] = credentials[name];
} else {
log.info("changePassword ignoring unsupported field", name);
}
}
return this._fxAccounts.updateUserAccountData(newCredentials)
.then(() => this._fxAccounts.updateDeviceRegistration());
},
/**
* Get the hash of account name of the previously signed in account
*/
getPreviousAccountNameHashPref() {
try {
return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
} catch (_) {
return "";
}
},
/**
* Given an account name, set the hash of the previously signed in account
*
* @param acctName the account name of the user's account.
*/
setPreviousAccountNameHashPref(acctName) {
let string = Cc["@mozilla.org/supports-string;1"]
.createInstance(Ci.nsISupportsString);
string.data = this.sha256(acctName);
Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
},
/**
* Given a string, returns the SHA265 hash in base64
*/
sha256(str) {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
// Data is an array of bytes.
let data = converter.convertToByteArray(str, {});
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
hasher.update(data, data.length);
return hasher.finish(true);
},
/**
* Open Sync Preferences in the current tab of the browser
*
* @param {Object} browser the browser in which to open preferences
* @param {String} [entryPoint] entryPoint to use for logging
*/
openSyncPreferences(browser, entryPoint) {
let uri = "about:preferences";
if (entryPoint) {
uri += "?entrypoint=" + encodeURIComponent(entryPoint);
}
uri += "#sync";
browser.loadURI(uri);
},
/**
* If a user signs in using a different account, the data from the
* previous account and the new account will be merged. Ask the user
* if they want to continue.
*
* @private
*/
_needRelinkWarning(acctName) {
let prevAcctHash = this.getPreviousAccountNameHashPref();
return prevAcctHash && prevAcctHash != this.sha256(acctName);
},
/**
* Show the user a warning dialog that the data from the previous account
* and the new account will be merged.
*
* @private
*/
_promptForRelink(acctName) {
let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
let continueLabel = sb.GetStringFromName("continue.label");
let title = sb.GetStringFromName("relinkVerify.title");
let description = sb.formatStringFromName("relinkVerify.description",
[acctName], 1);
let body = sb.GetStringFromName("relinkVerify.heading") +
"\n\n" + description;
let ps = Services.prompt;
let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) +
(ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) +
ps.BUTTON_POS_1_DEFAULT;
// If running in context of the browser chrome, window does not exist.
var targetWindow = typeof window === 'undefined' ? null : window;
let pressed = Services.prompt.confirmEx(targetWindow, title, body, buttonFlags,
continueLabel, null, null, null,
{});
return pressed === 0; // 0 is the "continue" button
}
};
var singleton;
// The entry-point for this module, which ensures only one of our channels is
// ever created - we require this because the WebChannel is global in scope
// (eg, it uses the observer service to tell interested parties of interesting
// things) and allowing multiple channels would cause such notifications to be
// sent multiple times.
this.EnsureFxAccountsWebChannel = function() {
let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
if (singleton && singleton._contentUri !== contentUri) {
singleton.tearDown();
singleton = null;
}
if (!singleton) {
try {
if (contentUri) {
// The FxAccountsWebChannel listens for events and updates
// the state machine accordingly.
singleton = new this.FxAccountsWebChannel({
content_uri: contentUri,
channel_id: WEBCHANNEL_ID,
});
} else {
log.warn("FxA WebChannel functionaly is disabled due to no URI pref.");
}
} catch (ex) {
log.error("Failed to create FxA WebChannel", ex);
}
}
}

View file

@ -1,11 +0,0 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
XPIDL_SOURCES += [
'nsIFxAccountsUIGlue.idl'
]
XPIDL_MODULE = 'services_fxaccounts'

View file

@ -1,15 +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/. */
#include "nsISupports.idl"
[scriptable, uuid(ab8d0700-9577-11e3-a5e2-0800200c9a66)]
interface nsIFxAccountsUIGlue : nsISupports
{
// Returns a Promise.
jsval signInFlow();
// Returns a Promise.
jsval refreshAuthentication(in DOMString email);
};

View file

@ -1,32 +0,0 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DIRS += ['interfaces']
MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
EXTRA_COMPONENTS += [
'FxAccountsComponents.manifest',
'FxAccountsPush.js',
]
EXTRA_JS_MODULES += [
'Credentials.jsm',
'FxAccounts.jsm',
'FxAccountsClient.jsm',
'FxAccountsCommon.js',
'FxAccountsConfig.jsm',
'FxAccountsOAuthClient.jsm',
'FxAccountsOAuthGrantClient.jsm',
'FxAccountsProfile.jsm',
'FxAccountsProfileClient.jsm',
'FxAccountsPush.js',
'FxAccountsStorage.jsm',
'FxAccountsWebChannel.jsm',
]

View file

@ -1,7 +0,0 @@
[DEFAULT]
skip-if = os == 'android'
support-files=
file_invalidEmailCase.sjs
[test_invalidEmailCase.html]

View file

@ -1,80 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This server simulates the behavior of /account/login on the Firefox Accounts
* auth server in the case where the user is trying to sign in with an email
* with the wrong capitalization.
*
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountlogin
*
* The expected behavior is that on the first attempt, with the wrong email,
* the server will respond with a 400 and the canonical email capitalization
* that the client should use. The client then has one chance to sign in with
* this different capitalization.
*
* In this test, the user with the account id "Greta.Garbo@gmail.COM" initially
* tries to sign in as "greta.garbo@gmail.com".
*
* On success, the client is responsible for updating its sign-in user state
* and recording the proper email capitalization.
*/
const CC = Components.Constructor;
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream");
const goodEmail = "Greta.Garbo@gmail.COM";
const badEmail = "greta.garbo@gmail.com";
function handleRequest(request, response) {
let body = new BinaryInputStream(request.bodyInputStream);
let bytes = [];
let available;
while ((available = body.available()) > 0) {
Array.prototype.push.apply(bytes, body.readByteArray(available));
}
let data = JSON.parse(String.fromCharCode.apply(null, bytes));
let message;
switch (data.email) {
case badEmail:
// Almost - try again with fixed email case
message = {
code: 400,
errno: 120,
error: "Incorrect email case",
email: goodEmail,
};
response.setStatusLine(request.httpVersion, 400, "Almost");
break;
case goodEmail:
// Successful login.
message = {
uid: "your-uid",
sessionToken: "your-sessionToken",
keyFetchToken: "your-keyFetchToken",
verified: true,
authAt: 1392144866,
};
response.setStatusLine(request.httpVersion, 200, "Yay");
break;
default:
// Anything else happening in this test is a failure.
message = {
code: 400,
errno: 999,
error: "What happened!?",
};
response.setStatusLine(request.httpVersion, 400, "Ouch");
break;
}
messageStr = JSON.stringify(message);
response.bodyOutputStream.write(messageStr, messageStr.length);
}

View file

@ -1,131 +0,0 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<!DOCTYPE HTML>
<html>
<!--
Tests for Firefox Accounts signin with invalid email case
https://bugzilla.mozilla.org/show_bug.cgi?id=963835
-->
<head>
<title>Test for Firefox Accounts (Bug 963835)</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=963835">Mozilla Bug 963835</a>
<p id="display"></p>
<div id="content" style="display: none">
Test for correction of invalid email case in Fx Accounts signIn
</div>
<pre id="test">
<script class="testbody" type="text/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
Components.utils.import("resource://gre/modules/Promise.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/FxAccounts.jsm");
Components.utils.import("resource://gre/modules/FxAccountsClient.jsm");
Components.utils.import("resource://services-common/hawkclient.js");
const TEST_SERVER =
"http://mochi.test:8888/chrome/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs?path=";
let MockStorage = function() {
this.data = null;
};
MockStorage.prototype = Object.freeze({
set: function (contents) {
this.data = contents;
return Promise.resolve(null);
},
get: function () {
return Promise.resolve(this.data);
},
getOAuthTokens() {
return Promise.resolve(null);
},
setOAuthTokens(contents) {
return Promise.resolve();
},
});
function MockFxAccounts() {
return new FxAccounts({
_now_is: new Date(),
now: function() {
return this._now_is;
},
signedInUserStorage: new MockStorage(),
fxAccountsClient: new FxAccountsClient(TEST_SERVER),
});
}
let wrongEmail = "greta.garbo@gmail.com";
let rightEmail = "Greta.Garbo@gmail.COM";
let password = "123456";
function runTest() {
is(Services.prefs.getCharPref("identity.fxaccounts.auth.uri"), TEST_SERVER,
"Pref for auth.uri should be set to test server");
let fxa = new MockFxAccounts();
let client = fxa.internal.fxAccountsClient;
ok(true, !!fxa, "Couldn't mock fxa");
ok(true, !!client, "Couldn't mock fxa client");
is(client.host, TEST_SERVER, "Should be using the test auth server uri");
// First try to sign in using the email with the wrong capitalization. The
// FxAccountsClient will receive a 400 from the server with the corrected email.
// It will automatically try to sign in again. We expect this to succeed.
client.signIn(wrongEmail, password).then(
user => {
// Now store the signed-in user state. This will include the correct
// email capitalization.
fxa.setSignedInUser(user).then(
() => {
// Confirm that the correct email got stored.
fxa.getSignedInUser().then(
data => {
is(data.email, rightEmail);
SimpleTest.finish();
},
getUserError => {
ok(false, JSON.stringify(getUserError));
}
);
},
setSignedInUserError => {
ok(false, JSON.stringify(setSignedInUserError));
}
);
},
signInError => {
ok(false, JSON.stringify(signInError));
}
);
};
SpecialPowers.pushPrefEnv({"set": [
["identity.fxaccounts.enabled", true], // fx accounts
["identity.fxaccounts.auth.uri", TEST_SERVER], // our sjs server
["toolkit.identity.debug", true], // verbose identity logging
["browser.dom.window.dump.enabled", true],
]},
function () { runTest(); }
);
</script>
</pre>
</body>
</html>

View file

@ -1,18 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
"use strict";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
(function initFxAccountsTestingInfrastructure() {
do_get_profile();
let ns = {};
Cu.import("resource://testing-common/services/common/logging.js", ns);
ns.initTestLogging("Trace");
}).call(this);

File diff suppressed because it is too large Load diff

View file

@ -1,526 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Log.jsm");
initTestLogging("Trace");
var log = Log.repository.getLogger("Services.FxAccounts.test");
log.level = Log.Level.Debug;
const BOGUS_PUBLICKEY = "BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc";
const BOGUS_AUTHKEY = "GSsIiaD2Mr83iPqwFNK4rw";
Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace");
Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace;
Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1");
Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123");
Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", "http://example.com/v1");
Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "http://accounts.example.com/");
const DEVICE_REGISTRATION_VERSION = 42;
function MockStorageManager() {
}
MockStorageManager.prototype = {
initialize(accountData) {
this.accountData = accountData;
},
finalize() {
return Promise.resolve();
},
getAccountData() {
return Promise.resolve(this.accountData);
},
updateAccountData(updatedFields) {
for (let [name, value] of Object.entries(updatedFields)) {
if (value == null) {
delete this.accountData[name];
} else {
this.accountData[name] = value;
}
}
return Promise.resolve();
},
deleteAccountData() {
this.accountData = null;
return Promise.resolve();
}
}
function MockFxAccountsClient(device) {
this._email = "nobody@example.com";
this._verified = false;
this._deletedOnServer = false; // for testing accountStatus
// mock calls up to the auth server to determine whether the
// user account has been verified
this.recoveryEmailStatus = function (sessionToken) {
// simulate a call to /recovery_email/status
return Promise.resolve({
email: this._email,
verified: this._verified
});
};
this.accountStatus = function(uid) {
let deferred = Promise.defer();
deferred.resolve(!!uid && (!this._deletedOnServer));
return deferred.promise;
};
const { id: deviceId, name: deviceName, type: deviceType, sessionToken } = device;
this.registerDevice = (st, name, type) => Promise.resolve({ id: deviceId, name });
this.updateDevice = (st, id, name) => Promise.resolve({ id, name });
this.signOutAndDestroyDevice = () => Promise.resolve({});
this.getDeviceList = (st) =>
Promise.resolve([
{ id: deviceId, name: deviceName, type: deviceType, isCurrentDevice: st === sessionToken }
]);
FxAccountsClient.apply(this);
}
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype
}
function MockFxAccounts(device = {}) {
return new FxAccounts({
_getDeviceName() {
return device.name || "mock device name";
},
fxAccountsClient: new MockFxAccountsClient(device),
fxaPushService: {
registerPushEndpoint() {
return new Promise((resolve) => {
resolve({
endpoint: "http://mochi.test:8888",
getKey: function(type) {
return ChromeUtils.base64URLDecode(
type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY,
{ padding: "ignore" });
}
});
});
},
},
DEVICE_REGISTRATION_VERSION
});
}
add_task(function* test_updateDeviceRegistration_with_new_device() {
const deviceName = "foo";
const deviceType = "bar";
const credentials = getTestUser("baz");
delete credentials.deviceId;
const fxa = new MockFxAccounts({ name: deviceName });
yield fxa.internal.setSignedInUser(credentials);
const spy = {
registerDevice: { count: 0, args: [] },
updateDevice: { count: 0, args: [] },
getDeviceList: { count: 0, args: [] }
};
const client = fxa.internal.fxAccountsClient;
client.registerDevice = function () {
spy.registerDevice.count += 1;
spy.registerDevice.args.push(arguments);
return Promise.resolve({
id: "newly-generated device id",
createdAt: Date.now(),
name: deviceName,
type: deviceType
});
};
client.updateDevice = function () {
spy.updateDevice.count += 1;
spy.updateDevice.args.push(arguments);
return Promise.resolve({});
};
client.getDeviceList = function () {
spy.getDeviceList.count += 1;
spy.getDeviceList.args.push(arguments);
return Promise.resolve([]);
};
const result = yield fxa.updateDeviceRegistration();
do_check_eq(result, "newly-generated device id");
do_check_eq(spy.updateDevice.count, 0);
do_check_eq(spy.getDeviceList.count, 0);
do_check_eq(spy.registerDevice.count, 1);
do_check_eq(spy.registerDevice.args[0].length, 4);
do_check_eq(spy.registerDevice.args[0][0], credentials.sessionToken);
do_check_eq(spy.registerDevice.args[0][1], deviceName);
do_check_eq(spy.registerDevice.args[0][2], "desktop");
do_check_eq(spy.registerDevice.args[0][3].pushCallback, "http://mochi.test:8888");
do_check_eq(spy.registerDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
do_check_eq(spy.registerDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_eq(data.deviceId, "newly-generated device id");
do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
});
add_task(function* test_updateDeviceRegistration_with_existing_device() {
const deviceName = "phil's device";
const deviceType = "desktop";
const credentials = getTestUser("pb");
const fxa = new MockFxAccounts({ name: deviceName });
yield fxa.internal.setSignedInUser(credentials);
const spy = {
registerDevice: { count: 0, args: [] },
updateDevice: { count: 0, args: [] },
getDeviceList: { count: 0, args: [] }
};
const client = fxa.internal.fxAccountsClient;
client.registerDevice = function () {
spy.registerDevice.count += 1;
spy.registerDevice.args.push(arguments);
return Promise.resolve({});
};
client.updateDevice = function () {
spy.updateDevice.count += 1;
spy.updateDevice.args.push(arguments);
return Promise.resolve({
id: credentials.deviceId,
name: deviceName
});
};
client.getDeviceList = function () {
spy.getDeviceList.count += 1;
spy.getDeviceList.args.push(arguments);
return Promise.resolve([]);
};
const result = yield fxa.updateDeviceRegistration();
do_check_eq(result, credentials.deviceId);
do_check_eq(spy.registerDevice.count, 0);
do_check_eq(spy.getDeviceList.count, 0);
do_check_eq(spy.updateDevice.count, 1);
do_check_eq(spy.updateDevice.args[0].length, 4);
do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
do_check_eq(spy.updateDevice.args[0][2], deviceName);
do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_eq(data.deviceId, credentials.deviceId);
do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
});
add_task(function* test_updateDeviceRegistration_with_unknown_device_error() {
const deviceName = "foo";
const deviceType = "bar";
const credentials = getTestUser("baz");
const fxa = new MockFxAccounts({ name: deviceName });
yield fxa.internal.setSignedInUser(credentials);
const spy = {
registerDevice: { count: 0, args: [] },
updateDevice: { count: 0, args: [] },
getDeviceList: { count: 0, args: [] }
};
const client = fxa.internal.fxAccountsClient;
client.registerDevice = function () {
spy.registerDevice.count += 1;
spy.registerDevice.args.push(arguments);
return Promise.resolve({
id: "a different newly-generated device id",
createdAt: Date.now(),
name: deviceName,
type: deviceType
});
};
client.updateDevice = function () {
spy.updateDevice.count += 1;
spy.updateDevice.args.push(arguments);
return Promise.reject({
code: 400,
errno: ERRNO_UNKNOWN_DEVICE
});
};
client.getDeviceList = function () {
spy.getDeviceList.count += 1;
spy.getDeviceList.args.push(arguments);
return Promise.resolve([]);
};
const result = yield fxa.updateDeviceRegistration();
do_check_null(result);
do_check_eq(spy.getDeviceList.count, 0);
do_check_eq(spy.registerDevice.count, 0);
do_check_eq(spy.updateDevice.count, 1);
do_check_eq(spy.updateDevice.args[0].length, 4);
do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
do_check_eq(spy.updateDevice.args[0][2], deviceName);
do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_null(data.deviceId);
do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
});
add_task(function* test_updateDeviceRegistration_with_device_session_conflict_error() {
const deviceName = "foo";
const deviceType = "bar";
const credentials = getTestUser("baz");
const fxa = new MockFxAccounts({ name: deviceName });
yield fxa.internal.setSignedInUser(credentials);
const spy = {
registerDevice: { count: 0, args: [] },
updateDevice: { count: 0, args: [], times: [] },
getDeviceList: { count: 0, args: [] }
};
const client = fxa.internal.fxAccountsClient;
client.registerDevice = function () {
spy.registerDevice.count += 1;
spy.registerDevice.args.push(arguments);
return Promise.resolve({});
};
client.updateDevice = function () {
spy.updateDevice.count += 1;
spy.updateDevice.args.push(arguments);
spy.updateDevice.time = Date.now();
if (spy.updateDevice.count === 1) {
return Promise.reject({
code: 400,
errno: ERRNO_DEVICE_SESSION_CONFLICT
});
}
return Promise.resolve({
id: credentials.deviceId,
name: deviceName
});
};
client.getDeviceList = function () {
spy.getDeviceList.count += 1;
spy.getDeviceList.args.push(arguments);
spy.getDeviceList.time = Date.now();
return Promise.resolve([
{ id: "ignore", name: "ignore", type: "ignore", isCurrentDevice: false },
{ id: credentials.deviceId, name: deviceName, type: deviceType, isCurrentDevice: true }
]);
};
const result = yield fxa.updateDeviceRegistration();
do_check_eq(result, credentials.deviceId);
do_check_eq(spy.registerDevice.count, 0);
do_check_eq(spy.updateDevice.count, 1);
do_check_eq(spy.updateDevice.args[0].length, 4);
do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
do_check_eq(spy.updateDevice.args[0][2], deviceName);
do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
do_check_eq(spy.getDeviceList.count, 1);
do_check_eq(spy.getDeviceList.args[0].length, 1);
do_check_eq(spy.getDeviceList.args[0][0], credentials.sessionToken);
do_check_true(spy.getDeviceList.time >= spy.updateDevice.time);
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_eq(data.deviceId, credentials.deviceId);
do_check_eq(data.deviceRegistrationVersion, null);
});
add_task(function* test_updateDeviceRegistration_with_unrecoverable_error() {
const deviceName = "foo";
const deviceType = "bar";
const credentials = getTestUser("baz");
delete credentials.deviceId;
const fxa = new MockFxAccounts({ name: deviceName });
yield fxa.internal.setSignedInUser(credentials);
const spy = {
registerDevice: { count: 0, args: [] },
updateDevice: { count: 0, args: [] },
getDeviceList: { count: 0, args: [] }
};
const client = fxa.internal.fxAccountsClient;
client.registerDevice = function () {
spy.registerDevice.count += 1;
spy.registerDevice.args.push(arguments);
return Promise.reject({
code: 400,
errno: ERRNO_TOO_MANY_CLIENT_REQUESTS
});
};
client.updateDevice = function () {
spy.updateDevice.count += 1;
spy.updateDevice.args.push(arguments);
return Promise.resolve({});
};
client.getDeviceList = function () {
spy.getDeviceList.count += 1;
spy.getDeviceList.args.push(arguments);
return Promise.resolve([]);
};
const result = yield fxa.updateDeviceRegistration();
do_check_null(result);
do_check_eq(spy.getDeviceList.count, 0);
do_check_eq(spy.updateDevice.count, 0);
do_check_eq(spy.registerDevice.count, 1);
do_check_eq(spy.registerDevice.args[0].length, 4);
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_null(data.deviceId);
});
add_task(function* test_getDeviceId_with_no_device_id_invokes_device_registration() {
const credentials = getTestUser("foo");
credentials.verified = true;
delete credentials.deviceId;
const fxa = new MockFxAccounts();
yield fxa.internal.setSignedInUser(credentials);
const spy = { count: 0, args: [] };
fxa.internal.currentAccountState.getUserAccountData =
() => Promise.resolve({ email: credentials.email,
deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION });
fxa.internal._registerOrUpdateDevice = function () {
spy.count += 1;
spy.args.push(arguments);
return Promise.resolve("bar");
};
const result = yield fxa.internal.getDeviceId();
do_check_eq(spy.count, 1);
do_check_eq(spy.args[0].length, 1);
do_check_eq(spy.args[0][0].email, credentials.email);
do_check_null(spy.args[0][0].deviceId);
do_check_eq(result, "bar");
});
add_task(function* test_getDeviceId_with_registration_version_outdated_invokes_device_registration() {
const credentials = getTestUser("foo");
credentials.verified = true;
const fxa = new MockFxAccounts();
yield fxa.internal.setSignedInUser(credentials);
const spy = { count: 0, args: [] };
fxa.internal.currentAccountState.getUserAccountData =
() => Promise.resolve({ deviceId: credentials.deviceId, deviceRegistrationVersion: 0 });
fxa.internal._registerOrUpdateDevice = function () {
spy.count += 1;
spy.args.push(arguments);
return Promise.resolve("wibble");
};
const result = yield fxa.internal.getDeviceId();
do_check_eq(spy.count, 1);
do_check_eq(spy.args[0].length, 1);
do_check_eq(spy.args[0][0].deviceId, credentials.deviceId);
do_check_eq(result, "wibble");
});
add_task(function* test_getDeviceId_with_device_id_and_uptodate_registration_version_doesnt_invoke_device_registration() {
const credentials = getTestUser("foo");
credentials.verified = true;
const fxa = new MockFxAccounts();
yield fxa.internal.setSignedInUser(credentials);
const spy = { count: 0 };
fxa.internal.currentAccountState.getUserAccountData =
() => Promise.resolve({ deviceId: credentials.deviceId, deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION });
fxa.internal._registerOrUpdateDevice = function () {
spy.count += 1;
return Promise.resolve("bar");
};
const result = yield fxa.internal.getDeviceId();
do_check_eq(spy.count, 0);
do_check_eq(result, "foo's device id");
});
add_task(function* test_getDeviceId_with_device_id_and_with_no_registration_version_invokes_device_registration() {
const credentials = getTestUser("foo");
credentials.verified = true;
const fxa = new MockFxAccounts();
yield fxa.internal.setSignedInUser(credentials);
const spy = { count: 0, args: [] };
fxa.internal.currentAccountState.getUserAccountData =
() => Promise.resolve({ deviceId: credentials.deviceId });
fxa.internal._registerOrUpdateDevice = function () {
spy.count += 1;
spy.args.push(arguments);
return Promise.resolve("wibble");
};
const result = yield fxa.internal.getDeviceId();
do_check_eq(spy.count, 1);
do_check_eq(spy.args[0].length, 1);
do_check_eq(spy.args[0][0].deviceId, credentials.deviceId);
do_check_eq(result, "wibble");
});
function expandHex(two_hex) {
// Return a 64-character hex string, encoding 32 identical bytes.
let eight_hex = two_hex + two_hex + two_hex + two_hex;
let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex;
return thirtytwo_hex + thirtytwo_hex;
};
function expandBytes(two_hex) {
return CommonUtils.hexToBytes(expandHex(two_hex));
};
function getTestUser(name) {
return {
email: name + "@example.com",
uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348",
deviceId: name + "'s device id",
sessionToken: name + "'s session token",
keyFetchToken: name + "'s keyfetch token",
unwrapBKey: expandHex("44"),
verified: false
};
}

View file

@ -1,917 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/hawkrequest.js");
Cu.import("resource://services-crypto/utils.js");
const FAKE_SESSION_TOKEN = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
function run_test() {
run_next_test();
}
// https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#.2Faccount.2Fkeys
var ACCOUNT_KEYS = {
keyFetch: h("8081828384858687 88898a8b8c8d8e8f"+
"9091929394959697 98999a9b9c9d9e9f"),
response: h("ee5c58845c7c9412 b11bbd20920c2fdd"+
"d83c33c9cd2c2de2 d66b222613364636"+
"c2c0f8cfbb7c6304 72c0bd88451342c6"+
"c05b14ce342c5ad4 6ad89e84464c993c"+
"3927d30230157d08 17a077eef4b20d97"+
"6f7a97363faf3f06 4c003ada7d01aa70"),
kA: h("2021222324252627 28292a2b2c2d2e2f"+
"3031323334353637 38393a3b3c3d3e3f"),
wrapKB: h("4041424344454647 48494a4b4c4d4e4f"+
"5051525354555657 58595a5b5c5d5e5f"),
};
function deferredStop(server) {
let deferred = Promise.defer();
server.stop(deferred.resolve);
return deferred.promise;
}
add_task(function* test_authenticated_get_request() {
let message = "{\"msg\": \"Great Success!\"}";
let credentials = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
let method = "GET";
let server = httpd_setup({"/foo": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new FxAccountsClient(server.baseURI);
let result = yield client._request("/foo", method, credentials);
do_check_eq("Great Success!", result.msg);
yield deferredStop(server);
});
add_task(function* test_authenticated_post_request() {
let credentials = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
let method = "POST";
let server = httpd_setup({"/foo": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/json");
response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available());
}
});
let client = new FxAccountsClient(server.baseURI);
let result = yield client._request("/foo", method, credentials, {foo: "bar"});
do_check_eq("bar", result.foo);
yield deferredStop(server);
});
add_task(function* test_500_error() {
let message = "<h1>Ooops!</h1>";
let method = "GET";
let server = httpd_setup({"/foo": function(request, response) {
response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new FxAccountsClient(server.baseURI);
try {
yield client._request("/foo", method);
do_throw("Expected to catch an exception");
} catch (e) {
do_check_eq(500, e.code);
do_check_eq("Internal Server Error", e.message);
}
yield deferredStop(server);
});
add_task(function* test_backoffError() {
let method = "GET";
let server = httpd_setup({
"/retryDelay": function(request, response) {
response.setHeader("Retry-After", "30");
response.setStatusLine(request.httpVersion, 429, "Client has sent too many requests");
let message = "<h1>Ooops!</h1>";
response.bodyOutputStream.write(message, message.length);
},
"/duringDelayIShouldNotBeCalled": function(request, response) {
response.setStatusLine(request.httpVersion, 200, "OK");
let jsonMessage = "{\"working\": \"yes\"}";
response.bodyOutputStream.write(jsonMessage, jsonMessage.length);
},
});
let client = new FxAccountsClient(server.baseURI);
// Retry-After header sets client.backoffError
do_check_eq(client.backoffError, null);
try {
yield client._request("/retryDelay", method);
} catch (e) {
do_check_eq(429, e.code);
do_check_eq(30, e.retryAfter);
do_check_neq(typeof(client.fxaBackoffTimer), "undefined");
do_check_neq(client.backoffError, null);
}
// While delay is in effect, client short-circuits any requests
// and re-rejects with previous error.
try {
yield client._request("/duringDelayIShouldNotBeCalled", method);
throw new Error("I should not be reached");
} catch (e) {
do_check_eq(e.retryAfter, 30);
do_check_eq(e.message, "Client has sent too many requests");
do_check_neq(client.backoffError, null);
}
// Once timer fires, client nulls error out and HTTP calls work again.
client._clearBackoff();
let result = yield client._request("/duringDelayIShouldNotBeCalled", method);
do_check_eq(client.backoffError, null);
do_check_eq(result.working, "yes");
yield deferredStop(server);
});
add_task(function* test_signUp() {
let creationMessage_noKey = JSON.stringify({
uid: "uid",
sessionToken: "sessionToken"
});
let creationMessage_withKey = JSON.stringify({
uid: "uid",
sessionToken: "sessionToken",
keyFetchToken: "keyFetchToken"
});
let errorMessage = JSON.stringify({code: 400, errno: 101, error: "account exists"});
let created = false;
// Note these strings must be unicode and not already utf-8 encoded.
let unicodeUsername = "andr\xe9@example.org"; // 'andré@example.org'
let unicodePassword = "p\xe4ssw\xf6rd"; // 'pässwörd'
let server = httpd_setup({
"/account/create": function(request, response) {
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
body = CommonUtils.decodeUTF8(body);
let jsonBody = JSON.parse(body);
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors
if (created) {
// Error trying to create same account a second time
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
}
if (jsonBody.email == unicodeUsername) {
do_check_eq("", request._queryString);
do_check_eq(jsonBody.authPW, "247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375");
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(creationMessage_noKey,
creationMessage_noKey.length);
return;
}
if (jsonBody.email == "you@example.org") {
do_check_eq("keys=true", request._queryString);
do_check_eq(jsonBody.authPW, "e5c1cdfdaa5fcee06142db865b212cc8ba8abee2a27d639d42c139f006cdb930");
created = true;
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(creationMessage_withKey,
creationMessage_withKey.length);
return;
}
// just throwing here doesn't make any log noise, so have an assertion
// fail instead.
do_check_true(false, "unexpected email: " + jsonBody.email);
},
});
// Try to create an account without retrieving optional keys.
let client = new FxAccountsClient(server.baseURI);
let result = yield client.signUp(unicodeUsername, unicodePassword);
do_check_eq("uid", result.uid);
do_check_eq("sessionToken", result.sessionToken);
do_check_eq(undefined, result.keyFetchToken);
do_check_eq(result.unwrapBKey,
"de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28");
// Try to create an account retrieving optional keys.
result = yield client.signUp('you@example.org', 'pässwörd', true);
do_check_eq("uid", result.uid);
do_check_eq("sessionToken", result.sessionToken);
do_check_eq("keyFetchToken", result.keyFetchToken);
do_check_eq(result.unwrapBKey,
"f589225b609e56075d76eb74f771ff9ab18a4dc0e901e131ba8f984c7fb0ca8c");
// Try to create an existing account. Triggers error path.
try {
result = yield client.signUp(unicodeUsername, unicodePassword);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(101, expectedError.errno);
}
yield deferredStop(server);
});
add_task(function* test_signIn() {
let sessionMessage_noKey = JSON.stringify({
sessionToken: FAKE_SESSION_TOKEN
});
let sessionMessage_withKey = JSON.stringify({
sessionToken: FAKE_SESSION_TOKEN,
keyFetchToken: "keyFetchToken"
});
let errorMessage_notExistent = JSON.stringify({
code: 400,
errno: 102,
error: "doesn't exist"
});
let errorMessage_wrongCap = JSON.stringify({
code: 400,
errno: 120,
error: "Incorrect email case",
email: "you@example.com"
});
// Note this strings must be unicode and not already utf-8 encoded.
let unicodeUsername = "m\xe9@example.com" // 'mé@example.com'
let server = httpd_setup({
"/account/login": function(request, response) {
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
body = CommonUtils.decodeUTF8(body);
let jsonBody = JSON.parse(body);
if (jsonBody.email == unicodeUsername) {
do_check_eq("", request._queryString);
do_check_eq(jsonBody.authPW, "08b9d111196b8408e8ed92439da49206c8ecfbf343df0ae1ecefcd1e0174a8b6");
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(sessionMessage_noKey,
sessionMessage_noKey.length);
return;
}
else if (jsonBody.email == "you@example.com") {
do_check_eq("keys=true", request._queryString);
do_check_eq(jsonBody.authPW, "93d20ec50304d496d0707ec20d7e8c89459b6396ec5dd5b9e92809c5e42856c7");
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(sessionMessage_withKey,
sessionMessage_withKey.length);
return;
}
else if (jsonBody.email == "You@example.com") {
// Error trying to sign in with a wrong capitalization
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage_wrongCap,
errorMessage_wrongCap.length);
return;
}
else {
// Error trying to sign in to nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage_notExistent,
errorMessage_notExistent.length);
return;
}
},
});
// Login without retrieving optional keys
let client = new FxAccountsClient(server.baseURI);
let result = yield client.signIn(unicodeUsername, 'bigsecret');
do_check_eq(FAKE_SESSION_TOKEN, result.sessionToken);
do_check_eq(result.unwrapBKey,
"c076ec3f4af123a615157154c6e1d0d6293e514fd7b0221e32d50517ecf002b8");
do_check_eq(undefined, result.keyFetchToken);
// Login with retrieving optional keys
result = yield client.signIn('you@example.com', 'bigsecret', true);
do_check_eq(FAKE_SESSION_TOKEN, result.sessionToken);
do_check_eq(result.unwrapBKey,
"65970516211062112e955d6420bebe020269d6b6a91ebd288319fc8d0cb49624");
do_check_eq("keyFetchToken", result.keyFetchToken);
// Retry due to wrong email capitalization
result = yield client.signIn('You@example.com', 'bigsecret', true);
do_check_eq(FAKE_SESSION_TOKEN, result.sessionToken);
do_check_eq(result.unwrapBKey,
"65970516211062112e955d6420bebe020269d6b6a91ebd288319fc8d0cb49624");
do_check_eq("keyFetchToken", result.keyFetchToken);
// Trigger error path
try {
result = yield client.signIn("yøü@bad.example.org", "nofear");
do_throw("Expected to catch an exception");
} catch (expectedError) {
do_check_eq(102, expectedError.errno);
}
yield deferredStop(server);
});
add_task(function* test_signOut() {
let signoutMessage = JSON.stringify({});
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
let signedOut = false;
let server = httpd_setup({
"/session/destroy": function(request, response) {
if (!signedOut) {
signedOut = true;
do_check_true(request.hasHeader("Authorization"));
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
return;
}
// Error trying to sign out of nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
let client = new FxAccountsClient(server.baseURI);
let result = yield client.signOut("FakeSession");
do_check_eq(typeof result, "object");
// Trigger error path
try {
result = yield client.signOut("FakeSession");
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
yield deferredStop(server);
});
add_task(function* test_recoveryEmailStatus() {
let emailStatus = JSON.stringify({verified: true});
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
let tries = 0;
let server = httpd_setup({
"/recovery_email/status": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
do_check_eq("", request._queryString);
if (tries === 0) {
tries += 1;
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(emailStatus, emailStatus.length);
return;
}
// Second call gets an error trying to query a nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
let client = new FxAccountsClient(server.baseURI);
let result = yield client.recoveryEmailStatus(FAKE_SESSION_TOKEN);
do_check_eq(result.verified, true);
// Trigger error path
try {
result = yield client.recoveryEmailStatus("some bogus session");
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
yield deferredStop(server);
});
add_task(function* test_recoveryEmailStatusWithReason() {
let emailStatus = JSON.stringify({verified: true});
let server = httpd_setup({
"/recovery_email/status": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
// if there is a query string then it will have a reason
do_check_eq("reason=push", request._queryString);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(emailStatus, emailStatus.length);
return;
},
});
let client = new FxAccountsClient(server.baseURI);
let result = yield client.recoveryEmailStatus(FAKE_SESSION_TOKEN, {
reason: "push",
});
do_check_eq(result.verified, true);
yield deferredStop(server);
});
add_task(function* test_resendVerificationEmail() {
let emptyMessage = "{}";
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
let tries = 0;
let server = httpd_setup({
"/recovery_email/resend_code": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
if (tries === 0) {
tries += 1;
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
return;
}
// Second call gets an error trying to query a nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
let client = new FxAccountsClient(server.baseURI);
let result = yield client.resendVerificationEmail(FAKE_SESSION_TOKEN);
do_check_eq(JSON.stringify(result), emptyMessage);
// Trigger error path
try {
result = yield client.resendVerificationEmail("some bogus session");
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
yield deferredStop(server);
});
add_task(function* test_accountKeys() {
// Four calls to accountKeys(). The first one should work correctly, and we
// should get a valid bundle back, in exchange for our keyFetch token, from
// which we correctly derive kA and wrapKB. The subsequent three calls
// should all trigger separate error paths.
let responseMessage = JSON.stringify({bundle: ACCOUNT_KEYS.response});
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
let emptyMessage = "{}";
let attempt = 0;
let server = httpd_setup({
"/account/keys": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
attempt += 1;
switch(attempt) {
case 1:
// First time succeeds
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(responseMessage, responseMessage.length);
break;
case 2:
// Second time, return no bundle to trigger client error
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
break;
case 3:
// Return gibberish to trigger client MAC error
// Tweak a byte
let garbageResponse = JSON.stringify({
bundle: ACCOUNT_KEYS.response.slice(0, -1) + "1"
});
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(garbageResponse, garbageResponse.length);
break;
case 4:
// Trigger error for nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage, errorMessage.length);
break;
}
},
});
let client = new FxAccountsClient(server.baseURI);
// First try, all should be good
let result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch);
do_check_eq(CommonUtils.hexToBytes(ACCOUNT_KEYS.kA), result.kA);
do_check_eq(CommonUtils.hexToBytes(ACCOUNT_KEYS.wrapKB), result.wrapKB);
// Second try, empty bundle should trigger error
try {
result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(expectedError.message, "failed to retrieve keys");
}
// Third try, bad bundle results in MAC error
try {
result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(expectedError.message, "error unbundling encryption keys");
}
// Fourth try, pretend account doesn't exist
try {
result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
yield deferredStop(server);
});
add_task(function* test_signCertificate() {
let certSignMessage = JSON.stringify({cert: {bar: "baz"}});
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
let tries = 0;
let server = httpd_setup({
"/certificate/sign": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
if (tries === 0) {
tries += 1;
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
let jsonBody = JSON.parse(body);
do_check_eq(JSON.parse(jsonBody.publicKey).foo, "bar");
do_check_eq(jsonBody.duration, 600);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
return;
}
// Second attempt, trigger error
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
let client = new FxAccountsClient(server.baseURI);
let result = yield client.signCertificate(FAKE_SESSION_TOKEN, JSON.stringify({foo: "bar"}), 600);
do_check_eq("baz", result.bar);
// Account doesn't exist
try {
result = yield client.signCertificate("bogus", JSON.stringify({foo: "bar"}), 600);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
yield deferredStop(server);
});
add_task(function* test_accountExists() {
let sessionMessage = JSON.stringify({sessionToken: FAKE_SESSION_TOKEN});
let existsMessage = JSON.stringify({error: "wrong password", code: 400, errno: 103});
let doesntExistMessage = JSON.stringify({error: "no such account", code: 400, errno: 102});
let emptyMessage = "{}";
let server = httpd_setup({
"/account/login": function(request, response) {
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
let jsonBody = JSON.parse(body);
switch (jsonBody.email) {
// We'll test that these users' accounts exist
case "i.exist@example.com":
case "i.also.exist@example.com":
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(existsMessage, existsMessage.length);
break;
// This user's account doesn't exist
case "i.dont.exist@example.com":
response.setStatusLine(request.httpVersion, 400, "Bad request");
response.bodyOutputStream.write(doesntExistMessage, doesntExistMessage.length);
break;
// This user throws an unexpected response
// This will reject the client signIn promise
case "i.break.things@example.com":
response.setStatusLine(request.httpVersion, 500, "Alas");
response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
break;
default:
throw new Error("Unexpected login from " + jsonBody.email);
break;
}
},
});
let client = new FxAccountsClient(server.baseURI);
let result;
result = yield client.accountExists("i.exist@example.com");
do_check_true(result);
result = yield client.accountExists("i.also.exist@example.com");
do_check_true(result);
result = yield client.accountExists("i.dont.exist@example.com");
do_check_false(result);
try {
result = yield client.accountExists("i.break.things@example.com");
do_throw("Expected to catch an exception");
} catch(unexpectedError) {
do_check_eq(unexpectedError.code, 500);
}
yield deferredStop(server);
});
add_task(function* test_registerDevice() {
const DEVICE_ID = "device id";
const DEVICE_NAME = "device name";
const DEVICE_TYPE = "device type";
const ERROR_NAME = "test that the client promise rejects";
const server = httpd_setup({
"/account/device": function(request, response) {
const body = JSON.parse(CommonUtils.readBytesFromInputStream(request.bodyInputStream));
if (body.id || !body.name || !body.type || Object.keys(body).length !== 2) {
response.setStatusLine(request.httpVersion, 400, "Invalid request");
return response.bodyOutputStream.write("{}", 2);
}
if (body.name === ERROR_NAME) {
response.setStatusLine(request.httpVersion, 500, "Alas");
return response.bodyOutputStream.write("{}", 2);
}
body.id = DEVICE_ID;
body.createdAt = Date.now();
const responseMessage = JSON.stringify(body);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(responseMessage, responseMessage.length);
},
});
const client = new FxAccountsClient(server.baseURI);
const result = yield client.registerDevice(FAKE_SESSION_TOKEN, DEVICE_NAME, DEVICE_TYPE);
do_check_true(result);
do_check_eq(Object.keys(result).length, 4);
do_check_eq(result.id, DEVICE_ID);
do_check_eq(typeof result.createdAt, 'number');
do_check_true(result.createdAt > 0);
do_check_eq(result.name, DEVICE_NAME);
do_check_eq(result.type, DEVICE_TYPE);
try {
yield client.registerDevice(FAKE_SESSION_TOKEN, ERROR_NAME, DEVICE_TYPE);
do_throw("Expected to catch an exception");
} catch(unexpectedError) {
do_check_eq(unexpectedError.code, 500);
}
yield deferredStop(server);
});
add_task(function* test_updateDevice() {
const DEVICE_ID = "some other id";
const DEVICE_NAME = "some other name";
const ERROR_ID = "test that the client promise rejects";
const server = httpd_setup({
"/account/device": function(request, response) {
const body = JSON.parse(CommonUtils.readBytesFromInputStream(request.bodyInputStream));
if (!body.id || !body.name || body.type || Object.keys(body).length !== 2) {
response.setStatusLine(request.httpVersion, 400, "Invalid request");
return response.bodyOutputStream.write("{}", 2);
}
if (body.id === ERROR_ID) {
response.setStatusLine(request.httpVersion, 500, "Alas");
return response.bodyOutputStream.write("{}", 2);
}
const responseMessage = JSON.stringify(body);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(responseMessage, responseMessage.length);
},
});
const client = new FxAccountsClient(server.baseURI);
const result = yield client.updateDevice(FAKE_SESSION_TOKEN, DEVICE_ID, DEVICE_NAME);
do_check_true(result);
do_check_eq(Object.keys(result).length, 2);
do_check_eq(result.id, DEVICE_ID);
do_check_eq(result.name, DEVICE_NAME);
try {
yield client.updateDevice(FAKE_SESSION_TOKEN, ERROR_ID, DEVICE_NAME);
do_throw("Expected to catch an exception");
} catch(unexpectedError) {
do_check_eq(unexpectedError.code, 500);
}
yield deferredStop(server);
});
add_task(function* test_signOutAndDestroyDevice() {
const DEVICE_ID = "device id";
const ERROR_ID = "test that the client promise rejects";
const server = httpd_setup({
"/account/device/destroy": function(request, response) {
const body = JSON.parse(CommonUtils.readBytesFromInputStream(request.bodyInputStream));
if (!body.id) {
response.setStatusLine(request.httpVersion, 400, "Invalid request");
return response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
}
if (body.id === ERROR_ID) {
response.setStatusLine(request.httpVersion, 500, "Alas");
return response.bodyOutputStream.write("{}", 2);
}
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write("{}", 2);
},
});
const client = new FxAccountsClient(server.baseURI);
const result = yield client.signOutAndDestroyDevice(FAKE_SESSION_TOKEN, DEVICE_ID);
do_check_true(result);
do_check_eq(Object.keys(result).length, 0);
try {
yield client.signOutAndDestroyDevice(FAKE_SESSION_TOKEN, ERROR_ID);
do_throw("Expected to catch an exception");
} catch(unexpectedError) {
do_check_eq(unexpectedError.code, 500);
}
yield deferredStop(server);
});
add_task(function* test_getDeviceList() {
let canReturnDevices;
const server = httpd_setup({
"/account/devices": function(request, response) {
if (canReturnDevices) {
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write("[]", 2);
} else {
response.setStatusLine(request.httpVersion, 500, "Alas");
response.bodyOutputStream.write("{}", 2);
}
},
});
const client = new FxAccountsClient(server.baseURI);
canReturnDevices = true;
const result = yield client.getDeviceList(FAKE_SESSION_TOKEN);
do_check_true(Array.isArray(result));
do_check_eq(result.length, 0);
try {
canReturnDevices = false;
yield client.getDeviceList(FAKE_SESSION_TOKEN);
do_throw("Expected to catch an exception");
} catch(unexpectedError) {
do_check_eq(unexpectedError.code, 500);
}
yield deferredStop(server);
});
add_task(function* test_client_metrics() {
function writeResp(response, msg) {
if (typeof msg === "object") {
msg = JSON.stringify(msg);
}
response.bodyOutputStream.write(msg, msg.length);
}
let server = httpd_setup(
{
"/session/destroy": function(request, response) {
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
writeResp(response, {
error: "invalid authentication timestamp",
code: 401,
errno: 111,
});
},
}
);
let client = new FxAccountsClient(server.baseURI);
yield rejects(client.signOut(FAKE_SESSION_TOKEN, {
service: "sync",
}), function(err) {
return err.errno == 111;
});
yield deferredStop(server);
});
add_task(function* test_email_case() {
let canonicalEmail = "greta.garbo@gmail.com";
let clientEmail = "Greta.Garbo@gmail.COM";
let attempts = 0;
function writeResp(response, msg) {
if (typeof msg === "object") {
msg = JSON.stringify(msg);
}
response.bodyOutputStream.write(msg, msg.length);
}
let server = httpd_setup(
{
"/account/login": function(request, response) {
response.setHeader("Content-Type", "application/json; charset=utf-8");
attempts += 1;
if (attempts > 2) {
response.setStatusLine(request.httpVersion, 429, "Sorry, you had your chance");
return writeResp(response, "");
}
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
let jsonBody = JSON.parse(body);
let email = jsonBody.email;
// If the client has the wrong case on the email, we return a 400, with
// the capitalization of the email as saved in the accounts database.
if (email == canonicalEmail) {
response.setStatusLine(request.httpVersion, 200, "Yay");
return writeResp(response, {areWeHappy: "yes"});
}
response.setStatusLine(request.httpVersion, 400, "Incorrect email case");
return writeResp(response, {
code: 400,
errno: 120,
error: "Incorrect email case",
email: canonicalEmail
});
},
}
);
let client = new FxAccountsClient(server.baseURI);
let result = yield client.signIn(clientEmail, "123456");
do_check_eq(result.areWeHappy, "yes");
do_check_eq(attempts, 2);
yield deferredStop(server);
});
// turn formatted test vectors into normal hex strings
function h(hexStr) {
return hexStr.replace(/\s+/g, "");
}

View file

@ -1,110 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/Credentials.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-crypto/utils.js");
var {hexToBytes: h2b,
hexAsString: h2s,
stringAsHex: s2h,
bytesAsHex: b2h} = CommonUtils;
// Test vectors for the "onepw" protocol:
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors
var vectors = {
"client stretch-KDF": {
email:
h("616e6472c3a94065 78616d706c652e6f 7267"),
password:
h("70c3a4737377c3b6 7264"),
quickStretchedPW:
h("e4e8889bd8bd61ad 6de6b95c059d56e7 b50dacdaf62bd846 44af7e2add84345d"),
authPW:
h("247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375"),
authSalt:
h("00f0000000000000 0000000000000000 0000000000000000 0000000000000000"),
},
};
// A simple test suite with no utf8 encoding madness.
add_task(function* test_onepw_setup_credentials() {
let email = "francine@example.org";
let password = CommonUtils.encodeUTF8("i like pie");
let pbkdf2 = CryptoUtils.pbkdf2Generate;
let hkdf = CryptoUtils.hkdf;
// quickStretch the email
let saltyEmail = Credentials.keyWordExtended("quickStretch", email);
do_check_eq(b2h(saltyEmail), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a6672616e63696e65406578616d706c652e6f7267");
let pbkdf2Rounds = 1000;
let pbkdf2Len = 32;
let quickStretchedPW = pbkdf2(password, saltyEmail, pbkdf2Rounds, pbkdf2Len, Ci.nsICryptoHMAC.SHA256, 32);
let quickStretchedActual = "6b88094c1c73bbf133223f300d101ed70837af48d9d2c1b6e7d38804b20cdde4";
do_check_eq(b2h(quickStretchedPW), quickStretchedActual);
// obtain hkdf info
let authKeyInfo = Credentials.keyWord('authPW');
do_check_eq(b2h(authKeyInfo), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f617574685057");
// derive auth password
let hkdfSalt = h2b("00");
let hkdfLen = 32;
let authPW = hkdf(quickStretchedPW, hkdfSalt, authKeyInfo, hkdfLen);
do_check_eq(b2h(authPW), "4b8dec7f48e7852658163601ff766124c312f9392af6c3d4e1a247eb439be342");
// derive unwrap key
let unwrapKeyInfo = Credentials.keyWord('unwrapBkey');
let unwrapKey = hkdf(quickStretchedPW, hkdfSalt, unwrapKeyInfo, hkdfLen);
do_check_eq(b2h(unwrapKey), "8ff58975be391338e4ec5d7138b5ed7b65c7d1bfd1f3a4f93e05aa47d5b72be9");
});
add_task(function* test_client_stretch_kdf() {
let pbkdf2 = CryptoUtils.pbkdf2Generate;
let hkdf = CryptoUtils.hkdf;
let expected = vectors["client stretch-KDF"];
let email = h2s(expected.email);
let password = h2s(expected.password);
// Intermediate value from sjcl implementation in fxa-js-client
// The key thing is the c3a9 sequence in "andré"
let salt = Credentials.keyWordExtended("quickStretch", email);
do_check_eq(b2h(salt), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a616e6472c3a9406578616d706c652e6f7267");
let options = {
stretchedPassLength: 32,
pbkdf2Rounds: 1000,
hmacAlgorithm: Ci.nsICryptoHMAC.SHA256,
hmacLength: 32,
hkdfSalt: h2b("00"),
hkdfLength: 32,
};
let results = yield Credentials.setup(email, password, options);
do_check_eq(expected.quickStretchedPW, b2h(results.quickStretchedPW),
"quickStretchedPW is wrong");
do_check_eq(expected.authPW, b2h(results.authPW),
"authPW is wrong");
});
// End of tests
// Utility functions follow
function run_test() {
run_next_test();
}
// turn formatted test vectors into normal hex strings
function h(hexStr) {
return hexStr.replace(/\s+/g, "");
}

View file

@ -1,214 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests for FxAccounts, storage and the master password.
// Stop us hitting the real auth server.
Services.prefs.setCharPref("identity.fxaccounts.auth.uri", "http://localhost");
// See verbose logging from FxAccounts.jsm
Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
// Use a backstage pass to get at our LoginManagerStorage object, so we can
// mock the prototype.
var {LoginManagerStorage} = Cu.import("resource://gre/modules/FxAccountsStorage.jsm", {});
var isLoggedIn = true;
LoginManagerStorage.prototype.__defineGetter__("_isLoggedIn", () => isLoggedIn);
function setLoginMgrLoggedInState(loggedIn) {
isLoggedIn = loggedIn;
}
initTestLogging("Trace");
function run_test() {
run_next_test();
}
function getLoginMgrData() {
let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
if (logins.length == 0) {
return null;
}
Assert.equal(logins.length, 1, "only 1 login available");
return logins[0];
}
function createFxAccounts() {
return new FxAccounts({
_getDeviceName() {
return "mock device name";
},
fxaPushService: {
registerPushEndpoint() {
return new Promise((resolve) => {
resolve({
endpoint: "http://mochi.test:8888"
});
});
},
}
});
}
add_task(function* test_simple() {
let fxa = createFxAccounts();
let creds = {
uid: "abcd",
email: "test@example.com",
sessionToken: "sessionToken",
kA: "the kA value",
kB: "the kB value",
verified: true
};
yield fxa.setSignedInUser(creds);
// This should have stored stuff in both the .json file in the profile
// dir, and the login dir.
let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
let data = yield CommonUtils.readJSON(path);
Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
let login = getLoginMgrData();
Assert.strictEqual(login.username, creds.uid, "uid used for username");
let loginData = JSON.parse(login.password);
Assert.strictEqual(loginData.version, data.version, "same version flag in both places");
Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr");
Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr");
Assert.ok(!("email" in loginData), "email not stored in the login mgr json");
Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json");
Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json");
yield fxa.signOut(/* localOnly = */ true);
Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout");
});
add_task(function* test_MPLocked() {
let fxa = createFxAccounts();
let creds = {
uid: "abcd",
email: "test@example.com",
sessionToken: "sessionToken",
kA: "the kA value",
kB: "the kB value",
verified: true
};
Assert.strictEqual(getLoginMgrData(), null, "no login mgr at the start");
// tell the storage that the MP is locked.
setLoginMgrLoggedInState(false);
yield fxa.setSignedInUser(creds);
// This should have stored stuff in the .json, and the login manager stuff
// will not exist.
let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
let data = yield CommonUtils.readJSON(path);
Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
Assert.strictEqual(getLoginMgrData(), null, "login mgr data doesn't exist");
yield fxa.signOut(/* localOnly = */ true)
});
add_task(function* test_consistentWithMPEdgeCases() {
setLoginMgrLoggedInState(true);
let fxa = createFxAccounts();
let creds1 = {
uid: "uid1",
email: "test@example.com",
sessionToken: "sessionToken",
kA: "the kA value",
kB: "the kB value",
verified: true
};
let creds2 = {
uid: "uid2",
email: "test2@example.com",
sessionToken: "sessionToken2",
kA: "the kA value2",
kB: "the kB value2",
verified: false,
};
// Log a user in while MP is unlocked.
yield fxa.setSignedInUser(creds1);
// tell the storage that the MP is locked - this will prevent logout from
// being able to clear the data.
setLoginMgrLoggedInState(false);
// now set the second credentials.
yield fxa.setSignedInUser(creds2);
// We should still have creds1 data in the login manager.
let login = getLoginMgrData();
Assert.strictEqual(login.username, creds1.uid);
// and that we do have the first kA in the login manager.
Assert.strictEqual(JSON.parse(login.password).accountData.kA, creds1.kA,
"stale data still in login mgr");
// Make a new FxA instance (otherwise the values in memory will be used)
// and we want the login manager to be unlocked.
setLoginMgrLoggedInState(true);
fxa = createFxAccounts();
let accountData = yield fxa.getSignedInUser();
Assert.strictEqual(accountData.email, creds2.email);
// we should have no kA at all.
Assert.strictEqual(accountData.kA, undefined, "stale kA wasn't used");
yield fxa.signOut(/* localOnly = */ true)
});
// A test for the fact we will accept either a UID or email when looking in
// the login manager.
add_task(function* test_uidMigration() {
setLoginMgrLoggedInState(true);
Assert.strictEqual(getLoginMgrData(), null, "expect no logins at the start");
// create the login entry using email as a key.
let contents = {kA: "kA"};
let loginInfo = new Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
let login = new loginInfo(FXA_PWDMGR_HOST,
null, // aFormSubmitURL,
FXA_PWDMGR_REALM, // aHttpRealm,
"foo@bar.com", // aUsername
JSON.stringify(contents), // aPassword
"", // aUsernameField
"");// aPasswordField
Services.logins.addLogin(login);
// ensure we read it.
let storage = new LoginManagerStorage();
let got = yield storage.get("uid", "foo@bar.com");
Assert.deepEqual(got, contents);
});

View file

@ -1,55 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
function run_test() {
validationHelper(undefined,
"Error: Missing 'parameters' configuration option");
validationHelper({},
"Error: Missing 'parameters' configuration option");
validationHelper({ parameters: {} },
"Error: Missing 'parameters.oauth_uri' parameter");
validationHelper({ parameters: {
oauth_uri: "http://oauth.test/v1"
}},
"Error: Missing 'parameters.client_id' parameter");
validationHelper({ parameters: {
oauth_uri: "http://oauth.test/v1",
client_id: "client_id"
}},
"Error: Missing 'parameters.content_uri' parameter");
validationHelper({ parameters: {
oauth_uri: "http://oauth.test/v1",
client_id: "client_id",
content_uri: "http://content.test"
}},
"Error: Missing 'parameters.state' parameter");
validationHelper({ parameters: {
oauth_uri: "http://oauth.test/v1",
client_id: "client_id",
content_uri: "http://content.test",
state: "complete",
action: "force_auth"
}},
"Error: parameters.email is required for action 'force_auth'");
run_next_test();
}
function validationHelper(params, expected) {
try {
new FxAccountsOAuthClient(params);
} catch (e) {
return do_check_eq(e.toString(), expected);
}
throw new Error("Validation helper error");
}

View file

@ -1,292 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const CLIENT_OPTIONS = {
serverURL: "http://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: function () {},
post: function () {
this.response = response;
this.onComplete();
}
};
};
};
/**
* Mock request error responder
* @param {Error} error
* Error object
* @returns {Function}
*/
var mockResponseError = function (error) {
return function () {
return {
setHeader: function () {},
post: function () {
this.onComplete(error);
}
};
};
};
add_test(function missingParams () {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
try {
client.getTokenFromAssertion()
} catch (e) {
do_check_eq(e.message, "Missing 'assertion' parameter");
}
try {
client.getTokenFromAssertion("assertion")
} catch (e) {
do_check_eq(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: "{\"access_token\":\"http://example.com/image.jpeg\",\"id\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}",
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("assertion", "scope")
.then(
function (result) {
do_check_eq(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: "{}",
};
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")
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsOAuthGrantClientError");
do_check_eq(e.code, STATUS_SUCCESS);
do_check_eq(e.errno, ERRNO_PARSE);
do_check_eq(e.error, ERROR_PARSE);
do_check_eq(e.message, "unexpected");
run_next_test();
}
);
});
add_test(function serverErrorResponse () {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
status: 400,
body: "{ \"code\": 400, \"errno\": 104, \"error\": \"Bad Request\", \"message\": \"Unauthorized\", \"reason\": \"Invalid fxa assertion\" }",
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("blah", "scope")
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsOAuthGrantClientError");
do_check_eq(e.code, 400);
do_check_eq(e.errno, ERRNO_INVALID_FXA_ASSERTION);
do_check_eq(e.error, "Bad Request");
do_check_eq(e.message, "Unauthorized");
run_next_test();
}
);
});
add_test(function networkErrorResponse () {
let client = new FxAccountsOAuthGrantClient({
serverURL: "http://domain.dummy",
client_id: "abc123"
});
Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true);
client.getTokenFromAssertion("assertion", "scope")
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsOAuthGrantClientError");
do_check_eq(e.code, null);
do_check_eq(e.errno, ERRNO_NETWORK);
do_check_eq(e.error, ERROR_NETWORK);
run_next_test();
}
).catch(() => {}).then(() =>
Services.prefs.clearUserPref("identity.fxaccounts.skipDeviceRegistration"));
});
add_test(function unsupportedMethod () {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
return client._createRequest("/", "PUT")
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsOAuthGrantClientError");
do_check_eq(e.code, ERROR_CODE_METHOD_NOT_ALLOWED);
do_check_eq(e.errno, ERRNO_NETWORK);
do_check_eq(e.error, ERROR_NETWORK);
do_check_eq(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")
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsOAuthGrantClientError");
do_check_eq(e.code, null);
do_check_eq(e.errno, ERRNO_NETWORK);
do_check_eq(e.error, ERROR_NETWORK);
do_check_eq(e.message, "Error: onComplete error");
run_next_test();
}
);
});
add_test(function incorrectErrno() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
status: 400,
body: "{ \"code\": 400, \"errno\": \"bad errno\", \"error\": \"Bad Request\", \"message\": \"Unauthorized\", \"reason\": \"Invalid fxa assertion\" }",
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("blah", "scope")
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsOAuthGrantClientError");
do_check_eq(e.code, 400);
do_check_eq(e.errno, ERRNO_UNKNOWN_ERROR);
do_check_eq(e.error, "Bad Request");
do_check_eq(e.message, "Unauthorized");
run_next_test();
}
);
});
add_test(function constructorTests() {
validationHelper(undefined,
"Error: Missing configuration options");
validationHelper({},
"Error: Missing 'serverURL' parameter");
validationHelper({ serverURL: "http://example.com" },
"Error: Missing 'client_id' parameter");
validationHelper({ client_id: "123ABC" },
"Error: Missing 'serverURL' parameter");
validationHelper({ client_id: "123ABC", serverURL: "badUrl" },
"Error: Invalid 'serverURL'");
run_next_test();
});
add_test(function errorTests() {
let error1 = new FxAccountsOAuthGrantClientError();
do_check_eq(error1.name, "FxAccountsOAuthGrantClientError");
do_check_eq(error1.code, null);
do_check_eq(error1.errno, ERRNO_UNKNOWN_ERROR);
do_check_eq(error1.error, ERROR_UNKNOWN);
do_check_eq(error1.message, null);
let error2 = new FxAccountsOAuthGrantClientError({
code: STATUS_SUCCESS,
errno: 1,
error: "Error",
message: "Something",
});
let fields2 = error2._toStringFields();
let statusCode = 1;
do_check_eq(error2.name, "FxAccountsOAuthGrantClientError");
do_check_eq(error2.code, STATUS_SUCCESS);
do_check_eq(error2.errno, statusCode);
do_check_eq(error2.error, "Error");
do_check_eq(error2.message, "Something");
do_check_eq(fields2.name, "FxAccountsOAuthGrantClientError");
do_check_eq(fields2.code, STATUS_SUCCESS);
do_check_eq(fields2.errno, statusCode);
do_check_eq(fields2.error, "Error");
do_check_eq(fields2.message, "Something");
do_check_true(error2.toString().indexOf("Something") >= 0);
run_next_test();
});
function run_test() {
run_next_test();
}
/**
* Quick way to test the "FxAccountsOAuthGrantClient" constructor.
*
* @param {Object} options
* FxAccountsOAuthGrantClient constructor options
* @param {String} expected
* Expected error message
* @returns {*}
*/
function validationHelper(options, expected) {
try {
new FxAccountsOAuthGrantClient(options);
} catch (e) {
return do_check_eq(e.toString(), expected);
}
throw new Error("Validation helper error");
}

View file

@ -1,73 +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";
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.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;
}
function promiseStopServer(server) {
return new Promise(resolve => {
server.stop(resolve);
});
}
add_task(function* getAndRevokeToken () {
let server = startServer();
let clientOptions = {
serverURL: "http://localhost:" + server.identity.primaryPort + "/v1",
client_id: 'abc123',
}
let client = new FxAccountsOAuthGrantClient(clientOptions);
let result = yield client.getTokenFromAssertion("assertion", "scope");
equal(result.access_token, "token0");
equal(numTokenFetches, 1, "we hit the server to fetch a token");
yield client.destroyToken("token0");
equal(activeTokens.size, 0, "We hit the server to revoke it");
yield promiseStopServer(server);
});
// XXX - TODO - we should probably add more tests for unexpected responses etc.
function run_test() {
run_next_test();
}

View file

@ -1,165 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/osfile.jsm");
// We grab some additional stuff via backstage passes.
var {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
function promiseNotification(topic) {
return new Promise(resolve => {
let observe = () => {
Services.obs.removeObserver(observe, topic);
resolve();
}
Services.obs.addObserver(observe, topic, false);
});
}
// A storage manager that doesn't actually write anywhere.
function MockStorageManager() {
}
MockStorageManager.prototype = {
promiseInitialized: Promise.resolve(),
initialize(accountData) {
this.accountData = accountData;
},
finalize() {
return Promise.resolve();
},
getAccountData() {
return Promise.resolve(this.accountData);
},
updateAccountData(updatedFields) {
for (let [name, value] of Object.entries(updatedFields)) {
if (value == null) {
delete this.accountData[name];
} else {
this.accountData[name] = value;
}
}
return Promise.resolve();
},
deleteAccountData() {
this.accountData = null;
return Promise.resolve();
}
}
// Just enough mocks so we can avoid hawk etc.
function MockFxAccountsClient() {
this._email = "nobody@example.com";
this._verified = false;
this.accountStatus = function(uid) {
let deferred = Promise.defer();
deferred.resolve(!!uid && (!this._deletedOnServer));
return deferred.promise;
};
this.signOut = function() { return Promise.resolve(); };
this.registerDevice = function() { return Promise.resolve(); };
this.updateDevice = function() { return Promise.resolve(); };
this.signOutAndDestroyDevice = function() { return Promise.resolve(); };
this.getDeviceList = function() { return Promise.resolve(); };
FxAccountsClient.apply(this);
}
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype
}
function MockFxAccounts(device={}) {
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(),
newAccountState(credentials) {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(storage);
},
_getDeviceName() {
return "mock device name";
},
fxaPushService: {
registerPushEndpoint() {
return new Promise((resolve) => {
resolve({
endpoint: "http://mochi.test:8888"
});
});
},
},
});
}
function* createMockFxA() {
let fxa = new MockFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
kA: "beef",
kB: "cafe",
verified: true
};
yield fxa.setSignedInUser(credentials);
return fxa;
}
// The tests.
function run_test() {
run_next_test();
}
add_task(function* testCacheStorage() {
let fxa = yield createMockFxA();
// Hook what the impl calls to save to disk.
let cas = fxa.internal.currentAccountState;
let origPersistCached = cas._persistCachedTokens.bind(cas)
cas._persistCachedTokens = function() {
return origPersistCached().then(() => {
Services.obs.notifyObservers(null, "testhelper-fxa-cache-persist-done", null);
});
};
let promiseWritten = promiseNotification("testhelper-fxa-cache-persist-done");
let tokenData = {token: "token1", somethingelse: "something else"};
let scopeArray = ["foo", "bar"];
cas.setCachedToken(scopeArray, tokenData);
deepEqual(cas.getCachedToken(scopeArray), tokenData);
deepEqual(cas.oauthTokens, {"bar|foo": tokenData});
// wait for background write to complete.
yield promiseWritten;
// Check the token cache made it to our mocked storage.
deepEqual(cas.storageManager.accountData.oauthTokens, {"bar|foo": tokenData});
// Drop the token from the cache and ensure it is removed from the json.
promiseWritten = promiseNotification("testhelper-fxa-cache-persist-done");
yield cas.removeCachedToken("token1");
deepEqual(cas.oauthTokens, {});
yield promiseWritten;
deepEqual(cas.storageManager.accountData.oauthTokens, {});
// sign out and the token storage should end up with null.
let storageManager = cas.storageManager; // .signOut() removes the attribute.
yield fxa.signOut( /* localOnly = */ true);
deepEqual(storageManager.accountData, null);
});

View file

@ -1,251 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
Cu.import("resource://services-common/utils.js");
var {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
function promiseNotification(topic) {
return new Promise(resolve => {
let observe = () => {
Services.obs.removeObserver(observe, topic);
resolve();
}
Services.obs.addObserver(observe, topic, false);
});
}
// Just enough mocks so we can avoid hawk and storage.
function MockStorageManager() {
}
MockStorageManager.prototype = {
promiseInitialized: Promise.resolve(),
initialize(accountData) {
this.accountData = accountData;
},
finalize() {
return Promise.resolve();
},
getAccountData() {
return Promise.resolve(this.accountData);
},
updateAccountData(updatedFields) {
for (let [name, value] of Object.entries(updatedFields)) {
if (value == null) {
delete this.accountData[name];
} else {
this.accountData[name] = value;
}
}
return Promise.resolve();
},
deleteAccountData() {
this.accountData = null;
return Promise.resolve();
}
}
function MockFxAccountsClient() {
this._email = "nobody@example.com";
this._verified = false;
this.accountStatus = function(uid) {
let deferred = Promise.defer();
deferred.resolve(!!uid && (!this._deletedOnServer));
return deferred.promise;
};
this.signOut = function() { return Promise.resolve(); };
this.registerDevice = function() { return Promise.resolve(); };
this.updateDevice = function() { return Promise.resolve(); };
this.signOutAndDestroyDevice = function() { return Promise.resolve(); };
this.getDeviceList = function() { return Promise.resolve(); };
FxAccountsClient.apply(this);
}
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype
}
function MockFxAccounts(mockGrantClient) {
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(),
getAssertion: () => Promise.resolve("assertion"),
newAccountState(credentials) {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(storage);
},
_destroyOAuthToken: function(tokenData) {
// somewhat sad duplication of _destroyOAuthToken, but hard to avoid.
return mockGrantClient.destroyToken(tokenData.token).then( () => {
Services.obs.notifyObservers(null, "testhelper-fxa-revoke-complete", null);
});
},
_getDeviceName() {
return "mock device name";
},
fxaPushService: {
registerPushEndpoint() {
return new Promise((resolve) => {
resolve({
endpoint: "http://mochi.test:8888"
});
});
},
},
});
}
function* createMockFxA(mockGrantClient) {
let fxa = new MockFxAccounts(mockGrantClient);
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
kA: "beef",
kB: "cafe",
verified: true
};
yield fxa.setSignedInUser(credentials);
return fxa;
}
// The tests.
function run_test() {
run_next_test();
}
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(function* testRevoke() {
let client = new MockFxAccountsOAuthGrantClient();
let tokenOptions = { scope: "test-scope", client: client };
let fxa = yield createMockFxA(client);
// get our first token and check we hit the mock.
let token1 = yield fxa.getOAuthToken(tokenOptions);
equal(client.numTokenFetches, 1);
equal(client.activeTokens.size, 1);
ok(token1, "got a token");
equal(token1, "token0");
// drop the new token from our cache.
yield fxa.removeCachedOAuthToken({token: token1});
// FxA fires an observer when the "background" revoke is complete.
yield promiseNotification("testhelper-fxa-revoke-complete");
// the revoke should have been successful.
equal(client.activeTokens.size, 0);
// fetching it again hits the server.
let token2 = yield fxa.getOAuthToken(tokenOptions);
equal(client.numTokenFetches, 2);
equal(client.activeTokens.size, 1);
ok(token2, "got a token");
notEqual(token1, token2, "got a different token");
});
add_task(function* testSignOutDestroysTokens() {
let client = new MockFxAccountsOAuthGrantClient();
let fxa = yield createMockFxA(client);
// get our first token and check we hit the mock.
let token1 = yield fxa.getOAuthToken({ scope: "test-scope", client: client });
equal(client.numTokenFetches, 1);
equal(client.activeTokens.size, 1);
ok(token1, "got a token");
// get another
let token2 = yield fxa.getOAuthToken({ scope: "test-scope-2", client: client });
equal(client.numTokenFetches, 2);
equal(client.activeTokens.size, 2);
ok(token2, "got a token");
notEqual(token1, token2, "got a different token");
// now sign out - they should be removed.
yield fxa.signOut();
// FxA fires an observer when the "background" signout is complete.
yield promiseNotification("testhelper-fxa-signout-complete");
// No active tokens left.
equal(client.activeTokens.size, 0);
});
add_task(function* testTokenRaces() {
// Here we do 2 concurrent fetches each for 2 different token scopes (ie,
// 4 token fetches in total).
// This should provoke a potential race in the token fetching but we should
// handle and detect that leaving us with one of the fetch tokens being
// revoked and the same token value returned to both calls.
let client = new MockFxAccountsOAuthGrantClient();
let fxa = yield createMockFxA(client);
// We should see 2 notifications as part of this - set up the listeners
// now (and wait on them later)
let notifications = Promise.all([
promiseNotification("testhelper-fxa-revoke-complete"),
promiseNotification("testhelper-fxa-revoke-complete"),
]);
let results = yield Promise.all([
fxa.getOAuthToken({scope: "test-scope", client: client}),
fxa.getOAuthToken({scope: "test-scope", client: client}),
fxa.getOAuthToken({scope: "test-scope-2", client: client}),
fxa.getOAuthToken({scope: "test-scope-2", client: client}),
]);
equal(client.numTokenFetches, 4, "should have fetched 4 tokens.");
// We should see 2 of the 4 revoked due to the race.
yield notifications;
// Should have 2 unique tokens
results.sort();
equal(results[0], results[1]);
equal(results[2], results[3]);
// should be 2 active.
equal(client.activeTokens.size, 2);
// Which can each be revoked.
notifications = Promise.all([
promiseNotification("testhelper-fxa-revoke-complete"),
promiseNotification("testhelper-fxa-revoke-complete"),
]);
yield fxa.removeCachedOAuthToken({token: results[0]});
equal(client.activeTokens.size, 1);
yield fxa.removeCachedOAuthToken({token: results[2]});
equal(client.activeTokens.size, 0);
yield notifications;
});

View file

@ -1,409 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm");
Cu.import("resource://gre/modules/FxAccountsProfile.jsm");
const URL_STRING = "https://example.com";
Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "https://example.com/settings");
const STATUS_SUCCESS = 200;
/**
* Mock request responder
* @param {String} response
* Mocked raw response from the server
* @returns {Function}
*/
var mockResponse = function (response) {
let Request = function (requestUri) {
// Store the request uri so tests can inspect it
Request._requestUri = requestUri;
return {
setHeader: function () {},
head: function () {
this.response = response;
this.onComplete();
}
};
};
return Request;
};
/**
* Mock request error responder
* @param {Error} error
* Error object
* @returns {Function}
*/
var mockResponseError = function (error) {
return function () {
return {
setHeader: function () {},
head: function () {
this.onComplete(error);
}
};
};
};
var mockClient = function (fxa) {
let options = {
serverURL: "http://127.0.0.1:1111/v1",
fxa: fxa,
}
return new FxAccountsProfileClient(options);
};
const ACCOUNT_DATA = {
uid: "abc123"
};
function FxaMock() {
}
FxaMock.prototype = {
currentAccountState: {
profile: null,
get isCurrent() {
return true;
}
},
getSignedInUser: function () {
return Promise.resolve(ACCOUNT_DATA);
}
};
var mockFxa = function() {
return new FxaMock();
};
function CreateFxAccountsProfile(fxa = null, client = null) {
if (!fxa) {
fxa = mockFxa();
}
let options = {
fxa: fxa,
profileServerUrl: "http://127.0.0.1:1111/v1"
}
if (client) {
options.profileClient = client;
}
return new FxAccountsProfile(options);
}
add_test(function getCachedProfile() {
let profile = CreateFxAccountsProfile();
// a little pointless until bug 1157529 is fixed...
profile._cachedProfile = { avatar: "myurl" };
return profile._getCachedProfile()
.then(function (cached) {
do_check_eq(cached.avatar, "myurl");
run_next_test();
});
});
add_test(function cacheProfile_change() {
let fxa = mockFxa();
/* Saving profile data disabled - bug 1157529
let setUserAccountDataCalled = false;
fxa.setUserAccountData = function (data) {
setUserAccountDataCalled = true;
do_check_eq(data.profile.avatar, "myurl");
return Promise.resolve();
};
*/
let profile = CreateFxAccountsProfile(fxa);
makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
do_check_eq(data, ACCOUNT_DATA.uid);
// do_check_true(setUserAccountDataCalled); - bug 1157529
run_next_test();
});
return profile._cacheProfile({ avatar: "myurl" });
});
add_test(function cacheProfile_no_change() {
let fxa = mockFxa();
let profile = CreateFxAccountsProfile(fxa)
profile._cachedProfile = { avatar: "myurl" };
// XXX - saving is disabled (but we can leave that in for now as we are
// just checking it is *not* called)
fxa.setSignedInUser = function (data) {
throw new Error("should not update account data");
};
return profile._cacheProfile({ avatar: "myurl" })
.then((result) => {
do_check_false(!!result);
run_next_test();
});
});
add_test(function fetchAndCacheProfile_ok() {
let client = mockClient(mockFxa());
client.fetchProfile = function () {
return Promise.resolve({ avatar: "myimg"});
};
let profile = CreateFxAccountsProfile(null, client);
profile._cacheProfile = function (toCache) {
do_check_eq(toCache.avatar, "myimg");
return Promise.resolve();
};
return profile._fetchAndCacheProfile()
.then(result => {
do_check_eq(result.avatar, "myimg");
run_next_test();
});
});
// Check that a second profile request when one is already in-flight reuses
// the in-flight one.
add_task(function* fetchAndCacheProfileOnce() {
// A promise that remains unresolved while we fire off 2 requests for
// a profile.
let resolveProfile;
let promiseProfile = new Promise(resolve => {
resolveProfile = resolve;
});
let numFetches = 0;
let client = mockClient(mockFxa());
client.fetchProfile = function () {
numFetches += 1;
return promiseProfile;
};
let profile = CreateFxAccountsProfile(null, client);
let request1 = profile._fetchAndCacheProfile();
let request2 = profile._fetchAndCacheProfile();
// should be one request made to fetch the profile (but the promise returned
// by it remains unresolved)
do_check_eq(numFetches, 1);
// resolve the promise.
resolveProfile({ avatar: "myimg"});
// both requests should complete with the same data.
let got1 = yield request1;
do_check_eq(got1.avatar, "myimg");
let got2 = yield request1;
do_check_eq(got2.avatar, "myimg");
// and still only 1 request was made.
do_check_eq(numFetches, 1);
});
// Check that sharing a single fetch promise works correctly when the promise
// is rejected.
add_task(function* fetchAndCacheProfileOnce() {
// A promise that remains unresolved while we fire off 2 requests for
// a profile.
let rejectProfile;
let promiseProfile = new Promise((resolve,reject) => {
rejectProfile = reject;
});
let numFetches = 0;
let client = mockClient(mockFxa());
client.fetchProfile = function () {
numFetches += 1;
return promiseProfile;
};
let profile = CreateFxAccountsProfile(null, client);
let request1 = profile._fetchAndCacheProfile();
let request2 = profile._fetchAndCacheProfile();
// should be one request made to fetch the profile (but the promise returned
// by it remains unresolved)
do_check_eq(numFetches, 1);
// reject the promise.
rejectProfile("oh noes");
// both requests should reject.
try {
yield request1;
throw new Error("should have rejected");
} catch (ex) {
if (ex != "oh noes") {
throw ex;
}
}
try {
yield request2;
throw new Error("should have rejected");
} catch (ex) {
if (ex != "oh noes") {
throw ex;
}
}
// but a new request should work.
client.fetchProfile = function () {
return Promise.resolve({ avatar: "myimg"});
};
let got = yield profile._fetchAndCacheProfile();
do_check_eq(got.avatar, "myimg");
});
// Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the
// last one doesn't kick off a new request to check the cached copy is fresh.
add_task(function* fetchAndCacheProfileAfterThreshold() {
let numFetches = 0;
let client = mockClient(mockFxa());
client.fetchProfile = function () {
numFetches += 1;
return Promise.resolve({ avatar: "myimg"});
};
let profile = CreateFxAccountsProfile(null, client);
profile.PROFILE_FRESHNESS_THRESHOLD = 1000;
yield profile.getProfile();
do_check_eq(numFetches, 1);
yield profile.getProfile();
do_check_eq(numFetches, 1);
yield new Promise(resolve => {
do_timeout(1000, resolve);
});
yield profile.getProfile();
do_check_eq(numFetches, 2);
});
// Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the
// last one *does* kick off a new request if ON_PROFILE_CHANGE_NOTIFICATION
// is sent.
add_task(function* fetchAndCacheProfileBeforeThresholdOnNotification() {
let numFetches = 0;
let client = mockClient(mockFxa());
client.fetchProfile = function () {
numFetches += 1;
return Promise.resolve({ avatar: "myimg"});
};
let profile = CreateFxAccountsProfile(null, client);
profile.PROFILE_FRESHNESS_THRESHOLD = 1000;
yield profile.getProfile();
do_check_eq(numFetches, 1);
Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, null);
yield profile.getProfile();
do_check_eq(numFetches, 2);
});
add_test(function tearDown_ok() {
let profile = CreateFxAccountsProfile();
do_check_true(!!profile.client);
do_check_true(!!profile.fxa);
profile.tearDown();
do_check_null(profile.fxa);
do_check_null(profile.client);
run_next_test();
});
add_test(function getProfile_ok() {
let cachedUrl = "myurl";
let didFetch = false;
let profile = CreateFxAccountsProfile();
profile._getCachedProfile = function () {
return Promise.resolve({ avatar: cachedUrl });
};
profile._fetchAndCacheProfile = function () {
didFetch = true;
return Promise.resolve();
};
return profile.getProfile()
.then(result => {
do_check_eq(result.avatar, cachedUrl);
do_check_true(didFetch);
run_next_test();
});
});
add_test(function getProfile_no_cache() {
let fetchedUrl = "newUrl";
let profile = CreateFxAccountsProfile();
profile._getCachedProfile = function () {
return Promise.resolve();
};
profile._fetchAndCacheProfile = function () {
return Promise.resolve({ avatar: fetchedUrl });
};
return profile.getProfile()
.then(result => {
do_check_eq(result.avatar, fetchedUrl);
run_next_test();
});
});
add_test(function getProfile_has_cached_fetch_deleted() {
let cachedUrl = "myurl";
let fxa = mockFxa();
let client = mockClient(fxa);
client.fetchProfile = function () {
return Promise.resolve({ avatar: null });
};
let profile = CreateFxAccountsProfile(fxa, client);
profile._cachedProfile = { avatar: cachedUrl };
// instead of checking this in a mocked "save" function, just check after the
// observer
makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
profile.getProfile()
.then(profileData => {
do_check_null(profileData.avatar);
run_next_test();
});
});
return profile.getProfile()
.then(result => {
do_check_eq(result.avatar, "myurl");
});
});
function run_test() {
run_next_test();
}
function makeObserver(aObserveTopic, aObserveFunc) {
let callback = function (aSubject, aTopic, aData) {
log.debug("observed " + aTopic + " " + aData);
if (aTopic == aObserveTopic) {
removeMe();
aObserveFunc(aSubject, aTopic, aData);
}
};
function removeMe() {
log.debug("removing observer for " + aObserveTopic);
Services.obs.removeObserver(callback, aObserveTopic);
}
Services.obs.addObserver(callback, aObserveTopic, false);
return removeMe;
}

View file

@ -1,411 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm");
const STATUS_SUCCESS = 200;
/**
* Mock request responder
* @param {String} response
* Mocked raw response from the server
* @returns {Function}
*/
var mockResponse = function (response) {
let Request = function (requestUri) {
// Store the request uri so tests can inspect it
Request._requestUri = requestUri;
return {
setHeader: function () {},
get: function () {
this.response = response;
this.onComplete();
}
};
};
return Request;
};
// A simple mock FxA that hands out tokens without checking them and doesn't
// expect tokens to be revoked. We have specific token tests further down that
// has more checks here.
var mockFxa = {
getOAuthToken(options) {
do_check_eq(options.scope, "profile");
return "token";
}
}
const PROFILE_OPTIONS = {
serverURL: "http://127.0.0.1:1111/v1",
fxa: mockFxa,
};
/**
* Mock request error responder
* @param {Error} error
* Error object
* @returns {Function}
*/
var mockResponseError = function (error) {
return function () {
return {
setHeader: function () {},
get: function () {
this.onComplete(error);
}
};
};
};
add_test(function successfulResponse () {
let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
let response = {
success: true,
status: STATUS_SUCCESS,
body: "{\"email\":\"someone@restmail.net\",\"uid\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}",
};
client._Request = new mockResponse(response);
client.fetchProfile()
.then(
function (result) {
do_check_eq(client._Request._requestUri, "http://127.0.0.1:1111/v1/profile");
do_check_eq(result.email, "someone@restmail.net");
do_check_eq(result.uid, "0d5c1a89b8c54580b8e3e8adadae864a");
run_next_test();
}
);
});
add_test(function parseErrorResponse () {
let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
let response = {
success: true,
status: STATUS_SUCCESS,
body: "unexpected",
};
client._Request = new mockResponse(response);
client.fetchProfile()
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsProfileClientError");
do_check_eq(e.code, STATUS_SUCCESS);
do_check_eq(e.errno, ERRNO_PARSE);
do_check_eq(e.error, ERROR_PARSE);
do_check_eq(e.message, "unexpected");
run_next_test();
}
);
});
add_test(function serverErrorResponse () {
let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
let response = {
status: 500,
body: "{ \"code\": 500, \"errno\": 100, \"error\": \"Bad Request\", \"message\": \"Something went wrong\", \"reason\": \"Because the internet\" }",
};
client._Request = new mockResponse(response);
client.fetchProfile()
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsProfileClientError");
do_check_eq(e.code, 500);
do_check_eq(e.errno, 100);
do_check_eq(e.error, "Bad Request");
do_check_eq(e.message, "Something went wrong");
run_next_test();
}
);
});
// Test that we get a token, then if we get a 401 we revoke it, get a new one
// and retry.
add_test(function server401ResponseThenSuccess () {
// The last token we handed out.
let lastToken = -1;
// The number of times our removeCachedOAuthToken function was called.
let numTokensRemoved = 0;
let mockFxa = {
getOAuthToken(options) {
do_check_eq(options.scope, "profile");
return "" + ++lastToken; // tokens are strings.
},
removeCachedOAuthToken(options) {
// This test never has more than 1 token alive at once, so the token
// being revoked must always be the last token we handed out.
do_check_eq(parseInt(options.token), lastToken);
++numTokensRemoved;
}
}
let profileOptions = {
serverURL: "http://127.0.0.1:1111/v1",
fxa: mockFxa,
};
let client = new FxAccountsProfileClient(profileOptions);
// 2 responses - first one implying the token has expired, second works.
let responses = [
{
status: 401,
body: "{ \"code\": 401, \"errno\": 100, \"error\": \"Token expired\", \"message\": \"That token is too old\", \"reason\": \"Because security\" }",
},
{
success: true,
status: STATUS_SUCCESS,
body: "{\"avatar\":\"http://example.com/image.jpg\",\"id\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}",
},
];
let numRequests = 0;
let numAuthHeaders = 0;
// Like mockResponse but we want access to headers etc.
client._Request = function(requestUri) {
return {
setHeader: function (name, value) {
if (name == "Authorization") {
numAuthHeaders++;
do_check_eq(value, "Bearer " + lastToken);
}
},
get: function () {
this.response = responses[numRequests];
++numRequests;
this.onComplete();
}
};
}
client.fetchProfile()
.then(result => {
do_check_eq(result.avatar, "http://example.com/image.jpg");
do_check_eq(result.id, "0d5c1a89b8c54580b8e3e8adadae864a");
// should have been exactly 2 requests and exactly 2 auth headers.
do_check_eq(numRequests, 2);
do_check_eq(numAuthHeaders, 2);
// and we should have seen one token revoked.
do_check_eq(numTokensRemoved, 1);
run_next_test();
}
);
});
// Test that we get a token, then if we get a 401 we revoke it, get a new one
// and retry - but we *still* get a 401 on the retry, so the caller sees that.
add_test(function server401ResponsePersists () {
// The last token we handed out.
let lastToken = -1;
// The number of times our removeCachedOAuthToken function was called.
let numTokensRemoved = 0;
let mockFxa = {
getOAuthToken(options) {
do_check_eq(options.scope, "profile");
return "" + ++lastToken; // tokens are strings.
},
removeCachedOAuthToken(options) {
// This test never has more than 1 token alive at once, so the token
// being revoked must always be the last token we handed out.
do_check_eq(parseInt(options.token), lastToken);
++numTokensRemoved;
}
}
let profileOptions = {
serverURL: "http://127.0.0.1:1111/v1",
fxa: mockFxa,
};
let client = new FxAccountsProfileClient(profileOptions);
let response = {
status: 401,
body: "{ \"code\": 401, \"errno\": 100, \"error\": \"It's not your token, it's you!\", \"message\": \"I don't like you\", \"reason\": \"Because security\" }",
};
let numRequests = 0;
let numAuthHeaders = 0;
client._Request = function(requestUri) {
return {
setHeader: function (name, value) {
if (name == "Authorization") {
numAuthHeaders++;
do_check_eq(value, "Bearer " + lastToken);
}
},
get: function () {
this.response = response;
++numRequests;
this.onComplete();
}
};
}
client.fetchProfile().then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsProfileClientError");
do_check_eq(e.code, 401);
do_check_eq(e.errno, 100);
do_check_eq(e.error, "It's not your token, it's you!");
// should have been exactly 2 requests and exactly 2 auth headers.
do_check_eq(numRequests, 2);
do_check_eq(numAuthHeaders, 2);
// and we should have seen both tokens revoked.
do_check_eq(numTokensRemoved, 2);
run_next_test();
}
);
});
add_test(function networkErrorResponse () {
let client = new FxAccountsProfileClient({
serverURL: "http://domain.dummy",
fxa: mockFxa,
});
client.fetchProfile()
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsProfileClientError");
do_check_eq(e.code, null);
do_check_eq(e.errno, ERRNO_NETWORK);
do_check_eq(e.error, ERROR_NETWORK);
run_next_test();
}
);
});
add_test(function unsupportedMethod () {
let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
return client._createRequest("/profile", "PUT")
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsProfileClientError");
do_check_eq(e.code, ERROR_CODE_METHOD_NOT_ALLOWED);
do_check_eq(e.errno, ERRNO_NETWORK);
do_check_eq(e.error, ERROR_NETWORK);
do_check_eq(e.message, ERROR_MSG_METHOD_NOT_ALLOWED);
run_next_test();
}
);
});
add_test(function onCompleteRequestError () {
let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
client._Request = new mockResponseError(new Error("onComplete error"));
client.fetchProfile()
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsProfileClientError");
do_check_eq(e.code, null);
do_check_eq(e.errno, ERRNO_NETWORK);
do_check_eq(e.error, ERROR_NETWORK);
do_check_eq(e.message, "Error: onComplete error");
run_next_test();
}
);
});
add_test(function fetchProfileImage_successfulResponse () {
let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
let response = {
success: true,
status: STATUS_SUCCESS,
body: "{\"avatar\":\"http://example.com/image.jpg\",\"id\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}",
};
client._Request = new mockResponse(response);
client.fetchProfileImage()
.then(
function (result) {
do_check_eq(client._Request._requestUri, "http://127.0.0.1:1111/v1/avatar");
do_check_eq(result.avatar, "http://example.com/image.jpg");
do_check_eq(result.id, "0d5c1a89b8c54580b8e3e8adadae864a");
run_next_test();
}
);
});
add_test(function constructorTests() {
validationHelper(undefined,
"Error: Missing 'serverURL' configuration option");
validationHelper({},
"Error: Missing 'serverURL' configuration option");
validationHelper({ serverURL: "badUrl" },
"Error: Invalid 'serverURL'");
run_next_test();
});
add_test(function errorTests() {
let error1 = new FxAccountsProfileClientError();
do_check_eq(error1.name, "FxAccountsProfileClientError");
do_check_eq(error1.code, null);
do_check_eq(error1.errno, ERRNO_UNKNOWN_ERROR);
do_check_eq(error1.error, ERROR_UNKNOWN);
do_check_eq(error1.message, null);
let error2 = new FxAccountsProfileClientError({
code: STATUS_SUCCESS,
errno: 1,
error: "Error",
message: "Something",
});
let fields2 = error2._toStringFields();
let statusCode = 1;
do_check_eq(error2.name, "FxAccountsProfileClientError");
do_check_eq(error2.code, STATUS_SUCCESS);
do_check_eq(error2.errno, statusCode);
do_check_eq(error2.error, "Error");
do_check_eq(error2.message, "Something");
do_check_eq(fields2.name, "FxAccountsProfileClientError");
do_check_eq(fields2.code, STATUS_SUCCESS);
do_check_eq(fields2.errno, statusCode);
do_check_eq(fields2.error, "Error");
do_check_eq(fields2.message, "Something");
do_check_true(error2.toString().indexOf("Something") >= 0);
run_next_test();
});
function run_test() {
run_next_test();
}
/**
* Quick way to test the "FxAccountsProfileClient" constructor.
*
* @param {Object} options
* FxAccountsProfileClient constructor options
* @param {String} expected
* Expected error message
* @returns {*}
*/
function validationHelper(options, expected) {
// add fxa to options - that missing isn't what we are testing here.
if (options) {
options.fxa = mockFxa;
}
try {
new FxAccountsProfileClient(options);
} catch (e) {
return do_check_eq(e.toString(), expected);
}
throw new Error("Validation helper error");
}

View file

@ -1,236 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests for the FxA push service.
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccountsPush.js");
Cu.import("resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "pushService",
"@mozilla.org/push/Service;1", "nsIPushService");
initTestLogging("Trace");
log.level = Log.Level.Trace;
const MOCK_ENDPOINT = "http://mochi.test:8888";
// tests do not allow external connections, mock the PushService
let mockPushService = {
pushTopic: this.pushService.pushTopic,
subscriptionChangeTopic: this.pushService.subscriptionChangeTopic,
subscribe(scope, principal, cb) {
cb(Components.results.NS_OK, {
endpoint: MOCK_ENDPOINT
});
},
unsubscribe(scope, principal, cb) {
cb(Components.results.NS_OK, true);
}
};
let mockFxAccounts = {
checkVerificationStatus() {},
updateDeviceRegistration() {}
};
let mockLog = {
trace() {},
debug() {},
warn() {},
error() {}
};
add_task(function* initialize() {
let pushService = new FxAccountsPushService();
equal(pushService.initialize(), false);
});
add_task(function* registerPushEndpointSuccess() {
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: mockFxAccounts,
});
let subscription = yield pushService.registerPushEndpoint();
equal(subscription.endpoint, MOCK_ENDPOINT);
});
add_task(function* registerPushEndpointFailure() {
let failPushService = Object.assign(mockPushService, {
subscribe(scope, principal, cb) {
cb(Components.results.NS_ERROR_ABORT);
}
});
let pushService = new FxAccountsPushService({
pushService: failPushService,
fxAccounts: mockFxAccounts,
});
let subscription = yield pushService.registerPushEndpoint();
equal(subscription, null);
});
add_task(function* unsubscribeSuccess() {
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: mockFxAccounts,
});
let result = yield pushService.unsubscribe();
equal(result, true);
});
add_task(function* unsubscribeFailure() {
let failPushService = Object.assign(mockPushService, {
unsubscribe(scope, principal, cb) {
cb(Components.results.NS_ERROR_ABORT);
}
});
let pushService = new FxAccountsPushService({
pushService: failPushService,
fxAccounts: mockFxAccounts,
});
let result = yield pushService.unsubscribe();
equal(result, null);
});
add_test(function observeLogout() {
let customLog = Object.assign(mockLog, {
trace: function (msg) {
if (msg === "FxAccountsPushService unsubscribe") {
// logout means we unsubscribe
run_next_test();
}
}
});
let pushService = new FxAccountsPushService({
pushService: mockPushService,
log: customLog
});
pushService.observe(null, ONLOGOUT_NOTIFICATION);
});
add_test(function observePushTopicVerify() {
let emptyMsg = {
QueryInterface: function() {
return this;
}
};
let customAccounts = Object.assign(mockFxAccounts, {
checkVerificationStatus: function () {
// checking verification status on push messages without data
run_next_test();
}
});
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: customAccounts,
});
pushService.observe(emptyMsg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});
add_test(function observePushTopicDeviceDisconnected() {
const deviceId = "bogusid";
let msg = {
data: {
json: () => ({
command: ON_DEVICE_DISCONNECTED_NOTIFICATION,
data: {
id: deviceId
}
})
},
QueryInterface: function() {
return this;
}
};
let customAccounts = Object.assign(mockFxAccounts, {
handleDeviceDisconnection: function () {
// checking verification status on push messages without data
run_next_test();
}
});
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: customAccounts,
});
pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});
add_test(function observePushTopicPasswordChanged() {
let msg = {
data: {
json: () => ({
command: ON_PASSWORD_CHANGED_NOTIFICATION
})
},
QueryInterface: function() {
return this;
}
};
let pushService = new FxAccountsPushService({
pushService: mockPushService,
});
pushService._onPasswordChanged = function () {
run_next_test();
}
pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});
add_test(function observePushTopicPasswordReset() {
let msg = {
data: {
json: () => ({
command: ON_PASSWORD_RESET_NOTIFICATION
})
},
QueryInterface: function() {
return this;
}
};
let pushService = new FxAccountsPushService({
pushService: mockPushService
});
pushService._onPasswordChanged = function () {
run_next_test();
}
pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});
add_test(function observeSubscriptionChangeTopic() {
let customAccounts = Object.assign(mockFxAccounts, {
updateDeviceRegistration: function () {
// subscription change means updating the device registration
run_next_test();
}
});
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: customAccounts,
});
pushService.observe(null, mockPushService.subscriptionChangeTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});

View file

@ -1,477 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests for the FxA storage manager.
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/Log.jsm");
initTestLogging("Trace");
log.level = Log.Level.Trace;
const DEVICE_REGISTRATION_VERSION = 42;
// A couple of mocks we can use.
function MockedPlainStorage(accountData) {
let data = null;
if (accountData) {
data = {
version: DATA_FORMAT_VERSION,
accountData: accountData,
}
}
this.data = data;
this.numReads = 0;
}
MockedPlainStorage.prototype = {
get: Task.async(function* () {
this.numReads++;
Assert.equal(this.numReads, 1, "should only ever be 1 read of acct data");
return this.data;
}),
set: Task.async(function* (data) {
this.data = data;
}),
};
function MockedSecureStorage(accountData) {
let data = null;
if (accountData) {
data = {
version: DATA_FORMAT_VERSION,
accountData: accountData,
}
}
this.data = data;
this.numReads = 0;
}
MockedSecureStorage.prototype = {
fetchCount: 0,
locked: false,
STORAGE_LOCKED: function() {},
get: Task.async(function* (uid, email) {
this.fetchCount++;
if (this.locked) {
throw new this.STORAGE_LOCKED();
}
this.numReads++;
Assert.equal(this.numReads, 1, "should only ever be 1 read of unlocked data");
return this.data;
}),
set: Task.async(function* (uid, contents) {
this.data = contents;
}),
}
function add_storage_task(testFunction) {
add_task(function* () {
print("Starting test with secure storage manager");
yield testFunction(new FxAccountsStorageManager());
});
add_task(function* () {
print("Starting test with simple storage manager");
yield testFunction(new FxAccountsStorageManager({useSecure: false}));
});
}
// initialized without account data and there's nothing to read. Not logged in.
add_storage_task(function* checkInitializedEmpty(sm) {
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
yield sm.initialize();
Assert.strictEqual((yield sm.getAccountData()), null);
Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in")
});
// Initialized with account data (ie, simulating a new user being logged in).
// Should reflect the initial data and be written to storage.
add_storage_task(function* checkNewUser(sm) {
let initialAccountData = {
uid: "uid",
email: "someone@somewhere.com",
kA: "kA",
deviceId: "device id"
};
sm.plainStorage = new MockedPlainStorage()
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
yield sm.initialize(initialAccountData);
let accountData = yield sm.getAccountData();
Assert.equal(accountData.uid, initialAccountData.uid);
Assert.equal(accountData.email, initialAccountData.email);
Assert.equal(accountData.kA, initialAccountData.kA);
Assert.equal(accountData.deviceId, initialAccountData.deviceId);
// and it should have been written to storage.
Assert.equal(sm.plainStorage.data.accountData.uid, initialAccountData.uid);
Assert.equal(sm.plainStorage.data.accountData.email, initialAccountData.email);
Assert.equal(sm.plainStorage.data.accountData.deviceId, initialAccountData.deviceId);
// check secure
if (sm.secureStorage) {
Assert.equal(sm.secureStorage.data.accountData.kA, initialAccountData.kA);
} else {
Assert.equal(sm.plainStorage.data.accountData.kA, initialAccountData.kA);
}
});
// Initialized without account data but storage has it available.
add_storage_task(function* checkEverythingRead(sm) {
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
deviceId: "wibble",
deviceRegistrationVersion: null
});
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
yield sm.initialize();
let accountData = yield sm.getAccountData();
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
Assert.equal(accountData.deviceId, "wibble");
Assert.equal(accountData.deviceRegistrationVersion, null);
// Update the data - we should be able to fetch it back and it should appear
// in our storage.
yield sm.updateAccountData({
verified: true,
kA: "kA",
kB: "kB",
deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION
});
accountData = yield sm.getAccountData();
Assert.equal(accountData.kB, "kB");
Assert.equal(accountData.kA, "kA");
Assert.equal(accountData.deviceId, "wibble");
Assert.equal(accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
// Check the new value was written to storage.
yield sm._promiseStorageComplete; // storage is written in the background.
// "verified", "deviceId" and "deviceRegistrationVersion" are plain-text fields.
Assert.equal(sm.plainStorage.data.accountData.verified, true);
Assert.equal(sm.plainStorage.data.accountData.deviceId, "wibble");
Assert.equal(sm.plainStorage.data.accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
// "kA" and "foo" are secure
if (sm.secureStorage) {
Assert.equal(sm.secureStorage.data.accountData.kA, "kA");
Assert.equal(sm.secureStorage.data.accountData.kB, "kB");
} else {
Assert.equal(sm.plainStorage.data.accountData.kA, "kA");
Assert.equal(sm.plainStorage.data.accountData.kB, "kB");
}
});
add_storage_task(function* checkInvalidUpdates(sm) {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
Assert.rejects(sm.updateAccountData({uid: "another"}), "Can't change");
Assert.rejects(sm.updateAccountData({email: "someoneelse"}), "Can't change");
});
add_storage_task(function* checkNullUpdatesRemovedUnlocked(sm) {
if (sm.secureStorage) {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
} else {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com",
kA: "kA", kB: "kB"});
}
yield sm.initialize();
yield sm.updateAccountData({kA: null});
let accountData = yield sm.getAccountData();
Assert.ok(!accountData.kA);
Assert.equal(accountData.kB, "kB");
});
add_storage_task(function* checkDelete(sm) {
if (sm.secureStorage) {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
} else {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com",
kA: "kA", kB: "kB"});
}
yield sm.initialize();
yield sm.deleteAccountData();
// Storage should have been reset to null.
Assert.equal(sm.plainStorage.data, null);
if (sm.secureStorage) {
Assert.equal(sm.secureStorage.data, null);
}
// And everything should reflect no user.
Assert.equal((yield sm.getAccountData()), null);
});
// Some tests only for the secure storage manager.
add_task(function* checkNullUpdatesRemovedLocked() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
sm.secureStorage.locked = true;
yield sm.initialize();
yield sm.updateAccountData({kA: null});
let accountData = yield sm.getAccountData();
Assert.ok(!accountData.kA);
// still no kB as we are locked.
Assert.ok(!accountData.kB);
// now unlock - should still be no kA but kB should appear.
sm.secureStorage.locked = false;
accountData = yield sm.getAccountData();
Assert.ok(!accountData.kA);
Assert.equal(accountData.kB, "kB");
// And secure storage should have been written with our previously-cached
// data.
Assert.strictEqual(sm.secureStorage.data.accountData.kA, undefined);
Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB");
});
add_task(function* checkEverythingReadSecure() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
yield sm.initialize();
let accountData = yield sm.getAccountData();
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
Assert.equal(accountData.kA, "kA");
});
add_task(function* checkMemoryFieldsNotReturnedByDefault() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
yield sm.initialize();
// keyPair is a memory field.
yield sm.updateAccountData({keyPair: "the keypair value"});
let accountData = yield sm.getAccountData();
// Requesting everything should *not* return in memory fields.
Assert.strictEqual(accountData.keyPair, undefined);
// But requesting them specifically does get them.
accountData = yield sm.getAccountData("keyPair");
Assert.strictEqual(accountData.keyPair, "the keypair value");
});
add_task(function* checkExplicitGet() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
yield sm.initialize();
let accountData = yield sm.getAccountData(["uid", "kA"]);
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.kA, "kA");
// We didn't ask for email so shouldn't have got it.
Assert.strictEqual(accountData.email, undefined);
});
add_task(function* checkExplicitGetNoSecureRead() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
yield sm.initialize();
Assert.equal(sm.secureStorage.fetchCount, 0);
// request 2 fields in secure storage - it should have caused a single fetch.
let accountData = yield sm.getAccountData(["email", "uid"]);
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
Assert.strictEqual(accountData.kA, undefined);
Assert.equal(sm.secureStorage.fetchCount, 1);
});
add_task(function* checkLockedUpdates() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "old-kA", kB: "kB"});
sm.secureStorage.locked = true;
yield sm.initialize();
let accountData = yield sm.getAccountData();
// requesting kA and kB will fail as storage is locked.
Assert.ok(!accountData.kA);
Assert.ok(!accountData.kB);
// While locked we can still update it and see the updated value.
sm.updateAccountData({kA: "new-kA"});
accountData = yield sm.getAccountData();
Assert.equal(accountData.kA, "new-kA");
// unlock.
sm.secureStorage.locked = false;
accountData = yield sm.getAccountData();
// should reflect the value we updated and the one we didn't.
Assert.equal(accountData.kA, "new-kA");
Assert.equal(accountData.kB, "kB");
// And storage should also reflect it.
Assert.strictEqual(sm.secureStorage.data.accountData.kA, "new-kA");
Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB");
});
// Some tests for the "storage queue" functionality.
// A helper for our queued tests. It creates a StorageManager and then queues
// an unresolved promise. The tests then do additional setup and checks, then
// resolves or rejects the blocked promise.
var setupStorageManagerForQueueTest = Task.async(function* () {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
sm.secureStorage.locked = true;
yield sm.initialize();
let resolveBlocked, rejectBlocked;
let blockedPromise = new Promise((resolve, reject) => {
resolveBlocked = resolve;
rejectBlocked = reject;
});
sm._queueStorageOperation(() => blockedPromise);
return {sm, blockedPromise, resolveBlocked, rejectBlocked}
});
// First the general functionality.
add_task(function* checkQueueSemantics() {
let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest();
// We've one unresolved promise in the queue - add another promise.
let resolveSubsequent;
let subsequentPromise = new Promise(resolve => {
resolveSubsequent = resolve;
});
let subsequentCalled = false;
sm._queueStorageOperation(() => {
subsequentCalled = true;
resolveSubsequent();
return subsequentPromise;
});
// Our "subsequent" function should not have been called yet.
Assert.ok(!subsequentCalled);
// Release our blocked promise.
resolveBlocked();
// Our subsequent promise should end up resolved.
yield subsequentPromise;
Assert.ok(subsequentCalled);
yield sm.finalize();
});
// Check that a queued promise being rejected works correctly.
add_task(function* checkQueueSemanticsOnError() {
let { sm, blockedPromise, rejectBlocked } = yield setupStorageManagerForQueueTest();
let resolveSubsequent;
let subsequentPromise = new Promise(resolve => {
resolveSubsequent = resolve;
});
let subsequentCalled = false;
sm._queueStorageOperation(() => {
subsequentCalled = true;
resolveSubsequent();
return subsequentPromise;
});
// Our "subsequent" function should not have been called yet.
Assert.ok(!subsequentCalled);
// Reject our blocked promise - the subsequent operations should still work
// correctly.
rejectBlocked("oh no");
// Our subsequent promise should end up resolved.
yield subsequentPromise;
Assert.ok(subsequentCalled);
// But the first promise should reflect the rejection.
try {
yield blockedPromise;
Assert.ok(false, "expected this promise to reject");
} catch (ex) {
Assert.equal(ex, "oh no");
}
yield sm.finalize();
});
// And some tests for the specific operations that are queued.
add_task(function* checkQueuedReadAndUpdate() {
let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest();
// Mock the underlying operations
// _doReadAndUpdateSecure is queued by _maybeReadAndUpdateSecure
let _doReadCalled = false;
sm._doReadAndUpdateSecure = () => {
_doReadCalled = true;
return Promise.resolve();
}
let resultPromise = sm._maybeReadAndUpdateSecure();
Assert.ok(!_doReadCalled);
resolveBlocked();
yield resultPromise;
Assert.ok(_doReadCalled);
yield sm.finalize();
});
add_task(function* checkQueuedWrite() {
let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest();
// Mock the underlying operations
let __writeCalled = false;
sm.__write = () => {
__writeCalled = true;
return Promise.resolve();
}
let writePromise = sm._write();
Assert.ok(!__writeCalled);
resolveBlocked();
yield writePromise;
Assert.ok(__writeCalled);
yield sm.finalize();
});
add_task(function* checkQueuedDelete() {
let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest();
// Mock the underlying operations
let _deleteCalled = false;
sm._deleteAccountData = () => {
_deleteCalled = true;
return Promise.resolve();
}
let resultPromise = sm.deleteAccountData();
Assert.ok(!_deleteCalled);
resolveBlocked();
yield resultPromise;
Assert.ok(_deleteCalled);
yield sm.finalize();
});
function run_test() {
run_next_test();
}

View file

@ -1,499 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/FxAccountsCommon.js");
const { FxAccountsWebChannel, FxAccountsWebChannelHelpers } =
Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
const URL_STRING = "https://example.com";
const mockSendingContext = {
browser: {},
principal: {},
eventTarget: {}
};
add_test(function () {
validationHelper(undefined,
"Error: Missing configuration options");
validationHelper({
channel_id: WEBCHANNEL_ID
},
"Error: Missing 'content_uri' option");
validationHelper({
content_uri: 'bad uri',
channel_id: WEBCHANNEL_ID
},
/NS_ERROR_MALFORMED_URI/);
validationHelper({
content_uri: URL_STRING
},
'Error: Missing \'channel_id\' option');
run_next_test();
});
add_task(function* test_rejection_reporting() {
let mockMessage = {
command: 'fxaccounts:login',
messageId: '1234',
data: { email: 'testuser@testuser.com' },
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
login(accountData) {
equal(accountData.email, 'testuser@testuser.com',
'Should forward incoming message data to the helper');
return Promise.reject(new Error('oops'));
},
},
});
let promiseSend = new Promise(resolve => {
channel._channel.send = (message, context) => {
resolve({ message, context });
};
});
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
let { message, context } = yield promiseSend;
equal(context, mockSendingContext, 'Should forward the original context');
equal(message.command, 'fxaccounts:login',
'Should include the incoming command');
equal(message.messageId, '1234', 'Should include the message ID');
equal(message.data.error.message, 'Error: oops',
'Should convert the error message to a string');
notStrictEqual(message.data.error.stack, null,
'Should include the stack for JS error rejections');
});
add_test(function test_exception_reporting() {
let mockMessage = {
command: 'fxaccounts:sync_preferences',
messageId: '5678',
data: { entryPoint: 'fxa:verification_complete' }
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
openSyncPreferences(browser, entryPoint) {
equal(entryPoint, 'fxa:verification_complete',
'Should forward incoming message data to the helper');
throw new TypeError('splines not reticulated');
},
},
});
channel._channel.send = (message, context) => {
equal(context, mockSendingContext, 'Should forward the original context');
equal(message.command, 'fxaccounts:sync_preferences',
'Should include the incoming command');
equal(message.messageId, '5678', 'Should include the message ID');
equal(message.data.error.message, 'TypeError: splines not reticulated',
'Should convert the exception to a string');
notStrictEqual(message.data.error.stack, null,
'Should include the stack for JS exceptions');
run_next_test();
};
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});
add_test(function test_profile_image_change_message() {
var mockMessage = {
command: "profile:change",
data: { uid: "foo" }
};
makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
do_check_eq(data, "foo");
run_next_test();
});
var channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING
});
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});
add_test(function test_login_message() {
let mockMessage = {
command: 'fxaccounts:login',
data: { email: 'testuser@testuser.com' }
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
login: function (accountData) {
do_check_eq(accountData.email, 'testuser@testuser.com');
run_next_test();
return Promise.resolve();
}
}
});
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});
add_test(function test_logout_message() {
let mockMessage = {
command: 'fxaccounts:logout',
data: { uid: "foo" }
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
logout: function (uid) {
do_check_eq(uid, 'foo');
run_next_test();
return Promise.resolve();
}
}
});
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});
add_test(function test_delete_message() {
let mockMessage = {
command: 'fxaccounts:delete',
data: { uid: "foo" }
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
logout: function (uid) {
do_check_eq(uid, 'foo');
run_next_test();
return Promise.resolve();
}
}
});
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});
add_test(function test_can_link_account_message() {
let mockMessage = {
command: 'fxaccounts:can_link_account',
data: { email: 'testuser@testuser.com' }
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
shouldAllowRelink: function (email) {
do_check_eq(email, 'testuser@testuser.com');
run_next_test();
}
}
});
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});
add_test(function test_sync_preferences_message() {
let mockMessage = {
command: 'fxaccounts:sync_preferences',
data: { entryPoint: 'fxa:verification_complete' }
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
openSyncPreferences: function (browser, entryPoint) {
do_check_eq(entryPoint, 'fxa:verification_complete');
do_check_eq(browser, mockSendingContext.browser);
run_next_test();
}
}
});
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});
add_test(function test_unrecognized_message() {
let mockMessage = {
command: 'fxaccounts:unrecognized',
data: {}
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING
});
// no error is expected.
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
run_next_test();
});
add_test(function test_helpers_should_allow_relink_same_email() {
let helpers = new FxAccountsWebChannelHelpers();
helpers.setPreviousAccountNameHashPref('testuser@testuser.com');
do_check_true(helpers.shouldAllowRelink('testuser@testuser.com'));
run_next_test();
});
add_test(function test_helpers_should_allow_relink_different_email() {
let helpers = new FxAccountsWebChannelHelpers();
helpers.setPreviousAccountNameHashPref('testuser@testuser.com');
helpers._promptForRelink = (acctName) => {
return acctName === 'allowed_to_relink@testuser.com';
};
do_check_true(helpers.shouldAllowRelink('allowed_to_relink@testuser.com'));
do_check_false(helpers.shouldAllowRelink('not_allowed_to_relink@testuser.com'));
run_next_test();
});
add_task(function* test_helpers_login_without_customize_sync() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
setSignedInUser: function(accountData) {
return new Promise(resolve => {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
// verifiedCanLinkAccount should be stripped in the data.
do_check_false('verifiedCanLinkAccount' in accountData);
// the customizeSync pref should not update
do_check_false(helpers.getShowCustomizeSyncPref());
// previously signed in user preference is updated.
do_check_eq(helpers.getPreviousAccountNameHashPref(), helpers.sha256('testuser@testuser.com'));
resolve();
});
}
}
});
// the show customize sync pref should stay the same
helpers.setShowCustomizeSyncPref(false);
// ensure the previous account pref is overwritten.
helpers.setPreviousAccountNameHashPref('lastuser@testuser.com');
yield helpers.login({
email: 'testuser@testuser.com',
verifiedCanLinkAccount: true,
customizeSync: false
});
});
add_task(function* test_helpers_login_with_customize_sync() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
setSignedInUser: function(accountData) {
return new Promise(resolve => {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
// customizeSync should be stripped in the data.
do_check_false('customizeSync' in accountData);
// the customizeSync pref should not update
do_check_true(helpers.getShowCustomizeSyncPref());
resolve();
});
}
}
});
// the customize sync pref should be overwritten
helpers.setShowCustomizeSyncPref(false);
yield helpers.login({
email: 'testuser@testuser.com',
verifiedCanLinkAccount: true,
customizeSync: true
});
});
add_task(function* test_helpers_login_with_customize_sync_and_declined_engines() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
setSignedInUser: function(accountData) {
return new Promise(resolve => {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
// customizeSync should be stripped in the data.
do_check_false('customizeSync' in accountData);
do_check_false('declinedSyncEngines' in accountData);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), false);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), false);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
// the customizeSync pref should be disabled
do_check_false(helpers.getShowCustomizeSyncPref());
resolve();
});
}
}
});
// the customize sync pref should be overwritten
helpers.setShowCustomizeSyncPref(true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
yield helpers.login({
email: 'testuser@testuser.com',
verifiedCanLinkAccount: true,
customizeSync: true,
declinedSyncEngines: ['addons', 'prefs']
});
});
add_test(function test_helpers_open_sync_preferences() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
}
});
let mockBrowser = {
loadURI(uri) {
do_check_eq(uri, "about:preferences?entrypoint=fxa%3Averification_complete#sync");
run_next_test();
}
};
helpers.openSyncPreferences(mockBrowser, "fxa:verification_complete");
});
add_task(function* test_helpers_change_password() {
let wasCalled = {
updateUserAccountData: false,
updateDeviceRegistration: false
};
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
updateUserAccountData(credentials) {
return new Promise(resolve => {
do_check_true(credentials.hasOwnProperty("email"));
do_check_true(credentials.hasOwnProperty("uid"));
do_check_true(credentials.hasOwnProperty("kA"));
do_check_true(credentials.hasOwnProperty("deviceId"));
do_check_null(credentials.deviceId);
// "foo" isn't a field known by storage, so should be dropped.
do_check_false(credentials.hasOwnProperty("foo"));
wasCalled.updateUserAccountData = true;
resolve();
});
},
updateDeviceRegistration() {
do_check_eq(arguments.length, 0);
wasCalled.updateDeviceRegistration = true;
return Promise.resolve()
}
}
});
yield helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" });
do_check_true(wasCalled.updateUserAccountData);
do_check_true(wasCalled.updateDeviceRegistration);
});
add_task(function* test_helpers_change_password_with_error() {
let wasCalled = {
updateUserAccountData: false,
updateDeviceRegistration: false
};
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
updateUserAccountData() {
wasCalled.updateUserAccountData = true;
return Promise.reject();
},
updateDeviceRegistration() {
wasCalled.updateDeviceRegistration = true;
return Promise.resolve()
}
}
});
try {
yield helpers.changePassword({});
do_check_false('changePassword should have rejected');
} catch (_) {
do_check_true(wasCalled.updateUserAccountData);
do_check_false(wasCalled.updateDeviceRegistration);
}
});
function run_test() {
run_next_test();
}
function makeObserver(aObserveTopic, aObserveFunc) {
let callback = function (aSubject, aTopic, aData) {
log.debug("observed " + aTopic + " " + aData);
if (aTopic == aObserveTopic) {
removeMe();
aObserveFunc(aSubject, aTopic, aData);
}
};
function removeMe() {
log.debug("removing observer for " + aObserveTopic);
Services.obs.removeObserver(callback, aObserveTopic);
}
Services.obs.addObserver(callback, aObserveTopic, false);
return removeMe;
}
function validationHelper(params, expected) {
try {
new FxAccountsWebChannel(params);
} catch (e) {
if (typeof expected === 'string') {
return do_check_eq(e.toString(), expected);
} else {
return do_check_true(e.toString().match(expected));
}
}
throw new Error("Validation helper error");
}

View file

@ -1,23 +0,0 @@
[DEFAULT]
head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
tail =
skip-if = (toolkit == 'android' || appname == 'thunderbird')
support-files =
!/services/common/tests/unit/head_helpers.js
!/services/common/tests/unit/head_http.js
[test_accounts.js]
[test_accounts_device_registration.js]
[test_client.js]
[test_credentials.js]
[test_loginmgr_storage.js]
[test_oauth_client.js]
[test_oauth_grant_client.js]
[test_oauth_grant_client_server.js]
[test_oauth_tokens.js]
[test_oauth_token_storage.js]
[test_profile_client.js]
[test_push_service.js]
[test_web_channel.js]
[test_profile.js]
[test_storage_manager.js]

View file

@ -9,8 +9,5 @@ DIRS += [
'crypto',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
DIRS += ['fxaccounts']
if CONFIG['MOZ_SERVICES_SYNC']:
DIRS += ['sync']

View file

@ -19,13 +19,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
// FxAccountsCommon.js doesn't use a "namespace", so create one here.
XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
let FxAccountsCommon = {};
Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
return FxAccountsCommon;
});
/*
* Utility functions
*/
@ -599,9 +592,6 @@ this.Utils = {
*/
getSyncCredentialsHosts: function() {
let result = new Set(this.getSyncCredentialsHostsLegacy());
for (let host of this.getSyncCredentialsHostsFxA()) {
result.add(host);
}
return result;
},
@ -613,36 +603,6 @@ this.Utils = {
return new Set([PWDMGR_HOST]);
},
/*
* Get the FxA identity hosts.
*/
getSyncCredentialsHostsFxA: function() {
// This is somewhat expensive and the result static, so we cache the result.
if (this._syncCredentialsHostsFxA) {
return this._syncCredentialsHostsFxA;
}
let result = new Set();
// the FxA host
result.add(FxAccountsCommon.FXA_PWDMGR_HOST);
//
// The FxA hosts - these almost certainly all have the same hostname, but
// better safe than sorry...
for (let prefName of ["identity.fxaccounts.remote.force_auth.uri",
"identity.fxaccounts.remote.signup.uri",
"identity.fxaccounts.remote.signin.uri",
"identity.fxaccounts.settings.uri"]) {
let prefVal;
try {
prefVal = Services.prefs.getCharPref(prefName);
} catch (_) {
continue;
}
let uri = Services.io.newURI(prefVal, null, null);
result.add(uri.prePath);
}
return this._syncCredentialsHostsFxA = result;
},
getDefaultDeviceName() {
// Generate a client name if we don't have a useful one yet
let env = Cc["@mozilla.org/process/environment;1"]

View file

@ -486,8 +486,6 @@ add_identity_test(this, function test_shouldReportLoginFailureWithNoCluster() {
do_check_false(errorHandler.shouldReportError());
});
// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
// an fxaccounts environment?
add_task(function test_login_syncAndReportErrors_non_network_error() {
// Test non-network errors are reported
// when calling syncAndReportErrors
@ -536,8 +534,6 @@ add_identity_test(this, function test_sync_syncAndReportErrors_non_network_error
yield deferred.promise;
});
// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
// an fxaccounts environment?
add_task(function test_login_syncAndReportErrors_prolonged_non_network_error() {
// Test prolonged, non-network errors are
// reported when calling syncAndReportErrors.

View file

@ -74,9 +74,7 @@ const ACTIONS = [
ACTION_VERIFY_NOT,
];
const OBSERVER_TOPICS = ["fxaccounts:onlogin",
"fxaccounts:onlogout",
"private-browsing",
const OBSERVER_TOPICS = ["private-browsing",
"quit-application-requested",
"sessionstore-windows-restored",
"weave:engine:start-tracking",