Bug 1047667 - Unregister logged in user from the Loop server upon logout. r=jaws

This commit is contained in:
Matthew Noorenberghe 2014-09-18 13:53:44 -07:00
parent 44294c5842
commit 592c049281
7 changed files with 183 additions and 12 deletions

View File

@ -426,6 +426,14 @@ function injectLoopAPI(targetWindow) {
}
},
logOutFromFxA: {
enumerable: true,
writable: true,
value: function() {
return MozLoopService.logOutFromFxA();
}
},
/**
* Copies passed string onto the system clipboard.
*

View File

@ -73,7 +73,6 @@ XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
let gRegisteredDeferred = null;
let gPushHandler = null;
let gHawkClient = null;
let gRegisteredLoopServer = false;
let gLocalizedStrings = null;
let gInitializeTimer = null;
let gFxAOAuthClientPromise = null;
@ -292,6 +291,20 @@ let MozLoopServiceInternal = {
return true;
},
/**
* Clear the loop session token so we don't use it for Hawk Requests anymore.
*
* This should normally be used after unregistering with the server so it can
* clean up session state first.
*
* @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
* One of the LOOP_SESSION_TYPE members.
*/
clearSessionToken: function(sessionType) {
Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType));
},
/**
* Callback from MozLoopPushHandler - The push server has been registered
* and has given us a push url.
@ -314,7 +327,7 @@ let MozLoopServiceInternal = {
// No need to clear the promise here, everything was good, so we don't need
// to re-register.
}, (error) => {
Cu.reportError("Failed to register with Loop server: " + error.errno);
console.error("Failed to register with Loop server: ", error);
gRegisteredDeferred.reject(error.errno);
gRegisteredDeferred = null;
});
@ -349,20 +362,50 @@ let MozLoopServiceInternal = {
}
// Authorization failed, invalid token, we need to try again with a new token.
Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType));
this.clearSessionToken(sessionType);
if (retry) {
return this.registerWithLoopServer(sessionType, pushUrl, false);
}
}
// XXX Bubble the precise details up to the UI somehow (bug 1013248).
Cu.reportError("Failed to register with the loop server. error: " + error);
console.error("Failed to register with the loop server. Error: ", error);
this.setError("registration", error);
throw error;
}
);
},
/**
* Unregisters from the Loop server either as a guest or a FxA user.
*
* This is normally only wanted for FxA users as we normally want to keep the
* guest session with the device.
*
* @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
* @param {String} pushURL The push URL previously given by the push server.
* This may not be necessary to unregister in the future.
* @return {Promise} resolving when the unregistration request finishes
*/
unregisterFromLoopServer: function(sessionType, pushURL) {
let unregisterURL = "/registration?simplePushURL=" + encodeURIComponent(pushURL);
return this.hawkRequest(sessionType, unregisterURL, "DELETE")
.then(() => {
MozLoopServiceInternal.clearSessionToken(sessionType);
},
error => {
// Always clear the registration token regardless of whether the server acknowledges the logout.
MozLoopServiceInternal.clearSessionToken(sessionType);
if (error.code === 401 && error.errno === INVALID_AUTH_TOKEN) {
// Authorization failed, invalid token. This is fine since it may mean we already logged out.
return;
}
console.error("Failed to unregister with the loop server. Error: ", error);
throw error;
});
},
/**
* Callback from MozLoopPushHandler - A push notification has been received from
* the server.
@ -1038,6 +1081,30 @@ this.MozLoopService = {
});
},
/**
* Logs the user out from FxA.
*
* Gracefully handles if the user is already logged out.
*
* @return {Promise} that resolves when the FxA logout flow is complete.
*/
logOutFromFxA: Task.async(function*() {
yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA,
gPushHandler.pushUrl);
gFxAOAuthTokenData = null;
gFxAOAuthProfile = null;
// Reset the client since the initial promiseFxAOAuthParameters() call is
// what creates a new session.
gFxAOAuthClient = null;
gFxAOAuthClientPromise = null;
// clearError calls notifyStatusChanged so should be done last when the
// state is clean.
MozLoopServiceInternal.clearError("registration");
}),
/**
* Performs a hawk based request to the loop server.
*

View File

@ -221,7 +221,6 @@ loop.panel = (function(_, mozL10n) {
handleClickAuthEntry: function() {
if (this._isSignedIn()) {
// XXX to be implemented - bug 979845
navigator.mozLoop.logOutFromFxA();
} else {
navigator.mozLoop.logInToFxA();

View File

@ -221,7 +221,6 @@ loop.panel = (function(_, mozL10n) {
handleClickAuthEntry: function() {
if (this._isSignedIn()) {
// XXX to be implemented - bug 979845
navigator.mozLoop.logOutFromFxA();
} else {
navigator.mozLoop.logInToFxA();

View File

@ -250,7 +250,8 @@ add_task(function* basicAuthorizationAndRegistration() {
is(loopButton.getAttribute("state"), "active", "state of loop button should be active when logged in");
let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL");
ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa",
"Check registered push URL");
let loopPanel = document.getElementById("loop-notification-panel");
loopPanel.hidePopup();
@ -259,6 +260,15 @@ add_task(function* basicAuthorizationAndRegistration() {
yield statusChangedPromise;
is(loopButton.getAttribute("state"), "", "state of loop button should return to empty after panel is opened");
loopPanel.hidePopup();
info("logout");
yield MozLoopService.logOutFromFxA();
checkLoggedOutState();
registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response, null,
"Check registration was deleted on the server");
is(visibleEmail.textContent, "Guest", "Guest should be displayed on the panel again after logout");
is(MozLoopService.userProfile, null, "userProfile should be null after logout");
});
add_task(function* loginWithParams401() {
@ -284,6 +294,52 @@ add_task(function* loginWithParams401() {
});
});
add_task(function* logoutWithIncorrectPushURL() {
resetFxA();
let pushURL = "http://www.example.com/";
mockPushHandler.pushUrl = pushURL;
// Create a fake FxA hawk session token
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, pushURL);
let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, pushURL, "Check registered push URL");
mockPushHandler.pushUrl = "http://www.example.com/invalid";
let caught = false;
yield MozLoopService.logOutFromFxA().catch((error) => {
caught = true;
});
ok(caught, "Should have caught an error logging out with a mismatched push URL");
checkLoggedOutState();
registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, pushURL, "Check registered push URL wasn't deleted");
});
add_task(function* logoutWithNoPushURL() {
resetFxA();
let pushURL = "http://www.example.com/";
mockPushHandler.pushUrl = pushURL;
// Create a fake FxA hawk session token
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, pushURL);
let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, pushURL, "Check registered push URL");
mockPushHandler.pushUrl = null;
let caught = false;
yield MozLoopService.logOutFromFxA().catch((error) => {
caught = true;
});
ok(caught, "Should have caught an error logging out without a push URL");
checkLoggedOutState();
registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, pushURL, "Check registered push URL wasn't deleted");
});
add_task(function* loginWithRegistration401() {
resetFxA();
let params = {

View File

@ -123,6 +123,17 @@ function setInternalLoopGlobal(aName, aValue) {
global[aName] = aValue;
}
function checkLoggedOutState() {
let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
ise(global.gFxAOAuthClientPromise, null, "gFxAOAuthClientPromise should be cleared");
ise(global.gFxAOAuthProfile, null, "gFxAOAuthProfile should be cleared");
ise(global.gFxAOAuthClient, null, "gFxAOAuthClient should be cleared");
ise(global.gFxAOAuthTokenData, null, "gFxAOAuthTokenData should be cleared");
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
ise(Services.prefs.getPrefType(fxASessionPref), Services.prefs.PREF_INVALID,
"FxA hawk session should be cleared anyways");
}
function promiseDeletedOAuthParams(baseURL) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].

View File

@ -11,28 +11,34 @@ const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri",
const HAWK_TOKEN_LENGTH = 64;
Components.utils.import("resource://gre/modules/NetUtil.jsm");
Components.utils.importGlobalProperties(["URL"]);
/**
* Entry point for HTTP requests.
*/
function handleRequest(request, response) {
// Look at the query string but ignore past the encoded ? when deciding on the handler.
dump("loop_fxa.sjs request for: " + request.queryString + "\n");
switch (request.queryString.replace(/%3F.*/,"")) {
// Convert the query string to a path with a placeholder base of example.com
let url = new URL(request.queryString.replace(/%3F.*/,""), "http://www.example.com");
dump("loop_fxa.sjs request for: " + url.pathname + "\n");
switch (url.pathname) {
case "/setup_params": // Test-only
setup_params(request, response);
return;
case "/fxa-oauth/params":
params(request, response);
return;
case encodeURIComponent("/oauth/authorization"):
case "/" + encodeURIComponent("/oauth/authorization"):
oauth_authorization(request, response);
return;
case "/fxa-oauth/token":
token(request, response);
return;
case "/registration":
registration(request, response);
if (request.method == "DELETE") {
delete_registration(request, response);
} else {
registration(request, response);
}
return;
case "/get_registration": // Test-only
get_registration(request, response);
@ -201,6 +207,31 @@ function registration(request, response) {
setSharedState("/registration", body);
}
/**
* DELETE /registration
*
* Hawk Authorization headers are required.
*/
function delete_registration(request, response) {
if (!request.hasHeader("Authorization") ||
!request.getHeader("Authorization").startsWith("Hawk")) {
response.setStatusLine(request.httpVersion, 401, "Missing Hawk");
response.write("401 Missing Hawk Authorization header");
return;
}
// Do some query string munging due to the SJS file using a base with a trailing "?"
// making the path become a query parameter. This is because we aren't actually
// registering endpoints at the root of the hostname e.g. /registration.
let url = new URL(request.queryString.replace(/%3F.*/,""), "http://www.example.com");
let registration = JSON.parse(getSharedState("/registration"));
if (registration.simplePushURL == url.searchParams.get("simplePushURL")) {
setSharedState("/registration", "");
} else {
response.setStatusLine(request.httpVersion, 400, "Bad Request");
}
}
/**
* GET /get_registration
*