summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts/FxAccounts.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'services/fxaccounts/FxAccounts.jsm')
-rw-r--r--services/fxaccounts/FxAccounts.jsm1735
1 files changed, 0 insertions, 1735 deletions
diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm
deleted file mode 100644
index 5bed881ea..000000000
--- a/services/fxaccounts/FxAccounts.jsm
+++ /dev/null
@@ -1,1735 +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 = false;
- try {
- ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
- } catch(e) {
- // Pref doesn't exist
- }
- 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 = false;
- try {
- allowHttp = Services.prefs.getBoolPref("identity.fxaccounts.allowHttp");
- } catch(e) {
- // Pref doesn't exist
- }
- 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;
-});