services/fxaccounts/FxAccounts.jsm

Wed, 31 Dec 2014 07:53:36 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:53:36 +0100
branch
TOR_BUG_3246
changeset 5
4ab42b5ab56c
permissions
-rw-r--r--

Correct small whitespace inconsistency, lost while renaming variables.

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
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
michael@0 6
michael@0 7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
michael@0 8
michael@0 9 Cu.import("resource://gre/modules/Log.jsm");
michael@0 10 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 11 Cu.import("resource://gre/modules/osfile.jsm");
michael@0 12 Cu.import("resource://services-common/utils.js");
michael@0 13 Cu.import("resource://services-crypto/utils.js");
michael@0 14 Cu.import("resource://gre/modules/Services.jsm");
michael@0 15 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 16 Cu.import("resource://gre/modules/Timer.jsm");
michael@0 17 Cu.import("resource://gre/modules/Task.jsm");
michael@0 18 Cu.import("resource://gre/modules/FxAccountsCommon.js");
michael@0 19
michael@0 20 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient",
michael@0 21 "resource://gre/modules/FxAccountsClient.jsm");
michael@0 22
michael@0 23 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
michael@0 24 "resource://gre/modules/identity/jwcrypto.jsm");
michael@0 25
michael@0 26 // All properties exposed by the public FxAccounts API.
michael@0 27 let publicProperties = [
michael@0 28 "getAccountsClient",
michael@0 29 "getAccountsSignInURI",
michael@0 30 "getAccountsSignUpURI",
michael@0 31 "getAssertion",
michael@0 32 "getKeys",
michael@0 33 "getSignedInUser",
michael@0 34 "loadAndPoll",
michael@0 35 "localtimeOffsetMsec",
michael@0 36 "now",
michael@0 37 "promiseAccountsForceSigninURI",
michael@0 38 "resendVerificationEmail",
michael@0 39 "setSignedInUser",
michael@0 40 "signOut",
michael@0 41 "version",
michael@0 42 "whenVerified"
michael@0 43 ];
michael@0 44
michael@0 45 // An AccountState object holds all state related to one specific account.
michael@0 46 // Only one AccountState is ever "current" in the FxAccountsInternal object -
michael@0 47 // whenever a user logs out or logs in, the current AccountState is discarded,
michael@0 48 // making it impossible for the wrong state or state data to be accidentally
michael@0 49 // used.
michael@0 50 // In addition, it has some promise-related helpers to ensure that if an
michael@0 51 // attempt is made to resolve a promise on a "stale" state (eg, if an
michael@0 52 // operation starts, but a different user logs in before the operation
michael@0 53 // completes), the promise will be rejected.
michael@0 54 // It is intended to be used thusly:
michael@0 55 // somePromiseBasedFunction: function() {
michael@0 56 // let currentState = this.currentAccountState;
michael@0 57 // return someOtherPromiseFunction().then(
michael@0 58 // data => currentState.resolve(data)
michael@0 59 // );
michael@0 60 // }
michael@0 61 // If the state has changed between the function being called and the promise
michael@0 62 // being resolved, the .resolve() call will actually be rejected.
michael@0 63 AccountState = function(fxaInternal) {
michael@0 64 this.fxaInternal = fxaInternal;
michael@0 65 };
michael@0 66
michael@0 67 AccountState.prototype = {
michael@0 68 cert: null,
michael@0 69 keyPair: null,
michael@0 70 signedInUser: null,
michael@0 71 whenVerifiedDeferred: null,
michael@0 72 whenKeysReadyDeferred: null,
michael@0 73
michael@0 74 get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this,
michael@0 75
michael@0 76 abort: function() {
michael@0 77 if (this.whenVerifiedDeferred) {
michael@0 78 this.whenVerifiedDeferred.reject(
michael@0 79 new Error("Verification aborted; Another user signing in"));
michael@0 80 this.whenVerifiedDeferred = null;
michael@0 81 }
michael@0 82
michael@0 83 if (this.whenKeysReadyDeferred) {
michael@0 84 this.whenKeysReadyDeferred.reject(
michael@0 85 new Error("Verification aborted; Another user signing in"));
michael@0 86 this.whenKeysReadyDeferred = null;
michael@0 87 }
michael@0 88 this.cert = null;
michael@0 89 this.keyPair = null;
michael@0 90 this.signedInUser = null;
michael@0 91 this.fxaInternal = null;
michael@0 92 },
michael@0 93
michael@0 94 getUserAccountData: function() {
michael@0 95 // Skip disk if user is cached.
michael@0 96 if (this.signedInUser) {
michael@0 97 return this.resolve(this.signedInUser.accountData);
michael@0 98 }
michael@0 99
michael@0 100 return this.fxaInternal.signedInUserStorage.get().then(
michael@0 101 user => {
michael@0 102 if (logPII) {
michael@0 103 // don't stringify unless it will be written. We should replace this
michael@0 104 // check with param substitutions added in bug 966674
michael@0 105 log.debug("getUserAccountData -> " + JSON.stringify(user));
michael@0 106 }
michael@0 107 if (user && user.version == this.version) {
michael@0 108 log.debug("setting signed in user");
michael@0 109 this.signedInUser = user;
michael@0 110 }
michael@0 111 return this.resolve(user ? user.accountData : null);
michael@0 112 },
michael@0 113 err => {
michael@0 114 if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
michael@0 115 // File hasn't been created yet. That will be done
michael@0 116 // on the first call to getSignedInUser
michael@0 117 return this.resolve(null);
michael@0 118 }
michael@0 119 return this.reject(err);
michael@0 120 }
michael@0 121 );
michael@0 122 },
michael@0 123
michael@0 124 setUserAccountData: function(accountData) {
michael@0 125 return this.fxaInternal.signedInUserStorage.get().then(record => {
michael@0 126 if (!this.isCurrent) {
michael@0 127 return this.reject(new Error("Another user has signed in"));
michael@0 128 }
michael@0 129 record.accountData = accountData;
michael@0 130 this.signedInUser = record;
michael@0 131 return this.fxaInternal.signedInUserStorage.set(record)
michael@0 132 .then(() => this.resolve(accountData));
michael@0 133 });
michael@0 134 },
michael@0 135
michael@0 136
michael@0 137 getCertificate: function(data, keyPair, mustBeValidUntil) {
michael@0 138 if (logPII) {
michael@0 139 // don't stringify unless it will be written. We should replace this
michael@0 140 // check with param substitutions added in bug 966674
michael@0 141 log.debug("getCertificate" + JSON.stringify(this.signedInUser));
michael@0 142 }
michael@0 143 // TODO: get the lifetime from the cert's .exp field
michael@0 144 if (this.cert && this.cert.validUntil > mustBeValidUntil) {
michael@0 145 log.debug(" getCertificate already had one");
michael@0 146 return this.resolve(this.cert.cert);
michael@0 147 }
michael@0 148 // else get our cert signed
michael@0 149 let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME;
michael@0 150 return this.fxaInternal.getCertificateSigned(data.sessionToken,
michael@0 151 keyPair.serializedPublicKey,
michael@0 152 CERT_LIFETIME).then(
michael@0 153 cert => {
michael@0 154 log.debug("getCertificate got a new one: " + !!cert);
michael@0 155 this.cert = {
michael@0 156 cert: cert,
michael@0 157 validUntil: willBeValidUntil
michael@0 158 };
michael@0 159 return cert;
michael@0 160 }
michael@0 161 ).then(result => this.resolve(result));
michael@0 162 },
michael@0 163
michael@0 164 getKeyPair: function(mustBeValidUntil) {
michael@0 165 if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
michael@0 166 log.debug("getKeyPair: already have a keyPair");
michael@0 167 return this.resolve(this.keyPair.keyPair);
michael@0 168 }
michael@0 169 // Otherwse, create a keypair and set validity limit.
michael@0 170 let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME;
michael@0 171 let d = Promise.defer();
michael@0 172 jwcrypto.generateKeyPair("DS160", (err, kp) => {
michael@0 173 if (err) {
michael@0 174 return this.reject(err);
michael@0 175 }
michael@0 176 this.keyPair = {
michael@0 177 keyPair: kp,
michael@0 178 validUntil: willBeValidUntil
michael@0 179 };
michael@0 180 log.debug("got keyPair");
michael@0 181 delete this.cert;
michael@0 182 d.resolve(this.keyPair.keyPair);
michael@0 183 });
michael@0 184 return d.promise.then(result => this.resolve(result));
michael@0 185 },
michael@0 186
michael@0 187 resolve: function(result) {
michael@0 188 if (!this.isCurrent) {
michael@0 189 log.info("An accountState promise was resolved, but was actually rejected" +
michael@0 190 " due to a different user being signed in. Originally resolved" +
michael@0 191 " with: " + result);
michael@0 192 return Promise.reject(new Error("A different user signed in"));
michael@0 193 }
michael@0 194 return Promise.resolve(result);
michael@0 195 },
michael@0 196
michael@0 197 reject: function(error) {
michael@0 198 // It could be argued that we should just let it reject with the original
michael@0 199 // error - but this runs the risk of the error being (eg) a 401, which
michael@0 200 // might cause the consumer to attempt some remediation and cause other
michael@0 201 // problems.
michael@0 202 if (!this.isCurrent) {
michael@0 203 log.info("An accountState promise was rejected, but we are ignoring that" +
michael@0 204 "reason and rejecting it due to a different user being signed in." +
michael@0 205 "Originally rejected with: " + reason);
michael@0 206 return Promise.reject(new Error("A different user signed in"));
michael@0 207 }
michael@0 208 return Promise.reject(error);
michael@0 209 },
michael@0 210
michael@0 211 }
michael@0 212
michael@0 213 /**
michael@0 214 * Copies properties from a given object to another object.
michael@0 215 *
michael@0 216 * @param from (object)
michael@0 217 * The object we read property descriptors from.
michael@0 218 * @param to (object)
michael@0 219 * The object that we set property descriptors on.
michael@0 220 * @param options (object) (optional)
michael@0 221 * {keys: [...]}
michael@0 222 * Lets the caller pass the names of all properties they want to be
michael@0 223 * copied. Will copy all properties of the given source object by
michael@0 224 * default.
michael@0 225 * {bind: object}
michael@0 226 * Lets the caller specify the object that will be used to .bind()
michael@0 227 * all function properties we find to. Will bind to the given target
michael@0 228 * object by default.
michael@0 229 */
michael@0 230 function copyObjectProperties(from, to, opts = {}) {
michael@0 231 let keys = (opts && opts.keys) || Object.keys(from);
michael@0 232 let thisArg = (opts && opts.bind) || to;
michael@0 233
michael@0 234 for (let prop of keys) {
michael@0 235 let desc = Object.getOwnPropertyDescriptor(from, prop);
michael@0 236
michael@0 237 if (typeof(desc.value) == "function") {
michael@0 238 desc.value = desc.value.bind(thisArg);
michael@0 239 }
michael@0 240
michael@0 241 if (desc.get) {
michael@0 242 desc.get = desc.get.bind(thisArg);
michael@0 243 }
michael@0 244
michael@0 245 if (desc.set) {
michael@0 246 desc.set = desc.set.bind(thisArg);
michael@0 247 }
michael@0 248
michael@0 249 Object.defineProperty(to, prop, desc);
michael@0 250 }
michael@0 251 }
michael@0 252
michael@0 253 /**
michael@0 254 * The public API's constructor.
michael@0 255 */
michael@0 256 this.FxAccounts = function (mockInternal) {
michael@0 257 let internal = new FxAccountsInternal();
michael@0 258 let external = {};
michael@0 259
michael@0 260 // Copy all public properties to the 'external' object.
michael@0 261 let prototype = FxAccountsInternal.prototype;
michael@0 262 let options = {keys: publicProperties, bind: internal};
michael@0 263 copyObjectProperties(prototype, external, options);
michael@0 264
michael@0 265 // Copy all of the mock's properties to the internal object.
michael@0 266 if (mockInternal && !mockInternal.onlySetInternal) {
michael@0 267 copyObjectProperties(mockInternal, internal);
michael@0 268 }
michael@0 269
michael@0 270 if (mockInternal) {
michael@0 271 // Exposes the internal object for testing only.
michael@0 272 external.internal = internal;
michael@0 273 }
michael@0 274
michael@0 275 return Object.freeze(external);
michael@0 276 }
michael@0 277
michael@0 278 /**
michael@0 279 * The internal API's constructor.
michael@0 280 */
michael@0 281 function FxAccountsInternal() {
michael@0 282 this.version = DATA_FORMAT_VERSION;
michael@0 283
michael@0 284 // Make a local copy of these constants so we can mock it in testing
michael@0 285 this.POLL_STEP = POLL_STEP;
michael@0 286 this.POLL_SESSION = POLL_SESSION;
michael@0 287 // We will create this.pollTimeRemaining below; it will initially be
michael@0 288 // set to the value of POLL_SESSION.
michael@0 289
michael@0 290 // We interact with the Firefox Accounts auth server in order to confirm that
michael@0 291 // a user's email has been verified and also to fetch the user's keys from
michael@0 292 // the server. We manage these processes in possibly long-lived promises
michael@0 293 // that are internal to this object (never exposed to callers). Because
michael@0 294 // Firefox Accounts allows for only one logged-in user, and because it's
michael@0 295 // conceivable that while we are waiting to verify one identity, a caller
michael@0 296 // could start verification on a second, different identity, we need to be
michael@0 297 // able to abort all work on the first sign-in process. The currentTimer and
michael@0 298 // currentAccountState are used for this purpose.
michael@0 299 // (XXX - should the timer be directly on the currentAccountState?)
michael@0 300 this.currentTimer = null;
michael@0 301 this.currentAccountState = new AccountState(this);
michael@0 302
michael@0 303 // We don't reference |profileDir| in the top-level module scope
michael@0 304 // as we may be imported before we know where it is.
michael@0 305 this.signedInUserStorage = new JSONStorage({
michael@0 306 filename: DEFAULT_STORAGE_FILENAME,
michael@0 307 baseDir: OS.Constants.Path.profileDir,
michael@0 308 });
michael@0 309 }
michael@0 310
michael@0 311 /**
michael@0 312 * The internal API's prototype.
michael@0 313 */
michael@0 314 FxAccountsInternal.prototype = {
michael@0 315
michael@0 316 /**
michael@0 317 * The current data format's version number.
michael@0 318 */
michael@0 319 version: DATA_FORMAT_VERSION,
michael@0 320
michael@0 321 _fxAccountsClient: null,
michael@0 322
michael@0 323 get fxAccountsClient() {
michael@0 324 if (!this._fxAccountsClient) {
michael@0 325 this._fxAccountsClient = new FxAccountsClient();
michael@0 326 }
michael@0 327 return this._fxAccountsClient;
michael@0 328 },
michael@0 329
michael@0 330 /**
michael@0 331 * Return the current time in milliseconds as an integer. Allows tests to
michael@0 332 * manipulate the date to simulate certificate expiration.
michael@0 333 */
michael@0 334 now: function() {
michael@0 335 return this.fxAccountsClient.now();
michael@0 336 },
michael@0 337
michael@0 338 getAccountsClient: function() {
michael@0 339 return this.fxAccountsClient;
michael@0 340 },
michael@0 341
michael@0 342 /**
michael@0 343 * Return clock offset in milliseconds, as reported by the fxAccountsClient.
michael@0 344 * This can be overridden for testing.
michael@0 345 *
michael@0 346 * The offset is the number of milliseconds that must be added to the client
michael@0 347 * clock to make it equal to the server clock. For example, if the client is
michael@0 348 * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
michael@0 349 */
michael@0 350 get localtimeOffsetMsec() {
michael@0 351 return this.fxAccountsClient.localtimeOffsetMsec;
michael@0 352 },
michael@0 353
michael@0 354 /**
michael@0 355 * Ask the server whether the user's email has been verified
michael@0 356 */
michael@0 357 checkEmailStatus: function checkEmailStatus(sessionToken) {
michael@0 358 return this.fxAccountsClient.recoveryEmailStatus(sessionToken);
michael@0 359 },
michael@0 360
michael@0 361 /**
michael@0 362 * Once the user's email is verified, we can request the keys
michael@0 363 */
michael@0 364 fetchKeys: function fetchKeys(keyFetchToken) {
michael@0 365 log.debug("fetchKeys: " + !!keyFetchToken);
michael@0 366 if (logPII) {
michael@0 367 log.debug("fetchKeys - the token is " + keyFetchToken);
michael@0 368 }
michael@0 369 return this.fxAccountsClient.accountKeys(keyFetchToken);
michael@0 370 },
michael@0 371
michael@0 372 // set() makes sure that polling is happening, if necessary.
michael@0 373 // get() does not wait for verification, and returns an object even if
michael@0 374 // unverified. The caller of get() must check .verified .
michael@0 375 // The "fxaccounts:onverified" event will fire only when the verified
michael@0 376 // state goes from false to true, so callers must register their observer
michael@0 377 // and then call get(). In particular, it will not fire when the account
michael@0 378 // was found to be verified in a previous boot: if our stored state says
michael@0 379 // the account is verified, the event will never fire. So callers must do:
michael@0 380 // register notification observer (go)
michael@0 381 // userdata = get()
michael@0 382 // if (userdata.verified()) {go()}
michael@0 383
michael@0 384 /**
michael@0 385 * Get the user currently signed in to Firefox Accounts.
michael@0 386 *
michael@0 387 * @return Promise
michael@0 388 * The promise resolves to the credentials object of the signed-in user:
michael@0 389 * {
michael@0 390 * email: The user's email address
michael@0 391 * uid: The user's unique id
michael@0 392 * sessionToken: Session for the FxA server
michael@0 393 * kA: An encryption key from the FxA server
michael@0 394 * kB: An encryption key derived from the user's FxA password
michael@0 395 * verified: email verification status
michael@0 396 * authAt: The time (seconds since epoch) that this record was
michael@0 397 * authenticated
michael@0 398 * }
michael@0 399 * or null if no user is signed in.
michael@0 400 */
michael@0 401 getSignedInUser: function getSignedInUser() {
michael@0 402 let currentState = this.currentAccountState;
michael@0 403 return currentState.getUserAccountData().then(data => {
michael@0 404 if (!data) {
michael@0 405 return null;
michael@0 406 }
michael@0 407 if (!this.isUserEmailVerified(data)) {
michael@0 408 // If the email is not verified, start polling for verification,
michael@0 409 // but return null right away. We don't want to return a promise
michael@0 410 // that might not be fulfilled for a long time.
michael@0 411 this.startVerifiedCheck(data);
michael@0 412 }
michael@0 413 return data;
michael@0 414 }).then(result => currentState.resolve(result));
michael@0 415 },
michael@0 416
michael@0 417 /**
michael@0 418 * Set the current user signed in to Firefox Accounts.
michael@0 419 *
michael@0 420 * @param credentials
michael@0 421 * The credentials object obtained by logging in or creating
michael@0 422 * an account on the FxA server:
michael@0 423 * {
michael@0 424 * authAt: The time (seconds since epoch) that this record was
michael@0 425 * authenticated
michael@0 426 * email: The users email address
michael@0 427 * keyFetchToken: a keyFetchToken which has not yet been used
michael@0 428 * sessionToken: Session for the FxA server
michael@0 429 * uid: The user's unique id
michael@0 430 * unwrapBKey: used to unwrap kB, derived locally from the
michael@0 431 * password (not revealed to the FxA server)
michael@0 432 * verified: true/false
michael@0 433 * }
michael@0 434 * @return Promise
michael@0 435 * The promise resolves to null when the data is saved
michael@0 436 * successfully and is rejected on error.
michael@0 437 */
michael@0 438 setSignedInUser: function setSignedInUser(credentials) {
michael@0 439 log.debug("setSignedInUser - aborting any existing flows");
michael@0 440 this.abortExistingFlow();
michael@0 441
michael@0 442 let record = {version: this.version, accountData: credentials};
michael@0 443 let currentState = this.currentAccountState;
michael@0 444 // Cache a clone of the credentials object.
michael@0 445 currentState.signedInUser = JSON.parse(JSON.stringify(record));
michael@0 446
michael@0 447 // This promise waits for storage, but not for verification.
michael@0 448 // We're telling the caller that this is durable now.
michael@0 449 return this.signedInUserStorage.set(record).then(() => {
michael@0 450 this.notifyObservers(ONLOGIN_NOTIFICATION);
michael@0 451 if (!this.isUserEmailVerified(credentials)) {
michael@0 452 this.startVerifiedCheck(credentials);
michael@0 453 }
michael@0 454 }).then(result => currentState.resolve(result));
michael@0 455 },
michael@0 456
michael@0 457 /**
michael@0 458 * returns a promise that fires with the assertion. If there is no verified
michael@0 459 * signed-in user, fires with null.
michael@0 460 */
michael@0 461 getAssertion: function getAssertion(audience) {
michael@0 462 log.debug("enter getAssertion()");
michael@0 463 let currentState = this.currentAccountState;
michael@0 464 let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD;
michael@0 465 return currentState.getUserAccountData().then(data => {
michael@0 466 if (!data) {
michael@0 467 // No signed-in user
michael@0 468 return null;
michael@0 469 }
michael@0 470 if (!this.isUserEmailVerified(data)) {
michael@0 471 // Signed-in user has not verified email
michael@0 472 return null;
michael@0 473 }
michael@0 474 return currentState.getKeyPair(mustBeValidUntil).then(keyPair => {
michael@0 475 return currentState.getCertificate(data, keyPair, mustBeValidUntil)
michael@0 476 .then(cert => {
michael@0 477 return this.getAssertionFromCert(data, keyPair, cert, audience);
michael@0 478 });
michael@0 479 });
michael@0 480 }).then(result => currentState.resolve(result));
michael@0 481 },
michael@0 482
michael@0 483 /**
michael@0 484 * Resend the verification email fot the currently signed-in user.
michael@0 485 *
michael@0 486 */
michael@0 487 resendVerificationEmail: function resendVerificationEmail() {
michael@0 488 let currentState = this.currentAccountState;
michael@0 489 return this.getSignedInUser().then(data => {
michael@0 490 // If the caller is asking for verification to be re-sent, and there is
michael@0 491 // no signed-in user to begin with, this is probably best regarded as an
michael@0 492 // error.
michael@0 493 if (data) {
michael@0 494 this.pollEmailStatus(currentState, data.sessionToken, "start");
michael@0 495 return this.fxAccountsClient.resendVerificationEmail(data.sessionToken);
michael@0 496 }
michael@0 497 throw new Error("Cannot resend verification email; no signed-in user");
michael@0 498 });
michael@0 499 },
michael@0 500
michael@0 501 /*
michael@0 502 * Reset state such that any previous flow is canceled.
michael@0 503 */
michael@0 504 abortExistingFlow: function abortExistingFlow() {
michael@0 505 if (this.currentTimer) {
michael@0 506 log.debug("Polling aborted; Another user signing in");
michael@0 507 clearTimeout(this.currentTimer);
michael@0 508 this.currentTimer = 0;
michael@0 509 }
michael@0 510 this.currentAccountState.abort();
michael@0 511 this.currentAccountState = new AccountState(this);
michael@0 512 },
michael@0 513
michael@0 514 signOut: function signOut(localOnly) {
michael@0 515 let currentState = this.currentAccountState;
michael@0 516 let sessionToken;
michael@0 517 return currentState.getUserAccountData().then(data => {
michael@0 518 // Save the session token for use in the call to signOut below.
michael@0 519 sessionToken = data && data.sessionToken;
michael@0 520 return this._signOutLocal();
michael@0 521 }).then(() => {
michael@0 522 // FxAccountsManager calls here, then does its own call
michael@0 523 // to FxAccountsClient.signOut().
michael@0 524 if (!localOnly) {
michael@0 525 // Wrap this in a promise so *any* errors in signOut won't
michael@0 526 // block the local sign out. This is *not* returned.
michael@0 527 Promise.resolve().then(() => {
michael@0 528 // This can happen in the background and shouldn't block
michael@0 529 // the user from signing out. The server must tolerate
michael@0 530 // clients just disappearing, so this call should be best effort.
michael@0 531 return this._signOutServer(sessionToken);
michael@0 532 }).then(null, err => {
michael@0 533 log.error("Error during remote sign out of Firefox Accounts: " + err);
michael@0 534 });
michael@0 535 }
michael@0 536 }).then(() => {
michael@0 537 this.notifyObservers(ONLOGOUT_NOTIFICATION);
michael@0 538 });
michael@0 539 },
michael@0 540
michael@0 541 /**
michael@0 542 * This function should be called in conjunction with a server-side
michael@0 543 * signOut via FxAccountsClient.
michael@0 544 */
michael@0 545 _signOutLocal: function signOutLocal() {
michael@0 546 this.abortExistingFlow();
michael@0 547 this.currentAccountState.signedInUser = null; // clear in-memory cache
michael@0 548 return this.signedInUserStorage.set(null);
michael@0 549 },
michael@0 550
michael@0 551 _signOutServer: function signOutServer(sessionToken) {
michael@0 552 return this.fxAccountsClient.signOut(sessionToken);
michael@0 553 },
michael@0 554
michael@0 555 /**
michael@0 556 * Fetch encryption keys for the signed-in-user from the FxA API server.
michael@0 557 *
michael@0 558 * Not for user consumption. Exists to cause the keys to be fetch.
michael@0 559 *
michael@0 560 * Returns user data so that it can be chained with other methods.
michael@0 561 *
michael@0 562 * @return Promise
michael@0 563 * The promise resolves to the credentials object of the signed-in user:
michael@0 564 * {
michael@0 565 * email: The user's email address
michael@0 566 * uid: The user's unique id
michael@0 567 * sessionToken: Session for the FxA server
michael@0 568 * kA: An encryption key from the FxA server
michael@0 569 * kB: An encryption key derived from the user's FxA password
michael@0 570 * verified: email verification status
michael@0 571 * }
michael@0 572 * or null if no user is signed in
michael@0 573 */
michael@0 574 getKeys: function() {
michael@0 575 let currentState = this.currentAccountState;
michael@0 576 return currentState.getUserAccountData().then((userData) => {
michael@0 577 if (!userData) {
michael@0 578 throw new Error("Can't get keys; User is not signed in");
michael@0 579 }
michael@0 580 if (userData.kA && userData.kB) {
michael@0 581 return userData;
michael@0 582 }
michael@0 583 if (!currentState.whenKeysReadyDeferred) {
michael@0 584 currentState.whenKeysReadyDeferred = Promise.defer();
michael@0 585 if (userData.keyFetchToken) {
michael@0 586 this.fetchAndUnwrapKeys(userData.keyFetchToken).then(
michael@0 587 (dataWithKeys) => {
michael@0 588 if (!dataWithKeys.kA || !dataWithKeys.kB) {
michael@0 589 currentState.whenKeysReadyDeferred.reject(
michael@0 590 new Error("user data missing kA or kB")
michael@0 591 );
michael@0 592 return;
michael@0 593 }
michael@0 594 currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
michael@0 595 },
michael@0 596 (err) => {
michael@0 597 currentState.whenKeysReadyDeferred.reject(err);
michael@0 598 }
michael@0 599 );
michael@0 600 } else {
michael@0 601 currentState.whenKeysReadyDeferred.reject('No keyFetchToken');
michael@0 602 }
michael@0 603 }
michael@0 604 return currentState.whenKeysReadyDeferred.promise;
michael@0 605 }).then(result => currentState.resolve(result));
michael@0 606 },
michael@0 607
michael@0 608 fetchAndUnwrapKeys: function(keyFetchToken) {
michael@0 609 if (logPII) {
michael@0 610 log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
michael@0 611 }
michael@0 612 let currentState = this.currentAccountState;
michael@0 613 return Task.spawn(function* task() {
michael@0 614 // Sign out if we don't have a key fetch token.
michael@0 615 if (!keyFetchToken) {
michael@0 616 log.warn("improper fetchAndUnwrapKeys() call: token missing");
michael@0 617 yield this.signOut();
michael@0 618 return null;
michael@0 619 }
michael@0 620
michael@0 621 let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken);
michael@0 622
michael@0 623 let data = yield currentState.getUserAccountData();
michael@0 624
michael@0 625 // Sanity check that the user hasn't changed out from under us
michael@0 626 if (data.keyFetchToken !== keyFetchToken) {
michael@0 627 throw new Error("Signed in user changed while fetching keys!");
michael@0 628 }
michael@0 629
michael@0 630 // Next statements must be synchronous until we setUserAccountData
michael@0 631 // so that we don't risk getting into a weird state.
michael@0 632 let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
michael@0 633 wrapKB);
michael@0 634
michael@0 635 if (logPII) {
michael@0 636 log.debug("kB_hex: " + kB_hex);
michael@0 637 }
michael@0 638 data.kA = CommonUtils.bytesAsHex(kA);
michael@0 639 data.kB = CommonUtils.bytesAsHex(kB_hex);
michael@0 640
michael@0 641 delete data.keyFetchToken;
michael@0 642
michael@0 643 log.debug("Keys Obtained: kA=" + !!data.kA + ", kB=" + !!data.kB);
michael@0 644 if (logPII) {
michael@0 645 log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB);
michael@0 646 }
michael@0 647
michael@0 648 yield currentState.setUserAccountData(data);
michael@0 649 // We are now ready for business. This should only be invoked once
michael@0 650 // per setSignedInUser(), regardless of whether we've rebooted since
michael@0 651 // setSignedInUser() was called.
michael@0 652 this.notifyObservers(ONVERIFIED_NOTIFICATION);
michael@0 653 return data;
michael@0 654 }.bind(this)).then(result => currentState.resolve(result));
michael@0 655 },
michael@0 656
michael@0 657 getAssertionFromCert: function(data, keyPair, cert, audience) {
michael@0 658 log.debug("getAssertionFromCert");
michael@0 659 let payload = {};
michael@0 660 let d = Promise.defer();
michael@0 661 let options = {
michael@0 662 duration: ASSERTION_LIFETIME,
michael@0 663 localtimeOffsetMsec: this.localtimeOffsetMsec,
michael@0 664 now: this.now()
michael@0 665 };
michael@0 666 let currentState = this.currentAccountState;
michael@0 667 // "audience" should look like "http://123done.org".
michael@0 668 // The generated assertion will expire in two minutes.
michael@0 669 jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => {
michael@0 670 if (err) {
michael@0 671 log.error("getAssertionFromCert: " + err);
michael@0 672 d.reject(err);
michael@0 673 } else {
michael@0 674 log.debug("getAssertionFromCert returning signed: " + !!signed);
michael@0 675 if (logPII) {
michael@0 676 log.debug("getAssertionFromCert returning signed: " + signed);
michael@0 677 }
michael@0 678 d.resolve(signed);
michael@0 679 }
michael@0 680 });
michael@0 681 return d.promise.then(result => currentState.resolve(result));
michael@0 682 },
michael@0 683
michael@0 684 getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) {
michael@0 685 log.debug("getCertificateSigned: " + !!sessionToken + " " + !!serializedPublicKey);
michael@0 686 if (logPII) {
michael@0 687 log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey);
michael@0 688 }
michael@0 689 return this.fxAccountsClient.signCertificate(
michael@0 690 sessionToken,
michael@0 691 JSON.parse(serializedPublicKey),
michael@0 692 lifetime
michael@0 693 );
michael@0 694 },
michael@0 695
michael@0 696 getUserAccountData: function() {
michael@0 697 return this.currentAccountState.getUserAccountData();
michael@0 698 },
michael@0 699
michael@0 700 isUserEmailVerified: function isUserEmailVerified(data) {
michael@0 701 return !!(data && data.verified);
michael@0 702 },
michael@0 703
michael@0 704 /**
michael@0 705 * Setup for and if necessary do email verification polling.
michael@0 706 */
michael@0 707 loadAndPoll: function() {
michael@0 708 let currentState = this.currentAccountState;
michael@0 709 return currentState.getUserAccountData()
michael@0 710 .then(data => {
michael@0 711 if (data && !this.isUserEmailVerified(data)) {
michael@0 712 this.pollEmailStatus(currentState, data.sessionToken, "start");
michael@0 713 }
michael@0 714 return data;
michael@0 715 });
michael@0 716 },
michael@0 717
michael@0 718 startVerifiedCheck: function(data) {
michael@0 719 log.debug("startVerifiedCheck " + JSON.stringify(data));
michael@0 720 // Get us to the verified state, then get the keys. This returns a promise
michael@0 721 // that will fire when we are completely ready.
michael@0 722 //
michael@0 723 // Login is truly complete once keys have been fetched, so once getKeys()
michael@0 724 // obtains and stores kA and kB, it will fire the onverified observer
michael@0 725 // notification.
michael@0 726 return this.whenVerified(data)
michael@0 727 .then(() => this.getKeys());
michael@0 728 },
michael@0 729
michael@0 730 whenVerified: function(data) {
michael@0 731 let currentState = this.currentAccountState;
michael@0 732 if (data.verified) {
michael@0 733 log.debug("already verified");
michael@0 734 return currentState.resolve(data);
michael@0 735 }
michael@0 736 if (!currentState.whenVerifiedDeferred) {
michael@0 737 log.debug("whenVerified promise starts polling for verified email");
michael@0 738 this.pollEmailStatus(currentState, data.sessionToken, "start");
michael@0 739 }
michael@0 740 return currentState.whenVerifiedDeferred.promise.then(
michael@0 741 result => currentState.resolve(result)
michael@0 742 );
michael@0 743 },
michael@0 744
michael@0 745 notifyObservers: function(topic) {
michael@0 746 log.debug("Notifying observers of " + topic);
michael@0 747 Services.obs.notifyObservers(null, topic, null);
michael@0 748 },
michael@0 749
michael@0 750 // XXX - pollEmailStatus should maybe be on the AccountState object?
michael@0 751 pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) {
michael@0 752 log.debug("entering pollEmailStatus: " + why);
michael@0 753 if (why == "start") {
michael@0 754 // If we were already polling, stop and start again. This could happen
michael@0 755 // if the user requested the verification email to be resent while we
michael@0 756 // were already polling for receipt of an earlier email.
michael@0 757 this.pollTimeRemaining = this.POLL_SESSION;
michael@0 758 if (!currentState.whenVerifiedDeferred) {
michael@0 759 currentState.whenVerifiedDeferred = Promise.defer();
michael@0 760 // This deferred might not end up with any handlers (eg, if sync
michael@0 761 // is yet to start up.) This might cause "A promise chain failed to
michael@0 762 // handle a rejection" messages, so add an error handler directly
michael@0 763 // on the promise to log the error.
michael@0 764 currentState.whenVerifiedDeferred.promise.then(null, err => {
michael@0 765 log.info("the wait for user verification was stopped: " + err);
michael@0 766 });
michael@0 767 }
michael@0 768 }
michael@0 769
michael@0 770 this.checkEmailStatus(sessionToken)
michael@0 771 .then((response) => {
michael@0 772 log.debug("checkEmailStatus -> " + JSON.stringify(response));
michael@0 773 if (response && response.verified) {
michael@0 774 // Bug 947056 - Server should be able to tell FxAccounts.jsm to back
michael@0 775 // off or stop polling altogether
michael@0 776 currentState.getUserAccountData()
michael@0 777 .then((data) => {
michael@0 778 data.verified = true;
michael@0 779 return currentState.setUserAccountData(data);
michael@0 780 })
michael@0 781 .then((data) => {
michael@0 782 // Now that the user is verified, we can proceed to fetch keys
michael@0 783 if (currentState.whenVerifiedDeferred) {
michael@0 784 currentState.whenVerifiedDeferred.resolve(data);
michael@0 785 delete currentState.whenVerifiedDeferred;
michael@0 786 }
michael@0 787 });
michael@0 788 } else {
michael@0 789 log.debug("polling with step = " + this.POLL_STEP);
michael@0 790 this.pollTimeRemaining -= this.POLL_STEP;
michael@0 791 log.debug("time remaining: " + this.pollTimeRemaining);
michael@0 792 if (this.pollTimeRemaining > 0) {
michael@0 793 this.currentTimer = setTimeout(() => {
michael@0 794 this.pollEmailStatus(currentState, sessionToken, "timer")}, this.POLL_STEP);
michael@0 795 log.debug("started timer " + this.currentTimer);
michael@0 796 } else {
michael@0 797 if (currentState.whenVerifiedDeferred) {
michael@0 798 currentState.whenVerifiedDeferred.reject(
michael@0 799 new Error("User email verification timed out.")
michael@0 800 );
michael@0 801 delete currentState.whenVerifiedDeferred;
michael@0 802 }
michael@0 803 }
michael@0 804 }
michael@0 805 });
michael@0 806 },
michael@0 807
michael@0 808 // Return the URI of the remote UI flows.
michael@0 809 getAccountsSignUpURI: function() {
michael@0 810 let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri");
michael@0 811 if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
michael@0 812 throw new Error("Firefox Accounts server must use HTTPS");
michael@0 813 }
michael@0 814 return url;
michael@0 815 },
michael@0 816
michael@0 817 // Return the URI of the remote UI flows.
michael@0 818 getAccountsSignInURI: function() {
michael@0 819 let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri");
michael@0 820 if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
michael@0 821 throw new Error("Firefox Accounts server must use HTTPS");
michael@0 822 }
michael@0 823 return url;
michael@0 824 },
michael@0 825
michael@0 826 // Returns a promise that resolves with the URL to use to force a re-signin
michael@0 827 // of the current account.
michael@0 828 promiseAccountsForceSigninURI: function() {
michael@0 829 let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri");
michael@0 830 if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
michael@0 831 throw new Error("Firefox Accounts server must use HTTPS");
michael@0 832 }
michael@0 833 let currentState = this.currentAccountState;
michael@0 834 // but we need to append the email address onto a query string.
michael@0 835 return this.getSignedInUser().then(accountData => {
michael@0 836 if (!accountData) {
michael@0 837 return null;
michael@0 838 }
michael@0 839 let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
michael@0 840 newQueryPortion += "email=" + encodeURIComponent(accountData.email);
michael@0 841 return url + newQueryPortion;
michael@0 842 }).then(result => currentState.resolve(result));
michael@0 843 }
michael@0 844 };
michael@0 845
michael@0 846 /**
michael@0 847 * JSONStorage constructor that creates instances that may set/get
michael@0 848 * to a specified file, in a directory that will be created if it
michael@0 849 * doesn't exist.
michael@0 850 *
michael@0 851 * @param options {
michael@0 852 * filename: of the file to write to
michael@0 853 * baseDir: directory where the file resides
michael@0 854 * }
michael@0 855 * @return instance
michael@0 856 */
michael@0 857 function JSONStorage(options) {
michael@0 858 this.baseDir = options.baseDir;
michael@0 859 this.path = OS.Path.join(options.baseDir, options.filename);
michael@0 860 };
michael@0 861
michael@0 862 JSONStorage.prototype = {
michael@0 863 set: function(contents) {
michael@0 864 return OS.File.makeDir(this.baseDir, {ignoreExisting: true})
michael@0 865 .then(CommonUtils.writeJSON.bind(null, contents, this.path));
michael@0 866 },
michael@0 867
michael@0 868 get: function() {
michael@0 869 return CommonUtils.readJSON(this.path);
michael@0 870 }
michael@0 871 };
michael@0 872
michael@0 873 // A getter for the instance to export
michael@0 874 XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
michael@0 875 let a = new FxAccounts();
michael@0 876
michael@0 877 // XXX Bug 947061 - We need a strategy for resuming email verification after
michael@0 878 // browser restart
michael@0 879 a.loadAndPoll();
michael@0 880
michael@0 881 return a;
michael@0 882 });

mercurial