michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: Cu.import("resource://services-crypto/utils.js"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Timer.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/FxAccountsCommon.js"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient", michael@0: "resource://gre/modules/FxAccountsClient.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", michael@0: "resource://gre/modules/identity/jwcrypto.jsm"); michael@0: michael@0: // All properties exposed by the public FxAccounts API. michael@0: let publicProperties = [ michael@0: "getAccountsClient", michael@0: "getAccountsSignInURI", michael@0: "getAccountsSignUpURI", michael@0: "getAssertion", michael@0: "getKeys", michael@0: "getSignedInUser", michael@0: "loadAndPoll", michael@0: "localtimeOffsetMsec", michael@0: "now", michael@0: "promiseAccountsForceSigninURI", michael@0: "resendVerificationEmail", michael@0: "setSignedInUser", michael@0: "signOut", michael@0: "version", michael@0: "whenVerified" michael@0: ]; michael@0: michael@0: // An AccountState object holds all state related to one specific account. michael@0: // Only one AccountState is ever "current" in the FxAccountsInternal object - michael@0: // whenever a user logs out or logs in, the current AccountState is discarded, michael@0: // making it impossible for the wrong state or state data to be accidentally michael@0: // used. michael@0: // In addition, it has some promise-related helpers to ensure that if an michael@0: // attempt is made to resolve a promise on a "stale" state (eg, if an michael@0: // operation starts, but a different user logs in before the operation michael@0: // completes), the promise will be rejected. michael@0: // It is intended to be used thusly: michael@0: // somePromiseBasedFunction: function() { michael@0: // let currentState = this.currentAccountState; michael@0: // return someOtherPromiseFunction().then( michael@0: // data => currentState.resolve(data) michael@0: // ); michael@0: // } michael@0: // If the state has changed between the function being called and the promise michael@0: // being resolved, the .resolve() call will actually be rejected. michael@0: AccountState = function(fxaInternal) { michael@0: this.fxaInternal = fxaInternal; michael@0: }; michael@0: michael@0: AccountState.prototype = { michael@0: cert: null, michael@0: keyPair: null, michael@0: signedInUser: null, michael@0: whenVerifiedDeferred: null, michael@0: whenKeysReadyDeferred: null, michael@0: michael@0: get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this, michael@0: michael@0: abort: function() { michael@0: if (this.whenVerifiedDeferred) { michael@0: this.whenVerifiedDeferred.reject( michael@0: new Error("Verification aborted; Another user signing in")); michael@0: this.whenVerifiedDeferred = null; michael@0: } michael@0: michael@0: if (this.whenKeysReadyDeferred) { michael@0: this.whenKeysReadyDeferred.reject( michael@0: new Error("Verification aborted; Another user signing in")); michael@0: this.whenKeysReadyDeferred = null; michael@0: } michael@0: this.cert = null; michael@0: this.keyPair = null; michael@0: this.signedInUser = null; michael@0: this.fxaInternal = null; michael@0: }, michael@0: michael@0: getUserAccountData: function() { michael@0: // Skip disk if user is cached. michael@0: if (this.signedInUser) { michael@0: return this.resolve(this.signedInUser.accountData); michael@0: } michael@0: michael@0: return this.fxaInternal.signedInUserStorage.get().then( michael@0: user => { michael@0: if (logPII) { michael@0: // don't stringify unless it will be written. We should replace this michael@0: // check with param substitutions added in bug 966674 michael@0: log.debug("getUserAccountData -> " + JSON.stringify(user)); michael@0: } michael@0: if (user && user.version == this.version) { michael@0: log.debug("setting signed in user"); michael@0: this.signedInUser = user; michael@0: } michael@0: return this.resolve(user ? user.accountData : null); michael@0: }, michael@0: err => { michael@0: if (err instanceof OS.File.Error && err.becauseNoSuchFile) { michael@0: // File hasn't been created yet. That will be done michael@0: // on the first call to getSignedInUser michael@0: return this.resolve(null); michael@0: } michael@0: return this.reject(err); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: setUserAccountData: function(accountData) { michael@0: return this.fxaInternal.signedInUserStorage.get().then(record => { michael@0: if (!this.isCurrent) { michael@0: return this.reject(new Error("Another user has signed in")); michael@0: } michael@0: record.accountData = accountData; michael@0: this.signedInUser = record; michael@0: return this.fxaInternal.signedInUserStorage.set(record) michael@0: .then(() => this.resolve(accountData)); michael@0: }); michael@0: }, michael@0: michael@0: michael@0: getCertificate: function(data, keyPair, mustBeValidUntil) { michael@0: if (logPII) { michael@0: // don't stringify unless it will be written. We should replace this michael@0: // check with param substitutions added in bug 966674 michael@0: log.debug("getCertificate" + JSON.stringify(this.signedInUser)); michael@0: } michael@0: // TODO: get the lifetime from the cert's .exp field michael@0: if (this.cert && this.cert.validUntil > mustBeValidUntil) { michael@0: log.debug(" getCertificate already had one"); michael@0: return this.resolve(this.cert.cert); michael@0: } michael@0: // else get our cert signed michael@0: let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME; michael@0: return this.fxaInternal.getCertificateSigned(data.sessionToken, michael@0: keyPair.serializedPublicKey, michael@0: CERT_LIFETIME).then( michael@0: cert => { michael@0: log.debug("getCertificate got a new one: " + !!cert); michael@0: this.cert = { michael@0: cert: cert, michael@0: validUntil: willBeValidUntil michael@0: }; michael@0: return cert; michael@0: } michael@0: ).then(result => this.resolve(result)); michael@0: }, michael@0: michael@0: getKeyPair: function(mustBeValidUntil) { michael@0: if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) { michael@0: log.debug("getKeyPair: already have a keyPair"); michael@0: return this.resolve(this.keyPair.keyPair); michael@0: } michael@0: // Otherwse, create a keypair and set validity limit. michael@0: let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME; michael@0: let d = Promise.defer(); michael@0: jwcrypto.generateKeyPair("DS160", (err, kp) => { michael@0: if (err) { michael@0: return this.reject(err); michael@0: } michael@0: this.keyPair = { michael@0: keyPair: kp, michael@0: validUntil: willBeValidUntil michael@0: }; michael@0: log.debug("got keyPair"); michael@0: delete this.cert; michael@0: d.resolve(this.keyPair.keyPair); michael@0: }); michael@0: return d.promise.then(result => this.resolve(result)); michael@0: }, michael@0: michael@0: resolve: function(result) { michael@0: if (!this.isCurrent) { michael@0: log.info("An accountState promise was resolved, but was actually rejected" + michael@0: " due to a different user being signed in. Originally resolved" + michael@0: " with: " + result); michael@0: return Promise.reject(new Error("A different user signed in")); michael@0: } michael@0: return Promise.resolve(result); michael@0: }, michael@0: michael@0: reject: function(error) { michael@0: // It could be argued that we should just let it reject with the original michael@0: // error - but this runs the risk of the error being (eg) a 401, which michael@0: // might cause the consumer to attempt some remediation and cause other michael@0: // problems. michael@0: if (!this.isCurrent) { michael@0: log.info("An accountState promise was rejected, but we are ignoring that" + michael@0: "reason and rejecting it due to a different user being signed in." + michael@0: "Originally rejected with: " + reason); michael@0: return Promise.reject(new Error("A different user signed in")); michael@0: } michael@0: return Promise.reject(error); michael@0: }, michael@0: michael@0: } michael@0: michael@0: /** michael@0: * Copies properties from a given object to another object. michael@0: * michael@0: * @param from (object) michael@0: * The object we read property descriptors from. michael@0: * @param to (object) michael@0: * The object that we set property descriptors on. michael@0: * @param options (object) (optional) michael@0: * {keys: [...]} michael@0: * Lets the caller pass the names of all properties they want to be michael@0: * copied. Will copy all properties of the given source object by michael@0: * default. michael@0: * {bind: object} michael@0: * Lets the caller specify the object that will be used to .bind() michael@0: * all function properties we find to. Will bind to the given target michael@0: * object by default. michael@0: */ michael@0: function copyObjectProperties(from, to, opts = {}) { michael@0: let keys = (opts && opts.keys) || Object.keys(from); michael@0: let thisArg = (opts && opts.bind) || to; michael@0: michael@0: for (let prop of keys) { michael@0: let desc = Object.getOwnPropertyDescriptor(from, prop); michael@0: michael@0: if (typeof(desc.value) == "function") { michael@0: desc.value = desc.value.bind(thisArg); michael@0: } michael@0: michael@0: if (desc.get) { michael@0: desc.get = desc.get.bind(thisArg); michael@0: } michael@0: michael@0: if (desc.set) { michael@0: desc.set = desc.set.bind(thisArg); michael@0: } michael@0: michael@0: Object.defineProperty(to, prop, desc); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * The public API's constructor. michael@0: */ michael@0: this.FxAccounts = function (mockInternal) { michael@0: let internal = new FxAccountsInternal(); michael@0: let external = {}; michael@0: michael@0: // Copy all public properties to the 'external' object. michael@0: let prototype = FxAccountsInternal.prototype; michael@0: let options = {keys: publicProperties, bind: internal}; michael@0: copyObjectProperties(prototype, external, options); michael@0: michael@0: // Copy all of the mock's properties to the internal object. michael@0: if (mockInternal && !mockInternal.onlySetInternal) { michael@0: copyObjectProperties(mockInternal, internal); michael@0: } michael@0: michael@0: if (mockInternal) { michael@0: // Exposes the internal object for testing only. michael@0: external.internal = internal; michael@0: } michael@0: michael@0: return Object.freeze(external); michael@0: } michael@0: michael@0: /** michael@0: * The internal API's constructor. michael@0: */ michael@0: function FxAccountsInternal() { michael@0: this.version = DATA_FORMAT_VERSION; michael@0: michael@0: // Make a local copy of these constants so we can mock it in testing michael@0: this.POLL_STEP = POLL_STEP; michael@0: this.POLL_SESSION = POLL_SESSION; michael@0: // We will create this.pollTimeRemaining below; it will initially be michael@0: // set to the value of POLL_SESSION. michael@0: michael@0: // We interact with the Firefox Accounts auth server in order to confirm that michael@0: // a user's email has been verified and also to fetch the user's keys from michael@0: // the server. We manage these processes in possibly long-lived promises michael@0: // that are internal to this object (never exposed to callers). Because michael@0: // Firefox Accounts allows for only one logged-in user, and because it's michael@0: // conceivable that while we are waiting to verify one identity, a caller michael@0: // could start verification on a second, different identity, we need to be michael@0: // able to abort all work on the first sign-in process. The currentTimer and michael@0: // currentAccountState are used for this purpose. michael@0: // (XXX - should the timer be directly on the currentAccountState?) michael@0: this.currentTimer = null; michael@0: this.currentAccountState = new AccountState(this); michael@0: michael@0: // We don't reference |profileDir| in the top-level module scope michael@0: // as we may be imported before we know where it is. michael@0: this.signedInUserStorage = new JSONStorage({ michael@0: filename: DEFAULT_STORAGE_FILENAME, michael@0: baseDir: OS.Constants.Path.profileDir, michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * The internal API's prototype. michael@0: */ michael@0: FxAccountsInternal.prototype = { michael@0: michael@0: /** michael@0: * The current data format's version number. michael@0: */ michael@0: version: DATA_FORMAT_VERSION, michael@0: michael@0: _fxAccountsClient: null, michael@0: michael@0: get fxAccountsClient() { michael@0: if (!this._fxAccountsClient) { michael@0: this._fxAccountsClient = new FxAccountsClient(); michael@0: } michael@0: return this._fxAccountsClient; michael@0: }, michael@0: michael@0: /** michael@0: * Return the current time in milliseconds as an integer. Allows tests to michael@0: * manipulate the date to simulate certificate expiration. michael@0: */ michael@0: now: function() { michael@0: return this.fxAccountsClient.now(); michael@0: }, michael@0: michael@0: getAccountsClient: function() { michael@0: return this.fxAccountsClient; michael@0: }, michael@0: michael@0: /** michael@0: * Return clock offset in milliseconds, as reported by the fxAccountsClient. michael@0: * This can be overridden for testing. michael@0: * michael@0: * The offset is the number of milliseconds that must be added to the client michael@0: * clock to make it equal to the server clock. For example, if the client is michael@0: * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. michael@0: */ michael@0: get localtimeOffsetMsec() { michael@0: return this.fxAccountsClient.localtimeOffsetMsec; michael@0: }, michael@0: michael@0: /** michael@0: * Ask the server whether the user's email has been verified michael@0: */ michael@0: checkEmailStatus: function checkEmailStatus(sessionToken) { michael@0: return this.fxAccountsClient.recoveryEmailStatus(sessionToken); michael@0: }, michael@0: michael@0: /** michael@0: * Once the user's email is verified, we can request the keys michael@0: */ michael@0: fetchKeys: function fetchKeys(keyFetchToken) { michael@0: log.debug("fetchKeys: " + !!keyFetchToken); michael@0: if (logPII) { michael@0: log.debug("fetchKeys - the token is " + keyFetchToken); michael@0: } michael@0: return this.fxAccountsClient.accountKeys(keyFetchToken); michael@0: }, michael@0: michael@0: // set() makes sure that polling is happening, if necessary. michael@0: // get() does not wait for verification, and returns an object even if michael@0: // unverified. The caller of get() must check .verified . michael@0: // The "fxaccounts:onverified" event will fire only when the verified michael@0: // state goes from false to true, so callers must register their observer michael@0: // and then call get(). In particular, it will not fire when the account michael@0: // was found to be verified in a previous boot: if our stored state says michael@0: // the account is verified, the event will never fire. So callers must do: michael@0: // register notification observer (go) michael@0: // userdata = get() michael@0: // if (userdata.verified()) {go()} michael@0: michael@0: /** michael@0: * Get the user currently signed in to Firefox Accounts. michael@0: * michael@0: * @return Promise michael@0: * The promise resolves to the credentials object of the signed-in user: michael@0: * { michael@0: * email: The user's email address michael@0: * uid: The user's unique id michael@0: * sessionToken: Session for the FxA server michael@0: * kA: An encryption key from the FxA server michael@0: * kB: An encryption key derived from the user's FxA password michael@0: * verified: email verification status michael@0: * authAt: The time (seconds since epoch) that this record was michael@0: * authenticated michael@0: * } michael@0: * or null if no user is signed in. michael@0: */ michael@0: getSignedInUser: function getSignedInUser() { michael@0: let currentState = this.currentAccountState; michael@0: return currentState.getUserAccountData().then(data => { michael@0: if (!data) { michael@0: return null; michael@0: } michael@0: if (!this.isUserEmailVerified(data)) { michael@0: // If the email is not verified, start polling for verification, michael@0: // but return null right away. We don't want to return a promise michael@0: // that might not be fulfilled for a long time. michael@0: this.startVerifiedCheck(data); michael@0: } michael@0: return data; michael@0: }).then(result => currentState.resolve(result)); michael@0: }, michael@0: michael@0: /** michael@0: * Set the current user signed in to Firefox Accounts. michael@0: * michael@0: * @param credentials michael@0: * The credentials object obtained by logging in or creating michael@0: * an account on the FxA server: michael@0: * { michael@0: * authAt: The time (seconds since epoch) that this record was michael@0: * authenticated michael@0: * email: The users email address michael@0: * keyFetchToken: a keyFetchToken which has not yet been used michael@0: * sessionToken: Session for the FxA server michael@0: * uid: The user's unique id michael@0: * unwrapBKey: used to unwrap kB, derived locally from the michael@0: * password (not revealed to the FxA server) michael@0: * verified: true/false michael@0: * } michael@0: * @return Promise michael@0: * The promise resolves to null when the data is saved michael@0: * successfully and is rejected on error. michael@0: */ michael@0: setSignedInUser: function setSignedInUser(credentials) { michael@0: log.debug("setSignedInUser - aborting any existing flows"); michael@0: this.abortExistingFlow(); michael@0: michael@0: let record = {version: this.version, accountData: credentials}; michael@0: let currentState = this.currentAccountState; michael@0: // Cache a clone of the credentials object. michael@0: currentState.signedInUser = JSON.parse(JSON.stringify(record)); michael@0: michael@0: // This promise waits for storage, but not for verification. michael@0: // We're telling the caller that this is durable now. michael@0: return this.signedInUserStorage.set(record).then(() => { michael@0: this.notifyObservers(ONLOGIN_NOTIFICATION); michael@0: if (!this.isUserEmailVerified(credentials)) { michael@0: this.startVerifiedCheck(credentials); michael@0: } michael@0: }).then(result => currentState.resolve(result)); michael@0: }, michael@0: michael@0: /** michael@0: * returns a promise that fires with the assertion. If there is no verified michael@0: * signed-in user, fires with null. michael@0: */ michael@0: getAssertion: function getAssertion(audience) { michael@0: log.debug("enter getAssertion()"); michael@0: let currentState = this.currentAccountState; michael@0: let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD; michael@0: return currentState.getUserAccountData().then(data => { michael@0: if (!data) { michael@0: // No signed-in user michael@0: return null; michael@0: } michael@0: if (!this.isUserEmailVerified(data)) { michael@0: // Signed-in user has not verified email michael@0: return null; michael@0: } michael@0: return currentState.getKeyPair(mustBeValidUntil).then(keyPair => { michael@0: return currentState.getCertificate(data, keyPair, mustBeValidUntil) michael@0: .then(cert => { michael@0: return this.getAssertionFromCert(data, keyPair, cert, audience); michael@0: }); michael@0: }); michael@0: }).then(result => currentState.resolve(result)); michael@0: }, michael@0: michael@0: /** michael@0: * Resend the verification email fot the currently signed-in user. michael@0: * michael@0: */ michael@0: resendVerificationEmail: function resendVerificationEmail() { michael@0: let currentState = this.currentAccountState; michael@0: return this.getSignedInUser().then(data => { michael@0: // If the caller is asking for verification to be re-sent, and there is michael@0: // no signed-in user to begin with, this is probably best regarded as an michael@0: // error. michael@0: if (data) { michael@0: this.pollEmailStatus(currentState, data.sessionToken, "start"); michael@0: return this.fxAccountsClient.resendVerificationEmail(data.sessionToken); michael@0: } michael@0: throw new Error("Cannot resend verification email; no signed-in user"); michael@0: }); michael@0: }, michael@0: michael@0: /* michael@0: * Reset state such that any previous flow is canceled. michael@0: */ michael@0: abortExistingFlow: function abortExistingFlow() { michael@0: if (this.currentTimer) { michael@0: log.debug("Polling aborted; Another user signing in"); michael@0: clearTimeout(this.currentTimer); michael@0: this.currentTimer = 0; michael@0: } michael@0: this.currentAccountState.abort(); michael@0: this.currentAccountState = new AccountState(this); michael@0: }, michael@0: michael@0: signOut: function signOut(localOnly) { michael@0: let currentState = this.currentAccountState; michael@0: let sessionToken; michael@0: return currentState.getUserAccountData().then(data => { michael@0: // Save the session token for use in the call to signOut below. michael@0: sessionToken = data && data.sessionToken; michael@0: return this._signOutLocal(); michael@0: }).then(() => { michael@0: // FxAccountsManager calls here, then does its own call michael@0: // to FxAccountsClient.signOut(). michael@0: if (!localOnly) { michael@0: // Wrap this in a promise so *any* errors in signOut won't michael@0: // block the local sign out. This is *not* returned. michael@0: Promise.resolve().then(() => { michael@0: // This can happen in the background and shouldn't block michael@0: // the user from signing out. The server must tolerate michael@0: // clients just disappearing, so this call should be best effort. michael@0: return this._signOutServer(sessionToken); michael@0: }).then(null, err => { michael@0: log.error("Error during remote sign out of Firefox Accounts: " + err); michael@0: }); michael@0: } michael@0: }).then(() => { michael@0: this.notifyObservers(ONLOGOUT_NOTIFICATION); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * This function should be called in conjunction with a server-side michael@0: * signOut via FxAccountsClient. michael@0: */ michael@0: _signOutLocal: function signOutLocal() { michael@0: this.abortExistingFlow(); michael@0: this.currentAccountState.signedInUser = null; // clear in-memory cache michael@0: return this.signedInUserStorage.set(null); michael@0: }, michael@0: michael@0: _signOutServer: function signOutServer(sessionToken) { michael@0: return this.fxAccountsClient.signOut(sessionToken); michael@0: }, michael@0: michael@0: /** michael@0: * Fetch encryption keys for the signed-in-user from the FxA API server. michael@0: * michael@0: * Not for user consumption. Exists to cause the keys to be fetch. michael@0: * michael@0: * Returns user data so that it can be chained with other methods. michael@0: * michael@0: * @return Promise michael@0: * The promise resolves to the credentials object of the signed-in user: michael@0: * { michael@0: * email: The user's email address michael@0: * uid: The user's unique id michael@0: * sessionToken: Session for the FxA server michael@0: * kA: An encryption key from the FxA server michael@0: * kB: An encryption key derived from the user's FxA password michael@0: * verified: email verification status michael@0: * } michael@0: * or null if no user is signed in michael@0: */ michael@0: getKeys: function() { michael@0: let currentState = this.currentAccountState; michael@0: return currentState.getUserAccountData().then((userData) => { michael@0: if (!userData) { michael@0: throw new Error("Can't get keys; User is not signed in"); michael@0: } michael@0: if (userData.kA && userData.kB) { michael@0: return userData; michael@0: } michael@0: if (!currentState.whenKeysReadyDeferred) { michael@0: currentState.whenKeysReadyDeferred = Promise.defer(); michael@0: if (userData.keyFetchToken) { michael@0: this.fetchAndUnwrapKeys(userData.keyFetchToken).then( michael@0: (dataWithKeys) => { michael@0: if (!dataWithKeys.kA || !dataWithKeys.kB) { michael@0: currentState.whenKeysReadyDeferred.reject( michael@0: new Error("user data missing kA or kB") michael@0: ); michael@0: return; michael@0: } michael@0: currentState.whenKeysReadyDeferred.resolve(dataWithKeys); michael@0: }, michael@0: (err) => { michael@0: currentState.whenKeysReadyDeferred.reject(err); michael@0: } michael@0: ); michael@0: } else { michael@0: currentState.whenKeysReadyDeferred.reject('No keyFetchToken'); michael@0: } michael@0: } michael@0: return currentState.whenKeysReadyDeferred.promise; michael@0: }).then(result => currentState.resolve(result)); michael@0: }, michael@0: michael@0: fetchAndUnwrapKeys: function(keyFetchToken) { michael@0: if (logPII) { michael@0: log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken); michael@0: } michael@0: let currentState = this.currentAccountState; michael@0: return Task.spawn(function* task() { michael@0: // Sign out if we don't have a key fetch token. michael@0: if (!keyFetchToken) { michael@0: log.warn("improper fetchAndUnwrapKeys() call: token missing"); michael@0: yield this.signOut(); michael@0: return null; michael@0: } michael@0: michael@0: let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken); michael@0: michael@0: let data = yield currentState.getUserAccountData(); michael@0: michael@0: // Sanity check that the user hasn't changed out from under us michael@0: if (data.keyFetchToken !== keyFetchToken) { michael@0: throw new Error("Signed in user changed while fetching keys!"); michael@0: } michael@0: michael@0: // Next statements must be synchronous until we setUserAccountData michael@0: // so that we don't risk getting into a weird state. michael@0: let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey), michael@0: wrapKB); michael@0: michael@0: if (logPII) { michael@0: log.debug("kB_hex: " + kB_hex); michael@0: } michael@0: data.kA = CommonUtils.bytesAsHex(kA); michael@0: data.kB = CommonUtils.bytesAsHex(kB_hex); michael@0: michael@0: delete data.keyFetchToken; michael@0: michael@0: log.debug("Keys Obtained: kA=" + !!data.kA + ", kB=" + !!data.kB); michael@0: if (logPII) { michael@0: log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB); michael@0: } michael@0: michael@0: yield currentState.setUserAccountData(data); michael@0: // We are now ready for business. This should only be invoked once michael@0: // per setSignedInUser(), regardless of whether we've rebooted since michael@0: // setSignedInUser() was called. michael@0: this.notifyObservers(ONVERIFIED_NOTIFICATION); michael@0: return data; michael@0: }.bind(this)).then(result => currentState.resolve(result)); michael@0: }, michael@0: michael@0: getAssertionFromCert: function(data, keyPair, cert, audience) { michael@0: log.debug("getAssertionFromCert"); michael@0: let payload = {}; michael@0: let d = Promise.defer(); michael@0: let options = { michael@0: duration: ASSERTION_LIFETIME, michael@0: localtimeOffsetMsec: this.localtimeOffsetMsec, michael@0: now: this.now() michael@0: }; michael@0: let currentState = this.currentAccountState; michael@0: // "audience" should look like "http://123done.org". michael@0: // The generated assertion will expire in two minutes. michael@0: jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => { michael@0: if (err) { michael@0: log.error("getAssertionFromCert: " + err); michael@0: d.reject(err); michael@0: } else { michael@0: log.debug("getAssertionFromCert returning signed: " + !!signed); michael@0: if (logPII) { michael@0: log.debug("getAssertionFromCert returning signed: " + signed); michael@0: } michael@0: d.resolve(signed); michael@0: } michael@0: }); michael@0: return d.promise.then(result => currentState.resolve(result)); michael@0: }, michael@0: michael@0: getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) { michael@0: log.debug("getCertificateSigned: " + !!sessionToken + " " + !!serializedPublicKey); michael@0: if (logPII) { michael@0: log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey); michael@0: } michael@0: return this.fxAccountsClient.signCertificate( michael@0: sessionToken, michael@0: JSON.parse(serializedPublicKey), michael@0: lifetime michael@0: ); michael@0: }, michael@0: michael@0: getUserAccountData: function() { michael@0: return this.currentAccountState.getUserAccountData(); michael@0: }, michael@0: michael@0: isUserEmailVerified: function isUserEmailVerified(data) { michael@0: return !!(data && data.verified); michael@0: }, michael@0: michael@0: /** michael@0: * Setup for and if necessary do email verification polling. michael@0: */ michael@0: loadAndPoll: function() { michael@0: let currentState = this.currentAccountState; michael@0: return currentState.getUserAccountData() michael@0: .then(data => { michael@0: if (data && !this.isUserEmailVerified(data)) { michael@0: this.pollEmailStatus(currentState, data.sessionToken, "start"); michael@0: } michael@0: return data; michael@0: }); michael@0: }, michael@0: michael@0: startVerifiedCheck: function(data) { michael@0: log.debug("startVerifiedCheck " + JSON.stringify(data)); michael@0: // Get us to the verified state, then get the keys. This returns a promise michael@0: // that will fire when we are completely ready. michael@0: // michael@0: // Login is truly complete once keys have been fetched, so once getKeys() michael@0: // obtains and stores kA and kB, it will fire the onverified observer michael@0: // notification. michael@0: return this.whenVerified(data) michael@0: .then(() => this.getKeys()); michael@0: }, michael@0: michael@0: whenVerified: function(data) { michael@0: let currentState = this.currentAccountState; michael@0: if (data.verified) { michael@0: log.debug("already verified"); michael@0: return currentState.resolve(data); michael@0: } michael@0: if (!currentState.whenVerifiedDeferred) { michael@0: log.debug("whenVerified promise starts polling for verified email"); michael@0: this.pollEmailStatus(currentState, data.sessionToken, "start"); michael@0: } michael@0: return currentState.whenVerifiedDeferred.promise.then( michael@0: result => currentState.resolve(result) michael@0: ); michael@0: }, michael@0: michael@0: notifyObservers: function(topic) { michael@0: log.debug("Notifying observers of " + topic); michael@0: Services.obs.notifyObservers(null, topic, null); michael@0: }, michael@0: michael@0: // XXX - pollEmailStatus should maybe be on the AccountState object? michael@0: pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) { michael@0: log.debug("entering pollEmailStatus: " + why); michael@0: if (why == "start") { michael@0: // If we were already polling, stop and start again. This could happen michael@0: // if the user requested the verification email to be resent while we michael@0: // were already polling for receipt of an earlier email. michael@0: this.pollTimeRemaining = this.POLL_SESSION; michael@0: if (!currentState.whenVerifiedDeferred) { michael@0: currentState.whenVerifiedDeferred = Promise.defer(); michael@0: // This deferred might not end up with any handlers (eg, if sync michael@0: // is yet to start up.) This might cause "A promise chain failed to michael@0: // handle a rejection" messages, so add an error handler directly michael@0: // on the promise to log the error. michael@0: currentState.whenVerifiedDeferred.promise.then(null, err => { michael@0: log.info("the wait for user verification was stopped: " + err); michael@0: }); michael@0: } michael@0: } michael@0: michael@0: this.checkEmailStatus(sessionToken) michael@0: .then((response) => { michael@0: log.debug("checkEmailStatus -> " + JSON.stringify(response)); michael@0: if (response && response.verified) { michael@0: // Bug 947056 - Server should be able to tell FxAccounts.jsm to back michael@0: // off or stop polling altogether michael@0: currentState.getUserAccountData() michael@0: .then((data) => { michael@0: data.verified = true; michael@0: return currentState.setUserAccountData(data); michael@0: }) michael@0: .then((data) => { michael@0: // Now that the user is verified, we can proceed to fetch keys michael@0: if (currentState.whenVerifiedDeferred) { michael@0: currentState.whenVerifiedDeferred.resolve(data); michael@0: delete currentState.whenVerifiedDeferred; michael@0: } michael@0: }); michael@0: } else { michael@0: log.debug("polling with step = " + this.POLL_STEP); michael@0: this.pollTimeRemaining -= this.POLL_STEP; michael@0: log.debug("time remaining: " + this.pollTimeRemaining); michael@0: if (this.pollTimeRemaining > 0) { michael@0: this.currentTimer = setTimeout(() => { michael@0: this.pollEmailStatus(currentState, sessionToken, "timer")}, this.POLL_STEP); michael@0: log.debug("started timer " + this.currentTimer); michael@0: } else { michael@0: if (currentState.whenVerifiedDeferred) { michael@0: currentState.whenVerifiedDeferred.reject( michael@0: new Error("User email verification timed out.") michael@0: ); michael@0: delete currentState.whenVerifiedDeferred; michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: // Return the URI of the remote UI flows. michael@0: getAccountsSignUpURI: function() { michael@0: let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri"); michael@0: if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting michael@0: throw new Error("Firefox Accounts server must use HTTPS"); michael@0: } michael@0: return url; michael@0: }, michael@0: michael@0: // Return the URI of the remote UI flows. michael@0: getAccountsSignInURI: function() { michael@0: let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri"); michael@0: if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting michael@0: throw new Error("Firefox Accounts server must use HTTPS"); michael@0: } michael@0: return url; michael@0: }, michael@0: michael@0: // Returns a promise that resolves with the URL to use to force a re-signin michael@0: // of the current account. michael@0: promiseAccountsForceSigninURI: function() { michael@0: let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri"); michael@0: if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting michael@0: throw new Error("Firefox Accounts server must use HTTPS"); michael@0: } michael@0: let currentState = this.currentAccountState; michael@0: // but we need to append the email address onto a query string. michael@0: return this.getSignedInUser().then(accountData => { michael@0: if (!accountData) { michael@0: return null; michael@0: } michael@0: let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; michael@0: newQueryPortion += "email=" + encodeURIComponent(accountData.email); michael@0: return url + newQueryPortion; michael@0: }).then(result => currentState.resolve(result)); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * JSONStorage constructor that creates instances that may set/get michael@0: * to a specified file, in a directory that will be created if it michael@0: * doesn't exist. michael@0: * michael@0: * @param options { michael@0: * filename: of the file to write to michael@0: * baseDir: directory where the file resides michael@0: * } michael@0: * @return instance michael@0: */ michael@0: function JSONStorage(options) { michael@0: this.baseDir = options.baseDir; michael@0: this.path = OS.Path.join(options.baseDir, options.filename); michael@0: }; michael@0: michael@0: JSONStorage.prototype = { michael@0: set: function(contents) { michael@0: return OS.File.makeDir(this.baseDir, {ignoreExisting: true}) michael@0: .then(CommonUtils.writeJSON.bind(null, contents, this.path)); michael@0: }, michael@0: michael@0: get: function() { michael@0: return CommonUtils.readJSON(this.path); michael@0: } michael@0: }; michael@0: michael@0: // A getter for the instance to export michael@0: XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() { michael@0: let a = new FxAccounts(); michael@0: michael@0: // XXX Bug 947061 - We need a strategy for resuming email verification after michael@0: // browser restart michael@0: a.loadAndPoll(); michael@0: michael@0: return a; michael@0: });