services/sync/modules/browserid_identity.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = ["BrowserIDManager"];
     9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
    11 Cu.import("resource://gre/modules/Log.jsm");
    12 Cu.import("resource://services-common/async.js");
    13 Cu.import("resource://services-common/utils.js");
    14 Cu.import("resource://services-common/tokenserverclient.js");
    15 Cu.import("resource://services-crypto/utils.js");
    16 Cu.import("resource://services-sync/identity.js");
    17 Cu.import("resource://services-sync/util.js");
    18 Cu.import("resource://services-common/tokenserverclient.js");
    19 Cu.import("resource://gre/modules/Services.jsm");
    20 Cu.import("resource://services-sync/constants.js");
    21 Cu.import("resource://gre/modules/Promise.jsm");
    22 Cu.import("resource://services-sync/stages/cluster.js");
    23 Cu.import("resource://gre/modules/FxAccounts.jsm");
    25 // Lazy imports to prevent unnecessary load on startup.
    26 XPCOMUtils.defineLazyModuleGetter(this, "Weave",
    27                                   "resource://services-sync/main.js");
    29 XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle",
    30                                   "resource://services-sync/keys.js");
    32 XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
    33                                   "resource://gre/modules/FxAccounts.jsm");
    35 XPCOMUtils.defineLazyGetter(this, 'log', function() {
    36   let log = Log.repository.getLogger("Sync.BrowserIDManager");
    37   log.level = Log.Level[Svc.Prefs.get("log.logger.identity")] || Log.Level.Error;
    38   return log;
    39 });
    41 // FxAccountsCommon.js doesn't use a "namespace", so create one here.
    42 let fxAccountsCommon = {};
    43 Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
    45 const OBSERVER_TOPICS = [
    46   fxAccountsCommon.ONLOGIN_NOTIFICATION,
    47   fxAccountsCommon.ONLOGOUT_NOTIFICATION,
    48 ];
    50 const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog";
    52 function deriveKeyBundle(kB) {
    53   let out = CryptoUtils.hkdf(kB, undefined,
    54                              "identity.mozilla.com/picl/v1/oldsync", 2*32);
    55   let bundle = new BulkKeyBundle();
    56   // [encryptionKey, hmacKey]
    57   bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
    58   return bundle;
    59 }
    61 /*
    62   General authentication error for abstracting authentication
    63   errors from multiple sources (e.g., from FxAccounts, TokenServer).
    64   details is additional details about the error - it might be a string, or
    65   some other error object (which should do the right thing when toString() is
    66   called on it)
    67 */
    68 function AuthenticationError(details) {
    69   this.details = details;
    70 }
    72 AuthenticationError.prototype = {
    73   toString: function() {
    74     return "AuthenticationError(" + this.details + ")";
    75   }
    76 }
    78 this.BrowserIDManager = function BrowserIDManager() {
    79   // NOTE: _fxaService and _tokenServerClient are replaced with mocks by
    80   // the test suite.
    81   this._fxaService = fxAccounts;
    82   this._tokenServerClient = new TokenServerClient();
    83   this._tokenServerClient.observerPrefix = "weave:service";
    84   // will be a promise that resolves when we are ready to authenticate
    85   this.whenReadyToAuthenticate = null;
    86   this._log = log;
    87 };
    89 this.BrowserIDManager.prototype = {
    90   __proto__: IdentityManager.prototype,
    92   _fxaService: null,
    93   _tokenServerClient: null,
    94   // https://docs.services.mozilla.com/token/apis.html
    95   _token: null,
    96   _signedInUser: null, // the signedinuser we got from FxAccounts.
    98   // null if no error, otherwise a LOGIN_FAILED_* value that indicates why
    99   // we failed to authenticate (but note it might not be an actual
   100   // authentication problem, just a transient network error or similar)
   101   _authFailureReason: null,
   103   // it takes some time to fetch a sync key bundle, so until this flag is set,
   104   // we don't consider the lack of a keybundle as a failure state.
   105   _shouldHaveSyncKeyBundle: false,
   107   get readyToAuthenticate() {
   108     // We are finished initializing when we *should* have a sync key bundle,
   109     // although we might not actually have one due to auth failures etc.
   110     return this._shouldHaveSyncKeyBundle;
   111   },
   113   get needsCustomization() {
   114     try {
   115       return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
   116     } catch (e) {
   117       return false;
   118     }
   119   },
   121   initialize: function() {
   122     for (let topic of OBSERVER_TOPICS) {
   123       Services.obs.addObserver(this, topic, false);
   124     }
   125     return this.initializeWithCurrentIdentity();
   126   },
   128   /**
   129    * Ensure the user is logged in.  Returns a promise that resolves when
   130    * the user is logged in, or is rejected if the login attempt has failed.
   131    */
   132   ensureLoggedIn: function() {
   133     if (!this._shouldHaveSyncKeyBundle) {
   134       // We are already in the process of logging in.
   135       return this.whenReadyToAuthenticate.promise;
   136     }
   138     // If we are already happy then there is nothing more to do.
   139     if (this._syncKeyBundle) {
   140       return Promise.resolve();
   141     }
   143     // Similarly, if we have a previous failure that implies an explicit
   144     // re-entering of credentials by the user is necessary we don't take any
   145     // further action - an observer will fire when the user does that.
   146     if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
   147       return Promise.reject();
   148     }
   150     // So - we've a previous auth problem and aren't currently attempting to
   151     // log in - so fire that off.
   152     this.initializeWithCurrentIdentity();
   153     return this.whenReadyToAuthenticate.promise;
   154   },
   156   finalize: function() {
   157     // After this is called, we can expect Service.identity != this.
   158     for (let topic of OBSERVER_TOPICS) {
   159       Services.obs.removeObserver(this, topic);
   160     }
   161     this.resetCredentials();
   162     this._signedInUser = null;
   163     return Promise.resolve();
   164   },
   166   offerSyncOptions: function () {
   167     // If the user chose to "Customize sync options" when signing
   168     // up with Firefox Accounts, ask them to choose what to sync.
   169     const url = "chrome://browser/content/sync/customize.xul";
   170     const features = "centerscreen,chrome,modal,dialog,resizable=no";
   171     let win = Services.wm.getMostRecentWindow("navigator:browser");
   173     let data = {accepted: false};
   174     win.openDialog(url, "_blank", features, data);
   176     return data;
   177   },
   179   initializeWithCurrentIdentity: function(isInitialSync=false) {
   180     // While this function returns a promise that resolves once we've started
   181     // the auth process, that process is complete when
   182     // this.whenReadyToAuthenticate.promise resolves.
   183     this._log.trace("initializeWithCurrentIdentity");
   185     // Reset the world before we do anything async.
   186     this.whenReadyToAuthenticate = Promise.defer();
   187     this.whenReadyToAuthenticate.promise.then(null, (err) => {
   188       this._log.error("Could not authenticate: " + err);
   189     });
   191     this._shouldHaveSyncKeyBundle = false;
   192     this._authFailureReason = null;
   194     return this._fxaService.getSignedInUser().then(accountData => {
   195       if (!accountData) {
   196         this._log.info("initializeWithCurrentIdentity has no user logged in");
   197         this.account = null;
   198         // and we are as ready as we can ever be for auth.
   199         this._shouldHaveSyncKeyBundle = true;
   200         this.whenReadyToAuthenticate.reject("no user is logged in");
   201         return;
   202       }
   204       this.account = accountData.email;
   205       this._updateSignedInUser(accountData);
   206       // The user must be verified before we can do anything at all; we kick
   207       // this and the rest of initialization off in the background (ie, we
   208       // don't return the promise)
   209       this._log.info("Waiting for user to be verified.");
   210       this._fxaService.whenVerified(accountData).then(accountData => {
   211         this._updateSignedInUser(accountData);
   212         this._log.info("Starting fetch for key bundle.");
   213         if (this.needsCustomization) {
   214           let data = this.offerSyncOptions();
   215           if (data.accepted) {
   216             Services.prefs.clearUserPref(PREF_SYNC_SHOW_CUSTOMIZATION);
   218             // Mark any non-selected engines as declined.
   219             Weave.Service.engineManager.declineDisabled();
   220           } else {
   221             // Log out if the user canceled the dialog.
   222             return this._fxaService.signOut();
   223           }
   224         }
   225       }).then(() => {
   226         return this._fetchTokenForUser();
   227       }).then(token => {
   228         this._token = token;
   229         this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
   230         this.whenReadyToAuthenticate.resolve();
   231         this._log.info("Background fetch for key bundle done");
   232         Weave.Status.login = LOGIN_SUCCEEDED;
   233         if (isInitialSync) {
   234           this._log.info("Doing initial sync actions");
   235           Svc.Prefs.set("firstSync", "resetClient");
   236           Services.obs.notifyObservers(null, "weave:service:setup-complete", null);
   237           Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
   238         }
   239       }).then(null, err => {
   240         this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
   241         this.whenReadyToAuthenticate.reject(err);
   242         // report what failed...
   243         this._log.error("Background fetch for key bundle failed: " + err);
   244       });
   245       // and we are done - the fetch continues on in the background...
   246     }).then(null, err => {
   247       this._log.error("Processing logged in account: " + err);
   248     });
   249   },
   251   _updateSignedInUser: function(userData) {
   252     // This object should only ever be used for a single user.  It is an
   253     // error to update the data if the user changes (but updates are still
   254     // necessary, as each call may add more attributes to the user).
   255     // We start with no user, so an initial update is always ok.
   256     if (this._signedInUser && this._signedInUser.email != userData.email) {
   257       throw new Error("Attempting to update to a different user.")
   258     }
   259     this._signedInUser = userData;
   260   },
   262   logout: function() {
   263     // This will be called when sync fails (or when the account is being
   264     // unlinked etc).  It may have failed because we got a 401 from a sync
   265     // server, so we nuke the token.  Next time sync runs and wants an
   266     // authentication header, we will notice the lack of the token and fetch a
   267     // new one.
   268     this._token = null;
   269   },
   271   observe: function (subject, topic, data) {
   272     this._log.debug("observed " + topic);
   273     switch (topic) {
   274     case fxAccountsCommon.ONLOGIN_NOTIFICATION:
   275       // This should only happen if we've been initialized without a current
   276       // user - otherwise we'd have seen the LOGOUT notification and been
   277       // thrown away.
   278       // The exception is when we've initialized with a user that needs to
   279       // reauth with the server - in that case we will also get here, but
   280       // should have the same identity.
   281       // initializeWithCurrentIdentity will throw and log if these contraints
   282       // aren't met, so just go ahead and do the init.
   283       this.initializeWithCurrentIdentity(true);
   284       break;
   286     case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
   287       Weave.Service.startOver();
   288       // startOver will cause this instance to be thrown away, so there's
   289       // nothing else to do.
   290       break;
   291     }
   292   },
   294   /**
   295    * Compute the sha256 of the message bytes.  Return bytes.
   296    */
   297   _sha256: function(message) {
   298     let hasher = Cc["@mozilla.org/security/hash;1"]
   299                     .createInstance(Ci.nsICryptoHash);
   300     hasher.init(hasher.SHA256);
   301     return CryptoUtils.digestBytes(message, hasher);
   302   },
   304   /**
   305    * Compute the X-Client-State header given the byte string kB.
   306    *
   307    * Return string: hex(first16Bytes(sha256(kBbytes)))
   308    */
   309   _computeXClientState: function(kBbytes) {
   310     return CommonUtils.bytesAsHex(this._sha256(kBbytes).slice(0, 16), false);
   311   },
   313   /**
   314    * Provide override point for testing token expiration.
   315    */
   316   _now: function() {
   317     return this._fxaService.now()
   318   },
   320   get _localtimeOffsetMsec() {
   321     return this._fxaService.localtimeOffsetMsec;
   322   },
   324   usernameFromAccount: function(val) {
   325     // we don't differentiate between "username" and "account"
   326     return val;
   327   },
   329   /**
   330    * Obtains the HTTP Basic auth password.
   331    *
   332    * Returns a string if set or null if it is not set.
   333    */
   334   get basicPassword() {
   335     this._log.error("basicPassword getter should be not used in BrowserIDManager");
   336     return null;
   337   },
   339   /**
   340    * Set the HTTP basic password to use.
   341    *
   342    * Changes will not persist unless persistSyncCredentials() is called.
   343    */
   344   set basicPassword(value) {
   345     throw "basicPassword setter should be not used in BrowserIDManager";
   346   },
   348   /**
   349    * Obtain the Sync Key.
   350    *
   351    * This returns a 26 character "friendly" Base32 encoded string on success or
   352    * null if no Sync Key could be found.
   353    *
   354    * If the Sync Key hasn't been set in this session, this will look in the
   355    * password manager for the sync key.
   356    */
   357   get syncKey() {
   358     if (this.syncKeyBundle) {
   359       // TODO: This is probably fine because the code shouldn't be
   360       // using the sync key directly (it should use the sync key
   361       // bundle), but I don't like it. We should probably refactor
   362       // code that is inspecting this to not do validation on this
   363       // field directly and instead call a isSyncKeyValid() function
   364       // that we can override.
   365       return "99999999999999999999999999";
   366     }
   367     else {
   368       return null;
   369     }
   370   },
   372   set syncKey(value) {
   373     throw "syncKey setter should be not used in BrowserIDManager";
   374   },
   376   get syncKeyBundle() {
   377     return this._syncKeyBundle;
   378   },
   380   /**
   381    * Resets/Drops all credentials we hold for the current user.
   382    */
   383   resetCredentials: function() {
   384     this.resetSyncKey();
   385     this._token = null;
   386   },
   388   /**
   389    * Resets/Drops the sync key we hold for the current user.
   390    */
   391   resetSyncKey: function() {
   392     this._syncKey = null;
   393     this._syncKeyBundle = null;
   394     this._syncKeyUpdated = true;
   395     this._shouldHaveSyncKeyBundle = false;
   396   },
   398   /**
   399    * The current state of the auth credentials.
   400    *
   401    * This essentially validates that enough credentials are available to use
   402    * Sync.
   403    */
   404   get currentAuthState() {
   405     if (this._authFailureReason) {
   406       this._log.info("currentAuthState returning " + this._authFailureReason +
   407                      " due to previous failure");
   408       return this._authFailureReason;
   409     }
   410     // TODO: need to revisit this. Currently this isn't ready to go until
   411     // both the username and syncKeyBundle are both configured and having no
   412     // username seems to make things fail fast so that's good.
   413     if (!this.username) {
   414       return LOGIN_FAILED_NO_USERNAME;
   415     }
   417     // No need to check this.syncKey as our getter for that attribute
   418     // uses this.syncKeyBundle
   419     // If bundle creation started, but failed.
   420     if (this._shouldHaveSyncKeyBundle && !this.syncKeyBundle) {
   421       return LOGIN_FAILED_NO_PASSPHRASE;
   422     }
   424     return STATUS_OK;
   425   },
   427   /**
   428    * Do we have a non-null, not yet expired token for the user currently
   429    * signed in?
   430    */
   431   hasValidToken: function() {
   432     if (!this._token) {
   433       return false;
   434     }
   435     if (this._token.expiration < this._now()) {
   436       return false;
   437     }
   438     return true;
   439   },
   441   // Refresh the sync token for our user.
   442   _fetchTokenForUser: function() {
   443     let tokenServerURI = Svc.Prefs.get("tokenServerURI");
   444     let log = this._log;
   445     let client = this._tokenServerClient;
   446     let fxa = this._fxaService;
   447     let userData = this._signedInUser;
   449     log.info("Fetching assertion and token from: " + tokenServerURI);
   451     let maybeFetchKeys = () => {
   452       // This is called at login time and every time we need a new token - in
   453       // the latter case we already have kA and kB, so optimise that case.
   454       if (userData.kA && userData.kB) {
   455         return;
   456       }
   457       return this._fxaService.getKeys().then(
   458         newUserData => {
   459           userData = newUserData;
   460           this._updateSignedInUser(userData); // throws if the user changed.
   461         }
   462       );
   463     }
   465     let getToken = (tokenServerURI, assertion) => {
   466       log.debug("Getting a token");
   467       let deferred = Promise.defer();
   468       let cb = function (err, token) {
   469         if (err) {
   470           return deferred.reject(err);
   471         }
   472         log.debug("Successfully got a sync token");
   473         return deferred.resolve(token);
   474       };
   476       let kBbytes = CommonUtils.hexToBytes(userData.kB);
   477       let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
   478       client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
   479       return deferred.promise;
   480     }
   482     let getAssertion = () => {
   483       log.debug("Getting an assertion");
   484       let audience = Services.io.newURI(tokenServerURI, null, null).prePath;
   485       return fxa.getAssertion(audience);
   486     };
   488     // wait until the account email is verified and we know that
   489     // getAssertion() will return a real assertion (not null).
   490     return fxa.whenVerified(this._signedInUser)
   491       .then(() => maybeFetchKeys())
   492       .then(() => getAssertion())
   493       .then(assertion => getToken(tokenServerURI, assertion))
   494       .then(token => {
   495         // TODO: Make it be only 80% of the duration, so refresh the token
   496         // before it actually expires. This is to avoid sync storage errors
   497         // otherwise, we get a nasty notification bar briefly. Bug 966568.
   498         token.expiration = this._now() + (token.duration * 1000) * 0.80;
   499         if (!this._syncKeyBundle) {
   500           // We are given kA/kB as hex.
   501           this._syncKeyBundle = deriveKeyBundle(Utils.hexToBytes(userData.kB));
   502         }
   503         return token;
   504       })
   505       .then(null, err => {
   506         // TODO: unify these errors - we need to handle errors thrown by
   507         // both tokenserverclient and hawkclient.
   508         // A tokenserver error thrown based on a bad response.
   509         if (err.response && err.response.status === 401) {
   510           err = new AuthenticationError(err);
   511         // A hawkclient error.
   512         } else if (err.code === 401) {
   513           err = new AuthenticationError(err);
   514         }
   516         // TODO: write tests to make sure that different auth error cases are handled here
   517         // properly: auth error getting assertion, auth error getting token (invalid generation
   518         // and client-state error)
   519         if (err instanceof AuthenticationError) {
   520           this._log.error("Authentication error in _fetchTokenForUser: " + err);
   521           // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
   522           this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
   523         } else {
   524           this._log.error("Non-authentication error in _fetchTokenForUser: " + err.message);
   525           // for now assume it is just a transient network related problem.
   526           this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
   527         }
   528         // Drop the sync key bundle, but still expect to have one.
   529         // This will arrange for us to be in the right 'currentAuthState'
   530         // such that UI will show the right error.
   531         this._shouldHaveSyncKeyBundle = true;
   532         Weave.Status.login = this._authFailureReason;
   533         Services.obs.notifyObservers(null, "weave:service:login:error", null);
   534         throw err;
   535       });
   536   },
   538   // Returns a promise that is resolved when we have a valid token for the
   539   // current user stored in this._token.  When resolved, this._token is valid.
   540   _ensureValidToken: function() {
   541     if (this.hasValidToken()) {
   542       this._log.debug("_ensureValidToken already has one");
   543       return Promise.resolve();
   544     }
   545     return this._fetchTokenForUser().then(
   546       token => {
   547         this._token = token;
   548       }
   549     );
   550   },
   552   getResourceAuthenticator: function () {
   553     return this._getAuthenticationHeader.bind(this);
   554   },
   556   /**
   557    * Obtain a function to be used for adding auth to RESTRequest instances.
   558    */
   559   getRESTRequestAuthenticator: function() {
   560     return this._addAuthenticationHeader.bind(this);
   561   },
   563   /**
   564    * @return a Hawk HTTP Authorization Header, lightly wrapped, for the .uri
   565    * of a RESTRequest or AsyncResponse object.
   566    */
   567   _getAuthenticationHeader: function(httpObject, method) {
   568     let cb = Async.makeSpinningCallback();
   569     this._ensureValidToken().then(cb, cb);
   570     try {
   571       cb.wait();
   572     } catch (ex) {
   573       this._log.error("Failed to fetch a token for authentication: " + ex);
   574       return null;
   575     }
   576     if (!this._token) {
   577       return null;
   578     }
   579     let credentials = {algorithm: "sha256",
   580                        id: this._token.id,
   581                        key: this._token.key,
   582                       };
   583     method = method || httpObject.method;
   585     // Get the local clock offset from the Firefox Accounts server.  This should
   586     // be close to the offset from the storage server.
   587     let options = {
   588       now: this._now(),
   589       localtimeOffsetMsec: this._localtimeOffsetMsec,
   590       credentials: credentials,
   591     };
   593     let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method, options);
   594     return {headers: {authorization: headerValue.field}};
   595   },
   597   _addAuthenticationHeader: function(request, method) {
   598     let header = this._getAuthenticationHeader(request, method);
   599     if (!header) {
   600       return null;
   601     }
   602     request.setHeader("authorization", header.headers.authorization);
   603     return request;
   604   },
   606   createClusterManager: function(service) {
   607     return new BrowserIDClusterManager(service);
   608   }
   610 };
   612 /* An implementation of the ClusterManager for this identity
   613  */
   615 function BrowserIDClusterManager(service) {
   616   ClusterManager.call(this, service);
   617 }
   619 BrowserIDClusterManager.prototype = {
   620   __proto__: ClusterManager.prototype,
   622   _findCluster: function() {
   623     let endPointFromIdentityToken = function() {
   624       let endpoint = this.identity._token.endpoint;
   625       // For Sync 1.5 storage endpoints, we use the base endpoint verbatim.
   626       // However, it should end in "/" because we will extend it with
   627       // well known path components. So we add a "/" if it's missing.
   628       if (!endpoint.endsWith("/")) {
   629         endpoint += "/";
   630       }
   631       log.debug("_findCluster returning " + endpoint);
   632       return endpoint;
   633     }.bind(this);
   635     // Spinningly ensure we are ready to authenticate and have a valid token.
   636     let promiseClusterURL = function() {
   637       return this.identity.whenReadyToAuthenticate.promise.then(
   638         () => {
   639           // We need to handle node reassignment here.  If we are being asked
   640           // for a clusterURL while the service already has a clusterURL, then
   641           // it's likely a 401 was received using the existing token - in which
   642           // case we just discard the existing token and fetch a new one.
   643           if (this.service.clusterURL) {
   644             log.debug("_findCluster found existing clusterURL, so discarding the current token");
   645             this.identity._token = null;
   646           }
   647           return this.identity._ensureValidToken();
   648         }
   649       ).then(endPointFromIdentityToken
   650       );
   651     }.bind(this);
   653     let cb = Async.makeSpinningCallback();
   654     promiseClusterURL().then(function (clusterURL) {
   655       cb(null, clusterURL);
   656     }).then(
   657       null, err => {
   658       // service.js's verifyLogin() method will attempt to fetch a cluster
   659       // URL when it sees a 401.  If it gets null, it treats it as a "real"
   660       // auth error and sets Status.login to LOGIN_FAILED_LOGIN_REJECTED, which
   661       // in turn causes a notification bar to appear informing the user they
   662       // need to re-authenticate.
   663       // On the other hand, if fetching the cluster URL fails with an exception,
   664       // verifyLogin() assumes it is a transient error, and thus doesn't show
   665       // the notification bar under the assumption the issue will resolve
   666       // itself.
   667       // Thus:
   668       // * On a real 401, we must return null.
   669       // * On any other problem we must let an exception bubble up.
   670       if (err instanceof AuthenticationError) {
   671         // callback with no error and a null result - cb.wait() returns null.
   672         cb(null, null);
   673       } else {
   674         // callback with an error - cb.wait() completes by raising an exception.
   675         cb(err);
   676       }
   677     });
   678     return cb.wait();
   679   },
   681   getUserBaseURL: function() {
   682     // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy
   683     // Sync appends path components onto an empty path, and in FxA Sync the
   684     // token server constructs this for us in an opaque manner. Since the
   685     // cluster manager already sets the clusterURL on Service and also has
   686     // access to the current identity, we added this functionality here.
   687     return this.service.clusterURL;
   688   }
   689 }

mercurial