diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2018-11-30 14:01:51 +0100 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2018-11-30 14:01:51 +0100 |
commit | 5ec8aef6cb2b236f38d5343514d5061ff36ec79d (patch) | |
tree | e0b7d7f75c6645e17c7693445896bd2d5f35edb4 /services/sync/modules/browserid_identity.js | |
parent | 07f9cb86813ccb19d045eb95068cb1c09fdfb230 (diff) | |
download | UXP-5ec8aef6cb2b236f38d5343514d5061ff36ec79d.tar UXP-5ec8aef6cb2b236f38d5343514d5061ff36ec79d.tar.gz UXP-5ec8aef6cb2b236f38d5343514d5061ff36ec79d.tar.lz UXP-5ec8aef6cb2b236f38d5343514d5061ff36ec79d.tar.xz UXP-5ec8aef6cb2b236f38d5343514d5061ff36ec79d.zip |
Remove FxA BrowserIDManager auth
Diffstat (limited to 'services/sync/modules/browserid_identity.js')
-rw-r--r-- | services/sync/modules/browserid_identity.js | 786 |
1 files changed, 0 insertions, 786 deletions
diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js deleted file mode 100644 index 9709b9196..000000000 --- a/services/sync/modules/browserid_identity.js +++ /dev/null @@ -1,786 +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 = ["BrowserIDManager"]; - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/async.js"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://services-common/tokenserverclient.js"); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://services-sync/identity.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-common/tokenserverclient.js"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://services-sync/stages/cluster.js"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); - -// Lazy imports to prevent unnecessary load on startup. -XPCOMUtils.defineLazyModuleGetter(this, "Weave", - "resource://services-sync/main.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle", - "resource://services-sync/keys.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); - -XPCOMUtils.defineLazyGetter(this, 'log', function() { - let log = Log.repository.getLogger("Sync.BrowserIDManager"); - log.level = Log.Level[Svc.Prefs.get("log.logger.identity")] || Log.Level.Error; - return log; -}); - -// FxAccountsCommon.js doesn't use a "namespace", so create one here. -var fxAccountsCommon = {}; -Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon); - -const OBSERVER_TOPICS = [ - fxAccountsCommon.ONLOGIN_NOTIFICATION, - fxAccountsCommon.ONLOGOUT_NOTIFICATION, -]; - -const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog"; - -function deriveKeyBundle(kB) { - let out = CryptoUtils.hkdf(kB, undefined, - "identity.mozilla.com/picl/v1/oldsync", 2*32); - let bundle = new BulkKeyBundle(); - // [encryptionKey, hmacKey] - bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)]; - return bundle; -} - -/* - General authentication error for abstracting authentication - errors from multiple sources (e.g., from FxAccounts, TokenServer). - details is additional details about the error - it might be a string, or - some other error object (which should do the right thing when toString() is - called on it) -*/ -function AuthenticationError(details) { - this.details = details; -} - -AuthenticationError.prototype = { - toString: function() { - return "AuthenticationError(" + this.details + ")"; - } -} - -this.BrowserIDManager = function BrowserIDManager() { - // NOTE: _fxaService and _tokenServerClient are replaced with mocks by - // the test suite. - this._fxaService = fxAccounts; - this._tokenServerClient = new TokenServerClient(); - this._tokenServerClient.observerPrefix = "weave:service"; - // will be a promise that resolves when we are ready to authenticate - this.whenReadyToAuthenticate = null; - this._log = log; -}; - -this.BrowserIDManager.prototype = { - __proto__: IdentityManager.prototype, - - _fxaService: null, - _tokenServerClient: null, - // https://docs.services.mozilla.com/token/apis.html - _token: null, - _signedInUser: null, // the signedinuser we got from FxAccounts. - - // null if no error, otherwise a LOGIN_FAILED_* value that indicates why - // we failed to authenticate (but note it might not be an actual - // authentication problem, just a transient network error or similar) - _authFailureReason: null, - - // it takes some time to fetch a sync key bundle, so until this flag is set, - // we don't consider the lack of a keybundle as a failure state. - _shouldHaveSyncKeyBundle: false, - - get readyToAuthenticate() { - // We are finished initializing when we *should* have a sync key bundle, - // although we might not actually have one due to auth failures etc. - return this._shouldHaveSyncKeyBundle; - }, - - get needsCustomization() { - try { - return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION); - } catch (e) { - return false; - } - }, - - initialize: function() { - for (let topic of OBSERVER_TOPICS) { - Services.obs.addObserver(this, topic, false); - } - return this.initializeWithCurrentIdentity(); - }, - - /** - * Ensure the user is logged in. Returns a promise that resolves when - * the user is logged in, or is rejected if the login attempt has failed. - */ - ensureLoggedIn: function() { - if (!this._shouldHaveSyncKeyBundle) { - // We are already in the process of logging in. - return this.whenReadyToAuthenticate.promise; - } - - // If we are already happy then there is nothing more to do. - if (this._syncKeyBundle) { - return Promise.resolve(); - } - - // Similarly, if we have a previous failure that implies an explicit - // re-entering of credentials by the user is necessary we don't take any - // further action - an observer will fire when the user does that. - if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) { - return Promise.reject(new Error("User needs to re-authenticate")); - } - - // So - we've a previous auth problem and aren't currently attempting to - // log in - so fire that off. - this.initializeWithCurrentIdentity(); - return this.whenReadyToAuthenticate.promise; - }, - - finalize: function() { - // After this is called, we can expect Service.identity != this. - for (let topic of OBSERVER_TOPICS) { - Services.obs.removeObserver(this, topic); - } - this.resetCredentials(); - this._signedInUser = null; - return Promise.resolve(); - }, - - offerSyncOptions: function () { - // If the user chose to "Customize sync options" when signing - // up with Firefox Accounts, ask them to choose what to sync. - const url = "chrome://browser/content/sync/customize.xul"; - const features = "centerscreen,chrome,modal,dialog,resizable=no"; - let win = Services.wm.getMostRecentWindow("navigator:browser"); - - let data = {accepted: false}; - win.openDialog(url, "_blank", features, data); - - return data; - }, - - initializeWithCurrentIdentity: function(isInitialSync=false) { - // While this function returns a promise that resolves once we've started - // the auth process, that process is complete when - // this.whenReadyToAuthenticate.promise resolves. - this._log.trace("initializeWithCurrentIdentity"); - - // Reset the world before we do anything async. - this.whenReadyToAuthenticate = Promise.defer(); - this.whenReadyToAuthenticate.promise.then(null, (err) => { - this._log.error("Could not authenticate", err); - }); - - // initializeWithCurrentIdentity() can be called after the - // identity module was first initialized, e.g., after the - // user completes a force authentication, so we should make - // sure all credentials are reset before proceeding. - this.resetCredentials(); - this._authFailureReason = null; - - return this._fxaService.getSignedInUser().then(accountData => { - if (!accountData) { - this._log.info("initializeWithCurrentIdentity has no user logged in"); - this.account = null; - // and we are as ready as we can ever be for auth. - this._shouldHaveSyncKeyBundle = true; - this.whenReadyToAuthenticate.reject("no user is logged in"); - return; - } - - this.account = accountData.email; - this._updateSignedInUser(accountData); - // The user must be verified before we can do anything at all; we kick - // this and the rest of initialization off in the background (ie, we - // don't return the promise) - this._log.info("Waiting for user to be verified."); - this._fxaService.whenVerified(accountData).then(accountData => { - this._updateSignedInUser(accountData); - this._log.info("Starting fetch for key bundle."); - if (this.needsCustomization) { - let data = this.offerSyncOptions(); - if (data.accepted) { - Services.prefs.clearUserPref(PREF_SYNC_SHOW_CUSTOMIZATION); - - // Mark any non-selected engines as declined. - Weave.Service.engineManager.declineDisabled(); - } else { - // Log out if the user canceled the dialog. - return this._fxaService.signOut(); - } - } - }).then(() => { - return this._fetchTokenForUser(); - }).then(token => { - this._token = token; - this._shouldHaveSyncKeyBundle = true; // and we should actually have one... - this.whenReadyToAuthenticate.resolve(); - this._log.info("Background fetch for key bundle done"); - Weave.Status.login = LOGIN_SUCCEEDED; - if (isInitialSync) { - this._log.info("Doing initial sync actions"); - Svc.Prefs.set("firstSync", "resetClient"); - Services.obs.notifyObservers(null, "weave:service:setup-complete", null); - Weave.Utils.nextTick(Weave.Service.sync, Weave.Service); - } - }).then(null, err => { - this._shouldHaveSyncKeyBundle = true; // but we probably don't have one... - this.whenReadyToAuthenticate.reject(err); - // report what failed... - this._log.error("Background fetch for key bundle failed", err); - }); - // and we are done - the fetch continues on in the background... - }).then(null, err => { - this._log.error("Processing logged in account", err); - }); - }, - - _updateSignedInUser: function(userData) { - // This object should only ever be used for a single user. It is an - // error to update the data if the user changes (but updates are still - // necessary, as each call may add more attributes to the user). - // We start with no user, so an initial update is always ok. - if (this._signedInUser && this._signedInUser.email != userData.email) { - throw new Error("Attempting to update to a different user.") - } - this._signedInUser = userData; - }, - - logout: function() { - // This will be called when sync fails (or when the account is being - // unlinked etc). It may have failed because we got a 401 from a sync - // server, so we nuke the token. Next time sync runs and wants an - // authentication header, we will notice the lack of the token and fetch a - // new one. - this._token = null; - }, - - observe: function (subject, topic, data) { - this._log.debug("observed " + topic); - switch (topic) { - case fxAccountsCommon.ONLOGIN_NOTIFICATION: - // This should only happen if we've been initialized without a current - // user - otherwise we'd have seen the LOGOUT notification and been - // thrown away. - // The exception is when we've initialized with a user that needs to - // reauth with the server - in that case we will also get here, but - // should have the same identity. - // initializeWithCurrentIdentity will throw and log if these constraints - // aren't met, so just go ahead and do the init. - this.initializeWithCurrentIdentity(true); - break; - - case fxAccountsCommon.ONLOGOUT_NOTIFICATION: - Weave.Service.startOver(); - // startOver will cause this instance to be thrown away, so there's - // nothing else to do. - break; - } - }, - - /** - * Compute the sha256 of the message bytes. Return bytes. - */ - _sha256: function(message) { - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA256); - return CryptoUtils.digestBytes(message, hasher); - }, - - /** - * Compute the X-Client-State header given the byte string kB. - * - * Return string: hex(first16Bytes(sha256(kBbytes))) - */ - _computeXClientState: function(kBbytes) { - return CommonUtils.bytesAsHex(this._sha256(kBbytes).slice(0, 16), false); - }, - - /** - * Provide override point for testing token expiration. - */ - _now: function() { - return this._fxaService.now() - }, - - get _localtimeOffsetMsec() { - return this._fxaService.localtimeOffsetMsec; - }, - - usernameFromAccount: function(val) { - // we don't differentiate between "username" and "account" - return val; - }, - - /** - * Obtains the HTTP Basic auth password. - * - * Returns a string if set or null if it is not set. - */ - get basicPassword() { - this._log.error("basicPassword getter should be not used in BrowserIDManager"); - return null; - }, - - /** - * Set the HTTP basic password to use. - * - * Changes will not persist unless persistSyncCredentials() is called. - */ - set basicPassword(value) { - throw "basicPassword setter should be not used in BrowserIDManager"; - }, - - /** - * Obtain the Sync Key. - * - * This returns a 26 character "friendly" Base32 encoded string on success or - * null if no Sync Key could be found. - * - * If the Sync Key hasn't been set in this session, this will look in the - * password manager for the sync key. - */ - get syncKey() { - if (this.syncKeyBundle) { - // TODO: This is probably fine because the code shouldn't be - // using the sync key directly (it should use the sync key - // bundle), but I don't like it. We should probably refactor - // code that is inspecting this to not do validation on this - // field directly and instead call a isSyncKeyValid() function - // that we can override. - return "99999999999999999999999999"; - } - else { - return null; - } - }, - - set syncKey(value) { - throw "syncKey setter should be not used in BrowserIDManager"; - }, - - get syncKeyBundle() { - return this._syncKeyBundle; - }, - - /** - * Resets/Drops all credentials we hold for the current user. - */ - resetCredentials: function() { - this.resetSyncKey(); - this._token = null; - }, - - /** - * Resets/Drops the sync key we hold for the current user. - */ - resetSyncKey: function() { - this._syncKey = null; - this._syncKeyBundle = null; - this._syncKeyUpdated = true; - this._shouldHaveSyncKeyBundle = false; - }, - - /** - * Pre-fetches any information that might help with migration away from this - * identity. Called after every sync and is really just an optimization that - * allows us to avoid a network request for when we actually need the - * migration info. - */ - prefetchMigrationSentinel: function(service) { - // nothing to do here until we decide to migrate away from FxA. - }, - - /** - * Return credentials hosts for this identity only. - */ - _getSyncCredentialsHosts: function() { - return Utils.getSyncCredentialsHostsFxA(); - }, - - /** - * The current state of the auth credentials. - * - * This essentially validates that enough credentials are available to use - * Sync. It doesn't check we have all the keys we need as the master-password - * may have been locked when we tried to get them - we rely on - * unlockAndVerifyAuthState to check that for us. - */ - get currentAuthState() { - if (this._authFailureReason) { - this._log.info("currentAuthState returning " + this._authFailureReason + - " due to previous failure"); - return this._authFailureReason; - } - // TODO: need to revisit this. Currently this isn't ready to go until - // both the username and syncKeyBundle are both configured and having no - // username seems to make things fail fast so that's good. - if (!this.username) { - return LOGIN_FAILED_NO_USERNAME; - } - - return STATUS_OK; - }, - - // Do we currently have keys, or do we have enough that we should be able - // to successfully fetch them? - _canFetchKeys: function() { - let userData = this._signedInUser; - // a keyFetchToken means we can almost certainly grab them. - // kA and kB means we already have them. - return userData && (userData.keyFetchToken || (userData.kA && userData.kB)); - }, - - /** - * Verify the current auth state, unlocking the master-password if necessary. - * - * Returns a promise that resolves with the current auth state after - * attempting to unlock. - */ - unlockAndVerifyAuthState: function() { - if (this._canFetchKeys()) { - log.debug("unlockAndVerifyAuthState already has (or can fetch) sync keys"); - return Promise.resolve(STATUS_OK); - } - // so no keys - ensure MP unlocked. - if (!Utils.ensureMPUnlocked()) { - // user declined to unlock, so we don't know if they are stored there. - log.debug("unlockAndVerifyAuthState: user declined to unlock master-password"); - return Promise.resolve(MASTER_PASSWORD_LOCKED); - } - // now we are unlocked we must re-fetch the user data as we may now have - // the details that were previously locked away. - return this._fxaService.getSignedInUser().then( - accountData => { - this._updateSignedInUser(accountData); - // If we still can't get keys it probably means the user authenticated - // without unlocking the MP or cleared the saved logins, so we've now - // lost them - the user will need to reauth before continuing. - let result = this._canFetchKeys() ? STATUS_OK : LOGIN_FAILED_LOGIN_REJECTED; - log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result); - return result; - } - ); - }, - - /** - * Do we have a non-null, not yet expired token for the user currently - * signed in? - */ - hasValidToken: function() { - // If pref is set to ignore cached authentication credentials for debugging, - // then return false to force the fetching of a new token. - let ignoreCachedAuthCredentials = false; - try { - ignoreCachedAuthCredentials = Svc.Prefs.get("debug.ignoreCachedAuthCredentials"); - } catch(e) { - // Pref doesn't exist - } - if (ignoreCachedAuthCredentials) { - return false; - } - if (!this._token) { - return false; - } - if (this._token.expiration < this._now()) { - return false; - } - return true; - }, - - // Refresh the sync token for our user. Returns a promise that resolves - // with a token (which may be null in one sad edge-case), or rejects with an - // error. - _fetchTokenForUser: function() { - let tokenServerURI = Svc.Prefs.get("tokenServerURI"); - if (tokenServerURI.endsWith("/")) { // trailing slashes cause problems... - tokenServerURI = tokenServerURI.slice(0, -1); - } - let log = this._log; - let client = this._tokenServerClient; - let fxa = this._fxaService; - let userData = this._signedInUser; - - // We need kA and kB for things to work. If we don't have them, just - // return null for the token - sync calling unlockAndVerifyAuthState() - // before actually syncing will setup the error states if necessary. - if (!this._canFetchKeys()) { - log.info("Unable to fetch keys (master-password locked?), so aborting token fetch"); - return Promise.resolve(null); - } - - let maybeFetchKeys = () => { - // This is called at login time and every time we need a new token - in - // the latter case we already have kA and kB, so optimise that case. - if (userData.kA && userData.kB) { - return; - } - log.info("Fetching new keys"); - return this._fxaService.getKeys().then( - newUserData => { - userData = newUserData; - this._updateSignedInUser(userData); // throws if the user changed. - } - ); - } - - let getToken = (tokenServerURI, assertion) => { - log.debug("Getting a token"); - let deferred = Promise.defer(); - let cb = function (err, token) { - if (err) { - return deferred.reject(err); - } - log.debug("Successfully got a sync token"); - return deferred.resolve(token); - }; - - let kBbytes = CommonUtils.hexToBytes(userData.kB); - let headers = {"X-Client-State": this._computeXClientState(kBbytes)}; - client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers); - return deferred.promise; - } - - let getAssertion = () => { - log.info("Getting an assertion from", tokenServerURI); - let audience = Services.io.newURI(tokenServerURI, null, null).prePath; - return fxa.getAssertion(audience); - }; - - // wait until the account email is verified and we know that - // getAssertion() will return a real assertion (not null). - return fxa.whenVerified(this._signedInUser) - .then(() => maybeFetchKeys()) - .then(() => getAssertion()) - .then(assertion => getToken(tokenServerURI, assertion)) - .then(token => { - // TODO: Make it be only 80% of the duration, so refresh the token - // before it actually expires. This is to avoid sync storage errors - // otherwise, we get a nasty notification bar briefly. Bug 966568. - token.expiration = this._now() + (token.duration * 1000) * 0.80; - if (!this._syncKeyBundle) { - // We are given kA/kB as hex. - this._syncKeyBundle = deriveKeyBundle(Utils.hexToBytes(userData.kB)); - } - return token; - }) - .then(null, err => { - // TODO: unify these errors - we need to handle errors thrown by - // both tokenserverclient and hawkclient. - // A tokenserver error thrown based on a bad response. - if (err.response && err.response.status === 401) { - err = new AuthenticationError(err); - // A hawkclient error. - } else if (err.code && err.code === 401) { - err = new AuthenticationError(err); - } - - // TODO: write tests to make sure that different auth error cases are handled here - // properly: auth error getting assertion, auth error getting token (invalid generation - // and client-state error) - if (err instanceof AuthenticationError) { - this._log.error("Authentication error in _fetchTokenForUser", err); - // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason. - this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED; - } else { - this._log.error("Non-authentication error in _fetchTokenForUser", err); - // for now assume it is just a transient network related problem - // (although sadly, it might also be a regular unhandled exception) - this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR; - } - // this._authFailureReason being set to be non-null in the above if clause - // ensures we are in the correct currentAuthState, and - // this._shouldHaveSyncKeyBundle being true ensures everything that cares knows - // that there is no authentication dance still under way. - this._shouldHaveSyncKeyBundle = true; - Weave.Status.login = this._authFailureReason; - Services.obs.notifyObservers(null, "weave:ui:login:error", null); - throw err; - }); - }, - - // Returns a promise that is resolved when we have a valid token for the - // current user stored in this._token. When resolved, this._token is valid. - _ensureValidToken: function() { - if (this.hasValidToken()) { - this._log.debug("_ensureValidToken already has one"); - return Promise.resolve(); - } - // reset this._token as a safety net to reduce the possibility of us - // repeatedly attempting to use an invalid token if _fetchTokenForUser throws. - this._token = null; - return this._fetchTokenForUser().then( - token => { - this._token = token; - } - ); - }, - - getResourceAuthenticator: function () { - return this._getAuthenticationHeader.bind(this); - }, - - /** - * Obtain a function to be used for adding auth to RESTRequest instances. - */ - getRESTRequestAuthenticator: function() { - return this._addAuthenticationHeader.bind(this); - }, - - /** - * @return a Hawk HTTP Authorization Header, lightly wrapped, for the .uri - * of a RESTRequest or AsyncResponse object. - */ - _getAuthenticationHeader: function(httpObject, method) { - let cb = Async.makeSpinningCallback(); - this._ensureValidToken().then(cb, cb); - try { - cb.wait(); - } catch (ex) { - this._log.error("Failed to fetch a token for authentication", ex); - return null; - } - if (!this._token) { - return null; - } - let credentials = {algorithm: "sha256", - id: this._token.id, - key: this._token.key, - }; - method = method || httpObject.method; - - // Get the local clock offset from the Firefox Accounts server. This should - // be close to the offset from the storage server. - let options = { - now: this._now(), - localtimeOffsetMsec: this._localtimeOffsetMsec, - credentials: credentials, - }; - - let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method, options); - return {headers: {authorization: headerValue.field}}; - }, - - _addAuthenticationHeader: function(request, method) { - let header = this._getAuthenticationHeader(request, method); - if (!header) { - return null; - } - request.setHeader("authorization", header.headers.authorization); - return request; - }, - - createClusterManager: function(service) { - return new BrowserIDClusterManager(service); - } - -}; - -/* An implementation of the ClusterManager for this identity - */ - -function BrowserIDClusterManager(service) { - ClusterManager.call(this, service); -} - -BrowserIDClusterManager.prototype = { - __proto__: ClusterManager.prototype, - - _findCluster: function() { - let endPointFromIdentityToken = function() { - // The only reason (in theory ;) that we can end up with a null token - // is when this.identity._canFetchKeys() returned false. In turn, this - // should only happen if the master-password is locked or the credentials - // storage is screwed, and in those cases we shouldn't have started - // syncing so shouldn't get here anyway. - // But better safe than sorry! To keep things clearer, throw an explicit - // exception - the message will appear in the logs and the error will be - // treated as transient. - if (!this.identity._token) { - throw new Error("Can't get a cluster URL as we can't fetch keys."); - } - let endpoint = this.identity._token.endpoint; - // For Sync 1.5 storage endpoints, we use the base endpoint verbatim. - // However, it should end in "/" because we will extend it with - // well known path components. So we add a "/" if it's missing. - if (!endpoint.endsWith("/")) { - endpoint += "/"; - } - log.debug("_findCluster returning " + endpoint); - return endpoint; - }.bind(this); - - // Spinningly ensure we are ready to authenticate and have a valid token. - let promiseClusterURL = function() { - return this.identity.whenReadyToAuthenticate.promise.then( - () => { - // We need to handle node reassignment here. If we are being asked - // for a clusterURL while the service already has a clusterURL, then - // it's likely a 401 was received using the existing token - in which - // case we just discard the existing token and fetch a new one. - if (this.service.clusterURL) { - log.debug("_findCluster found existing clusterURL, so discarding the current token"); - this.identity._token = null; - } - return this.identity._ensureValidToken(); - } - ).then(endPointFromIdentityToken - ); - }.bind(this); - - let cb = Async.makeSpinningCallback(); - promiseClusterURL().then(function (clusterURL) { - cb(null, clusterURL); - }).then( - null, err => { - log.info("Failed to fetch the cluster URL", err); - // service.js's verifyLogin() method will attempt to fetch a cluster - // URL when it sees a 401. If it gets null, it treats it as a "real" - // auth error and sets Status.login to LOGIN_FAILED_LOGIN_REJECTED, which - // in turn causes a notification bar to appear informing the user they - // need to re-authenticate. - // On the other hand, if fetching the cluster URL fails with an exception, - // verifyLogin() assumes it is a transient error, and thus doesn't show - // the notification bar under the assumption the issue will resolve - // itself. - // Thus: - // * On a real 401, we must return null. - // * On any other problem we must let an exception bubble up. - if (err instanceof AuthenticationError) { - // callback with no error and a null result - cb.wait() returns null. - cb(null, null); - } else { - // callback with an error - cb.wait() completes by raising an exception. - cb(err); - } - }); - return cb.wait(); - }, - - getUserBaseURL: function() { - // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy - // Sync appends path components onto an empty path, and in FxA Sync the - // token server constructs this for us in an opaque manner. Since the - // cluster manager already sets the clusterURL on Service and also has - // access to the current identity, we added this functionality here. - return this.service.clusterURL; - } -} |