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