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