Skip to content
Snippets Groups Projects
user.js 46.13 KiB
/*jshint browser:true, forin: true, laxbreak: true */
/*global _: true, BrowserID: true, console: true */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

BrowserID.User = (function() {
  "use strict";

  var jwcrypto, origin,
      bid = BrowserID,
      network = bid.Network,
      storage = bid.Storage,
      User,
      pollTimeout,
      provisioning = bid.Provisioning,
      addressCache = {},
      primaryAuthCache = {},
      complete = bid.Helpers.complete,
      registrationComplete = false,
      POLL_DURATION = 3000,
      pollDuration = POLL_DURATION;

  function prepareDeps() {
    if (!jwcrypto) {
      jwcrypto= require("./lib/jwcrypto");
    }
  }

  // remove identities that are no longer valid
  function cleanupIdentities(cb) {
    network.serverTime(function(serverTime) {
      network.domainKeyCreationTime(function(creationTime) {
        // Determine if a certificate is expired.  That will be
        // if it was issued *before* the domain key was last updated or
        // if the certificate expires in less that 5 minutes from now.
        function isExpired(cert) {
          // if it expires in less than 2 minutes, it's too old to use.
          var diff = cert.payload.exp.valueOf() - serverTime.valueOf();
          if (diff < (60 * 2 * 1000)) {
            return true;
          }

          // or if it was issued before the last time the domain key
          // was updated, it's invalid
          if (!cert.payload.iat || cert.payload.iat < creationTime) {
            return true;
          }

          return false;
        }

        var emails = storage.getEmails();
        var issued_identities = {};
        prepareDeps();
        _(emails).each(function(email_obj, email_address) {
          try {
            email_obj.pub = jwcrypto.loadPublicKeyFromObject(email_obj.pub);
          } catch (x) {
            storage.invalidateEmail(email_address);
            return;
          }

          // no cert? reset
          if (!email_obj.cert) {
            storage.invalidateEmail(email_address);
          } else {
            try {
              // parse the cert
              var cert = jwcrypto.extractComponents(emails[email_address].cert);

              // check if this certificate is still valid.
              if (isExpired(cert)) {
                storage.invalidateEmail(email_address);
              }

            } catch (e) {
              // error parsing the certificate!  Maybe it's of an old/different
              // format?  just delete it.
              try { console.log("error parsing cert for", email_address ,":", e); } catch(e2) { }
              storage.invalidateEmail(email_address);
            }
          }
        });
        cb();
      }, function(e) {
        // we couldn't get domain key creation time!  uh oh.
        cb();
      });
    });
  }

  function setAuthenticationStatus(authenticated) {
    if(window.$) {
      // TODO get this out of here!
      // jQuery is not included in the communication_iframe
      var func = authenticated ? 'addClass' : 'removeClass';
      $('body')[func]('authenticated');
    }

    if (!authenticated) {
      storage.clear();
    }
  }

  function handleStageAddressVerifictionResponse(onComplete, staged) {
    var status = { success: staged };

    if (!staged) status.reason = "throttle";
    // Used on the main site when the user verifies - once
    // verification is complete, the user is redirected back to the
    // RP and logged in.
    var site = User.getReturnTo();
    if (staged && site) storage.setReturnTo(site);

    complete(onComplete, status);
  }

  function markAddressVerified(email) {
    var idInfo = storage.getEmail(email) || {};
    idInfo.verified = true;
    storage.addSecondaryEmail(email, idInfo);
  }

  function completeAddressVerification(completeFunc, token, password, onComplete, onFailure) {
    User.tokenInfo(token, function(info) {
      var invalidInfo = { valid: false };
      if (info) {
        completeFunc(token, password, function (valid) {
          var result = invalidInfo;

          if (valid) {
            result = _.extend({ valid: valid }, info);
            // Now that the address is verified, its verified bit has to be
            // updated as well or else the user will be forced to verify the
            // address again.
            markAddressVerified(info.email);
            storage.setReturnTo("");
          }
          complete(onComplete, result);
        }, onFailure);
      } else if (onComplete) {
        onComplete(invalidInfo);
      }
    }, onFailure);

  }

  function addressVerificationPoll(checkFunc, email, onSuccess, onFailure) {
    function poll() {
      checkFunc(email, function(status) {
        // registration status checks the status of the last initiated registration,
        // it's possible return values are:
        //   'complete' - registration has been completed
        //   'pending'  - a registration is in progress
        //   'mustAuth' - user must authenticate
        //   'noRegistration' - no registration is in progress
        if (status === "complete" || status === "mustAuth") {
          // As soon as the registration comes back as complete, we should
          // ensure that the stagedOnBehalfOf is cleared so there is no stale
          // data.
          storage.setReturnTo("");

          // Now that the address is verified, its verified bit has to be
          // updated as well or else the user will be forced to verify the
          // address again.
          markAddressVerified(email);

          // To avoid too many address_info requests, returns from each
          // address_info request are cached.  If the user is doing
          // a addressVerificationPoll, it means the user was registering the address
          // and the registration has completed.  Because the status is
          // "complete" or "known", we know that the address is known, so we
          // toggle the field to be up to date.  If the known field remains
          // false, then the user is redirected back to the authentication
          // page and the system thinks the address must be verified again.
          if(addressCache[email]) {
            addressCache[email].known = true;
          }

          // registrationComplete is used in shouldAskIfUsersComputer to
          // prevent the user from seeing the "is this your computer" screen if
          // they just completed a registration.
          registrationComplete = true;

          if (status === "complete") {
            // If the response is complete but the user is not authenticated
            // to the password level, the user *must* authenticate or else
            // they will see an error when they try to certify a cert. Users
            // who have entered their password in this dialog session will be
            // automatically authenticated in modules/check_registration.js,
            // all others will have to enter their password. See issue #2088.
            network.checkAuth(function(authLevel) {
              if (authLevel !== "password") status = "mustAuth";
              complete(onSuccess, status);
            }, onFailure);
          }
          else complete(onSuccess, status);
        }
        else if (status === 'pending') {
          pollTimeout = setTimeout(poll, pollDuration);
        }
        else if (onFailure) {
            onFailure(status);
        }
      }, onFailure);
    }

    poll();
  }

  function cancelRegistrationPoll() {
    if (pollTimeout) {
      clearTimeout(pollTimeout);
      pollTimeout = null;
    }
  }

  function checkEmailType(type) {
    if (type !== 'secondary' && type !== 'primary')
      throw "invalid email type (should be 'secondary' or 'primary'): " + type;
  }

  function getIdPName(addressInfo) {
    return addressInfo.email.replace(/.*@/, "");
  }

  /**
   * Persist an address and key pair locally.
   * @method persistEmailKeypair
   * @param {string} email - Email address to persist.
   * @param {object} keypair - Key pair to save
   * @param {function} [onComplete] - Called on successful completion.
   * @param {function} [onFailure] - Called on error.
   */
  function persistEmailKeypair(email, type, keypair, cert, onComplete, onFailure) {
    checkEmailType(type);
    var now = new Date();
    var email_obj = storage.getEmails()[email] || {
      created: now,
      type: type
    };

    _.extend(email_obj, {
      updated: now,
      pub: keypair.publicKey.toSimpleObject(),
      priv: keypair.secretKey.toSimpleObject(),
      cert: cert
    });

    storage.addEmail(email, email_obj);
    if (onComplete) onComplete(true);
  }

  /**
   * Certify an identity with the server, persist it to storage if the server
   * says the identity is good
   * @method certifyEmailKeypair
   */
  function certifyEmailKeypair(email, keypair, onComplete, onFailure) {
    network.certKey(email, keypair.publicKey, function(cert) {
      // emails that *we* certify are always secondary emails
      persistEmailKeypair(email, "secondary", keypair, cert, onComplete, onFailure);
    }, onFailure);
  }

  /**
   * Persist an email address without a keypair
   * @method persistEmail
   * @param {object} options - options to save
   * @param {string} options.email - Email address to persist.
   * @param {string} options.type - Is the email a 'primary' or a 'secondary' address?
   * @param {string} options.verified - If the email is 'secondary', is it verified?
   */
  function persistEmail(options) {
    checkEmailType(options.type);
    storage.addEmail(options.email, {
      created: new Date(),
      type: options.type,
      verified: options.verified
    });
  }


  User = {
    init: function(config) {
      if (config.provisioning) {
        provisioning = config.provisioning;
      }

      // BEGIN TESTING API
      if (config.pollDuration) {
        pollDuration = config.pollDuration;
      }
      // END TESTING API
    },

    reset: function() {
      provisioning = BrowserID.Provisioning;
      User.resetCaches();
      registrationComplete = false;
      pollDuration = POLL_DURATION;
    },

    resetCaches: function() {
      addressCache = {};
      primaryAuthCache = {};
    },

    /**
     * Set the interface to use for networking.  Used for unit testing.
     * @method setNetwork
     * @param {BrowserID.Network} networkInterface - BrowserID.Network
     * compatible interface to use.
     */
    setNetwork: function(networkInterface) {
      network = networkInterface;
    },

    /**
     * setOrigin
     * @method setOrigin
     * @param {string} origin
     */
    setOrigin: function(originArg) {
      origin = originArg;
    },

    /**
     * Get the origin of the current host being signed in to.
     * @method getOrigin
     * @return {string} origin
     */
    getOrigin: function() {
      return origin;
    },

    setOriginEmail: function(email) {
      storage.site.set(origin, "email", email);
    },

    getOriginEmail: function() {
      return storage.site.get(origin, "email");
    },

    /**
     * Get the hostname for the set origin
     * @method getHostname
     * @returns {string}
     */
    getHostname: function() {
      return origin.replace(/^.*:\/\//, "").replace(/:\d*$/, "");
    },

    setReturnTo: function(returnTo) {
      this.returnTo = returnTo;
    },

    getReturnTo: function() {
      return this.returnTo;
    },

    /**
     * Create a user account - this creates an user account that must be verified.
     * @method createSecondaryUser
     * @param {string} email
     * @param {string} password
     * @param {function} [onComplete] - Called on completion.
     * @param {function} [onFailure] - Called on error.
     */
    createSecondaryUser: function(email, password, onComplete, onFailure) {
      network.createUser(email, password, origin,
        handleStageAddressVerifictionResponse.curry(onComplete), onFailure);
    },

    /**
     * Create a primary user.
     * @method createPrimaryUser
     * @param {object} info
     * @param {function} onComplete - function to call on complettion.  Called
     * with two parameters - status and info.
     * Status can be:
     *  primary.already_added
     *  primary.verified
     *  primary.verify
     *  primary.could_not_add
     *
     *  info is passed on primary.verify and contains the info necessary to
     *  verify the user with the IdP
     */
    createPrimaryUser: function(info, onComplete, onFailure) {
      var email = info.email;
      User.provisionPrimaryUser(email, info, function(status, provInfo) {
        if (status === "primary.verified") {
          User.authenticateWithAssertion(email, provInfo.assertion, function(status) {
            if (status) {
              onComplete("primary.verified");
            }
            else {
              onComplete("primary.could_not_add");
            }
          }, onFailure);
        }
        else {
          onComplete(status, provInfo);
        }
      }, onFailure);
    },

    /**
     * A full provision a primary user, if they are authenticated, save their
     * cert/keypair.  Note, we do not authenticate to login.persona.org but
     * merely get an assertion for login.persona.org so that we can either add the
     * email to the current account or authenticate the user if not
     * authenticated.
     * @method provisionPrimaryUser
     * @param {string} email
     * @param {object} info - provisioning info
     * @param {function} [onComplete] - called when complete.  Called with
     * status field and info. Status can be:
     *  primary.already_added
     *  primary.verified
     *  primary.verify
     *  primary.could_not_add
     * @param {function} [onFailure] - called on failure
     */
    provisionPrimaryUser: function(email, info, onComplete, onFailure) {
      User.primaryUserAuthenticationInfo(email, info, function(authInfo) {
        if(authInfo.authenticated) {
          persistEmailKeypair(email, "primary", authInfo.keypair, authInfo.cert,
            function() {
              // We are getting an assertion for persona.org.
              User.getAssertion(email, "https://login.persona.org", function(assertion) {
                if (assertion) {
                  onComplete("primary.verified", {
                    assertion: assertion
                  });
                }
                else {
                  onComplete("primary.could_not_add");
                }
              }, onFailure);
            }
          );
        }
        else {
          onComplete("primary.verify", info);
        }
      }, onFailure);
    },

    /**
     * Get the IdP authentication info for a user.
     * @method primaryUserAuthenticationInfo
     * @param {string} email
     * @param {object} info - provisioning info
     * @param {function} [onComplete] - called when complete.  Called with
     * provisioning info as well as keypair, cert, and authenticated.
     *   authenticated - boolean, true if user is authenticated with primary.
     *    false otw.
     *   keypair - returned if user is authenticated.
     *   cert - returned if user is authenticated.
     * @param {function} [onFailure] - called on failure
     */
    primaryUserAuthenticationInfo: function(email, info, onComplete, onFailure) {
      var idInfo = storage.getEmail(email),
          self=this;

      primaryAuthCache = primaryAuthCache || {};

      function complete(info) {
        primaryAuthCache[email] = info;
        onComplete && _.defer(onComplete.curry(info));
      }

      if(primaryAuthCache[email]) {
        // If we have the info in our cache, we most definitely do not have to
        // ask for it.
        complete(primaryAuthCache[email]);
        return;
      }
      else if(idInfo && idInfo.cert) {
        // If we already have the info in storage, we know the user has a valid
        // cert with their IdP, we say they are authenticated and pass back the
        // appropriate info.
        var userInfo = _.extend({authenticated: true}, idInfo, info);
        complete(userInfo);
        return;
      }

      provisioning(
        {
          email: email,
          url: info.prov,
          ephemeral: !storage.usersComputer.confirmed(email)
        },
        function(keypair, cert) {
          var userInfo = _.extend({
            keypair: keypair,
            cert: cert,
            authenticated: true
          }, info);

          complete(userInfo);
        },
        function(error) {
          if (error.code === "primaryError" && error.msg === "user is not authenticated as target user") {
            var userInfo = _.extend({
              authenticated: false
            }, info);
            complete(userInfo);
          }
          else {
            onFailure(info);
          }
        }
      );
    },

    /**
     * Get the IdP authentication status for a user.
     * @method isUserAuthenticatedToPrimary
     * @param {string} email
     * @param {object} info - provisioning info
     * @param {function} [onComplete] - called when complete.  Called with
     *   status field - true if user authenticated with IdP, false otw.
     * @param {function} [onFailure] - called on failure
     */
    isUserAuthenticatedToPrimary: function(email, info, onComplete, onFailure) {
      User.primaryUserAuthenticationInfo(email, info, function(authInfo) {
        onComplete(authInfo.authenticated);
      }, onFailure);
    },

    /**
     * Poll the server until user registration is complete.
     * @method waitForUserValidation
     * @param {string} email - email address to check.
     * @param {function} [onSuccess] - Called to give status updates.
     * @param {function} [onFailure] - Called on error.
     */
    waitForUserValidation: addressVerificationPoll.curry(network.checkUserRegistration),

    /**
     * Cancel the waitForUserValidation poll
     * @method cancelUserValidation
     */
    cancelUserValidation: function() {
      cancelRegistrationPoll();
    },

    /**
     * Get site and email info for a token
     * @method tokenInfo
     * @param {string} token
     * @param {function} [onComplete]
     * @param {function} [onFailure]
     */
    tokenInfo: function(token, onComplete, onFailure) {
      network.emailForVerificationToken(token, function (info) {
        if(info) {
          info = _.extend(info, { returnTo: storage.getReturnTo() });
        }

        complete(onComplete, info);
      }, onFailure);

    },

    /**
     * Verify a user
     * @method verifyUser
     * @param {string} token - token to verify.
     * @param {string} password
     * @param {function} [onComplete] - Called on completion.
     *   Called with an object with valid, email, and origin if valid, called
     *   with valid=false otw.
     * @param {function} [onFailure] - Called on error.
     */
    verifyUser: completeAddressVerification.curry(network.completeUserRegistration),

    /**
     * Check if the user can set their password.  Only returns true for users
     * with secondary accounts
     * @method canSetPassword
     * @param {function} [onComplete] - Called on with boolean flag on
     * successful completion.
     * @param {function} [onFailure] - Called on error.
     */
    canSetPassword: function(onComplete, onFailure) {
      User.hasSecondary(onComplete, onFailure);
    },

    /**
     * Set the initial password of the current user.
     * @method setPassword
     * @param {string} password - password to set
     * @param {function} [onComplete] - Called on successful completion.
     * @param {function} [onFailure] - Called on error.
     */
    setPassword: function(password, onComplete, onFailure) {
      network.setPassword(password, onComplete, onFailure);
    },

    /**
     * update the password of the current user.
     * @method changePassword
     * @param {string} oldpassword - the old password.
     * @param {string} newpassword - the new password.
     * @param {function} [onComplete] - called on completion.  Called with one
     * parameter, status - set to true if password update is successful, false
     * otw.
     * @param {function} [onFailure] - called on XHR failure.
     */
    changePassword: function(oldpassword, newpassword, onComplete, onFailure) {
      network.changePassword(oldpassword, newpassword, onComplete, onFailure);
    },

    /**
     * Request a password reset for the given email address.
     * @method requestPasswordReset
     * @param {string} email
     * @param {string} password
     * @param {function} [onComplete] - Callback to call when complete, called
     * with a single object, info.
     *    info.status {boolean} - true or false whether request was successful.
     *    info.reason {string} - if status false, reason of failure.
     * @param {function} [onFailure] - Called on XHR failure.
     */
    requestPasswordReset: function(email, password, onComplete, onFailure) {
      User.addressInfo(email, function(info) {
        // user is not known.  Can't request a password reset.
        if (!info.known) {
          complete(onComplete, { success: false, reason: "invalid_user" });
        }
        // user is trying to reset the password of a primary address.
        else if (info.type === "primary") {
          complete(onComplete, { success: false, reason: "primary_address" });
        }
        else {
          network.requestPasswordReset(email, password, origin,
            handleStageAddressVerifictionResponse.curry(onComplete), onFailure);
        }
      }, onFailure);
    },

    /**
     * Verify the password reset for a user.
     * @method completePasswordReset
     * @param {string} token - token to verify.
     * @param {string} password
     * @param {function} [onComplete] - Called on completion.
     *   Called with an object with valid, email, and origin if valid, called
     *   with valid=false otw.
     * @param {function} [onFailure] - Called on error.
     */
    completePasswordReset: completeAddressVerification.curry(network.completePasswordReset),

    /**
     * Wait for the password reset to complete
     * @method waitForPasswordResetComplete
     * @param {string} email - email address to check.
     * @param {function} [onSuccess] - Called to give status updates.
     * @param {function} [onFailure] - Called on error.
     */
    waitForPasswordResetComplete: addressVerificationPoll.curry(network.checkPasswordReset),

    /**
     * Cancel the waitForPasswordResetComplete poll
     * @method cancelWaitForPasswordResetComplete
     */
    cancelWaitForPasswordResetComplete: cancelRegistrationPoll,

    /**
     * Request the reverification of an unverified email address
     * @method requestEmailReverify
     * @param {string} email
     * @param {function} [onComplete]
     * @param {function} [onFailure]
     */
    requestEmailReverify: function(email, onComplete, onFailure) {
      var idInfo = storage.getEmail(email);
      if (!idInfo) {
        // user does not own this address.
        complete(onComplete, { success: false, reason: "invalid_email" });
      }
      else if (idInfo.verified) {
        // this email is already verified, cannot be reverified.
        complete(onComplete, { success: false, reason: "verified_email" });
      }
      else if (!idInfo.verified) {
        // this address is unverified, try to reverify it.
        network.requestEmailReverify(email, origin,
          handleStageAddressVerifictionResponse.curry(onComplete), onFailure);
      }
    },

    // the verification page for reverifying an email and adding an email to an
    // account are the same, both are handled by the /confirm page. the
    // /confirm page uses the verifyEmail function.  completeEmailReverify is
    // not needed.

    /**
     * Wait for the email reverification to complete
     * @method waitForEmailReverifyComplete
     * @param {string} email - email address to check.
     * @param {function} [onSuccess] - Called to give status updates.
     * @param {function} [onFailure] - Called on error.
     */
    waitForEmailReverifyComplete: addressVerificationPoll.curry(network.checkEmailReverify),

    /**
     * Cancel the waitForEmailReverifyComplete poll
     * @method cancelWaitForEmailReverifyComplete
     */
    cancelWaitForEmailReverifyComplete: cancelRegistrationPoll,

    /**
     * Cancel the current user's account.  Remove last traces of their
     * identity.
     * @method cancelUser
     * @param {function} [onComplete] - Called whenever complete.
     * @param {function} [onFailure] - called on error.
     */
    cancelUser: function(onComplete, onFailure) {
      network.cancelUser(function() {
        setAuthenticationStatus(false);
        if (onComplete) {
          onComplete();
        }
      }, onFailure);

    },

    /**
     * Log the current user out.
     * @method logoutUser
     * @param {function} [onComplete] - Called whenever complete.
     * @param {function} [onFailure] - called on error.
     */
    logoutUser: function(onComplete, onFailure) {
      // logout of all websites
      storage.logoutEverywhere();

      // log out of browserid
      network.logout(function() {
        setAuthenticationStatus(false);
        complete(onComplete);
      }, onFailure);
    },

    /**
     * Sync local identities with login.persona.org.  Generally should not need to
     * be called.
     * @method syncEmails
     * @param {function} [onComplete] - Called whenever complete.
     * @param {function} [onFailure] - Called on error.
     */
    syncEmails: function(onComplete, onFailure) {
      cleanupIdentities(function () {
        var issued_identities = User.getStoredEmailKeypairs();

        network.listEmails(function(emails) {
          // lists of emails
          var client_emails = _.keys(issued_identities);
          var server_emails = _.keys(emails);

          var emails_to_add = _.difference(server_emails, client_emails);
          var emails_to_remove = _.difference(client_emails, server_emails);
          var emails_to_update = _.intersection(client_emails, server_emails);

          // remove emails
          _.each(emails_to_remove, function(email) {
            storage.removeEmail(email);
          });

          // these are new emails
          _.each(emails_to_add, function(email) {
            var emailInfo = emails[email];

            persistEmail({
              email: email,
              type: emailInfo.type || "secondary",
              verified: emailInfo.verified
            });
          });

          // update the type and verified status of stored emails
          _.each(emails_to_update, function(email) {
            var emailInfo = emails[email],
                storedEmailInfo = storage.getEmail(email);

            _.extend(storedEmailInfo, {
              type: emailInfo.type,
              verified: emailInfo.verified
            });

            storage.addEmail(email, storedEmailInfo);
          });

          complete(onComplete);

        }, onFailure);
      });
    },

    /**
     * Check whether the current user is authenticated.  Calls the callback
     * with false if cookies are disabled.
     * @method checkAuthentication
     * @param {function} [onComplete] - Called when check is complete with one
     * boolean parameter, authenticated.  authenticated will be true if user is
     * authenticated, false otw.
     * @param {function} [onFailure] - Called on error.
     */
    checkAuthentication: function(onComplete, onFailure) {
      network.cookiesEnabled(function(cookiesEnabled) {
        if(cookiesEnabled) {
          network.checkAuth(function(authenticated) {
            setAuthenticationStatus(authenticated);
            if (!authenticated) authenticated = false;
            complete(onComplete, authenticated);
          }, onFailure);
        }
        else {
          complete(onComplete, cookiesEnabled);
        }
      }, onFailure);
    },

    /**
     * Check whether the current user is authenticated.  If authenticated, sync
     * identities.
     * @method checkAuthenticationAndSync
     * @param {function} [onComplete] - Called on sync completion with one
     * boolean parameter, authenticated.  authenticated will be true if user
     * is authenticated, false otw.
     * @param {function} [onFailure] - Called on error.
     */
    checkAuthenticationAndSync: function(onComplete, onFailure) {
      User.checkAuthentication(function(authenticated) {
        if (authenticated) {
          User.syncEmails(function() {
            onComplete && onComplete(authenticated);
          }, onFailure);
        }
        else {
          onComplete && onComplete(authenticated);
        }
      }, onFailure);
    },

    /**
     * Authenticate the user with the given email and password.  This will sync
     * the user's addresses.
     * @method authenticate
     * @param {string} email - Email address to authenticate.
     * @param {string} password - Password.
     * @param {function} [onComplete] - Called on completion with status. true
     * if user is authenticated, false otw.
     * @param {function} [onFailure] - Called on error.
     */
    authenticate: function(email, password, onComplete, onFailure) {
      // password is out of length range.  Don't even send the request
      // and waste backend cycles. See issue #2032.
      if (password.length < bid.PASSWORD_MIN_LENGTH
       || password.length > bid.PASSWORD_MAX_LENGTH) {
        complete(onComplete, false);
        return;
      }

      network.authenticate(email, password, function(authenticated) {
        setAuthenticationStatus(authenticated);

        if(authenticated) {
          User.syncEmails(function() {
            onComplete && onComplete(authenticated);
          }, onFailure);
        } else if (onComplete) {
          onComplete(authenticated);
        }
      }, onFailure);
    },

    /**
     * Authenticate the user with the given email and assertion.  This will sync
     * the user's addresses.
     * @method authenticateWithAssertion
     * @param {string} email
     * @param {string} assertion
     * @param {function} [onComplete] - Called on completion with status. true
     * if user is authenticated, false otw.
     * @param {function} [onFailure] - Called on error.
     */
    authenticateWithAssertion: function(email, assertion, onComplete, onFailure) {
      network.authenticateWithAssertion(email, assertion, function(authenticated) {
        setAuthenticationStatus(authenticated);

        if (authenticated) {
          User.syncEmails(function() {
            complete(onComplete, authenticated);
          }, onFailure);
        } else {
          complete(onComplete, authenticated);
        }
      }, onFailure);

    },
    /**
     * Check whether the email is already registered.
     * @method isEmailRegistered
     * @param {string} email - Email address to check.
     * @param {function} [onComplete] - Called with one boolean parameter when
     * complete.  Parameter is true if `email` is already registered, false
     * otw.
     * @param {function} [onFailure] - Called on XHR failure.
     */
    isEmailRegistered: function(email, onComplete, onFailure) {
      network.emailRegistered(email, onComplete, onFailure);
    },

    /**
     * Get information about an email address.  Who vouches for it?
     * (is it a primary or a secondary)
     * @method addressInfo
     * @param {string} email - Email address to check.
     * @param {function} [onComplete] - Called with an object on success,
     *   containing these properties:
     *     type: <secondary|primary>
     *     known: boolean, present if type is secondary.  True if email
     *        address is registered with BrowserID.
     *     authed: boolean, present if type is primary - whether the user
     *        is authenticated to the IdP as this user.
     *     auth: string - url to send users for auth - present if type is
     *        primary.
     *     prov: string - url to embed for silent provisioning - present
     *        if type is secondary.
     * @param {function} [onFailure] - Called on XHR failure.
     */
    addressInfo: function(email, onComplete, onFailure) {
      function complete(info) {
        info.email = email;

        addressCache[email] = info;
        onComplete && onComplete(info);
      }

      if(addressCache[email]) {
        complete(addressCache[email]);
      }
      else {
        network.addressInfo(email, function(info) {
          info.email = email;
          if(info.type === "primary") {
            User.isEmailRegistered(email, function(registered) {
              User.isUserAuthenticatedToPrimary(email, info, function(authed) {
                info.known = registered;
                info.authed = authed;
                info.idpName = getIdPName(info);
                complete(info);
              }, onFailure);
            }, onFailure);
          }
          else {
            complete(info);
          }
        }, onFailure);
      }
    },

    /**
     * Add an email address to an already created account.  Sends address and
     * keypair to the server, user then needs to verify account ownership. This
     * does not add the new email address/keypair to the local list of
     * valid identities.
     * @method addEmail
     * @param {string} email
     * @param {string} password
     * @param {function} [onComplete] - Called on successful completion.
     * @param {function} [onFailure] - Called on error.
     */
    addEmail: function(email, password, onComplete, onFailure) {
      network.addSecondaryEmail(email, password, origin, function(added) {
        // Used on the main site when the user verifies - once verification
        // is complete, the user is redirected back to the RP and logged in.
        var returnTo = User.getReturnTo();
        if (added && returnTo) storage.setReturnTo(returnTo);

        // we no longer send the keypair, since we will certify it later.
        complete(onComplete, added);
      }, onFailure);
    },

    /**
     * Check whether a password is needed to add a secondary email address to
     * an already existing account.
     * @method passwordNeededToAddSecondaryEmail
     * @param {function} [onComplete] - Called on successful completion, called
     * with true if password is needed, false otw.
     * @param {function} [onFailure] - Called on error.
     */
    passwordNeededToAddSecondaryEmail: function(onComplete, onFailure) {
      var emails = storage.getEmails(),
          passwordNeeded = true;

      for(var key in emails) {
        if(emails[key].type === "secondary") {
          passwordNeeded = false;
        }
      }

      complete(onComplete, passwordNeeded);
    },

    /**
     * Wait for the email registration to complete
     * @method waitForEmailValidation
     * @param {string} email - email address to check.
     * @param {function} [onSuccess] - Called to give status updates.
     * @param {function} [onFailure] - Called on error.
     */
    waitForEmailValidation: addressVerificationPoll.curry(network.checkEmailRegistration),

    /**
     * Cancel the waitForEmailValidation poll
     * @method cancelEmailValidation
     */
    cancelEmailValidation: function() {
      cancelRegistrationPoll();
    },

    /**
     * Verify a users email address given by the token
     * @method verifyEmail
     * @param {string} token
     * @param {string} password
     * @param {function} [onComplete] - Called on completion.
     *   Called with an object with valid, email, and origin if valid, called
     *   with valid=false otw.
     * @param {function} [onFailure] - Called on error.
     */
    verifyEmail: completeAddressVerification.curry(network.completeEmailRegistration),

    /**
     * Remove an email address.
     * @method removeEmail
     * @param {string} email - Email address to remove.
     * @param {function} [onComplete] - Called when complete.
     * @param {function} [onFailure] - Called on error.
     */
    removeEmail: function(email, onComplete, onFailure) {
      if (storage.getEmail(email)) {
        network.removeEmail(email, function() {
          storage.removeEmail(email);
          if (onComplete) {
            onComplete();
          }
        }, onFailure);
      } else if (onComplete) {
        onComplete();
      }
    },

    /**
     * Sync an identity with the server.  Creates and stores locally and on the
     * server a keypair for the given email address.
     * @method syncEmailKeypair
     * @param {string} email - Email address.
     * @param {string} [issuer] - Issuer of keypair.
     * @param {function} [onComplete] - Called on completion.  Called with
     * status parameter - true if successful, false otw.
     * @param {function} [onFailure] - Called on error.
     */
    syncEmailKeypair: function(email, onComplete, onFailure) {
      prepareDeps();
      // jwcrypto depends on a random seed being set to generate a keypair.
      // The seed is set with a call to network.withContext.  Ensure the
      // random seed is set before continuing or else the seed may not be set,
      // the key never created, and the onComplete callback never called.
      network.withContext(function() {
        jwcrypto.generateKeypair({algorithm: "DS", keysize: bid.KEY_LENGTH}, function(err, keypair) {
          certifyEmailKeypair(email, keypair, onComplete, onFailure);
        });
      });
    },


    /**
     * Get an assertion for an identity
     * @method getAssertion
     * @param {string} email - Email to get assertion for.
     * @param {string} audience - Audience to use for the assertion.
     * @param {function} [onComplete] - Called with assertion, null otw.
     * @param {function} [onFailure] - Called on error.
     */
    getAssertion: function(email, audience, onComplete, onFailure) {
      // we use the current time from the browserid servers
      // to avoid issues with clock drift on user's machine.
      // (issue #329)
        function complete(status) {
          onComplete && onComplete(status);
        }

        var storedID = storage.getEmail(email),
            assertion,
            self=this;

        function createAssertion(idInfo) {
          network.serverTime(function(serverTime) {
            var sk = jwcrypto.loadSecretKeyFromObject(idInfo.priv);

            setTimeout(function() {
              // assertions are valid for 2 minutes
              var expirationMS = serverTime.getTime() + (2 * 60 * 1000);
              var expirationDate = new Date(expirationMS);

              jwcrypto.assertion.sign(
                {}, {audience: audience, expiresAt: expirationDate},
                sk,
                function(err, signedAssertion) {
                  assertion = jwcrypto.cert.bundle([idInfo.cert], signedAssertion);
                  storage.site.set(audience, "email", email);
                  complete(assertion);
                });
            }, 0);
          }, onFailure);
        }

        if (storedID) {
          prepareDeps();
          if (storedID.priv) {
            // parse the secret key
            // yield to the render thread!
            setTimeout(function() {
              createAssertion(storedID);
            }, 0);
          }
          else {
            if (storedID.type === "primary") {
              // first we have to get the address info, then attempt
              // a provision, then if the user is provisioned, go and get an
              // assertion.
              User.addressInfo(email, function(info) {
                User.provisionPrimaryUser(email, info, function(status) {
                  if (status === "primary.verified") {
                    User.getAssertion(email, audience, onComplete, onFailure);
                  }
                  else {
                    complete(null);
                  }
                }, onFailure);
              }, onFailure);
            }
            else {
              // we have no key for this identity, go generate the key,
              // sync it and then get the assertion recursively.
              User.syncEmailKeypair(email, function(status) {
                User.getAssertion(email, audience, onComplete, onFailure);
              }, onFailure);
            }
          }
        }
        else {
          complete(null);
        }
    },

    /**
     * Get the list of identities stored locally.
     * @method getStoredEmailKeypairs
     * @return {object} identities.
     */
    getStoredEmailKeypairs: function() {
      return storage.getEmails();
    },

    /**
     * Get the list of identities sorted by address.
     * @method getSortedEmailKeypairs
     * @return {array} of objects, with two fields, address, data
     */
    getSortedEmailKeypairs: function() {
      var identities = User.getStoredEmailKeypairs(),
          sortedIdentities = [];

      for(var key in identities) {
        if(identities.hasOwnProperty(key)) {
          sortedIdentities.push({ address: key, info: identities[key] });
        }
      }

      sortedIdentities.sort(function(a, b) {
        var retval = a.address > b.address ? 1 : a.address < b.address ? -1 : 0;
        return retval;
      });

      return sortedIdentities;
    },

    /**
     * Get an individual stored identity.
     * @method getStoredEmailKeypair
     * @return {object} identity information for email, if exists, undefined
     * otw.
     */
    getStoredEmailKeypair: function(email) {
      return storage.getEmail(email);
    },

    /**
     * Clear the list of identities stored locally.
     * @method clearStoredEmailKeypairs
     */
    clearStoredEmailKeypairs: function() {
      storage.clear();
    },

    /**
     * Get an assertion for the current domain if the user is signed into it
     * @method getSilentAssertion
     * @param {function} onComplete - called on completion.  Called with an
     * an email and assertion if successful, null otw.
     * @param {function} onFailure - called on XHR failure.
     */
    getSilentAssertion: function(siteSpecifiedEmail, onComplete, onFailure) {
      // XXX: why do we need to check authentication status here explicitly.
      //      why can't we fail later?  the problem with doing this is that
      //      knowing correct present authentication status requires that we
      //      talk to the server, because you can be logged in or logged out
      //      in many different contexts (dialog, manage page, cookies expire).
      //      so if we rely on localstorage only and check authentication status
      //      only when we know a network request will be required, we very well
      //      might have fewer race conditions and do fewer network requests.
      User.checkAuthenticationAndSync(function(authenticated) {
        if (authenticated) {
          var loggedInEmail = storage.getLoggedIn(origin);
          if (loggedInEmail !== siteSpecifiedEmail) {
            if (loggedInEmail) {
              User.getAssertion(loggedInEmail, origin, function(assertion) {
                onComplete(assertion ? loggedInEmail : null, assertion);
              }, onFailure);
            } else {
              onComplete(null, null);
            }
          } else {
            onComplete(loggedInEmail, null);
          }
        }
        else if (onComplete) {
          onComplete(null, null);
        }
      }, onFailure);
    },

    /**
     * Clear the persistent signin field for the current origin
     * @method logout
     * @param {function} onComplete - called on completion.  Called with
     * a boolean, true if successful, false otw.
     * @param {function} onFailure - called on XHR failure.
     */
    logout: function(onComplete, onFailure) {
      User.checkAuthentication(function(authenticated) {
        if (authenticated) {
          storage.setLoggedIn(origin, false);
        }

        if (onComplete) {
          onComplete(!!authenticated);
        }
      }, onFailure);
    },

    /**
     * Check if the user has any secondary addresses.
     * @method hasSecondary
     * @param {function} onComplete - called with true if user has at least one
     * email address, false otw.
     * @param {function} onFailure - called on XHR failure.
     */
    hasSecondary: function(onComplete, onFailure) {
      var hasSecondary = false,
          emails = storage.getEmails();

      for(var key in emails) {
        if(emails[key].type === "secondary") {
          hasSecondary = true;
          break;
        }
      }

      onComplete(hasSecondary);
    },

    /**
     * Set whether the user owns the computer or not.
     * @method setComputerOwnershipStatus
     * @param {boolean} userOwnsComputer - true if user owns computer, false otw.
     * @param {function} onComplete - called on successful completion.
     * @param {function} onFailure - called on XHR failure.
     */
    setComputerOwnershipStatus: function(userOwnsComputer, onComplete, onFailure) {
      var userID = network.userid();
      if(typeof userID !== "undefined") {
        if (userOwnsComputer) {
          storage.usersComputer.setConfirmed(userID);
          network.prolongSession(onComplete, onFailure);
        }
        else {
          storage.usersComputer.setDenied(userID);
          complete(onComplete);
        }
      } else {
        complete(onFailure, "user is not authenticated");
      }
    },

    /**
     * Check if the user owns the computer
     * @method isUsersComputer
     */
    isUsersComputer: function(onComplete, onFailure) {
      var userID = network.userid();
      if(typeof userID !== "undefined") {
        complete(onComplete, storage.usersComputer.confirmed(userID));
      } else {
        complete(onFailure, "user is not authenticated");
      }
    },

    /**
     * Check whether the user should be asked if this is their computer
     * @method shouldAskIfUsersComputer
     */
    shouldAskIfUsersComputer: function(onComplete, onFailure) {
      var userID = network.userid();
      if(typeof userID !== "undefined") {
        // A user should never be asked if they completed an email
        // registration/validation in this dialog session.
        var shouldAsk = storage.usersComputer.shouldAsk(userID)
                        && !registrationComplete;
        complete(onComplete, shouldAsk);
      } else {
        complete(onFailure, "user is not authenticated");
      }
    }

  };

  // Set origin to default to the current domain.  Other contexts that use user.js,
  // like dialogs or iframes, will call setOrigin themselves to update this to
  // the origin of the of the RP.  On login.persona.org, it will remain the origin of
  // login.persona.org
  var currentOrigin = window.location.protocol + '//' + window.location.hostname;
  if (window.location.port) {
    currentOrigin += ':' + window.location.port;
  }
  User.setOrigin(currentOrigin);

  return User;
}());