services/fxaccounts/FxAccounts.jsm

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

mercurial