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 +}