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

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

mercurial