mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-29 11:57:32 +09:00
Remove Firefox Accounts service and tie-ins.
See previous commit for removal of browser identity module.
This commit is contained in:
parent
ccf32a6b79
commit
c64dc935f9
420 changed files with 9 additions and 51958 deletions
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}),
|
||||
|
||||
};
|
||||
|
|
@ -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'");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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()) + ")";
|
||||
};
|
||||
|
|
@ -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,
|
||||
]),
|
||||
};
|
||||
|
|
@ -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()) + ")";
|
||||
};
|
||||
|
|
@ -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"];
|
||||
|
|
@ -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;
|
||||
}),
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
[DEFAULT]
|
||||
skip-if = os == 'android'
|
||||
support-files=
|
||||
file_invalidEmailCase.sjs
|
||||
|
||||
[test_invalidEmailCase.html]
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -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, "");
|
||||
}
|
||||
|
|
@ -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, "");
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
@ -9,8 +9,5 @@ DIRS += [
|
|||
'crypto',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
|
||||
DIRS += ['fxaccounts']
|
||||
|
||||
if CONFIG['MOZ_SERVICES_SYNC']:
|
||||
DIRS += ['sync']
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue