services/sync/modules/browserid_identity.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/sync/modules/browserid_identity.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,689 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = ["BrowserIDManager"];
    1.11 +
    1.12 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
    1.13 +
    1.14 +Cu.import("resource://gre/modules/Log.jsm");
    1.15 +Cu.import("resource://services-common/async.js");
    1.16 +Cu.import("resource://services-common/utils.js");
    1.17 +Cu.import("resource://services-common/tokenserverclient.js");
    1.18 +Cu.import("resource://services-crypto/utils.js");
    1.19 +Cu.import("resource://services-sync/identity.js");
    1.20 +Cu.import("resource://services-sync/util.js");
    1.21 +Cu.import("resource://services-common/tokenserverclient.js");
    1.22 +Cu.import("resource://gre/modules/Services.jsm");
    1.23 +Cu.import("resource://services-sync/constants.js");
    1.24 +Cu.import("resource://gre/modules/Promise.jsm");
    1.25 +Cu.import("resource://services-sync/stages/cluster.js");
    1.26 +Cu.import("resource://gre/modules/FxAccounts.jsm");
    1.27 +
    1.28 +// Lazy imports to prevent unnecessary load on startup.
    1.29 +XPCOMUtils.defineLazyModuleGetter(this, "Weave",
    1.30 +                                  "resource://services-sync/main.js");
    1.31 +
    1.32 +XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle",
    1.33 +                                  "resource://services-sync/keys.js");
    1.34 +
    1.35 +XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
    1.36 +                                  "resource://gre/modules/FxAccounts.jsm");
    1.37 +
    1.38 +XPCOMUtils.defineLazyGetter(this, 'log', function() {
    1.39 +  let log = Log.repository.getLogger("Sync.BrowserIDManager");
    1.40 +  log.level = Log.Level[Svc.Prefs.get("log.logger.identity")] || Log.Level.Error;
    1.41 +  return log;
    1.42 +});
    1.43 +
    1.44 +// FxAccountsCommon.js doesn't use a "namespace", so create one here.
    1.45 +let fxAccountsCommon = {};
    1.46 +Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
    1.47 +
    1.48 +const OBSERVER_TOPICS = [
    1.49 +  fxAccountsCommon.ONLOGIN_NOTIFICATION,
    1.50 +  fxAccountsCommon.ONLOGOUT_NOTIFICATION,
    1.51 +];
    1.52 +
    1.53 +const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog";
    1.54 +
    1.55 +function deriveKeyBundle(kB) {
    1.56 +  let out = CryptoUtils.hkdf(kB, undefined,
    1.57 +                             "identity.mozilla.com/picl/v1/oldsync", 2*32);
    1.58 +  let bundle = new BulkKeyBundle();
    1.59 +  // [encryptionKey, hmacKey]
    1.60 +  bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
    1.61 +  return bundle;
    1.62 +}
    1.63 +
    1.64 +/*
    1.65 +  General authentication error for abstracting authentication
    1.66 +  errors from multiple sources (e.g., from FxAccounts, TokenServer).
    1.67 +  details is additional details about the error - it might be a string, or
    1.68 +  some other error object (which should do the right thing when toString() is
    1.69 +  called on it)
    1.70 +*/
    1.71 +function AuthenticationError(details) {
    1.72 +  this.details = details;
    1.73 +}
    1.74 +
    1.75 +AuthenticationError.prototype = {
    1.76 +  toString: function() {
    1.77 +    return "AuthenticationError(" + this.details + ")";
    1.78 +  }
    1.79 +}
    1.80 +
    1.81 +this.BrowserIDManager = function BrowserIDManager() {
    1.82 +  // NOTE: _fxaService and _tokenServerClient are replaced with mocks by
    1.83 +  // the test suite.
    1.84 +  this._fxaService = fxAccounts;
    1.85 +  this._tokenServerClient = new TokenServerClient();
    1.86 +  this._tokenServerClient.observerPrefix = "weave:service";
    1.87 +  // will be a promise that resolves when we are ready to authenticate
    1.88 +  this.whenReadyToAuthenticate = null;
    1.89 +  this._log = log;
    1.90 +};
    1.91 +
    1.92 +this.BrowserIDManager.prototype = {
    1.93 +  __proto__: IdentityManager.prototype,
    1.94 +
    1.95 +  _fxaService: null,
    1.96 +  _tokenServerClient: null,
    1.97 +  // https://docs.services.mozilla.com/token/apis.html
    1.98 +  _token: null,
    1.99 +  _signedInUser: null, // the signedinuser we got from FxAccounts.
   1.100 +
   1.101 +  // null if no error, otherwise a LOGIN_FAILED_* value that indicates why
   1.102 +  // we failed to authenticate (but note it might not be an actual
   1.103 +  // authentication problem, just a transient network error or similar)
   1.104 +  _authFailureReason: null,
   1.105 +
   1.106 +  // it takes some time to fetch a sync key bundle, so until this flag is set,
   1.107 +  // we don't consider the lack of a keybundle as a failure state.
   1.108 +  _shouldHaveSyncKeyBundle: false,
   1.109 +
   1.110 +  get readyToAuthenticate() {
   1.111 +    // We are finished initializing when we *should* have a sync key bundle,
   1.112 +    // although we might not actually have one due to auth failures etc.
   1.113 +    return this._shouldHaveSyncKeyBundle;
   1.114 +  },
   1.115 +
   1.116 +  get needsCustomization() {
   1.117 +    try {
   1.118 +      return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
   1.119 +    } catch (e) {
   1.120 +      return false;
   1.121 +    }
   1.122 +  },
   1.123 +
   1.124 +  initialize: function() {
   1.125 +    for (let topic of OBSERVER_TOPICS) {
   1.126 +      Services.obs.addObserver(this, topic, false);
   1.127 +    }
   1.128 +    return this.initializeWithCurrentIdentity();
   1.129 +  },
   1.130 +
   1.131 +  /**
   1.132 +   * Ensure the user is logged in.  Returns a promise that resolves when
   1.133 +   * the user is logged in, or is rejected if the login attempt has failed.
   1.134 +   */
   1.135 +  ensureLoggedIn: function() {
   1.136 +    if (!this._shouldHaveSyncKeyBundle) {
   1.137 +      // We are already in the process of logging in.
   1.138 +      return this.whenReadyToAuthenticate.promise;
   1.139 +    }
   1.140 +
   1.141 +    // If we are already happy then there is nothing more to do.
   1.142 +    if (this._syncKeyBundle) {
   1.143 +      return Promise.resolve();
   1.144 +    }
   1.145 +
   1.146 +    // Similarly, if we have a previous failure that implies an explicit
   1.147 +    // re-entering of credentials by the user is necessary we don't take any
   1.148 +    // further action - an observer will fire when the user does that.
   1.149 +    if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
   1.150 +      return Promise.reject();
   1.151 +    }
   1.152 +
   1.153 +    // So - we've a previous auth problem and aren't currently attempting to
   1.154 +    // log in - so fire that off.
   1.155 +    this.initializeWithCurrentIdentity();
   1.156 +    return this.whenReadyToAuthenticate.promise;
   1.157 +  },
   1.158 +
   1.159 +  finalize: function() {
   1.160 +    // After this is called, we can expect Service.identity != this.
   1.161 +    for (let topic of OBSERVER_TOPICS) {
   1.162 +      Services.obs.removeObserver(this, topic);
   1.163 +    }
   1.164 +    this.resetCredentials();
   1.165 +    this._signedInUser = null;
   1.166 +    return Promise.resolve();
   1.167 +  },
   1.168 +
   1.169 +  offerSyncOptions: function () {
   1.170 +    // If the user chose to "Customize sync options" when signing
   1.171 +    // up with Firefox Accounts, ask them to choose what to sync.
   1.172 +    const url = "chrome://browser/content/sync/customize.xul";
   1.173 +    const features = "centerscreen,chrome,modal,dialog,resizable=no";
   1.174 +    let win = Services.wm.getMostRecentWindow("navigator:browser");
   1.175 +
   1.176 +    let data = {accepted: false};
   1.177 +    win.openDialog(url, "_blank", features, data);
   1.178 +
   1.179 +    return data;
   1.180 +  },
   1.181 +
   1.182 +  initializeWithCurrentIdentity: function(isInitialSync=false) {
   1.183 +    // While this function returns a promise that resolves once we've started
   1.184 +    // the auth process, that process is complete when
   1.185 +    // this.whenReadyToAuthenticate.promise resolves.
   1.186 +    this._log.trace("initializeWithCurrentIdentity");
   1.187 +
   1.188 +    // Reset the world before we do anything async.
   1.189 +    this.whenReadyToAuthenticate = Promise.defer();
   1.190 +    this.whenReadyToAuthenticate.promise.then(null, (err) => {
   1.191 +      this._log.error("Could not authenticate: " + err);
   1.192 +    });
   1.193 +
   1.194 +    this._shouldHaveSyncKeyBundle = false;
   1.195 +    this._authFailureReason = null;
   1.196 +
   1.197 +    return this._fxaService.getSignedInUser().then(accountData => {
   1.198 +      if (!accountData) {
   1.199 +        this._log.info("initializeWithCurrentIdentity has no user logged in");
   1.200 +        this.account = null;
   1.201 +        // and we are as ready as we can ever be for auth.
   1.202 +        this._shouldHaveSyncKeyBundle = true;
   1.203 +        this.whenReadyToAuthenticate.reject("no user is logged in");
   1.204 +        return;
   1.205 +      }
   1.206 +
   1.207 +      this.account = accountData.email;
   1.208 +      this._updateSignedInUser(accountData);
   1.209 +      // The user must be verified before we can do anything at all; we kick
   1.210 +      // this and the rest of initialization off in the background (ie, we
   1.211 +      // don't return the promise)
   1.212 +      this._log.info("Waiting for user to be verified.");
   1.213 +      this._fxaService.whenVerified(accountData).then(accountData => {
   1.214 +        this._updateSignedInUser(accountData);
   1.215 +        this._log.info("Starting fetch for key bundle.");
   1.216 +        if (this.needsCustomization) {
   1.217 +          let data = this.offerSyncOptions();
   1.218 +          if (data.accepted) {
   1.219 +            Services.prefs.clearUserPref(PREF_SYNC_SHOW_CUSTOMIZATION);
   1.220 +
   1.221 +            // Mark any non-selected engines as declined.
   1.222 +            Weave.Service.engineManager.declineDisabled();
   1.223 +          } else {
   1.224 +            // Log out if the user canceled the dialog.
   1.225 +            return this._fxaService.signOut();
   1.226 +          }
   1.227 +        }
   1.228 +      }).then(() => {
   1.229 +        return this._fetchTokenForUser();
   1.230 +      }).then(token => {
   1.231 +        this._token = token;
   1.232 +        this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
   1.233 +        this.whenReadyToAuthenticate.resolve();
   1.234 +        this._log.info("Background fetch for key bundle done");
   1.235 +        Weave.Status.login = LOGIN_SUCCEEDED;
   1.236 +        if (isInitialSync) {
   1.237 +          this._log.info("Doing initial sync actions");
   1.238 +          Svc.Prefs.set("firstSync", "resetClient");
   1.239 +          Services.obs.notifyObservers(null, "weave:service:setup-complete", null);
   1.240 +          Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
   1.241 +        }
   1.242 +      }).then(null, err => {
   1.243 +        this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
   1.244 +        this.whenReadyToAuthenticate.reject(err);
   1.245 +        // report what failed...
   1.246 +        this._log.error("Background fetch for key bundle failed: " + err);
   1.247 +      });
   1.248 +      // and we are done - the fetch continues on in the background...
   1.249 +    }).then(null, err => {
   1.250 +      this._log.error("Processing logged in account: " + err);
   1.251 +    });
   1.252 +  },
   1.253 +
   1.254 +  _updateSignedInUser: function(userData) {
   1.255 +    // This object should only ever be used for a single user.  It is an
   1.256 +    // error to update the data if the user changes (but updates are still
   1.257 +    // necessary, as each call may add more attributes to the user).
   1.258 +    // We start with no user, so an initial update is always ok.
   1.259 +    if (this._signedInUser && this._signedInUser.email != userData.email) {
   1.260 +      throw new Error("Attempting to update to a different user.")
   1.261 +    }
   1.262 +    this._signedInUser = userData;
   1.263 +  },
   1.264 +
   1.265 +  logout: function() {
   1.266 +    // This will be called when sync fails (or when the account is being
   1.267 +    // unlinked etc).  It may have failed because we got a 401 from a sync
   1.268 +    // server, so we nuke the token.  Next time sync runs and wants an
   1.269 +    // authentication header, we will notice the lack of the token and fetch a
   1.270 +    // new one.
   1.271 +    this._token = null;
   1.272 +  },
   1.273 +
   1.274 +  observe: function (subject, topic, data) {
   1.275 +    this._log.debug("observed " + topic);
   1.276 +    switch (topic) {
   1.277 +    case fxAccountsCommon.ONLOGIN_NOTIFICATION:
   1.278 +      // This should only happen if we've been initialized without a current
   1.279 +      // user - otherwise we'd have seen the LOGOUT notification and been
   1.280 +      // thrown away.
   1.281 +      // The exception is when we've initialized with a user that needs to
   1.282 +      // reauth with the server - in that case we will also get here, but
   1.283 +      // should have the same identity.
   1.284 +      // initializeWithCurrentIdentity will throw and log if these contraints
   1.285 +      // aren't met, so just go ahead and do the init.
   1.286 +      this.initializeWithCurrentIdentity(true);
   1.287 +      break;
   1.288 +
   1.289 +    case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
   1.290 +      Weave.Service.startOver();
   1.291 +      // startOver will cause this instance to be thrown away, so there's
   1.292 +      // nothing else to do.
   1.293 +      break;
   1.294 +    }
   1.295 +  },
   1.296 +
   1.297 +  /**
   1.298 +   * Compute the sha256 of the message bytes.  Return bytes.
   1.299 +   */
   1.300 +  _sha256: function(message) {
   1.301 +    let hasher = Cc["@mozilla.org/security/hash;1"]
   1.302 +                    .createInstance(Ci.nsICryptoHash);
   1.303 +    hasher.init(hasher.SHA256);
   1.304 +    return CryptoUtils.digestBytes(message, hasher);
   1.305 +  },
   1.306 +
   1.307 +  /**
   1.308 +   * Compute the X-Client-State header given the byte string kB.
   1.309 +   *
   1.310 +   * Return string: hex(first16Bytes(sha256(kBbytes)))
   1.311 +   */
   1.312 +  _computeXClientState: function(kBbytes) {
   1.313 +    return CommonUtils.bytesAsHex(this._sha256(kBbytes).slice(0, 16), false);
   1.314 +  },
   1.315 +
   1.316 +  /**
   1.317 +   * Provide override point for testing token expiration.
   1.318 +   */
   1.319 +  _now: function() {
   1.320 +    return this._fxaService.now()
   1.321 +  },
   1.322 +
   1.323 +  get _localtimeOffsetMsec() {
   1.324 +    return this._fxaService.localtimeOffsetMsec;
   1.325 +  },
   1.326 +
   1.327 +  usernameFromAccount: function(val) {
   1.328 +    // we don't differentiate between "username" and "account"
   1.329 +    return val;
   1.330 +  },
   1.331 +
   1.332 +  /**
   1.333 +   * Obtains the HTTP Basic auth password.
   1.334 +   *
   1.335 +   * Returns a string if set or null if it is not set.
   1.336 +   */
   1.337 +  get basicPassword() {
   1.338 +    this._log.error("basicPassword getter should be not used in BrowserIDManager");
   1.339 +    return null;
   1.340 +  },
   1.341 +
   1.342 +  /**
   1.343 +   * Set the HTTP basic password to use.
   1.344 +   *
   1.345 +   * Changes will not persist unless persistSyncCredentials() is called.
   1.346 +   */
   1.347 +  set basicPassword(value) {
   1.348 +    throw "basicPassword setter should be not used in BrowserIDManager";
   1.349 +  },
   1.350 +
   1.351 +  /**
   1.352 +   * Obtain the Sync Key.
   1.353 +   *
   1.354 +   * This returns a 26 character "friendly" Base32 encoded string on success or
   1.355 +   * null if no Sync Key could be found.
   1.356 +   *
   1.357 +   * If the Sync Key hasn't been set in this session, this will look in the
   1.358 +   * password manager for the sync key.
   1.359 +   */
   1.360 +  get syncKey() {
   1.361 +    if (this.syncKeyBundle) {
   1.362 +      // TODO: This is probably fine because the code shouldn't be
   1.363 +      // using the sync key directly (it should use the sync key
   1.364 +      // bundle), but I don't like it. We should probably refactor
   1.365 +      // code that is inspecting this to not do validation on this
   1.366 +      // field directly and instead call a isSyncKeyValid() function
   1.367 +      // that we can override.
   1.368 +      return "99999999999999999999999999";
   1.369 +    }
   1.370 +    else {
   1.371 +      return null;
   1.372 +    }
   1.373 +  },
   1.374 +
   1.375 +  set syncKey(value) {
   1.376 +    throw "syncKey setter should be not used in BrowserIDManager";
   1.377 +  },
   1.378 +
   1.379 +  get syncKeyBundle() {
   1.380 +    return this._syncKeyBundle;
   1.381 +  },
   1.382 +
   1.383 +  /**
   1.384 +   * Resets/Drops all credentials we hold for the current user.
   1.385 +   */
   1.386 +  resetCredentials: function() {
   1.387 +    this.resetSyncKey();
   1.388 +    this._token = null;
   1.389 +  },
   1.390 +
   1.391 +  /**
   1.392 +   * Resets/Drops the sync key we hold for the current user.
   1.393 +   */
   1.394 +  resetSyncKey: function() {
   1.395 +    this._syncKey = null;
   1.396 +    this._syncKeyBundle = null;
   1.397 +    this._syncKeyUpdated = true;
   1.398 +    this._shouldHaveSyncKeyBundle = false;
   1.399 +  },
   1.400 +
   1.401 +  /**
   1.402 +   * The current state of the auth credentials.
   1.403 +   *
   1.404 +   * This essentially validates that enough credentials are available to use
   1.405 +   * Sync.
   1.406 +   */
   1.407 +  get currentAuthState() {
   1.408 +    if (this._authFailureReason) {
   1.409 +      this._log.info("currentAuthState returning " + this._authFailureReason +
   1.410 +                     " due to previous failure");
   1.411 +      return this._authFailureReason;
   1.412 +    }
   1.413 +    // TODO: need to revisit this. Currently this isn't ready to go until
   1.414 +    // both the username and syncKeyBundle are both configured and having no
   1.415 +    // username seems to make things fail fast so that's good.
   1.416 +    if (!this.username) {
   1.417 +      return LOGIN_FAILED_NO_USERNAME;
   1.418 +    }
   1.419 +
   1.420 +    // No need to check this.syncKey as our getter for that attribute
   1.421 +    // uses this.syncKeyBundle
   1.422 +    // If bundle creation started, but failed.
   1.423 +    if (this._shouldHaveSyncKeyBundle && !this.syncKeyBundle) {
   1.424 +      return LOGIN_FAILED_NO_PASSPHRASE;
   1.425 +    }
   1.426 +
   1.427 +    return STATUS_OK;
   1.428 +  },
   1.429 +
   1.430 +  /**
   1.431 +   * Do we have a non-null, not yet expired token for the user currently
   1.432 +   * signed in?
   1.433 +   */
   1.434 +  hasValidToken: function() {
   1.435 +    if (!this._token) {
   1.436 +      return false;
   1.437 +    }
   1.438 +    if (this._token.expiration < this._now()) {
   1.439 +      return false;
   1.440 +    }
   1.441 +    return true;
   1.442 +  },
   1.443 +
   1.444 +  // Refresh the sync token for our user.
   1.445 +  _fetchTokenForUser: function() {
   1.446 +    let tokenServerURI = Svc.Prefs.get("tokenServerURI");
   1.447 +    let log = this._log;
   1.448 +    let client = this._tokenServerClient;
   1.449 +    let fxa = this._fxaService;
   1.450 +    let userData = this._signedInUser;
   1.451 +
   1.452 +    log.info("Fetching assertion and token from: " + tokenServerURI);
   1.453 +
   1.454 +    let maybeFetchKeys = () => {
   1.455 +      // This is called at login time and every time we need a new token - in
   1.456 +      // the latter case we already have kA and kB, so optimise that case.
   1.457 +      if (userData.kA && userData.kB) {
   1.458 +        return;
   1.459 +      }
   1.460 +      return this._fxaService.getKeys().then(
   1.461 +        newUserData => {
   1.462 +          userData = newUserData;
   1.463 +          this._updateSignedInUser(userData); // throws if the user changed.
   1.464 +        }
   1.465 +      );
   1.466 +    }
   1.467 +
   1.468 +    let getToken = (tokenServerURI, assertion) => {
   1.469 +      log.debug("Getting a token");
   1.470 +      let deferred = Promise.defer();
   1.471 +      let cb = function (err, token) {
   1.472 +        if (err) {
   1.473 +          return deferred.reject(err);
   1.474 +        }
   1.475 +        log.debug("Successfully got a sync token");
   1.476 +        return deferred.resolve(token);
   1.477 +      };
   1.478 +
   1.479 +      let kBbytes = CommonUtils.hexToBytes(userData.kB);
   1.480 +      let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
   1.481 +      client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
   1.482 +      return deferred.promise;
   1.483 +    }
   1.484 +
   1.485 +    let getAssertion = () => {
   1.486 +      log.debug("Getting an assertion");
   1.487 +      let audience = Services.io.newURI(tokenServerURI, null, null).prePath;
   1.488 +      return fxa.getAssertion(audience);
   1.489 +    };
   1.490 +
   1.491 +    // wait until the account email is verified and we know that
   1.492 +    // getAssertion() will return a real assertion (not null).
   1.493 +    return fxa.whenVerified(this._signedInUser)
   1.494 +      .then(() => maybeFetchKeys())
   1.495 +      .then(() => getAssertion())
   1.496 +      .then(assertion => getToken(tokenServerURI, assertion))
   1.497 +      .then(token => {
   1.498 +        // TODO: Make it be only 80% of the duration, so refresh the token
   1.499 +        // before it actually expires. This is to avoid sync storage errors
   1.500 +        // otherwise, we get a nasty notification bar briefly. Bug 966568.
   1.501 +        token.expiration = this._now() + (token.duration * 1000) * 0.80;
   1.502 +        if (!this._syncKeyBundle) {
   1.503 +          // We are given kA/kB as hex.
   1.504 +          this._syncKeyBundle = deriveKeyBundle(Utils.hexToBytes(userData.kB));
   1.505 +        }
   1.506 +        return token;
   1.507 +      })
   1.508 +      .then(null, err => {
   1.509 +        // TODO: unify these errors - we need to handle errors thrown by
   1.510 +        // both tokenserverclient and hawkclient.
   1.511 +        // A tokenserver error thrown based on a bad response.
   1.512 +        if (err.response && err.response.status === 401) {
   1.513 +          err = new AuthenticationError(err);
   1.514 +        // A hawkclient error.
   1.515 +        } else if (err.code === 401) {
   1.516 +          err = new AuthenticationError(err);
   1.517 +        }
   1.518 +
   1.519 +        // TODO: write tests to make sure that different auth error cases are handled here
   1.520 +        // properly: auth error getting assertion, auth error getting token (invalid generation
   1.521 +        // and client-state error)
   1.522 +        if (err instanceof AuthenticationError) {
   1.523 +          this._log.error("Authentication error in _fetchTokenForUser: " + err);
   1.524 +          // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
   1.525 +          this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
   1.526 +        } else {
   1.527 +          this._log.error("Non-authentication error in _fetchTokenForUser: " + err.message);
   1.528 +          // for now assume it is just a transient network related problem.
   1.529 +          this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
   1.530 +        }
   1.531 +        // Drop the sync key bundle, but still expect to have one.
   1.532 +        // This will arrange for us to be in the right 'currentAuthState'
   1.533 +        // such that UI will show the right error.
   1.534 +        this._shouldHaveSyncKeyBundle = true;
   1.535 +        Weave.Status.login = this._authFailureReason;
   1.536 +        Services.obs.notifyObservers(null, "weave:service:login:error", null);
   1.537 +        throw err;
   1.538 +      });
   1.539 +  },
   1.540 +
   1.541 +  // Returns a promise that is resolved when we have a valid token for the
   1.542 +  // current user stored in this._token.  When resolved, this._token is valid.
   1.543 +  _ensureValidToken: function() {
   1.544 +    if (this.hasValidToken()) {
   1.545 +      this._log.debug("_ensureValidToken already has one");
   1.546 +      return Promise.resolve();
   1.547 +    }
   1.548 +    return this._fetchTokenForUser().then(
   1.549 +      token => {
   1.550 +        this._token = token;
   1.551 +      }
   1.552 +    );
   1.553 +  },
   1.554 +
   1.555 +  getResourceAuthenticator: function () {
   1.556 +    return this._getAuthenticationHeader.bind(this);
   1.557 +  },
   1.558 +
   1.559 +  /**
   1.560 +   * Obtain a function to be used for adding auth to RESTRequest instances.
   1.561 +   */
   1.562 +  getRESTRequestAuthenticator: function() {
   1.563 +    return this._addAuthenticationHeader.bind(this);
   1.564 +  },
   1.565 +
   1.566 +  /**
   1.567 +   * @return a Hawk HTTP Authorization Header, lightly wrapped, for the .uri
   1.568 +   * of a RESTRequest or AsyncResponse object.
   1.569 +   */
   1.570 +  _getAuthenticationHeader: function(httpObject, method) {
   1.571 +    let cb = Async.makeSpinningCallback();
   1.572 +    this._ensureValidToken().then(cb, cb);
   1.573 +    try {
   1.574 +      cb.wait();
   1.575 +    } catch (ex) {
   1.576 +      this._log.error("Failed to fetch a token for authentication: " + ex);
   1.577 +      return null;
   1.578 +    }
   1.579 +    if (!this._token) {
   1.580 +      return null;
   1.581 +    }
   1.582 +    let credentials = {algorithm: "sha256",
   1.583 +                       id: this._token.id,
   1.584 +                       key: this._token.key,
   1.585 +                      };
   1.586 +    method = method || httpObject.method;
   1.587 +
   1.588 +    // Get the local clock offset from the Firefox Accounts server.  This should
   1.589 +    // be close to the offset from the storage server.
   1.590 +    let options = {
   1.591 +      now: this._now(),
   1.592 +      localtimeOffsetMsec: this._localtimeOffsetMsec,
   1.593 +      credentials: credentials,
   1.594 +    };
   1.595 +
   1.596 +    let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method, options);
   1.597 +    return {headers: {authorization: headerValue.field}};
   1.598 +  },
   1.599 +
   1.600 +  _addAuthenticationHeader: function(request, method) {
   1.601 +    let header = this._getAuthenticationHeader(request, method);
   1.602 +    if (!header) {
   1.603 +      return null;
   1.604 +    }
   1.605 +    request.setHeader("authorization", header.headers.authorization);
   1.606 +    return request;
   1.607 +  },
   1.608 +
   1.609 +  createClusterManager: function(service) {
   1.610 +    return new BrowserIDClusterManager(service);
   1.611 +  }
   1.612 +
   1.613 +};
   1.614 +
   1.615 +/* An implementation of the ClusterManager for this identity
   1.616 + */
   1.617 +
   1.618 +function BrowserIDClusterManager(service) {
   1.619 +  ClusterManager.call(this, service);
   1.620 +}
   1.621 +
   1.622 +BrowserIDClusterManager.prototype = {
   1.623 +  __proto__: ClusterManager.prototype,
   1.624 +
   1.625 +  _findCluster: function() {
   1.626 +    let endPointFromIdentityToken = function() {
   1.627 +      let endpoint = this.identity._token.endpoint;
   1.628 +      // For Sync 1.5 storage endpoints, we use the base endpoint verbatim.
   1.629 +      // However, it should end in "/" because we will extend it with
   1.630 +      // well known path components. So we add a "/" if it's missing.
   1.631 +      if (!endpoint.endsWith("/")) {
   1.632 +        endpoint += "/";
   1.633 +      }
   1.634 +      log.debug("_findCluster returning " + endpoint);
   1.635 +      return endpoint;
   1.636 +    }.bind(this);
   1.637 +
   1.638 +    // Spinningly ensure we are ready to authenticate and have a valid token.
   1.639 +    let promiseClusterURL = function() {
   1.640 +      return this.identity.whenReadyToAuthenticate.promise.then(
   1.641 +        () => {
   1.642 +          // We need to handle node reassignment here.  If we are being asked
   1.643 +          // for a clusterURL while the service already has a clusterURL, then
   1.644 +          // it's likely a 401 was received using the existing token - in which
   1.645 +          // case we just discard the existing token and fetch a new one.
   1.646 +          if (this.service.clusterURL) {
   1.647 +            log.debug("_findCluster found existing clusterURL, so discarding the current token");
   1.648 +            this.identity._token = null;
   1.649 +          }
   1.650 +          return this.identity._ensureValidToken();
   1.651 +        }
   1.652 +      ).then(endPointFromIdentityToken
   1.653 +      );
   1.654 +    }.bind(this);
   1.655 +
   1.656 +    let cb = Async.makeSpinningCallback();
   1.657 +    promiseClusterURL().then(function (clusterURL) {
   1.658 +      cb(null, clusterURL);
   1.659 +    }).then(
   1.660 +      null, err => {
   1.661 +      // service.js's verifyLogin() method will attempt to fetch a cluster
   1.662 +      // URL when it sees a 401.  If it gets null, it treats it as a "real"
   1.663 +      // auth error and sets Status.login to LOGIN_FAILED_LOGIN_REJECTED, which
   1.664 +      // in turn causes a notification bar to appear informing the user they
   1.665 +      // need to re-authenticate.
   1.666 +      // On the other hand, if fetching the cluster URL fails with an exception,
   1.667 +      // verifyLogin() assumes it is a transient error, and thus doesn't show
   1.668 +      // the notification bar under the assumption the issue will resolve
   1.669 +      // itself.
   1.670 +      // Thus:
   1.671 +      // * On a real 401, we must return null.
   1.672 +      // * On any other problem we must let an exception bubble up.
   1.673 +      if (err instanceof AuthenticationError) {
   1.674 +        // callback with no error and a null result - cb.wait() returns null.
   1.675 +        cb(null, null);
   1.676 +      } else {
   1.677 +        // callback with an error - cb.wait() completes by raising an exception.
   1.678 +        cb(err);
   1.679 +      }
   1.680 +    });
   1.681 +    return cb.wait();
   1.682 +  },
   1.683 +
   1.684 +  getUserBaseURL: function() {
   1.685 +    // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy
   1.686 +    // Sync appends path components onto an empty path, and in FxA Sync the
   1.687 +    // token server constructs this for us in an opaque manner. Since the
   1.688 +    // cluster manager already sets the clusterURL on Service and also has
   1.689 +    // access to the current identity, we added this functionality here.
   1.690 +    return this.service.clusterURL;
   1.691 +  }
   1.692 +}

mercurial