Bug 1004319 - Handle server-side account changes in Gecko. r=jedp

This commit is contained in:
Sam Penrose 2014-06-06 08:54:27 -07:00
parent 2761ee703f
commit a5c496c317
3 changed files with 151 additions and 41 deletions

View File

@ -240,8 +240,11 @@ this.FxAccountsClient.prototype = {
* @param lifetime * @param lifetime
* The lifetime of the certificate * The lifetime of the certificate
* @return Promise * @return Promise
* Returns a promise that resolves to the signed certificate. The certificate * Returns a promise that resolves to the signed certificate.
* can be used to generate a Persona assertion. * The certificate can be used to generate a Persona assertion.
* @throws a new Error
* wrapping any of these HTTP code/errno pairs:
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12
*/ */
signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) { signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) {
let creds = this._deriveHawkCredentials(sessionTokenHex, "sessionToken"); let creds = this._deriveHawkCredentials(sessionTokenHex, "sessionToken");

View File

@ -152,8 +152,82 @@ this.FxAccountsManager = {
); );
}, },
/**
* Determine whether the incoming error means that the current account
* has new server-side state via deletion or password change, and if so,
* spawn the appropriate UI (sign in or refresh); otherwise re-reject.
*
* As of May 2014, the only HTTP call triggered by this._getAssertion()
* is to /certificate/sign via:
* FxAccounts.getAssertion()
* FxAccountsInternal.getCertificateSigned()
* FxAccountsClient.signCertificate()
* See the latter method for possible (error code, errno) pairs.
*/
_handleGetAssertionError: function(reason, aAudience) {
let errno = (reason ? reason.errno : NaN) || NaN;
// If the previously valid email/password pair is no longer valid ...
if (errno == ERRNO_INVALID_AUTH_TOKEN) {
return this._fxAccounts.accountStatus().then(
(exists) => {
// ... if the email still maps to an account, the password
// must have changed, so ask the user to enter the new one ...
if (exists) {
return this.getAccount().then(
(user) => {
return this._refreshAuthentication(aAudience, user.email);
}
);
// ... otherwise, the account was deleted, so ask for Sign In/Up
} else {
return this._localSignOut().then(
() => {
return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience);
},
(reason) => { // reject primary problem, not signout failure
log.error("Signing out in response to server error threw: " + reason);
return this._error(reason);
}
);
}
}
);
}
return rejection;
},
_getAssertion: function(aAudience) { _getAssertion: function(aAudience) {
return this._fxAccounts.getAssertion(aAudience); return this._fxAccounts.getAssertion(aAudience).then(
(result) => {
return result;
},
(reason) => {
return this._handleGetAssertionError(reason, aAudience);
}
);
},
_refreshAuthentication: function(aAudience, aEmail) {
this._refreshing = true;
return this._uiRequest(UI_REQUEST_REFRESH_AUTH,
aAudience, aEmail).then(
(assertion) => {
this._refreshing = false;
return assertion;
},
(reason) => {
this._refreshing = false;
return this._signOut().then(
() => {
return this._error(reason);
}
);
}
);
},
_localSignOut: function() {
return this._fxAccounts.signOut(true);
}, },
_signOut: function() { _signOut: function() {
@ -167,7 +241,7 @@ this.FxAccountsManager = {
// in case that we have network connection. // in case that we have network connection.
let sessionToken = this._activeSession.sessionToken; let sessionToken = this._activeSession.sessionToken;
return this._fxAccounts.signOut(true).then( return this._localSignOut().then(
() => { () => {
// At this point the local session should already be removed. // At this point the local session should already be removed.
@ -362,36 +436,41 @@ this.FxAccountsManager = {
}, },
/* /*
* Try to get an assertion for the given audience. * Try to get an assertion for the given audience. Here we implement
* the heart of the response to navigator.mozId.request() on device.
* (We can also be called via the IAC API, but it's request() that
* makes this method complex.) The state machine looks like this,
* ignoring simple errors:
* If no one is signed in, and we aren't suppressing the UI:
* trigger the sign in flow.
* else if we were asked to refresh and the grace period is up:
* trigger the refresh flow.
* else ask the core code for an assertion, which might itself
* trigger either the sign in or refresh flows (if our account
* changed on the server).
* *
* aOptions can include: * aOptions can include:
*
* refreshAuthentication - (bool) Force re-auth. * refreshAuthentication - (bool) Force re-auth.
*
* silent - (bool) Prevent any UI interaction. * silent - (bool) Prevent any UI interaction.
* I.e., try to get an automatic assertion. * I.e., try to get an automatic assertion.
*
*/ */
getAssertion: function(aAudience, aOptions) { getAssertion: function(aAudience, aOptions) {
if (!aAudience) { if (!aAudience) {
return this._error(ERROR_INVALID_AUDIENCE); return this._error(ERROR_INVALID_AUDIENCE);
} }
if (Services.io.offline) { if (Services.io.offline) {
return this._error(ERROR_OFFLINE); return this._error(ERROR_OFFLINE);
} }
return this.getAccount().then( return this.getAccount().then(
user => { user => {
if (user) { if (user) {
// We cannot get assertions for unverified accounts. // Three have-user cases to consider. First: are we unverified?
if (!user.verified) { if (!user.verified) {
return this._error(ERROR_UNVERIFIED_ACCOUNT, { return this._error(ERROR_UNVERIFIED_ACCOUNT, {
user: user user: user
}); });
} }
// Second case: do we need to refresh?
// RPs might require an authentication refresh.
if (aOptions && if (aOptions &&
(typeof(aOptions.refreshAuthentication) != "undefined")) { (typeof(aOptions.refreshAuthentication) != "undefined")) {
let gracePeriod = aOptions.refreshAuthentication; let gracePeriod = aOptions.refreshAuthentication;
@ -399,44 +478,24 @@ this.FxAccountsManager = {
return this._error(ERROR_INVALID_REFRESH_AUTH_VALUE); return this._error(ERROR_INVALID_REFRESH_AUTH_VALUE);
} }
// Forcing refreshAuth to silent is a contradiction in terms, // Forcing refreshAuth to silent is a contradiction in terms,
// though it will sometimes succeed silently. // though it might succeed silently if we didn't reject here.
if (aOptions.silent) { if (aOptions.silent) {
return this._error(ERROR_NO_SILENT_REFRESH_AUTH); return this._error(ERROR_NO_SILENT_REFRESH_AUTH);
} }
if ((Date.now() / 1000) - this._activeSession.authAt > gracePeriod) { let secondsSinceAuth = (Date.now() / 1000) - this._activeSession.authAt;
// Grace period expired, so we sign out and request the user to if (secondsSinceAuth > gracePeriod) {
// authenticate herself again. If the authentication succeeds, we return this._refreshAuthentication(aAudience, user.email);
// will return the assertion. Otherwise, we will return an error.
this._refreshing = true;
return this._uiRequest(UI_REQUEST_REFRESH_AUTH,
aAudience, user.email).then(
(assertion) => {
this._refreshing = false;
return assertion;
},
(reason) => {
this._refreshing = false;
return this._signOut().then(
() => {
return this._error(reason);
}
);
}
);
} }
} }
// Third case: we are all set *locally*. Probably we just return
// the assertion, but the attempt might lead to the server saying
// we are deleted or have a new password, which will trigger a flow.
return this._getAssertion(aAudience); return this._getAssertion(aAudience);
} }
log.debug("No signed in user"); log.debug("No signed in user");
if (aOptions && aOptions.silent) { if (aOptions && aOptions.silent) {
return Promise.resolve(null); return Promise.resolve(null);
} }
// If there is no currently signed in user, we trigger the signIn UI
// flow.
return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience); return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience);
} }
); );

View File

@ -12,6 +12,10 @@ Cu.import("resource://gre/modules/Promise.jsm");
// === Mocks === // === Mocks ===
// Globals representing server state
let passwordResetOnServer = false;
let deletedOnServer = false;
// Override FxAccountsUIGlue. // Override FxAccountsUIGlue.
const kFxAccountsUIGlueUUID = "{8f6d5d87-41ed-4bb5-aa28-625de57564c5}"; const kFxAccountsUIGlueUUID = "{8f6d5d87-41ed-4bb5-aa28-625de57564c5}";
const kFxAccountsUIGlueContractID = const kFxAccountsUIGlueContractID =
@ -54,6 +58,7 @@ let FxAccountsUIGlue = {
if (this._reject) { if (this._reject) {
deferred.reject(this._error); deferred.reject(this._error);
} else { } else {
passwordResetOnServer = false;
FxAccountsManager._activeSession = this._activeSession || { FxAccountsManager._activeSession = this._activeSession || {
email: "user@domain.org", email: "user@domain.org",
verified: false, verified: false,
@ -68,6 +73,7 @@ let FxAccountsUIGlue = {
}, },
signInFlow: function() { signInFlow: function() {
deletedOnServer = false;
this._signInFlowCalled = true; this._signInFlowCalled = true;
return this._promise(); return this._promise();
}, },
@ -104,13 +110,23 @@ FxAccountsManager._fxAccounts = {
this._reject = false; this._reject = false;
}, },
accountStatus: function() {
let deferred = Promise.defer();
deferred.resolve(!deletedOnServer);
return deferred.promise;
},
getAssertion: function() { getAssertion: function() {
if (!this._signedInUser) { if (!this._signedInUser) {
return null; return null;
} }
let deferred = Promise.defer(); let deferred = Promise.defer();
if (passwordResetOnServer || deletedOnServer) {
deferred.reject({errno: ERRNO_INVALID_AUTH_TOKEN});
} else {
deferred.resolve(this._assertion); deferred.resolve(this._assertion);
}
return deferred.promise; return deferred.promise;
}, },
@ -376,6 +392,38 @@ add_test(function(test_getAssertion_refreshAuth) {
); );
}); });
add_test(function(test_getAssertion_server_state_change) {
FxAccountsManager._fxAccounts._signedInUser.verified = true;
FxAccountsManager._activeSession.verified = true;
passwordResetOnServer = true;
FxAccountsManager.getAssertion("audience").then(
(result) => {
// For password reset, the UIGlue mock simulates sucessful
// refreshAuth which supplies new password, not signin/signup.
do_check_true(FxAccountsUIGlue._refreshAuthCalled);
do_check_false(FxAccountsUIGlue._signInFlowCalled)
do_check_eq(result, "assertion");
FxAccountsUIGlue._refreshAuthCalled = false;
}
).then(
() => {
deletedOnServer = true;
FxAccountsManager.getAssertion("audience").then(
(result) => {
// For account deletion, the UIGlue's signin/signup is called.
do_check_true(FxAccountsUIGlue._signInFlowCalled)
do_check_false(FxAccountsUIGlue._refreshAuthCalled);
do_check_eq(result, "assertion");
deletedOnServer = false;
passwordResetOnServer = false;
FxAccountsUIGlue._reset()
run_next_test();
}
);
}
);
});
add_test(function(test_getAssertion_refreshAuth_NaN) { add_test(function(test_getAssertion_refreshAuth_NaN) {
do_print("= getAssertion refreshAuth NaN="); do_print("= getAssertion refreshAuth NaN=");
let gracePeriod = "NaN"; let gracePeriod = "NaN";