diff options
Diffstat (limited to 'services/fxaccounts')
36 files changed, 0 insertions, 11799 deletions
diff --git a/services/fxaccounts/Credentials.jsm b/services/fxaccounts/Credentials.jsm deleted file mode 100644 index 56e8b3db7..000000000 --- a/services/fxaccounts/Credentials.jsm +++ /dev/null @@ -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; - } -}); - diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm deleted file mode 100644 index 0e072ee74..000000000 --- a/services/fxaccounts/FxAccounts.jsm +++ /dev/null @@ -1,1725 +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 = ["fxAccounts", "FxAccounts"]; - -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://services-common/utils.js"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Timer.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/FxAccountsStorage.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient", - "resource://gre/modules/FxAccountsClient.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsConfig", - "resource://gre/modules/FxAccountsConfig.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", - "resource://gre/modules/identity/jwcrypto.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthGrantClient", - "resource://gre/modules/FxAccountsOAuthGrantClient.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfile", - "resource://gre/modules/FxAccountsProfile.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Utils", - "resource://services-sync/util.js"); - -// All properties exposed by the public FxAccounts API. -var publicProperties = [ - "accountStatus", - "checkVerificationStatus", - "getAccountsClient", - "getAssertion", - "getDeviceId", - "getKeys", - "getOAuthToken", - "getSignedInUser", - "getSignedInUserProfile", - "handleDeviceDisconnection", - "invalidateCertificate", - "loadAndPoll", - "localtimeOffsetMsec", - "notifyDevices", - "now", - "promiseAccountsChangeProfileURI", - "promiseAccountsForceSigninURI", - "promiseAccountsManageURI", - "promiseAccountsSignUpURI", - "promiseAccountsSignInURI", - "removeCachedOAuthToken", - "requiresHttps", - "resendVerificationEmail", - "resetCredentials", - "sessionStatus", - "setSignedInUser", - "signOut", - "updateDeviceRegistration", - "updateUserAccountData", - "whenVerified", -]; - -// An AccountState object holds all state related to one specific account. -// Only one AccountState is ever "current" in the FxAccountsInternal object - -// whenever a user logs out or logs in, the current AccountState is discarded, -// making it impossible for the wrong state or state data to be accidentally -// used. -// In addition, it has some promise-related helpers to ensure that if an -// attempt is made to resolve a promise on a "stale" state (eg, if an -// operation starts, but a different user logs in before the operation -// completes), the promise will be rejected. -// It is intended to be used thusly: -// somePromiseBasedFunction: function() { -// let currentState = this.currentAccountState; -// return someOtherPromiseFunction().then( -// data => currentState.resolve(data) -// ); -// } -// If the state has changed between the function being called and the promise -// being resolved, the .resolve() call will actually be rejected. -var AccountState = this.AccountState = function(storageManager) { - this.storageManager = storageManager; - this.promiseInitialized = this.storageManager.getAccountData().then(data => { - this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {}; - }).catch(err => { - log.error("Failed to initialize the storage manager", err); - // Things are going to fall apart, but not much we can do about it here. - }); -}; - -AccountState.prototype = { - oauthTokens: null, - whenVerifiedDeferred: null, - whenKeysReadyDeferred: null, - - // If the storage manager has been nuked then we are no longer current. - get isCurrent() { - return this.storageManager != null; - }, - - abort() { - if (this.whenVerifiedDeferred) { - this.whenVerifiedDeferred.reject( - new Error("Verification aborted; Another user signing in")); - this.whenVerifiedDeferred = null; - } - - if (this.whenKeysReadyDeferred) { - this.whenKeysReadyDeferred.reject( - new Error("Verification aborted; Another user signing in")); - this.whenKeysReadyDeferred = null; - } - - this.cert = null; - this.keyPair = null; - this.oauthTokens = null; - // Avoid finalizing the storageManager multiple times (ie, .signOut() - // followed by .abort()) - if (!this.storageManager) { - return Promise.resolve(); - } - let storageManager = this.storageManager; - this.storageManager = null; - return storageManager.finalize(); - }, - - // Clobber all cached data and write that empty data to storage. - signOut() { - this.cert = null; - this.keyPair = null; - this.oauthTokens = null; - let storageManager = this.storageManager; - this.storageManager = null; - return storageManager.deleteAccountData().then(() => { - return storageManager.finalize(); - }); - }, - - // Get user account data. Optionally specify explicit field names to fetch - // (and note that if you require an in-memory field you *must* specify the - // field name(s).) - getUserAccountData(fieldNames = null) { - if (!this.isCurrent) { - return Promise.reject(new Error("Another user has signed in")); - } - return this.storageManager.getAccountData(fieldNames).then(result => { - return this.resolve(result); - }); - }, - - updateUserAccountData(updatedFields) { - if (!this.isCurrent) { - return Promise.reject(new Error("Another user has signed in")); - } - return this.storageManager.updateAccountData(updatedFields); - }, - - resolve: function(result) { - if (!this.isCurrent) { - log.info("An accountState promise was resolved, but was actually rejected" + - " due to a different user being signed in. Originally resolved" + - " with", result); - return Promise.reject(new Error("A different user signed in")); - } - return Promise.resolve(result); - }, - - reject: function(error) { - // It could be argued that we should just let it reject with the original - // error - but this runs the risk of the error being (eg) a 401, which - // might cause the consumer to attempt some remediation and cause other - // problems. - if (!this.isCurrent) { - log.info("An accountState promise was rejected, but we are ignoring that" + - "reason and rejecting it due to a different user being signed in." + - "Originally rejected with", error); - return Promise.reject(new Error("A different user signed in")); - } - return Promise.reject(error); - }, - - // Abstractions for storage of cached tokens - these are all sync, and don't - // handle revocation etc - it's just storage (and the storage itself is async, - // but we don't return the storage promises, so it *looks* sync) - // These functions are sync simply so we can handle "token races" - when there - // are multiple in-flight requests for the same scope, we can detect this - // and revoke the redundant token. - - // A preamble for the cache helpers... - _cachePreamble() { - if (!this.isCurrent) { - throw new Error("Another user has signed in"); - } - }, - - // Set a cached token. |tokenData| must have a 'token' element, but may also - // have additional fields (eg, it probably specifies the server to revoke - // from). The 'get' functions below return the entire |tokenData| value. - setCachedToken(scopeArray, tokenData) { - this._cachePreamble(); - if (!tokenData.token) { - throw new Error("No token"); - } - let key = getScopeKey(scopeArray); - this.oauthTokens[key] = tokenData; - // And a background save... - this._persistCachedTokens(); - }, - - // Return data for a cached token or null (or throws on bad state etc) - getCachedToken(scopeArray) { - this._cachePreamble(); - let key = getScopeKey(scopeArray); - let result = this.oauthTokens[key]; - if (result) { - // later we might want to check an expiry date - but we currently - // have no such concept, so just return it. - log.trace("getCachedToken returning cached token"); - return result; - } - return null; - }, - - // Remove a cached token from the cache. Does *not* revoke it from anywhere. - // Returns the entire token entry if found, null otherwise. - removeCachedToken(token) { - this._cachePreamble(); - let data = this.oauthTokens; - for (let [key, tokenValue] of Object.entries(data)) { - if (tokenValue.token == token) { - delete data[key]; - // And a background save... - this._persistCachedTokens(); - return tokenValue; - } - } - return null; - }, - - // A hook-point for tests. Returns a promise that's ignored in most cases - // (notable exceptions are tests and when we explicitly are saving the entire - // set of user data.) - _persistCachedTokens() { - this._cachePreamble(); - return this.updateUserAccountData({ oauthTokens: this.oauthTokens }).catch(err => { - log.error("Failed to update cached tokens", err); - }); - }, -} - -/* Given an array of scopes, make a string key by normalizing. */ -function getScopeKey(scopeArray) { - let normalizedScopes = scopeArray.map(item => item.toLowerCase()); - return normalizedScopes.sort().join("|"); -} - -/** - * Copies properties from a given object to another object. - * - * @param from (object) - * The object we read property descriptors from. - * @param to (object) - * The object that we set property descriptors on. - * @param options (object) (optional) - * {keys: [...]} - * Lets the caller pass the names of all properties they want to be - * copied. Will copy all properties of the given source object by - * default. - * {bind: object} - * Lets the caller specify the object that will be used to .bind() - * all function properties we find to. Will bind to the given target - * object by default. - */ -function copyObjectProperties(from, to, opts = {}) { - let keys = (opts && opts.keys) || Object.keys(from); - let thisArg = (opts && opts.bind) || to; - - for (let prop of keys) { - let desc = Object.getOwnPropertyDescriptor(from, prop); - - if (typeof(desc.value) == "function") { - desc.value = desc.value.bind(thisArg); - } - - if (desc.get) { - desc.get = desc.get.bind(thisArg); - } - - if (desc.set) { - desc.set = desc.set.bind(thisArg); - } - - Object.defineProperty(to, prop, desc); - } -} - -function urlsafeBase64Encode(key) { - return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false }); -} - -/** - * The public API's constructor. - */ -this.FxAccounts = function (mockInternal) { - let internal = new FxAccountsInternal(); - let external = {}; - - // Copy all public properties to the 'external' object. - let prototype = FxAccountsInternal.prototype; - let options = {keys: publicProperties, bind: internal}; - copyObjectProperties(prototype, external, options); - - // Copy all of the mock's properties to the internal object. - if (mockInternal && !mockInternal.onlySetInternal) { - copyObjectProperties(mockInternal, internal); - } - - if (mockInternal) { - // Exposes the internal object for testing only. - external.internal = internal; - } - - if (!internal.fxaPushService) { - // internal.fxaPushService option is used in testing. - // Otherwise we load the service lazily. - XPCOMUtils.defineLazyGetter(internal, "fxaPushService", function () { - return Components.classes["@mozilla.org/fxaccounts/push;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - }); - } - - // wait until after the mocks are setup before initializing. - internal.initialize(); - - return Object.freeze(external); -} - -/** - * The internal API's constructor. - */ -function FxAccountsInternal() { - // Make a local copy of this constant so we can mock it in testing - this.POLL_SESSION = POLL_SESSION; - - // All significant initialization should be done in the initialize() method - // below as it helps with testing. -} - -/** - * The internal API's prototype. - */ -FxAccountsInternal.prototype = { - // The timeout (in ms) we use to poll for a verified mail for the first 2 mins. - VERIFICATION_POLL_TIMEOUT_INITIAL: 15000, // 15 seconds - // And how often we poll after the first 2 mins. - VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 30000, // 30 seconds. - // The current version of the device registration, we use this to re-register - // devices after we update what we send on device registration. - DEVICE_REGISTRATION_VERSION: 2, - - _fxAccountsClient: null, - - // All significant initialization should be done in this initialize() method, - // as it's called after this object has been mocked for tests. - initialize() { - this.currentTimer = null; - this.currentAccountState = this.newAccountState(); - }, - - get fxAccountsClient() { - if (!this._fxAccountsClient) { - this._fxAccountsClient = new FxAccountsClient(); - } - return this._fxAccountsClient; - }, - - // The profile object used to fetch the actual user profile. - _profile: null, - get profile() { - if (!this._profile) { - let profileServerUrl = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.profile.uri"); - this._profile = new FxAccountsProfile({ - fxa: this, - profileServerUrl: profileServerUrl, - }); - } - return this._profile; - }, - - // A hook-point for tests who may want a mocked AccountState or mocked storage. - newAccountState(credentials) { - let storage = new FxAccountsStorageManager(); - storage.initialize(credentials); - return new AccountState(storage); - }, - - /** - * Send a message to a set of devices in the same account - * - * @return Promise - */ - notifyDevices: function(deviceIds, payload, TTL) { - if (!Array.isArray(deviceIds)) { - deviceIds = [deviceIds]; - } - return this.currentAccountState.getUserAccountData() - .then(data => { - if (!data) { - throw this._error(ERROR_NO_ACCOUNT); - } - if (!data.sessionToken) { - throw this._error(ERROR_AUTH_ERROR, - "notifyDevices called without a session token"); - } - return this.fxAccountsClient.notifyDevices(data.sessionToken, deviceIds, - payload, TTL); - }); - }, - - /** - * Return the current time in milliseconds as an integer. Allows tests to - * manipulate the date to simulate certificate expiration. - */ - now: function() { - return this.fxAccountsClient.now(); - }, - - getAccountsClient: function() { - return this.fxAccountsClient; - }, - - /** - * Return clock offset in milliseconds, as reported by the fxAccountsClient. - * This can be overridden for testing. - * - * 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.fxAccountsClient.localtimeOffsetMsec; - }, - - /** - * Ask the server whether the user's email has been verified - */ - checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) { - if (!sessionToken) { - return Promise.reject(new Error( - "checkEmailStatus called without a session token")); - } - return this.fxAccountsClient.recoveryEmailStatus(sessionToken, - options).catch(error => this._handleTokenError(error)); - }, - - /** - * Once the user's email is verified, we can request the keys - */ - fetchKeys: function fetchKeys(keyFetchToken) { - log.debug("fetchKeys: " + !!keyFetchToken); - if (logPII) { - log.debug("fetchKeys - the token is " + keyFetchToken); - } - return this.fxAccountsClient.accountKeys(keyFetchToken); - }, - - // set() makes sure that polling is happening, if necessary. - // get() does not wait for verification, and returns an object even if - // unverified. The caller of get() must check .verified . - // The "fxaccounts:onverified" event will fire only when the verified - // state goes from false to true, so callers must register their observer - // and then call get(). In particular, it will not fire when the account - // was found to be verified in a previous boot: if our stored state says - // the account is verified, the event will never fire. So callers must do: - // register notification observer (go) - // userdata = get() - // if (userdata.verified()) {go()} - - /** - * Get the user currently signed in to Firefox Accounts. - * - * @return Promise - * The promise resolves to the credentials object of the signed-in user: - * { - * email: The user's email address - * uid: The user's unique id - * sessionToken: Session for the FxA server - * kA: An encryption key from the FxA server - * kB: An encryption key derived from the user's FxA password - * verified: email verification status - * authAt: The time (seconds since epoch) that this record was - * authenticated - * } - * or null if no user is signed in. - */ - getSignedInUser: function getSignedInUser() { - let currentState = this.currentAccountState; - return currentState.getUserAccountData().then(data => { - if (!data) { - return null; - } - if (!this.isUserEmailVerified(data)) { - // If the email is not verified, start polling for verification, - // but return null right away. We don't want to return a promise - // that might not be fulfilled for a long time. - this.startVerifiedCheck(data); - } - return data; - }).then(result => currentState.resolve(result)); - }, - - /** - * Set the current user signed in to Firefox Accounts. - * - * @param credentials - * The credentials object obtained by logging in or creating - * an account on the FxA server: - * { - * authAt: The time (seconds since epoch) that this record was - * authenticated - * email: The users email address - * keyFetchToken: a keyFetchToken which has not yet been used - * sessionToken: Session for the FxA server - * uid: The user's unique id - * unwrapBKey: used to unwrap kB, derived locally from the - * password (not revealed to the FxA server) - * verified: true/false - * } - * @return Promise - * The promise resolves to null when the data is saved - * successfully and is rejected on error. - */ - setSignedInUser: function setSignedInUser(credentials) { - log.debug("setSignedInUser - aborting any existing flows"); - return this.abortExistingFlow().then(() => { - let currentAccountState = this.currentAccountState = this.newAccountState( - Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object. - ); - // This promise waits for storage, but not for verification. - // We're telling the caller that this is durable now (although is that - // really something we should commit to? Why not let the write happen in - // the background? Already does for updateAccountData ;) - return currentAccountState.promiseInitialized.then(() => { - // Starting point for polling if new user - if (!this.isUserEmailVerified(credentials)) { - this.startVerifiedCheck(credentials); - } - - return this.updateDeviceRegistration(); - }).then(() => { - Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1); - this.notifyObservers(ONLOGIN_NOTIFICATION); - }).then(() => { - return currentAccountState.resolve(); - }); - }) - }, - - /** - * Update account data for the currently signed in user. - * - * @param credentials - * The credentials object containing the fields to be updated. - * This object must contain |email| and |uid| fields and they must - * match the currently signed in user. - */ - updateUserAccountData(credentials) { - log.debug("updateUserAccountData called with fields", Object.keys(credentials)); - if (logPII) { - log.debug("updateUserAccountData called with data", credentials); - } - let currentAccountState = this.currentAccountState; - return currentAccountState.promiseInitialized.then(() => { - return currentAccountState.getUserAccountData(["email", "uid"]); - }).then(existing => { - if (existing.email != credentials.email || existing.uid != credentials.uid) { - throw new Error("The specified credentials aren't for the current user"); - } - // We need to nuke email and uid as storage will complain if we try and - // update them (even when the value is the same) - credentials = Cu.cloneInto(credentials, {}); // clone it first - delete credentials.email; - delete credentials.uid; - return currentAccountState.updateUserAccountData(credentials); - }); - }, - - /** - * returns a promise that fires with the assertion. If there is no verified - * signed-in user, fires with null. - */ - getAssertion: function getAssertion(audience) { - return this._getAssertion(audience); - }, - - // getAssertion() is "public" so screws with our mock story. This - // implementation method *can* be (and is) mocked by tests. - _getAssertion: function _getAssertion(audience) { - log.debug("enter getAssertion()"); - let currentState = this.currentAccountState; - return currentState.getUserAccountData().then(data => { - if (!data) { - // No signed-in user - return null; - } - if (!this.isUserEmailVerified(data)) { - // Signed-in user has not verified email - return null; - } - if (!data.sessionToken) { - // can't get a signed certificate without a session token. This - // can happen if we request an assertion after clearing an invalid - // session token from storage. - throw this._error(ERROR_AUTH_ERROR, "getAssertion called without a session token"); - } - return this.getKeypairAndCertificate(currentState).then( - ({keyPair, certificate}) => { - return this.getAssertionFromCert(data, keyPair, certificate, audience); - } - ); - }).catch(err => - this._handleTokenError(err) - ).then(result => currentState.resolve(result)); - }, - - /** - * Invalidate the FxA certificate, so that it will be refreshed from the server - * the next time it is needed. - */ - invalidateCertificate() { - return this.currentAccountState.updateUserAccountData({ cert: null }); - }, - - getDeviceId() { - return this.currentAccountState.getUserAccountData() - .then(data => { - if (data) { - if (!data.deviceId || !data.deviceRegistrationVersion || - data.deviceRegistrationVersion < this.DEVICE_REGISTRATION_VERSION) { - // There is no device id or the device registration is outdated. - // Either way, we should register the device with FxA - // before returning the id to the caller. - return this._registerOrUpdateDevice(data); - } - - // Return the device id that we already registered with the server. - return data.deviceId; - } - - // Without a signed-in user, there can be no device id. - return null; - }); - }, - - /** - * Resend the verification email fot the currently signed-in user. - * - */ - resendVerificationEmail: function resendVerificationEmail() { - let currentState = this.currentAccountState; - return this.getSignedInUser().then(data => { - // If the caller is asking for verification to be re-sent, and there is - // no signed-in user to begin with, this is probably best regarded as an - // error. - if (data) { - if (!data.sessionToken) { - return Promise.reject(new Error( - "resendVerificationEmail called without a session token")); - } - this.pollEmailStatus(currentState, data.sessionToken, "start"); - return this.fxAccountsClient.resendVerificationEmail( - data.sessionToken).catch(err => this._handleTokenError(err)); - } - throw new Error("Cannot resend verification email; no signed-in user"); - }); - }, - - /* - * Reset state such that any previous flow is canceled. - */ - abortExistingFlow: function abortExistingFlow() { - if (this.currentTimer) { - log.debug("Polling aborted; Another user signing in"); - clearTimeout(this.currentTimer); - this.currentTimer = 0; - } - if (this._profile) { - this._profile.tearDown(); - this._profile = null; - } - // We "abort" the accountState and assume our caller is about to throw it - // away and replace it with a new one. - return this.currentAccountState.abort(); - }, - - accountStatus: function accountStatus() { - return this.currentAccountState.getUserAccountData().then(data => { - if (!data) { - return false; - } - return this.fxAccountsClient.accountStatus(data.uid); - }); - }, - - checkVerificationStatus: function() { - log.trace('checkVerificationStatus'); - let currentState = this.currentAccountState; - return currentState.getUserAccountData().then(data => { - if (!data) { - log.trace("checkVerificationStatus - no user data"); - return null; - } - - // Always check the verification status, even if the local state indicates - // we're already verified. If the user changed their password, the check - // will fail, and we'll enter the reauth state. - log.trace("checkVerificationStatus - forcing verification status check"); - return this.pollEmailStatus(currentState, data.sessionToken, "push"); - }); - }, - - _destroyOAuthToken: function(tokenData) { - let client = new FxAccountsOAuthGrantClient({ - serverURL: tokenData.server, - client_id: FX_OAUTH_CLIENT_ID - }); - return client.destroyToken(tokenData.token) - }, - - _destroyAllOAuthTokens: function(tokenInfos) { - // let's just destroy them all in parallel... - let promises = []; - for (let [key, tokenInfo] of Object.entries(tokenInfos || {})) { - promises.push(this._destroyOAuthToken(tokenInfo)); - } - return Promise.all(promises); - }, - - signOut: function signOut(localOnly) { - let currentState = this.currentAccountState; - let sessionToken; - let tokensToRevoke; - let deviceId; - return currentState.getUserAccountData().then(data => { - // Save the session token, tokens to revoke and the - // device id for use in the call to signOut below. - if (data) { - sessionToken = data.sessionToken; - tokensToRevoke = data.oauthTokens; - deviceId = data.deviceId; - } - return this._signOutLocal(); - }).then(() => { - // FxAccountsManager calls here, then does its own call - // to FxAccountsClient.signOut(). - if (!localOnly) { - // Wrap this in a promise so *any* errors in signOut won't - // block the local sign out. This is *not* returned. - Promise.resolve().then(() => { - // This can happen in the background and shouldn't block - // the user from signing out. The server must tolerate - // clients just disappearing, so this call should be best effort. - if (sessionToken) { - return this._signOutServer(sessionToken, deviceId); - } - log.warn("Missing session token; skipping remote sign out"); - }).catch(err => { - log.error("Error during remote sign out of Firefox Accounts", err); - }).then(() => { - return this._destroyAllOAuthTokens(tokensToRevoke); - }).catch(err => { - log.error("Error during destruction of oauth tokens during signout", err); - }).then(() => { - FxAccountsConfig.resetConfigURLs(); - // just for testing - notifications are cheap when no observers. - this.notifyObservers("testhelper-fxa-signout-complete"); - }) - } else { - // We want to do this either way -- but if we're signing out remotely we - // need to wait until we destroy the oauth tokens if we want that to succeed. - FxAccountsConfig.resetConfigURLs(); - } - }).then(() => { - this.notifyObservers(ONLOGOUT_NOTIFICATION); - }); - }, - - /** - * This function should be called in conjunction with a server-side - * signOut via FxAccountsClient. - */ - _signOutLocal: function signOutLocal() { - let currentAccountState = this.currentAccountState; - return currentAccountState.signOut().then(() => { - // this "aborts" this.currentAccountState but doesn't make a new one. - return this.abortExistingFlow(); - }).then(() => { - this.currentAccountState = this.newAccountState(); - return this.currentAccountState.promiseInitialized; - }); - }, - - _signOutServer(sessionToken, deviceId) { - // For now we assume the service being logged out from is Sync, so - // we must tell the server to either destroy the device or sign out - // (if no device exists). We might need to revisit this when this - // FxA code is used in a context that isn't Sync. - - const options = { service: "sync" }; - - if (deviceId) { - log.debug("destroying device and session"); - return this.fxAccountsClient.signOutAndDestroyDevice(sessionToken, deviceId, options); - } - - log.debug("destroying session"); - return this.fxAccountsClient.signOut(sessionToken, options); - }, - - /** - * Check the status of the current session using cached credentials. - * - * @return Promise - * Resolves with a boolean indicating if the session is still valid - */ - sessionStatus() { - return this.getSignedInUser().then(data => { - if (!data.sessionToken) { - return Promise.reject(new Error( - "sessionStatus called without a session token")); - } - return this.fxAccountsClient.sessionStatus(data.sessionToken); - }); - }, - - /** - * Fetch encryption keys for the signed-in-user from the FxA API server. - * - * Not for user consumption. Exists to cause the keys to be fetch. - * - * Returns user data so that it can be chained with other methods. - * - * @return Promise - * The promise resolves to the credentials object of the signed-in user: - * { - * email: The user's email address - * uid: The user's unique id - * sessionToken: Session for the FxA server - * kA: An encryption key from the FxA server - * kB: An encryption key derived from the user's FxA password - * verified: email verification status - * } - * or null if no user is signed in - */ - getKeys: function() { - let currentState = this.currentAccountState; - return currentState.getUserAccountData().then((userData) => { - if (!userData) { - throw new Error("Can't get keys; User is not signed in"); - } - if (userData.kA && userData.kB) { - return userData; - } - if (!currentState.whenKeysReadyDeferred) { - currentState.whenKeysReadyDeferred = Promise.defer(); - if (userData.keyFetchToken) { - this.fetchAndUnwrapKeys(userData.keyFetchToken).then( - (dataWithKeys) => { - if (!dataWithKeys.kA || !dataWithKeys.kB) { - currentState.whenKeysReadyDeferred.reject( - new Error("user data missing kA or kB") - ); - return; - } - currentState.whenKeysReadyDeferred.resolve(dataWithKeys); - }, - (err) => { - currentState.whenKeysReadyDeferred.reject(err); - } - ); - } else { - currentState.whenKeysReadyDeferred.reject('No keyFetchToken'); - } - } - return currentState.whenKeysReadyDeferred.promise; - }).catch(err => - this._handleTokenError(err) - ).then(result => currentState.resolve(result)); - }, - - fetchAndUnwrapKeys: function(keyFetchToken) { - if (logPII) { - log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken); - } - let currentState = this.currentAccountState; - return Task.spawn(function* task() { - // Sign out if we don't have a key fetch token. - if (!keyFetchToken) { - log.warn("improper fetchAndUnwrapKeys() call: token missing"); - yield this.signOut(); - return null; - } - - let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken); - - let data = yield currentState.getUserAccountData(); - - // Sanity check that the user hasn't changed out from under us - if (data.keyFetchToken !== keyFetchToken) { - throw new Error("Signed in user changed while fetching keys!"); - } - - // Next statements must be synchronous until we setUserAccountData - // so that we don't risk getting into a weird state. - let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey), - wrapKB); - - if (logPII) { - log.debug("kB_hex: " + kB_hex); - } - let updateData = { - kA: CommonUtils.bytesAsHex(kA), - kB: CommonUtils.bytesAsHex(kB_hex), - keyFetchToken: null, // null values cause the item to be removed. - unwrapBKey: null, - } - - log.debug("Keys Obtained: kA=" + !!updateData.kA + ", kB=" + !!updateData.kB); - if (logPII) { - log.debug("Keys Obtained: kA=" + updateData.kA + ", kB=" + updateData.kB); - } - - yield currentState.updateUserAccountData(updateData); - // We are now ready for business. This should only be invoked once - // per setSignedInUser(), regardless of whether we've rebooted since - // setSignedInUser() was called. - this.notifyObservers(ONVERIFIED_NOTIFICATION); - return currentState.getUserAccountData(); - }.bind(this)).then(result => currentState.resolve(result)); - }, - - getAssertionFromCert: function(data, keyPair, cert, audience) { - log.debug("getAssertionFromCert"); - let payload = {}; - let d = Promise.defer(); - let options = { - duration: ASSERTION_LIFETIME, - localtimeOffsetMsec: this.localtimeOffsetMsec, - now: this.now() - }; - let currentState = this.currentAccountState; - // "audience" should look like "http://123done.org". - // The generated assertion will expire in two minutes. - jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => { - if (err) { - log.error("getAssertionFromCert: " + err); - d.reject(err); - } else { - log.debug("getAssertionFromCert returning signed: " + !!signed); - if (logPII) { - log.debug("getAssertionFromCert returning signed: " + signed); - } - d.resolve(signed); - } - }); - return d.promise.then(result => currentState.resolve(result)); - }, - - getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) { - log.debug("getCertificateSigned: " + !!sessionToken + " " + !!serializedPublicKey); - if (logPII) { - log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey); - } - return this.fxAccountsClient.signCertificate( - sessionToken, - JSON.parse(serializedPublicKey), - lifetime - ); - }, - - /** - * returns a promise that fires with {keyPair, certificate}. - */ - getKeypairAndCertificate: Task.async(function* (currentState) { - // If the debugging pref to ignore cached authentication credentials is set for Sync, - // then don't use any cached key pair/certificate, i.e., generate a new - // one and get it signed. - // The purpose of this pref is to expedite any auth errors as the result of a - // expired or revoked FxA session token, e.g., from resetting or changing the FxA - // password. - let ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials", false); - let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD; - let accountData = yield currentState.getUserAccountData(["cert", "keyPair", "sessionToken"]); - - let keyPairValid = !ignoreCachedAuthCredentials && - accountData.keyPair && - (accountData.keyPair.validUntil > mustBeValidUntil); - let certValid = !ignoreCachedAuthCredentials && - accountData.cert && - (accountData.cert.validUntil > mustBeValidUntil); - // TODO: get the lifetime from the cert's .exp field - if (keyPairValid && certValid) { - log.debug("getKeypairAndCertificate: already have keyPair and certificate"); - return { - keyPair: accountData.keyPair.rawKeyPair, - certificate: accountData.cert.rawCert - } - } - // We are definately going to generate a new cert, either because it has - // already expired, or the keyPair has - and a new keyPair means we must - // generate a new cert. - - // A keyPair has a longer lifetime than a cert, so it's possible we will - // have a valid keypair but an expired cert, which means we can skip - // keypair generation. - // Either way, the cert will require hitting the network, so bail now if - // we know that's going to fail. - if (Services.io.offline) { - throw new Error(ERROR_OFFLINE); - } - - let keyPair; - if (keyPairValid) { - keyPair = accountData.keyPair; - } else { - let keyWillBeValidUntil = this.now() + KEY_LIFETIME; - keyPair = yield new Promise((resolve, reject) => { - jwcrypto.generateKeyPair("DS160", (err, kp) => { - if (err) { - return reject(err); - } - log.debug("got keyPair"); - resolve({ - rawKeyPair: kp, - validUntil: keyWillBeValidUntil, - }); - }); - }); - } - - // and generate the cert. - let certWillBeValidUntil = this.now() + CERT_LIFETIME; - let certificate = yield this.getCertificateSigned(accountData.sessionToken, - keyPair.rawKeyPair.serializedPublicKey, - CERT_LIFETIME); - log.debug("getCertificate got a new one: " + !!certificate); - if (certificate) { - // Cache both keypair and cert. - let toUpdate = { - keyPair, - cert: { - rawCert: certificate, - validUntil: certWillBeValidUntil, - }, - }; - yield currentState.updateUserAccountData(toUpdate); - } - return { - keyPair: keyPair.rawKeyPair, - certificate: certificate, - } - }), - - getUserAccountData: function() { - return this.currentAccountState.getUserAccountData(); - }, - - isUserEmailVerified: function isUserEmailVerified(data) { - return !!(data && data.verified); - }, - - /** - * Setup for and if necessary do email verification polling. - */ - loadAndPoll: function() { - let currentState = this.currentAccountState; - return currentState.getUserAccountData() - .then(data => { - if (data) { - Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1); - if (!this.isUserEmailVerified(data)) { - this.pollEmailStatus(currentState, data.sessionToken, "start"); - } - } - return data; - }); - }, - - startVerifiedCheck: function(data) { - log.debug("startVerifiedCheck", data && data.verified); - if (logPII) { - log.debug("startVerifiedCheck with user data", data); - } - - // Get us to the verified state, then get the keys. This returns a promise - // that will fire when we are completely ready. - // - // Login is truly complete once keys have been fetched, so once getKeys() - // obtains and stores kA and kB, it will fire the onverified observer - // notification. - - // The callers of startVerifiedCheck never consume a returned promise (ie, - // this is simply kicking off a background fetch) so we must add a rejection - // handler to avoid runtime warnings about the rejection not being handled. - this.whenVerified(data).then( - () => this.getKeys(), - err => log.info("startVerifiedCheck promise was rejected: " + err) - ); - }, - - whenVerified: function(data) { - let currentState = this.currentAccountState; - if (data.verified) { - log.debug("already verified"); - return currentState.resolve(data); - } - if (!currentState.whenVerifiedDeferred) { - log.debug("whenVerified promise starts polling for verified email"); - this.pollEmailStatus(currentState, data.sessionToken, "start"); - } - return currentState.whenVerifiedDeferred.promise.then( - result => currentState.resolve(result) - ); - }, - - notifyObservers: function(topic, data) { - log.debug("Notifying observers of " + topic); - Services.obs.notifyObservers(null, topic, data); - }, - - // XXX - pollEmailStatus should maybe be on the AccountState object? - pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) { - log.debug("entering pollEmailStatus: " + why); - if (why == "start" || why == "push") { - if (this.currentTimer) { - log.debug("pollEmailStatus starting while existing timer is running"); - clearTimeout(this.currentTimer); - this.currentTimer = null; - } - - // If we were already polling, stop and start again. This could happen - // if the user requested the verification email to be resent while we - // were already polling for receipt of an earlier email. - this.pollStartDate = Date.now(); - if (!currentState.whenVerifiedDeferred) { - currentState.whenVerifiedDeferred = Promise.defer(); - // This deferred might not end up with any handlers (eg, if sync - // is yet to start up.) This might cause "A promise chain failed to - // handle a rejection" messages, so add an error handler directly - // on the promise to log the error. - currentState.whenVerifiedDeferred.promise.then(null, err => { - log.info("the wait for user verification was stopped: " + err); - }); - } - } - - // We return a promise for testing only. Other callers can ignore this, - // since verification polling continues in the background. - return this.checkEmailStatus(sessionToken, { reason: why }) - .then((response) => { - log.debug("checkEmailStatus -> " + JSON.stringify(response)); - if (response && response.verified) { - currentState.updateUserAccountData({ verified: true }) - .then(() => { - return currentState.getUserAccountData(); - }) - .then(data => { - // Now that the user is verified, we can proceed to fetch keys - if (currentState.whenVerifiedDeferred) { - currentState.whenVerifiedDeferred.resolve(data); - delete currentState.whenVerifiedDeferred; - } - // Tell FxAccountsManager to clear its cache - this.notifyObservers(ON_FXA_UPDATE_NOTIFICATION, ONVERIFIED_NOTIFICATION); - }); - } else { - // Poll email status again after a short delay. - this.pollEmailStatusAgain(currentState, sessionToken); - } - }, error => { - let timeoutMs = undefined; - if (error && error.retryAfter) { - // If the server told us to back off, back off the requested amount. - timeoutMs = (error.retryAfter + 3) * 1000; - } - // The server will return 401 if a request parameter is erroneous or - // if the session token expired. Let's continue polling otherwise. - if (!error || !error.code || error.code != 401) { - this.pollEmailStatusAgain(currentState, sessionToken, timeoutMs); - } else { - let error = new Error("Verification status check failed"); - this._rejectWhenVerified(currentState, error); - } - }); - }, - - _rejectWhenVerified(currentState, error) { - currentState.whenVerifiedDeferred.reject(error); - delete currentState.whenVerifiedDeferred; - }, - - // Poll email status using truncated exponential back-off. - pollEmailStatusAgain: function (currentState, sessionToken, timeoutMs) { - let ageMs = Date.now() - this.pollStartDate; - if (ageMs >= this.POLL_SESSION) { - if (currentState.whenVerifiedDeferred) { - let error = new Error("User email verification timed out."); - this._rejectWhenVerified(currentState, error); - } - log.debug("polling session exceeded, giving up"); - return; - } - if (timeoutMs === undefined) { - let currentMinute = Math.ceil(ageMs / 60000); - timeoutMs = currentMinute <= 2 ? this.VERIFICATION_POLL_TIMEOUT_INITIAL - : this.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT; - } - log.debug("polling with timeout = " + timeoutMs); - this.currentTimer = setTimeout(() => { - this.pollEmailStatus(currentState, sessionToken, "timer"); - }, timeoutMs); - }, - - requiresHttps: function() { - let allowHttp = Services.prefs.getBoolPref("identity.fxaccounts.allowHttp", false); - return allowHttp !== true; - }, - - promiseAccountsSignUpURI() { - return FxAccountsConfig.promiseAccountsSignUpURI(); - }, - - promiseAccountsSignInURI() { - return FxAccountsConfig.promiseAccountsSignInURI(); - }, - - // Returns a promise that resolves with the URL to use to force a re-signin - // of the current account. - promiseAccountsForceSigninURI: Task.async(function *() { - yield FxAccountsConfig.ensureConfigured(); - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri"); - if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting - throw new Error("Firefox Accounts server must use HTTPS"); - } - let currentState = this.currentAccountState; - // but we need to append the email address onto a query string. - return this.getSignedInUser().then(accountData => { - if (!accountData) { - return null; - } - let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; - newQueryPortion += "email=" + encodeURIComponent(accountData.email); - return url + newQueryPortion; - }).then(result => currentState.resolve(result)); - }), - - // Returns a promise that resolves with the URL to use to change - // the current account's profile image. - // if settingToEdit is set, the profile page should hightlight that setting - // for the user to edit. - promiseAccountsChangeProfileURI: function(entrypoint, settingToEdit = null) { - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri"); - - if (settingToEdit) { - url += (url.indexOf("?") == -1 ? "?" : "&") + - "setting=" + encodeURIComponent(settingToEdit); - } - - if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting - throw new Error("Firefox Accounts server must use HTTPS"); - } - let currentState = this.currentAccountState; - // but we need to append the email address onto a query string. - return this.getSignedInUser().then(accountData => { - if (!accountData) { - return null; - } - let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; - newQueryPortion += "email=" + encodeURIComponent(accountData.email); - newQueryPortion += "&uid=" + encodeURIComponent(accountData.uid); - if (entrypoint) { - newQueryPortion += "&entrypoint=" + encodeURIComponent(entrypoint); - } - return url + newQueryPortion; - }).then(result => currentState.resolve(result)); - }, - - // Returns a promise that resolves with the URL to use to manage the current - // user's FxA acct. - promiseAccountsManageURI: function(entrypoint) { - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri"); - if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting - throw new Error("Firefox Accounts server must use HTTPS"); - } - let currentState = this.currentAccountState; - // but we need to append the uid and email address onto a query string - // (if the server has no matching uid it will offer to sign in with the - // email address) - return this.getSignedInUser().then(accountData => { - if (!accountData) { - return null; - } - let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; - newQueryPortion += "uid=" + encodeURIComponent(accountData.uid) + - "&email=" + encodeURIComponent(accountData.email); - if (entrypoint) { - newQueryPortion += "&entrypoint=" + encodeURIComponent(entrypoint); - } - return url + newQueryPortion; - }).then(result => currentState.resolve(result)); - }, - - /** - * Get an OAuth token for the user - * - * @param options - * { - * scope: (string/array) the oauth scope(s) being requested. As a - * convenience, you may pass a string if only one scope is - * required, or an array of strings if multiple are needed. - * } - * - * @return Promise.<string | Error> - * The promise resolves the oauth token as a string or rejects with - * an error object ({error: ERROR, details: {}}) of the following: - * INVALID_PARAMETER - * NO_ACCOUNT - * UNVERIFIED_ACCOUNT - * NETWORK_ERROR - * AUTH_ERROR - * UNKNOWN_ERROR - */ - getOAuthToken: Task.async(function* (options = {}) { - log.debug("getOAuthToken enter"); - let scope = options.scope; - if (typeof scope === "string") { - scope = [scope]; - } - - if (!scope || !scope.length) { - throw this._error(ERROR_INVALID_PARAMETER, "Missing or invalid 'scope' option"); - } - - yield this._getVerifiedAccountOrReject(); - - // Early exit for a cached token. - let currentState = this.currentAccountState; - let cached = currentState.getCachedToken(scope); - if (cached) { - log.debug("getOAuthToken returning a cached token"); - return cached.token; - } - - // We are going to hit the server - this is the string we pass to it. - let scopeString = scope.join(" "); - let client = options.client; - - if (!client) { - try { - let defaultURL = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.oauth.uri"); - client = new FxAccountsOAuthGrantClient({ - serverURL: defaultURL, - client_id: FX_OAUTH_CLIENT_ID - }); - } catch (e) { - throw this._error(ERROR_INVALID_PARAMETER, e); - } - } - let oAuthURL = client.serverURL.href; - - try { - log.debug("getOAuthToken fetching new token from", oAuthURL); - let assertion = yield this.getAssertion(oAuthURL); - let result = yield client.getTokenFromAssertion(assertion, scopeString); - let token = result.access_token; - // If we got one, cache it. - if (token) { - let entry = {token: token, server: oAuthURL}; - // But before we do, check the cache again - if we find one now, it - // means someone else concurrently requested the same scope and beat - // us to the cache write. To be nice to the server, we revoke the one - // we just got and return the newly cached value. - let cached = currentState.getCachedToken(scope); - if (cached) { - log.debug("Detected a race for this token - revoking the new one."); - this._destroyOAuthToken(entry); - return cached.token; - } - currentState.setCachedToken(scope, entry); - } - return token; - } catch (err) { - throw this._errorToErrorClass(err); - } - }), - - /** - * Remove an OAuth token from the token cache. Callers should call this - * after they determine a token is invalid, so a new token will be fetched - * on the next call to getOAuthToken(). - * - * @param options - * { - * token: (string) A previously fetched token. - * } - * @return Promise.<undefined> This function will always resolve, even if - * an unknown token is passed. - */ - removeCachedOAuthToken: Task.async(function* (options) { - if (!options.token || typeof options.token !== "string") { - throw this._error(ERROR_INVALID_PARAMETER, "Missing or invalid 'token' option"); - } - let currentState = this.currentAccountState; - let existing = currentState.removeCachedToken(options.token); - if (existing) { - // background destroy. - this._destroyOAuthToken(existing).catch(err => { - log.warn("FxA failed to revoke a cached token", err); - }); - } - }), - - _getVerifiedAccountOrReject: Task.async(function* () { - let data = yield this.currentAccountState.getUserAccountData(); - if (!data) { - // No signed-in user - throw this._error(ERROR_NO_ACCOUNT); - } - if (!this.isUserEmailVerified(data)) { - // Signed-in user has not verified email - throw this._error(ERROR_UNVERIFIED_ACCOUNT); - } - }), - - /* - * Coerce an error into one of the general error cases: - * NETWORK_ERROR - * AUTH_ERROR - * UNKNOWN_ERROR - * - * These errors will pass through: - * INVALID_PARAMETER - * NO_ACCOUNT - * UNVERIFIED_ACCOUNT - */ - _errorToErrorClass: function (aError) { - if (aError.errno) { - let error = SERVER_ERRNO_TO_ERROR[aError.errno]; - return this._error(ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN, aError); - } else if (aError.message && - (aError.message === "INVALID_PARAMETER" || - aError.message === "NO_ACCOUNT" || - aError.message === "UNVERIFIED_ACCOUNT" || - aError.message === "AUTH_ERROR")) { - return aError; - } - return this._error(ERROR_UNKNOWN, aError); - }, - - _error: function(aError, aDetails) { - log.error("FxA rejecting with error ${aError}, details: ${aDetails}", {aError, aDetails}); - let reason = new Error(aError); - if (aDetails) { - reason.details = aDetails; - } - return reason; - }, - - /** - * Get the user's account and profile data - * - * @param options - * { - * contentUrl: (string) Used by the FxAccountsWebChannel. - * Defaults to pref identity.fxaccounts.settings.uri - * profileServerUrl: (string) Used by the FxAccountsWebChannel. - * Defaults to pref identity.fxaccounts.remote.profile.uri - * } - * - * @return Promise.<object | Error> - * The promise resolves to an accountData object with extra profile - * information such as profileImageUrl, or rejects with - * an error object ({error: ERROR, details: {}}) of the following: - * INVALID_PARAMETER - * NO_ACCOUNT - * UNVERIFIED_ACCOUNT - * NETWORK_ERROR - * AUTH_ERROR - * UNKNOWN_ERROR - */ - getSignedInUserProfile: function () { - let currentState = this.currentAccountState; - return this.profile.getProfile().then( - profileData => { - let profile = Cu.cloneInto(profileData, {}); - return currentState.resolve(profile); - }, - error => { - log.error("Could not retrieve profile data", error); - return currentState.reject(error); - } - ).catch(err => Promise.reject(this._errorToErrorClass(err))); - }, - - // Attempt to update the auth server with whatever device details are stored - // in the account data. Returns a promise that always resolves, never rejects. - // If the promise resolves to a value, that value is the device id. - updateDeviceRegistration() { - return this.getSignedInUser().then(signedInUser => { - if (signedInUser) { - return this._registerOrUpdateDevice(signedInUser); - } - }).catch(error => this._logErrorAndResetDeviceRegistrationVersion(error)); - }, - - handleDeviceDisconnection(deviceId) { - return this.currentAccountState.getUserAccountData() - .then(data => data ? data.deviceId : null) - .then(localDeviceId => { - if (deviceId == localDeviceId) { - this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, deviceId); - return this.signOut(true); - } - log.error( - "The device ID to disconnect doesn't match with the local device ID.\n" - + "Local: " + localDeviceId + ", ID to disconnect: " + deviceId); - }); - }, - - /** - * Delete all the cached persisted credentials we store for FxA. - * - * @return Promise resolves when the user data has been persisted - */ - resetCredentials() { - // Delete all fields except those required for the user to - // reauthenticate. - let updateData = {}; - let clearField = field => { - if (!FXA_PWDMGR_REAUTH_WHITELIST.has(field)) { - updateData[field] = null; - } - } - FXA_PWDMGR_PLAINTEXT_FIELDS.forEach(clearField); - FXA_PWDMGR_SECURE_FIELDS.forEach(clearField); - FXA_PWDMGR_MEMORY_FIELDS.forEach(clearField); - - let currentState = this.currentAccountState; - return currentState.updateUserAccountData(updateData); - }, - - // If you change what we send to the FxA servers during device registration, - // you'll have to bump the DEVICE_REGISTRATION_VERSION number to force older - // devices to re-register when Firefox updates - _registerOrUpdateDevice(signedInUser) { - try { - // Allow tests to skip device registration because: - // 1. It makes remote requests to the auth server. - // 2. _getDeviceName does not work from xpcshell. - // 3. The B2G tests fail when attempting to import services-sync/util.js. - if (Services.prefs.getBoolPref("identity.fxaccounts.skipDeviceRegistration")) { - return Promise.resolve(); - } - } catch(ignore) {} - - if (!signedInUser.sessionToken) { - return Promise.reject(new Error( - "_registerOrUpdateDevice called without a session token")); - } - - return this.fxaPushService.registerPushEndpoint().then(subscription => { - const deviceName = this._getDeviceName(); - let deviceOptions = {}; - - // if we were able to obtain a subscription - if (subscription && subscription.endpoint) { - deviceOptions.pushCallback = subscription.endpoint; - let publicKey = subscription.getKey('p256dh'); - let authKey = subscription.getKey('auth'); - if (publicKey && authKey) { - deviceOptions.pushPublicKey = urlsafeBase64Encode(publicKey); - deviceOptions.pushAuthKey = urlsafeBase64Encode(authKey); - } - } - - if (signedInUser.deviceId) { - log.debug("updating existing device details"); - return this.fxAccountsClient.updateDevice( - signedInUser.sessionToken, signedInUser.deviceId, deviceName, deviceOptions); - } - - log.debug("registering new device details"); - return this.fxAccountsClient.registerDevice( - signedInUser.sessionToken, deviceName, this._getDeviceType(), deviceOptions); - }).then(device => - this.currentAccountState.updateUserAccountData({ - deviceId: device.id, - deviceRegistrationVersion: this.DEVICE_REGISTRATION_VERSION - }).then(() => device.id) - ).catch(error => this._handleDeviceError(error, signedInUser.sessionToken)); - }, - - _getDeviceName() { - return Utils.getDeviceName(); - }, - - _getDeviceType() { - return Utils.getDeviceType(); - }, - - _handleDeviceError(error, sessionToken) { - return Promise.resolve().then(() => { - if (error.code === 400) { - if (error.errno === ERRNO_UNKNOWN_DEVICE) { - return this._recoverFromUnknownDevice(); - } - - if (error.errno === ERRNO_DEVICE_SESSION_CONFLICT) { - return this._recoverFromDeviceSessionConflict(error, sessionToken); - } - } - - // `_handleTokenError` re-throws the error. - return this._handleTokenError(error); - }).catch(error => - this._logErrorAndResetDeviceRegistrationVersion(error) - ).catch(() => {}); - }, - - _recoverFromUnknownDevice() { - // FxA did not recognise the device id. Handle it by clearing the device - // id on the account data. At next sync or next sign-in, registration is - // retried and should succeed. - log.warn("unknown device id, clearing the local device data"); - return this.currentAccountState.updateUserAccountData({ deviceId: null }) - .catch(error => this._logErrorAndResetDeviceRegistrationVersion(error)); - }, - - _recoverFromDeviceSessionConflict(error, sessionToken) { - // FxA has already associated this session with a different device id. - // Perhaps we were beaten in a race to register. Handle the conflict: - // 1. Fetch the list of devices for the current user from FxA. - // 2. Look for ourselves in the list. - // 3. If we find a match, set the correct device id and device registration - // version on the account data and return the correct device id. At next - // sync or next sign-in, registration is retried and should succeed. - // 4. If we don't find a match, log the original error. - log.warn("device session conflict, attempting to ascertain the correct device id"); - return this.fxAccountsClient.getDeviceList(sessionToken) - .then(devices => { - const matchingDevices = devices.filter(device => device.isCurrentDevice); - const length = matchingDevices.length; - if (length === 1) { - const deviceId = matchingDevices[0].id; - return this.currentAccountState.updateUserAccountData({ - deviceId, - deviceRegistrationVersion: null - }).then(() => deviceId); - } - if (length > 1) { - log.error("insane server state, " + length + " devices for this session"); - } - return this._logErrorAndResetDeviceRegistrationVersion(error); - }).catch(secondError => { - log.error("failed to recover from device-session conflict", secondError); - this._logErrorAndResetDeviceRegistrationVersion(error) - }); - }, - - _logErrorAndResetDeviceRegistrationVersion(error) { - // Device registration should never cause other operations to fail. - // If we've reached this point, just log the error and reset the device - // registration version on the account data. At next sync or next sign-in, - // registration will be retried. - log.error("device registration failed", error); - return this.currentAccountState.updateUserAccountData({ - deviceRegistrationVersion: null - }).catch(secondError => { - log.error( - "failed to reset the device registration version, device registration won't be retried", - secondError); - }).then(() => {}); - }, - - _handleTokenError(err) { - if (!err || err.code != 401 || err.errno != ERRNO_INVALID_AUTH_TOKEN) { - throw err; - } - log.warn("recovering from invalid token error", err); - return this.accountStatus().then(exists => { - if (!exists) { - // Delete all local account data. Since the account no longer - // exists, we can skip the remote calls. - log.info("token invalidated because the account no longer exists"); - return this.signOut(true); - } - log.info("clearing credentials to handle invalid token error"); - return this.resetCredentials(); - }).then(() => Promise.reject(err)); - }, -}; - - -// A getter for the instance to export -XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() { - let a = new FxAccounts(); - - // XXX Bug 947061 - We need a strategy for resuming email verification after - // browser restart - a.loadAndPoll(); - - return a; -}); diff --git a/services/fxaccounts/FxAccountsClient.jsm b/services/fxaccounts/FxAccountsClient.jsm deleted file mode 100644 index fbe8da2fe..000000000 --- a/services/fxaccounts/FxAccountsClient.jsm +++ /dev/null @@ -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; -} diff --git a/services/fxaccounts/FxAccountsCommon.js b/services/fxaccounts/FxAccountsCommon.js deleted file mode 100644 index 71fe78a50..000000000 --- a/services/fxaccounts/FxAccountsCommon.js +++ /dev/null @@ -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; diff --git a/services/fxaccounts/FxAccountsComponents.manifest b/services/fxaccounts/FxAccountsComponents.manifest deleted file mode 100644 index 5069755bc..000000000 --- a/services/fxaccounts/FxAccountsComponents.manifest +++ /dev/null @@ -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 diff --git a/services/fxaccounts/FxAccountsConfig.jsm b/services/fxaccounts/FxAccountsConfig.jsm deleted file mode 100644 index 9dcf532ab..000000000 --- a/services/fxaccounts/FxAccountsConfig.jsm +++ /dev/null @@ -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; - } - }), - -}; diff --git a/services/fxaccounts/FxAccountsOAuthClient.jsm b/services/fxaccounts/FxAccountsOAuthClient.jsm deleted file mode 100644 index c59f1a869..000000000 --- a/services/fxaccounts/FxAccountsOAuthClient.jsm +++ /dev/null @@ -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'"); - } - }, -}; diff --git a/services/fxaccounts/FxAccountsOAuthGrantClient.jsm b/services/fxaccounts/FxAccountsOAuthGrantClient.jsm deleted file mode 100644 index 4319a07ab..000000000 --- a/services/fxaccounts/FxAccountsOAuthGrantClient.jsm +++ /dev/null @@ -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()) + ")"; -}; diff --git a/services/fxaccounts/FxAccountsProfile.jsm b/services/fxaccounts/FxAccountsProfile.jsm deleted file mode 100644 index b63cd64c1..000000000 --- a/services/fxaccounts/FxAccountsProfile.jsm +++ /dev/null @@ -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, - ]), -}; diff --git a/services/fxaccounts/FxAccountsProfileClient.jsm b/services/fxaccounts/FxAccountsProfileClient.jsm deleted file mode 100644 index 1e5edc634..000000000 --- a/services/fxaccounts/FxAccountsProfileClient.jsm +++ /dev/null @@ -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()) + ")"; -}; diff --git a/services/fxaccounts/FxAccountsPush.js b/services/fxaccounts/FxAccountsPush.js deleted file mode 100644 index 358be06ee..000000000 --- a/services/fxaccounts/FxAccountsPush.js +++ /dev/null @@ -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"]; diff --git a/services/fxaccounts/FxAccountsStorage.jsm b/services/fxaccounts/FxAccountsStorage.jsm deleted file mode 100644 index 4362cdf5b..000000000 --- a/services/fxaccounts/FxAccountsStorage.jsm +++ /dev/null @@ -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; - }), -} - diff --git a/services/fxaccounts/FxAccountsWebChannel.jsm b/services/fxaccounts/FxAccountsWebChannel.jsm deleted file mode 100644 index 810d93c65..000000000 --- a/services/fxaccounts/FxAccountsWebChannel.jsm +++ /dev/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); - } - } -} diff --git a/services/fxaccounts/interfaces/moz.build b/services/fxaccounts/interfaces/moz.build deleted file mode 100644 index ac80b3e93..000000000 --- a/services/fxaccounts/interfaces/moz.build +++ /dev/null @@ -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' diff --git a/services/fxaccounts/interfaces/nsIFxAccountsUIGlue.idl b/services/fxaccounts/interfaces/nsIFxAccountsUIGlue.idl deleted file mode 100644 index 950fdbc25..000000000 --- a/services/fxaccounts/interfaces/nsIFxAccountsUIGlue.idl +++ /dev/null @@ -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); -}; diff --git a/services/fxaccounts/moz.build b/services/fxaccounts/moz.build deleted file mode 100644 index b1cd3b59c..000000000 --- a/services/fxaccounts/moz.build +++ /dev/null @@ -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', -] - diff --git a/services/fxaccounts/tests/mochitest/chrome.ini b/services/fxaccounts/tests/mochitest/chrome.ini deleted file mode 100644 index ab2e77053..000000000 --- a/services/fxaccounts/tests/mochitest/chrome.ini +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] -skip-if = os == 'android' -support-files= - file_invalidEmailCase.sjs - -[test_invalidEmailCase.html] - diff --git a/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs b/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs deleted file mode 100644 index 9d97ac70c..000000000 --- a/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs +++ /dev/null @@ -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); -} - diff --git a/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html b/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html deleted file mode 100644 index 52866cc4b..000000000 --- a/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html +++ /dev/null @@ -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> - diff --git a/services/fxaccounts/tests/xpcshell/head.js b/services/fxaccounts/tests/xpcshell/head.js deleted file mode 100644 index ed70fdac5..000000000 --- a/services/fxaccounts/tests/xpcshell/head.js +++ /dev/null @@ -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); - diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js deleted file mode 100644 index d6139a076..000000000 --- a/services/fxaccounts/tests/xpcshell/test_accounts.js +++ /dev/null @@ -1,1531 +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/FxAccountsOAuthGrantClient.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); - -// We grab some additional stuff via backstage passes. -var {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {}); - -const ONE_HOUR_MS = 1000 * 60 * 60; -const ONE_DAY_MS = ONE_HOUR_MS * 24; -const TWO_MINUTES_MS = 1000 * 60 * 2; - -initTestLogging("Trace"); - -// XXX until bug 937114 is fixed -Cu.importGlobalProperties(['atob']); - -var log = Log.repository.getLogger("Services.FxAccounts.test"); -log.level = Log.Level.Debug; - -// See verbose logging from FxAccounts.jsm -Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace"); -Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace; - -// The oauth server is mocked, but set these prefs to pass param checks -Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); -Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123"); - - -const PROFILE_SERVER_URL = "http://example.com/v1"; -const CONTENT_URL = "http://accounts.example.com/"; - -Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", PROFILE_SERVER_URL); -Services.prefs.setCharPref("identity.fxaccounts.settings.uri", CONTENT_URL); - -/* - * The FxAccountsClient communicates with the remote Firefox - * Accounts auth server. Mock the server calls, with a little - * lag time to simulate some latency. - * - * We add the _verified attribute to mock the change in verification - * state on the FXA server. - */ - -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._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; - }; - - this.accountKeys = function (keyFetchToken) { - let deferred = Promise.defer(); - - do_timeout(50, () => { - let response = { - kA: expandBytes("11"), - wrapKB: expandBytes("22") - }; - deferred.resolve(response); - }); - return deferred.promise; - }; - - this.resendVerificationEmail = function(sessionToken) { - // Return the session token to show that we received it in the first place - return Promise.resolve(sessionToken); - }; - - this.signCertificate = function() { throw "no" }; - - this.signOut = () => Promise.resolve(); - this.signOutAndDestroyDevice = () => Promise.resolve({}); - - FxAccountsClient.apply(this); -} -MockFxAccountsClient.prototype = { - __proto__: FxAccountsClient.prototype -} - -/* - * We need to mock the FxAccounts module's interfaces to external - * services, such as storage and the FxAccounts client. We also - * mock the now() method, so that we can simulate the passing of - * time and verify that signatures expire correctly. - */ -function MockFxAccounts() { - return new FxAccounts({ - VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms - - _getCertificateSigned_calls: [], - _d_signCertificate: Promise.defer(), - _now_is: new Date(), - now: function () { - return this._now_is; - }, - newAccountState(credentials) { - // we use a real accountState but mocked storage. - let storage = new MockStorageManager(); - storage.initialize(credentials); - return new AccountState(storage); - }, - getCertificateSigned: function (sessionToken, serializedPublicKey) { - _("mock getCertificateSigned\n"); - this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]); - return this._d_signCertificate.promise; - }, - _registerOrUpdateDevice() { - return Promise.resolve(); - }, - fxAccountsClient: new MockFxAccountsClient() - }); -} - -/* - * Some tests want a "real" fxa instance - however, we still mock the storage - * to keep the tests fast on b2g. - */ -function MakeFxAccounts(internal = {}) { - if (!internal.newAccountState) { - // we use a real accountState but mocked storage. - internal.newAccountState = function(credentials) { - let storage = new MockStorageManager(); - storage.initialize(credentials); - return new AccountState(storage); - }; - } - if (!internal._signOutServer) { - internal._signOutServer = () => Promise.resolve(); - } - if (!internal._registerOrUpdateDevice) { - internal._registerOrUpdateDevice = () => Promise.resolve(); - } - return new FxAccounts(internal); -} - -add_task(function* test_non_https_remote_server_uri_with_requireHttps_false() { - Services.prefs.setBoolPref( - "identity.fxaccounts.allowHttp", - true); - Services.prefs.setCharPref( - "identity.fxaccounts.remote.signup.uri", - "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); - do_check_eq(yield fxAccounts.promiseAccountsSignUpURI(), - "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); - - Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri"); - Services.prefs.clearUserPref("identity.fxaccounts.allowHttp"); -}); - -add_task(function* test_non_https_remote_server_uri() { - Services.prefs.setCharPref( - "identity.fxaccounts.remote.signup.uri", - "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); - rejects(fxAccounts.promiseAccountsSignUpURI(), null, "Firefox Accounts server must use HTTPS"); - Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri"); -}); - -add_task(function* test_get_signed_in_user_initially_unset() { - _("Check getSignedInUser initially and after signout reports no user"); - let account = MakeFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - assertion: "foobar", - sessionToken: "dead", - kA: "beef", - kB: "cafe", - verified: true - }; - let result = yield account.getSignedInUser(); - do_check_eq(result, null); - - yield account.setSignedInUser(credentials); - let histogram = Services.telemetry.getHistogramById("FXA_CONFIGURED"); - do_check_eq(histogram.snapshot().sum, 1); - histogram.clear(); - - result = yield account.getSignedInUser(); - do_check_eq(result.email, credentials.email); - do_check_eq(result.assertion, credentials.assertion); - do_check_eq(result.kB, credentials.kB); - - // Delete the memory cache and force the user - // to be read and parsed from storage (e.g. disk via JSONStorage). - delete account.internal.signedInUser; - result = yield account.getSignedInUser(); - do_check_eq(result.email, credentials.email); - do_check_eq(result.assertion, credentials.assertion); - do_check_eq(result.kB, credentials.kB); - - // sign out - let localOnly = true; - yield account.signOut(localOnly); - - // user should be undefined after sign out - result = yield account.getSignedInUser(); - do_check_eq(result, null); -}); - -add_task(function* test_update_account_data() { - _("Check updateUserAccountData does the right thing."); - let account = MakeFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - assertion: "foobar", - sessionToken: "dead", - kA: "beef", - kB: "cafe", - verified: true - }; - yield account.setSignedInUser(credentials); - - let newCreds = { - email: credentials.email, - uid: credentials.uid, - assertion: "new_assertion", - } - yield account.updateUserAccountData(newCreds); - do_check_eq((yield account.getSignedInUser()).assertion, "new_assertion", - "new field value was saved"); - - // but we should fail attempting to change email or uid. - newCreds = { - email: "someoneelse@example.com", - uid: credentials.uid, - assertion: "new_assertion", - } - yield Assert.rejects(account.updateUserAccountData(newCreds)); - newCreds = { - email: credentials.email, - uid: "another_uid", - assertion: "new_assertion", - } - yield Assert.rejects(account.updateUserAccountData(newCreds)); - - // should fail without email or uid. - newCreds = { - assertion: "new_assertion", - } - yield Assert.rejects(account.updateUserAccountData(newCreds)); - - // and should fail with a field name that's not known by storage. - newCreds = { - email: credentials.email, - uid: "another_uid", - foo: "bar", - } - yield Assert.rejects(account.updateUserAccountData(newCreds)); -}); - -add_task(function* test_getCertificateOffline() { - _("getCertificateOffline()"); - let fxa = MakeFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - sessionToken: "dead", - verified: true, - }; - - yield fxa.setSignedInUser(credentials); - - // Test that an expired cert throws if we're offline. - let offline = Services.io.offline; - Services.io.offline = true; - yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState).then( - result => { - Services.io.offline = offline; - do_throw("Unexpected success"); - }, - err => { - Services.io.offline = offline; - // ... so we have to check the error string. - do_check_eq(err, "Error: OFFLINE"); - } - ); - yield fxa.signOut(/*localOnly = */true); -}); - -add_task(function* test_getCertificateCached() { - _("getCertificateCached()"); - let fxa = MakeFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - sessionToken: "dead", - verified: true, - // A cached keypair and cert that remain valid. - keyPair: { - validUntil: Date.now() + KEY_LIFETIME + 10000, - rawKeyPair: "good-keypair", - }, - cert: { - validUntil: Date.now() + CERT_LIFETIME + 10000, - rawCert: "good-cert", - }, - }; - - yield fxa.setSignedInUser(credentials); - let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); - // should have the same keypair and cert. - do_check_eq(keyPair, credentials.keyPair.rawKeyPair); - do_check_eq(certificate, credentials.cert.rawCert); - yield fxa.signOut(/*localOnly = */true); -}); - -add_task(function* test_getCertificateExpiredCert() { - _("getCertificateExpiredCert()"); - let fxa = MakeFxAccounts({ - getCertificateSigned() { - return "new cert"; - } - }); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - sessionToken: "dead", - verified: true, - // A cached keypair that remains valid. - keyPair: { - validUntil: Date.now() + KEY_LIFETIME + 10000, - rawKeyPair: "good-keypair", - }, - // A cached certificate which has expired. - cert: { - validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT"), - rawCert: "expired-cert", - }, - }; - yield fxa.setSignedInUser(credentials); - let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); - // should have the same keypair but a new cert. - do_check_eq(keyPair, credentials.keyPair.rawKeyPair); - do_check_neq(certificate, credentials.cert.rawCert); - yield fxa.signOut(/*localOnly = */true); -}); - -add_task(function* test_getCertificateExpiredKeypair() { - _("getCertificateExpiredKeypair()"); - let fxa = MakeFxAccounts({ - getCertificateSigned() { - return "new cert"; - }, - }); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - sessionToken: "dead", - verified: true, - // A cached keypair that has expired. - keyPair: { - validUntil: Date.now() - 1000, - rawKeyPair: "expired-keypair", - }, - // A cached certificate which remains valid. - cert: { - validUntil: Date.now() + CERT_LIFETIME + 10000, - rawCert: "expired-cert", - }, - }; - - yield fxa.setSignedInUser(credentials); - let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); - // even though the cert was valid, the fact the keypair was not means we - // should have fetched both. - do_check_neq(keyPair, credentials.keyPair.rawKeyPair); - do_check_neq(certificate, credentials.cert.rawCert); - yield fxa.signOut(/*localOnly = */true); -}); - -// Sanity-check that our mocked client is working correctly -add_test(function test_client_mock() { - let fxa = new MockFxAccounts(); - let client = fxa.internal.fxAccountsClient; - do_check_eq(client._verified, false); - do_check_eq(typeof client.signIn, "function"); - - // The recoveryEmailStatus function eventually fulfills its promise - client.recoveryEmailStatus() - .then(response => { - do_check_eq(response.verified, false); - run_next_test(); - }); -}); - -// Sign in a user, and after a little while, verify the user's email. -// Right after signing in the user, we should get the 'onlogin' notification. -// Polling should detect that the email is verified, and eventually -// 'onverified' should be observed -add_test(function test_verification_poll() { - let fxa = new MockFxAccounts(); - let test_user = getTestUser("francine"); - let login_notification_received = false; - - makeObserver(ONVERIFIED_NOTIFICATION, function() { - log.debug("test_verification_poll observed onverified"); - // Once email verification is complete, we will observe onverified - fxa.internal.getUserAccountData().then(user => { - // And confirm that the user's state has changed - do_check_eq(user.verified, true); - do_check_eq(user.email, test_user.email); - do_check_true(login_notification_received); - run_next_test(); - }); - }); - - makeObserver(ONLOGIN_NOTIFICATION, function() { - log.debug("test_verification_poll observer onlogin"); - login_notification_received = true; - }); - - fxa.setSignedInUser(test_user).then(() => { - fxa.internal.getUserAccountData().then(user => { - // The user is signing in, but email has not been verified yet - do_check_eq(user.verified, false); - do_timeout(200, function() { - log.debug("Mocking verification of francine's email"); - fxa.internal.fxAccountsClient._email = test_user.email; - fxa.internal.fxAccountsClient._verified = true; - }); - }); - }); -}); - -// Sign in the user, but never verify the email. The check-email -// poll should time out. No verifiedlogin event should be observed, and the -// internal whenVerified promise should be rejected -add_test(function test_polling_timeout() { - // This test could be better - the onverified observer might fire on - // somebody else's stack, and we're not making sure that we're not receiving - // such a message. In other words, this tests either failure, or success, but - // not both. - - let fxa = new MockFxAccounts(); - let test_user = getTestUser("carol"); - - let removeObserver = makeObserver(ONVERIFIED_NOTIFICATION, function() { - do_throw("We should not be getting a login event!"); - }); - - fxa.internal.POLL_SESSION = 1; - - let p = fxa.internal.whenVerified({}); - - fxa.setSignedInUser(test_user).then(() => { - p.then( - (success) => { - do_throw("this should not succeed"); - }, - (fail) => { - removeObserver(); - fxa.signOut().then(run_next_test); - } - ); - }); -}); - -add_test(function test_getKeys() { - let fxa = new MockFxAccounts(); - let user = getTestUser("eusebius"); - - // Once email has been verified, we will be able to get keys - user.verified = true; - - fxa.setSignedInUser(user).then(() => { - fxa.getSignedInUser().then((user) => { - // Before getKeys, we have no keys - do_check_eq(!!user.kA, false); - do_check_eq(!!user.kB, false); - // And we still have a key-fetch token and unwrapBKey to use - do_check_eq(!!user.keyFetchToken, true); - do_check_eq(!!user.unwrapBKey, true); - - fxa.internal.getKeys().then(() => { - fxa.getSignedInUser().then((user) => { - // Now we should have keys - do_check_eq(fxa.internal.isUserEmailVerified(user), true); - do_check_eq(!!user.verified, true); - do_check_eq(user.kA, expandHex("11")); - do_check_eq(user.kB, expandHex("66")); - do_check_eq(user.keyFetchToken, undefined); - do_check_eq(user.unwrapBKey, undefined); - run_next_test(); - }); - }); - }); - }); -}); - -add_task(function* test_getKeys_nonexistent_account() { - let fxa = new MockFxAccounts(); - let bismarck = getTestUser("bismarck"); - - let client = fxa.internal.fxAccountsClient; - client.accountStatus = () => Promise.resolve(false); - client.accountKeys = () => { - return Promise.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - }; - - yield fxa.setSignedInUser(bismarck); - - let promiseLogout = new Promise(resolve => { - makeObserver(ONLOGOUT_NOTIFICATION, function() { - log.debug("test_getKeys_nonexistent_account observed logout"); - resolve(); - }); - }); - - try { - yield fxa.internal.getKeys(); - do_check_true(false); - } catch (err) { - do_check_eq(err.code, 401); - do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); - } - - yield promiseLogout; - - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user, null); -}); - -// getKeys with invalid keyFetchToken should delete keyFetchToken from storage -add_task(function* test_getKeys_invalid_token() { - let fxa = new MockFxAccounts(); - let yusuf = getTestUser("yusuf"); - - let client = fxa.internal.fxAccountsClient; - client.accountStatus = () => Promise.resolve(true); - client.accountKeys = () => { - return Promise.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - }; - - yield fxa.setSignedInUser(yusuf); - - try { - yield fxa.internal.getKeys(); - do_check_true(false); - } catch (err) { - do_check_eq(err.code, 401); - do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); - } - - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, yusuf.email); - do_check_eq(user.keyFetchToken, null); -}); - -// fetchAndUnwrapKeys with no keyFetchToken should trigger signOut -add_test(function test_fetchAndUnwrapKeys_no_token() { - let fxa = new MockFxAccounts(); - let user = getTestUser("lettuce.protheroe"); - delete user.keyFetchToken - - makeObserver(ONLOGOUT_NOTIFICATION, function() { - log.debug("test_fetchAndUnwrapKeys_no_token observed logout"); - fxa.internal.getUserAccountData().then(user => { - run_next_test(); - }); - }); - - fxa.setSignedInUser(user).then( - user => { - return fxa.internal.fetchAndUnwrapKeys(); - } - ).then( - null, - error => { - log.info("setSignedInUser correctly rejected"); - } - ) -}); - -// Alice (User A) signs up but never verifies her email. Then Bob (User B) -// signs in with a verified email. Ensure that no sign-in events are triggered -// on Alice's behalf. In the end, Bob should be the signed-in user. -add_test(function test_overlapping_signins() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - let bob = getTestUser("bob"); - - makeObserver(ONVERIFIED_NOTIFICATION, function() { - log.debug("test_overlapping_signins observed onverified"); - // Once email verification is complete, we will observe onverified - fxa.internal.getUserAccountData().then(user => { - do_check_eq(user.email, bob.email); - do_check_eq(user.verified, true); - run_next_test(); - }); - }); - - // Alice is the user signing in; her email is unverified. - fxa.setSignedInUser(alice).then(() => { - log.debug("Alice signing in ..."); - fxa.internal.getUserAccountData().then(user => { - do_check_eq(user.email, alice.email); - do_check_eq(user.verified, false); - log.debug("Alice has not verified her email ..."); - - // Now Bob signs in instead and actually verifies his email - log.debug("Bob signing in ..."); - fxa.setSignedInUser(bob).then(() => { - do_timeout(200, function() { - // Mock email verification ... - log.debug("Bob verifying his email ..."); - fxa.internal.fxAccountsClient._verified = true; - }); - }); - }); - }); -}); - -add_task(function* test_getAssertion_invalid_token() { - let fxa = new MockFxAccounts(); - - let client = fxa.internal.fxAccountsClient; - client.accountStatus = () => Promise.resolve(true); - - let creds = { - sessionToken: "sessionToken", - kA: expandHex("11"), - kB: expandHex("66"), - verified: true, - email: "sonia@example.com", - }; - yield fxa.setSignedInUser(creds); - - try { - let promiseAssertion = fxa.getAssertion("audience.example.com"); - fxa.internal._d_signCertificate.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - yield promiseAssertion; - do_check_true(false, "getAssertion should reject invalid session token"); - } catch (err) { - do_check_eq(err.code, 401); - do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); - } - - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, creds.email); - do_check_eq(user.sessionToken, null); -}); - -add_task(function* test_getAssertion() { - let fxa = new MockFxAccounts(); - - do_check_throws(function* () { - yield fxa.getAssertion("nonaudience"); - }); - - let creds = { - sessionToken: "sessionToken", - kA: expandHex("11"), - kB: expandHex("66"), - verified: true - }; - // By putting kA/kB/verified in "creds", we skip ahead - // to the "we're ready" stage. - yield fxa.setSignedInUser(creds); - - _("== ready to go\n"); - // Start with a nice arbitrary but realistic date. Here we use a nice RFC - // 1123 date string like we would get from an HTTP header. Over the course of - // the test, we will update 'now', but leave 'start' where it is. - let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT"); - let start = now; - fxa.internal._now_is = now; - - let d = fxa.getAssertion("audience.example.com"); - // At this point, a thread has been spawned to generate the keys. - _("-- back from fxa.getAssertion\n"); - fxa.internal._d_signCertificate.resolve("cert1"); - let assertion = yield d; - do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1); - do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken"); - do_check_neq(assertion, null); - _("ASSERTION: " + assertion + "\n"); - let pieces = assertion.split("~"); - do_check_eq(pieces[0], "cert1"); - let userData = yield fxa.getSignedInUser(); - let keyPair = userData.keyPair; - let cert = userData.cert; - do_check_neq(keyPair, undefined); - _(keyPair.validUntil + "\n"); - let p2 = pieces[1].split("."); - let header = JSON.parse(atob(p2[0])); - _("HEADER: " + JSON.stringify(header) + "\n"); - do_check_eq(header.alg, "DS128"); - let payload = JSON.parse(atob(p2[1])); - _("PAYLOAD: " + JSON.stringify(payload) + "\n"); - do_check_eq(payload.aud, "audience.example.com"); - do_check_eq(keyPair.validUntil, start + KEY_LIFETIME); - do_check_eq(cert.validUntil, start + CERT_LIFETIME); - _("delta: " + Date.parse(payload.exp - start) + "\n"); - let exp = Number(payload.exp); - - do_check_eq(exp, now + ASSERTION_LIFETIME); - - // Reset for next call. - fxa.internal._d_signCertificate = Promise.defer(); - - // Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for - // a new audience, should not provoke key generation or a signing request. - assertion = yield fxa.getAssertion("other.example.com"); - - // There were no additional calls - same number of getcert calls as before - do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1); - - // Wait an hour; assertion use period expires, but not the certificate - now += ONE_HOUR_MS; - fxa.internal._now_is = now; - - // This won't block on anything - will make an assertion, but not get a - // new certificate. - assertion = yield fxa.getAssertion("third.example.com"); - - // Test will time out if that failed (i.e., if that had to go get a new cert) - pieces = assertion.split("~"); - do_check_eq(pieces[0], "cert1"); - p2 = pieces[1].split("."); - header = JSON.parse(atob(p2[0])); - payload = JSON.parse(atob(p2[1])); - do_check_eq(payload.aud, "third.example.com"); - - // The keypair and cert should have the same validity as before, but the - // expiration time of the assertion should be different. We compare this to - // the initial start time, to which they are relative, not the current value - // of "now". - userData = yield fxa.getSignedInUser(); - - keyPair = userData.keyPair; - cert = userData.cert; - do_check_eq(keyPair.validUntil, start + KEY_LIFETIME); - do_check_eq(cert.validUntil, start + CERT_LIFETIME); - exp = Number(payload.exp); - do_check_eq(exp, now + ASSERTION_LIFETIME); - - // Now we wait even longer, and expect both assertion and cert to expire. So - // we will have to get a new keypair and cert. - now += ONE_DAY_MS; - fxa.internal._now_is = now; - d = fxa.getAssertion("fourth.example.com"); - fxa.internal._d_signCertificate.resolve("cert2"); - assertion = yield d; - do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2); - do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken"); - pieces = assertion.split("~"); - do_check_eq(pieces[0], "cert2"); - p2 = pieces[1].split("."); - header = JSON.parse(atob(p2[0])); - payload = JSON.parse(atob(p2[1])); - do_check_eq(payload.aud, "fourth.example.com"); - userData = yield fxa.getSignedInUser(); - keyPair = userData.keyPair; - cert = userData.cert; - do_check_eq(keyPair.validUntil, now + KEY_LIFETIME); - do_check_eq(cert.validUntil, now + CERT_LIFETIME); - exp = Number(payload.exp); - - do_check_eq(exp, now + ASSERTION_LIFETIME); - _("----- DONE ----\n"); -}); - -add_task(function* test_resend_email_not_signed_in() { - let fxa = new MockFxAccounts(); - - try { - yield fxa.resendVerificationEmail(); - } catch(err) { - do_check_eq(err.message, - "Cannot resend verification email; no signed-in user"); - return; - } - do_throw("Should not be able to resend email when nobody is signed in"); -}); - -add_test(function test_accountStatus() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - - // If we have no user, we have no account server-side - fxa.accountStatus().then( - (result) => { - do_check_false(result); - } - ).then( - () => { - fxa.setSignedInUser(alice).then( - () => { - fxa.accountStatus().then( - (result) => { - // FxAccounts.accountStatus() should match Client.accountStatus() - do_check_true(result); - fxa.internal.fxAccountsClient._deletedOnServer = true; - fxa.accountStatus().then( - (result) => { - do_check_false(result); - fxa.internal.fxAccountsClient._deletedOnServer = false; - fxa.signOut().then(run_next_test); - } - ); - } - ) - } - ); - } - ); -}); - -add_task(function* test_resend_email_invalid_token() { - let fxa = new MockFxAccounts(); - let sophia = getTestUser("sophia"); - do_check_neq(sophia.sessionToken, null); - - let client = fxa.internal.fxAccountsClient; - client.resendVerificationEmail = () => { - return Promise.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - }; - client.accountStatus = () => Promise.resolve(true); - - yield fxa.setSignedInUser(sophia); - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, sophia.email); - do_check_eq(user.verified, false); - log.debug("Sophia wants verification email resent"); - - try { - yield fxa.resendVerificationEmail(); - do_check_true(false, "resendVerificationEmail should reject invalid session token"); - } catch (err) { - do_check_eq(err.code, 401); - do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); - } - - user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, sophia.email); - do_check_eq(user.sessionToken, null); -}); - -add_test(function test_resend_email() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - - let initialState = fxa.internal.currentAccountState; - - // Alice is the user signing in; her email is unverified. - fxa.setSignedInUser(alice).then(() => { - log.debug("Alice signing in"); - - // We're polling for the first email - do_check_true(fxa.internal.currentAccountState !== initialState); - let aliceState = fxa.internal.currentAccountState; - - // The polling timer is ticking - do_check_true(fxa.internal.currentTimer > 0); - - fxa.internal.getUserAccountData().then(user => { - do_check_eq(user.email, alice.email); - do_check_eq(user.verified, false); - log.debug("Alice wants verification email resent"); - - fxa.resendVerificationEmail().then((result) => { - // Mock server response; ensures that the session token actually was - // passed to the client to make the hawk call - do_check_eq(result, "alice's session token"); - - // Timer was not restarted - do_check_true(fxa.internal.currentAccountState === aliceState); - - // Timer is still ticking - do_check_true(fxa.internal.currentTimer > 0); - - // Ok abort polling before we go on to the next test - fxa.internal.abortExistingFlow(); - run_next_test(); - }); - }); - }); -}); - -add_task(function* test_sign_out_with_device() { - const fxa = new MockFxAccounts(); - - const credentials = getTestUser("alice"); - yield fxa.internal.setSignedInUser(credentials); - - const user = yield fxa.internal.getUserAccountData(); - do_check_true(user); - Object.keys(credentials).forEach(key => do_check_eq(credentials[key], user[key])); - - const spy = { - signOut: { count: 0 }, - signOutAndDeviceDestroy: { count: 0, args: [] } - }; - const client = fxa.internal.fxAccountsClient; - client.signOut = function () { - spy.signOut.count += 1; - return Promise.resolve(); - }; - client.signOutAndDestroyDevice = function () { - spy.signOutAndDeviceDestroy.count += 1; - spy.signOutAndDeviceDestroy.args.push(arguments); - return Promise.resolve(); - }; - - const promise = new Promise(resolve => { - makeObserver(ONLOGOUT_NOTIFICATION, () => { - log.debug("test_sign_out_with_device observed onlogout"); - // user should be undefined after sign out - fxa.internal.getUserAccountData().then(user2 => { - do_check_eq(user2, null); - do_check_eq(spy.signOut.count, 0); - do_check_eq(spy.signOutAndDeviceDestroy.count, 1); - do_check_eq(spy.signOutAndDeviceDestroy.args[0].length, 3); - do_check_eq(spy.signOutAndDeviceDestroy.args[0][0], credentials.sessionToken); - do_check_eq(spy.signOutAndDeviceDestroy.args[0][1], credentials.deviceId); - do_check_true(spy.signOutAndDeviceDestroy.args[0][2]); - do_check_eq(spy.signOutAndDeviceDestroy.args[0][2].service, "sync"); - resolve(); - }); - }); - }); - - yield fxa.signOut(); - - yield promise; -}); - -add_task(function* test_sign_out_without_device() { - const fxa = new MockFxAccounts(); - - const credentials = getTestUser("alice"); - delete credentials.deviceId; - yield fxa.internal.setSignedInUser(credentials); - - const user = yield fxa.internal.getUserAccountData(); - - const spy = { - signOut: { count: 0, args: [] }, - signOutAndDeviceDestroy: { count: 0 } - }; - const client = fxa.internal.fxAccountsClient; - client.signOut = function () { - spy.signOut.count += 1; - spy.signOut.args.push(arguments); - return Promise.resolve(); - }; - client.signOutAndDestroyDevice = function () { - spy.signOutAndDeviceDestroy.count += 1; - return Promise.resolve(); - }; - - const promise = new Promise(resolve => { - makeObserver(ONLOGOUT_NOTIFICATION, () => { - log.debug("test_sign_out_without_device observed onlogout"); - // user should be undefined after sign out - fxa.internal.getUserAccountData().then(user2 => { - do_check_eq(user2, null); - do_check_eq(spy.signOut.count, 1); - do_check_eq(spy.signOut.args[0].length, 2); - do_check_eq(spy.signOut.args[0][0], credentials.sessionToken); - do_check_true(spy.signOut.args[0][1]); - do_check_eq(spy.signOut.args[0][1].service, "sync"); - do_check_eq(spy.signOutAndDeviceDestroy.count, 0); - resolve(); - }); - }); - }); - - yield fxa.signOut(); - - yield promise; -}); - -add_task(function* test_sign_out_with_remote_error() { - let fxa = new MockFxAccounts(); - let client = fxa.internal.fxAccountsClient; - let remoteSignOutCalled = false; - // Force remote sign out to trigger an error - client.signOutAndDestroyDevice = function() { remoteSignOutCalled = true; throw "Remote sign out error"; }; - let promiseLogout = new Promise(resolve => { - makeObserver(ONLOGOUT_NOTIFICATION, function() { - log.debug("test_sign_out_with_remote_error observed onlogout"); - resolve(); - }); - }); - - let jane = getTestUser("jane"); - yield fxa.setSignedInUser(jane); - yield fxa.signOut(); - yield promiseLogout; - - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user, null); - do_check_true(remoteSignOutCalled); -}); - -add_test(function test_getOAuthToken() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - let getTokenFromAssertionCalled = false; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - getTokenFromAssertionCalled = true; - return Promise.resolve({ access_token: "token" }); - }; - - fxa.setSignedInUser(alice).then( - () => { - fxa.getOAuthToken({ scope: "profile", client: client }).then( - (result) => { - do_check_true(getTokenFromAssertionCalled); - do_check_eq(result, "token"); - run_next_test(); - } - ) - } - ); - -}); - -add_test(function test_getOAuthTokenScoped() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - let getTokenFromAssertionCalled = false; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function (assertion, scopeString) { - equal(scopeString, "foo bar"); - getTokenFromAssertionCalled = true; - return Promise.resolve({ access_token: "token" }); - }; - - fxa.setSignedInUser(alice).then( - () => { - fxa.getOAuthToken({ scope: ["foo", "bar"], client: client }).then( - (result) => { - do_check_true(getTokenFromAssertionCalled); - do_check_eq(result, "token"); - run_next_test(); - } - ) - } - ); - -}); - -add_task(function* test_getOAuthTokenCached() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - let numTokenFromAssertionCalls = 0; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - numTokenFromAssertionCalls += 1; - return Promise.resolve({ access_token: "token" }); - }; - - yield fxa.setSignedInUser(alice); - let result = yield fxa.getOAuthToken({ scope: "profile", client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - - // requesting it again should not re-fetch the token. - result = yield fxa.getOAuthToken({ scope: "profile", client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - // But requesting the same service and a different scope *will* get a new one. - result = yield fxa.getOAuthToken({ scope: "something-else", client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 2); - do_check_eq(result, "token"); -}); - -add_task(function* test_getOAuthTokenCachedScopeNormalization() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - let numTokenFromAssertionCalls = 0; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - numTokenFromAssertionCalls += 1; - return Promise.resolve({ access_token: "token" }); - }; - - yield fxa.setSignedInUser(alice); - let result = yield fxa.getOAuthToken({ scope: ["foo", "bar"], client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - - // requesting it again with the scope array in a different order not re-fetch the token. - result = yield fxa.getOAuthToken({ scope: ["bar", "foo"], client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - // requesting it again with the scope array in different case not re-fetch the token. - result = yield fxa.getOAuthToken({ scope: ["Bar", "Foo"], client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - // But requesting with a new entry in the array does fetch one. - result = yield fxa.getOAuthToken({ scope: ["foo", "bar", "etc"], client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 2); - do_check_eq(result, "token"); -}); - -Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); -add_test(function test_getOAuthToken_invalid_param() { - let fxa = new MockFxAccounts(); - - fxa.getOAuthToken() - .then(null, err => { - do_check_eq(err.message, "INVALID_PARAMETER"); - fxa.signOut().then(run_next_test); - }); -}); - -add_test(function test_getOAuthToken_invalid_scope_array() { - let fxa = new MockFxAccounts(); - - fxa.getOAuthToken({scope: []}) - .then(null, err => { - do_check_eq(err.message, "INVALID_PARAMETER"); - fxa.signOut().then(run_next_test); - }); -}); - -add_test(function test_getOAuthToken_misconfigure_oauth_uri() { - let fxa = new MockFxAccounts(); - - Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri"); - - fxa.getOAuthToken() - .then(null, err => { - do_check_eq(err.message, "INVALID_PARAMETER"); - // revert the pref - Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); - fxa.signOut().then(run_next_test); - }); -}); - -add_test(function test_getOAuthToken_no_account() { - let fxa = new MockFxAccounts(); - - fxa.internal.currentAccountState.getUserAccountData = function () { - return Promise.resolve(null); - }; - - fxa.getOAuthToken({ scope: "profile" }) - .then(null, err => { - do_check_eq(err.message, "NO_ACCOUNT"); - fxa.signOut().then(run_next_test); - }); -}); - -add_test(function test_getOAuthToken_unverified() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - - fxa.setSignedInUser(alice).then(() => { - fxa.getOAuthToken({ scope: "profile" }) - .then(null, err => { - do_check_eq(err.message, "UNVERIFIED_ACCOUNT"); - fxa.signOut().then(run_next_test); - }); - }); -}); - -add_test(function test_getOAuthToken_network_error() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - return Promise.reject(new FxAccountsOAuthGrantClientError({ - error: ERROR_NETWORK, - errno: ERRNO_NETWORK - })); - }; - - fxa.setSignedInUser(alice).then(() => { - fxa.getOAuthToken({ scope: "profile", client: client }) - .then(null, err => { - do_check_eq(err.message, "NETWORK_ERROR"); - do_check_eq(err.details.errno, ERRNO_NETWORK); - run_next_test(); - }); - }); -}); - -add_test(function test_getOAuthToken_auth_error() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - return Promise.reject(new FxAccountsOAuthGrantClientError({ - error: ERROR_INVALID_FXA_ASSERTION, - errno: ERRNO_INVALID_FXA_ASSERTION - })); - }; - - fxa.setSignedInUser(alice).then(() => { - fxa.getOAuthToken({ scope: "profile", client: client }) - .then(null, err => { - do_check_eq(err.message, "AUTH_ERROR"); - do_check_eq(err.details.errno, ERRNO_INVALID_FXA_ASSERTION); - run_next_test(); - }); - }); -}); - -add_test(function test_getOAuthToken_unknown_error() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - return Promise.reject("boom"); - }; - - fxa.setSignedInUser(alice).then(() => { - fxa.getOAuthToken({ scope: "profile", client: client }) - .then(null, err => { - do_check_eq(err.message, "UNKNOWN_ERROR"); - run_next_test(); - }); - }); -}); - -add_test(function test_getSignedInUserProfile() { - let alice = getTestUser("alice"); - alice.verified = true; - - let mockProfile = { - getProfile: function () { - return Promise.resolve({ avatar: "image" }); - }, - tearDown: function() {}, - }; - let fxa = new FxAccounts({ - _signOutServer() { return Promise.resolve(); }, - _registerOrUpdateDevice() { return Promise.resolve(); } - }); - - fxa.setSignedInUser(alice).then(() => { - fxa.internal._profile = mockProfile; - fxa.getSignedInUserProfile() - .then(result => { - do_check_true(!!result); - do_check_eq(result.avatar, "image"); - run_next_test(); - }); - }); -}); - -add_test(function test_getSignedInUserProfile_error_uses_account_data() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - fxa.internal.getSignedInUser = function () { - return Promise.resolve({ email: "foo@bar.com" }); - }; - - let teardownCalled = false; - fxa.setSignedInUser(alice).then(() => { - fxa.internal._profile = { - getProfile: function () { - return Promise.reject("boom"); - }, - tearDown: function() { - teardownCalled = true; - } - }; - - fxa.getSignedInUserProfile() - .catch(error => { - do_check_eq(error.message, "UNKNOWN_ERROR"); - fxa.signOut().then(() => { - do_check_true(teardownCalled); - run_next_test(); - }); - }); - }); -}); - -add_test(function test_getSignedInUserProfile_unverified_account() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - - fxa.setSignedInUser(alice).then(() => { - fxa.getSignedInUserProfile() - .catch(error => { - do_check_eq(error.message, "UNVERIFIED_ACCOUNT"); - fxa.signOut().then(run_next_test); - }); - }); - -}); - -add_test(function test_getSignedInUserProfile_no_account_data() { - let fxa = new MockFxAccounts(); - - fxa.internal.getSignedInUser = function () { - return Promise.resolve(null); - }; - - fxa.getSignedInUserProfile() - .catch(error => { - do_check_eq(error.message, "NO_ACCOUNT"); - fxa.signOut().then(run_next_test); - }); - -}); - -add_task(function* test_checkVerificationStatusFailed() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - let client = fxa.internal.fxAccountsClient; - client.recoveryEmailStatus = () => { - return Promise.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - }; - client.accountStatus = () => Promise.resolve(true); - - yield fxa.setSignedInUser(alice); - let user = yield fxa.internal.getUserAccountData(); - do_check_neq(alice.sessionToken, null); - do_check_eq(user.email, alice.email); - do_check_eq(user.verified, true); - - yield fxa.checkVerificationStatus(); - - user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, alice.email); - do_check_eq(user.sessionToken, null); -}); - -/* - * End of tests. - * Utility functions follow. - */ - -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 - }; -} - -function makeObserver(aObserveTopic, aObserveFunc) { - let observer = { - // nsISupports provides type management in C++ - // nsIObserver is to be an observer - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), - - observe: 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(observer, aObserveTopic); - } - - Services.obs.addObserver(observer, aObserveTopic, false); - return removeMe; -} - -function do_check_throws(func, result, stack) -{ - if (!stack) - stack = Components.stack.caller; - - try { - func(); - } catch (ex) { - if (ex.name == result) { - return; - } - do_throw("Expected result " + result + ", caught " + ex.name, stack); - } - - if (result) { - do_throw("Expected result " + result + ", none thrown", stack); - } -} diff --git a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js deleted file mode 100644 index 9a2d2c127..000000000 --- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js +++ /dev/null @@ -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 - }; -} - diff --git a/services/fxaccounts/tests/xpcshell/test_client.js b/services/fxaccounts/tests/xpcshell/test_client.js deleted file mode 100644 index 83f42bdf5..000000000 --- a/services/fxaccounts/tests/xpcshell/test_client.js +++ /dev/null @@ -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, ""); -} diff --git a/services/fxaccounts/tests/xpcshell/test_credentials.js b/services/fxaccounts/tests/xpcshell/test_credentials.js deleted file mode 100644 index cbd9e4c7a..000000000 --- a/services/fxaccounts/tests/xpcshell/test_credentials.js +++ /dev/null @@ -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, ""); -} diff --git a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js deleted file mode 100644 index 64ddb1fd1..000000000 --- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js +++ /dev/null @@ -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); -}); diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_client.js b/services/fxaccounts/tests/xpcshell/test_oauth_client.js deleted file mode 100644 index 9bcb1b1ab..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_client.js +++ /dev/null @@ -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"); -} diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js b/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js deleted file mode 100644 index 710a65ee5..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js +++ /dev/null @@ -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"); -} diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js b/services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js deleted file mode 100644 index bd446513e..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js +++ /dev/null @@ -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(); -} diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js deleted file mode 100644 index 08642846b..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js +++ /dev/null @@ -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); -}); diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js deleted file mode 100644 index f758bf405..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js +++ /dev/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; -}); diff --git a/services/fxaccounts/tests/xpcshell/test_profile.js b/services/fxaccounts/tests/xpcshell/test_profile.js deleted file mode 100644 index 13adf8cbb..000000000 --- a/services/fxaccounts/tests/xpcshell/test_profile.js +++ /dev/null @@ -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; -} diff --git a/services/fxaccounts/tests/xpcshell/test_profile_client.js b/services/fxaccounts/tests/xpcshell/test_profile_client.js deleted file mode 100644 index 20ff6efc6..000000000 --- a/services/fxaccounts/tests/xpcshell/test_profile_client.js +++ /dev/null @@ -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"); -} diff --git a/services/fxaccounts/tests/xpcshell/test_push_service.js b/services/fxaccounts/tests/xpcshell/test_push_service.js deleted file mode 100644 index 8d66f6fa8..000000000 --- a/services/fxaccounts/tests/xpcshell/test_push_service.js +++ /dev/null @@ -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); -}); diff --git a/services/fxaccounts/tests/xpcshell/test_storage_manager.js b/services/fxaccounts/tests/xpcshell/test_storage_manager.js deleted file mode 100644 index 6a293a0ff..000000000 --- a/services/fxaccounts/tests/xpcshell/test_storage_manager.js +++ /dev/null @@ -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(); -} diff --git a/services/fxaccounts/tests/xpcshell/test_web_channel.js b/services/fxaccounts/tests/xpcshell/test_web_channel.js deleted file mode 100644 index 3cf566278..000000000 --- a/services/fxaccounts/tests/xpcshell/test_web_channel.js +++ /dev/null @@ -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"); -} diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.ini b/services/fxaccounts/tests/xpcshell/xpcshell.ini deleted file mode 100644 index 56a3d2947..000000000 --- a/services/fxaccounts/tests/xpcshell/xpcshell.ini +++ /dev/null @@ -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] |