mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1004319 - Handle server-side account changes in Gecko. r=jedp
This commit is contained in:
parent
2761ee703f
commit
a5c496c317
@ -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");
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -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";
|
||||||
|
Loading…
Reference in New Issue
Block a user