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.

     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/. */
     5 this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
     7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
     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");
    20 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient",
    21   "resource://gre/modules/FxAccountsClient.jsm");
    23 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
    24   "resource://gre/modules/identity/jwcrypto.jsm");
    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 ];
    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 };
    67 AccountState.prototype = {
    68   cert: null,
    69   keyPair: null,
    70   signedInUser: null,
    71   whenVerifiedDeferred: null,
    72   whenKeysReadyDeferred: null,
    74   get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this,
    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     }
    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   },
    94   getUserAccountData: function() {
    95     // Skip disk if user is cached.
    96     if (this.signedInUser) {
    97       return this.resolve(this.signedInUser.accountData);
    98     }
   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   },
   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   },
   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   },
   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   },
   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   },
   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   },
   211 }
   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;
   234   for (let prop of keys) {
   235     let desc = Object.getOwnPropertyDescriptor(from, prop);
   237     if (typeof(desc.value) == "function") {
   238       desc.value = desc.value.bind(thisArg);
   239     }
   241     if (desc.get) {
   242       desc.get = desc.get.bind(thisArg);
   243     }
   245     if (desc.set) {
   246       desc.set = desc.set.bind(thisArg);
   247     }
   249     Object.defineProperty(to, prop, desc);
   250   }
   251 }
   253 /**
   254  * The public API's constructor.
   255  */
   256 this.FxAccounts = function (mockInternal) {
   257   let internal = new FxAccountsInternal();
   258   let external = {};
   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);
   265   // Copy all of the mock's properties to the internal object.
   266   if (mockInternal && !mockInternal.onlySetInternal) {
   267     copyObjectProperties(mockInternal, internal);
   268   }
   270   if (mockInternal) {
   271     // Exposes the internal object for testing only.
   272     external.internal = internal;
   273   }
   275   return Object.freeze(external);
   276 }
   278 /**
   279  * The internal API's constructor.
   280  */
   281 function FxAccountsInternal() {
   282   this.version = DATA_FORMAT_VERSION;
   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.
   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);
   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 }
   311 /**
   312  * The internal API's prototype.
   313  */
   314 FxAccountsInternal.prototype = {
   316   /**
   317    * The current data format's version number.
   318    */
   319   version: DATA_FORMAT_VERSION,
   321   _fxAccountsClient: null,
   323   get fxAccountsClient() {
   324     if (!this._fxAccountsClient) {
   325       this._fxAccountsClient = new FxAccountsClient();
   326     }
   327     return this._fxAccountsClient;
   328   },
   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   },
   338   getAccountsClient: function() {
   339     return this.fxAccountsClient;
   340   },
   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   },
   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   },
   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   },
   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()}
   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   },
   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();
   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));
   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   },
   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   },
   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   },
   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   },
   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   },
   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   },
   551   _signOutServer: function signOutServer(sessionToken) {
   552     return this.fxAccountsClient.signOut(sessionToken);
   553   },
   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    },
   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       }
   621       let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken);
   623       let data = yield currentState.getUserAccountData();
   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       }
   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);
   635       if (logPII) {
   636         log.debug("kB_hex: " + kB_hex);
   637       }
   638       data.kA = CommonUtils.bytesAsHex(kA);
   639       data.kB = CommonUtils.bytesAsHex(kB_hex);
   641       delete data.keyFetchToken;
   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       }
   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   },
   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   },
   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   },
   696   getUserAccountData: function() {
   697     return this.currentAccountState.getUserAccountData();
   698   },
   700   isUserEmailVerified: function isUserEmailVerified(data) {
   701     return !!(data && data.verified);
   702   },
   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   },
   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   },
   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   },
   745   notifyObservers: function(topic) {
   746     log.debug("Notifying observers of " + topic);
   747     Services.obs.notifyObservers(null, topic, null);
   748   },
   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     }
   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     },
   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   },
   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   },
   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 };
   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 };
   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   },
   868   get: function() {
   869     return CommonUtils.readJSON(this.path);
   870   }
   871 };
   873 // A getter for the instance to export
   874 XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
   875   let a = new FxAccounts();
   877   // XXX Bug 947061 - We need a strategy for resuming email verification after
   878   // browser restart
   879   a.loadAndPoll();
   881   return a;
   882 });

mercurial