Bug 1065777 - Store the Hawk Session token after /fxa-oauth/params for the Loop FxA login flow. r=abr,jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 11 Sep 2014 23:01:04 -0700
changeset 204995 2767fb9328d0caae48bb81f67c777562af5a6da4
parent 204994 4223912e9f76e1e6d0576fbbfde68e24915ee0fe
child 204996 49fc0135c2564920fed0f35e3f56113a0ce0ee37
child 205046 d996743af7a3816754ebe60c843b23f1e3180a76
push id27474
push usercbook@mozilla.com
push dateFri, 12 Sep 2014 12:30:46 +0000
treeherdermozilla-central@49fc0135c256 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersabr, jaws
bugs1065777
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1065777 - Store the Hawk Session token after /fxa-oauth/params for the Loop FxA login flow. r=abr,jaws
browser/components/loop/MozLoopService.jsm
browser/components/loop/test/mochitest/browser_fxa_login.js
browser/components/loop/test/mochitest/browser_loop_fxa_server.js
browser/components/loop/test/mochitest/head.js
browser/components/loop/test/mochitest/loop_fxa.sjs
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -556,17 +556,26 @@ let MozLoopServiceInternal = {
   },
 
   /**
    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
    *
    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
    */
   promiseFxAOAuthParameters: function() {
-    return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/params", "POST").then(response => {
+    const SESSION_TYPE = LOOP_SESSION_TYPE.FXA;
+    return this.hawkRequest(SESSION_TYPE, "/fxa-oauth/params", "POST").then(response => {
+      if (!this.storeSessionToken(SESSION_TYPE, response.headers)) {
+        throw new Error("Invalid FxA hawk token returned");
+      }
+      let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(SESSION_TYPE));
+      if (prefType == Services.prefs.PREF_INVALID) {
+        throw new Error("No FxA hawk token returned and we don't have one saved");
+      }
+
       return JSON.parse(response.body);
     });
   },
 
   /**
    * Get the OAuth client constructed with Loop OAauth parameters.
    *
    * @return {Promise}
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -3,49 +3,50 @@
 
 /**
  * Test FxA logins with Loop.
  */
 
 "use strict";
 
 const {
-  LOOP_SESSION_TYPE,
-  gFxAOAuthTokenData
+  gFxAOAuthTokenData,
 } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
 
 const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?";
-const HAWK_TOKEN_LENGTH = 64;
 
 add_task(function* setup() {
   Services.prefs.setCharPref("loop.server", BASE_URL);
   Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
   registerCleanupFunction(function* () {
     info("cleanup time");
     yield promiseDeletedOAuthParams(BASE_URL);
     Services.prefs.clearUserPref("loop.server");
     Services.prefs.clearUserPref("services.push.serverURL");
+    resetFxA();
     Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST));
-    Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA));
   });
 });
 
 add_task(function* checkOAuthParams() {
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
     profile_uri: BASE_URL + "/profile",
     state: "state",
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
   let client = yield MozLoopServiceInternal.promiseFxAOAuthClient();
   for (let key of Object.keys(params)) {
     ise(client.parameters[key], params[key], "Check " + key + " was passed to the OAuth client");
   }
+  let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
+  let padding = "X".repeat(HAWK_TOKEN_LENGTH - params.client_id.length);
+  ise(Services.prefs.getCharPref(prefName), params.client_id + padding, "Check FxA hawk token");
 });
 
 add_task(function* basicAuthorization() {
   let result = yield MozLoopServiceInternal.promiseFxAOAuthAuthorization();
   is(result.code, "code1", "Check code");
   is(result.state, "state", "Check state");
 });
 
@@ -66,25 +67,53 @@ add_task(function* paramsInvalid() {
   yield loginPromise.catch(() => {
     ok(true, "The login promise should be rejected due to invalid params");
     caught = true;
   });
   ok(caught, "Should have caught the rejection");
   is(result, null, "No token data should be returned");
 });
 
+add_task(function* params_no_hawk_session() {
+  resetFxA();
+  let params = {
+    client_id: "client_id",
+    content_uri: BASE_URL + "/content",
+    oauth_uri: BASE_URL + "/oauth",
+    profile_uri: BASE_URL + "/profile",
+    state: "state",
+    test_error: "params_no_hawk",
+  };
+  yield promiseOAuthParamsSetup(BASE_URL, params);
+
+  let loginPromise = MozLoopService.logInToFxA();
+  let caught = false;
+  yield loginPromise.catch(() => {
+    ok(true, "The login promise should be rejected due to a lack of a hawk session");
+    caught = true;
+  });
+  ok(caught, "Should have caught the rejection");
+  let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
+  ise(Services.prefs.getPrefType(prefName),
+      Services.prefs.PREF_INVALID,
+      "Check FxA hawk token is not set");
+});
+
 add_task(function* params_nonJSON() {
-  resetFxA();
   Services.prefs.setCharPref("loop.server", "https://loop.invalid");
-  let result = null;
+  // Reset after changing the server so a new HawkClient is created
+  resetFxA();
+
   let loginPromise = MozLoopService.logInToFxA();
+  let caught = false;
   yield loginPromise.catch(() => {
     ok(true, "The login promise should be rejected due to non-JSON params");
+    caught = true;
   });
-  is(result, null, "No token data should be returned");
+  ok(caught, "Should have caught the rejection");
   Services.prefs.setCharPref("loop.server", BASE_URL);
 });
 
 add_task(function* invalidState() {
   resetFxA();
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
@@ -94,26 +123,41 @@ add_task(function* invalidState() {
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
   let loginPromise = MozLoopService.logInToFxA();
   yield loginPromise.catch((error) => {
     ok(error, "The login promise should be rejected due to invalid state");
   });
 });
 
+add_task(function* basicRegistrationWithoutSession() {
+  resetFxA();
+  yield promiseDeletedOAuthParams(BASE_URL);
+
+  let caught = false;
+  yield MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state").catch((error) => {
+    caught = true;
+    is(error.code, 401, "Should have returned a 401");
+  });
+  ok(caught, "Should have caught the error requesting /token without a hawk session");
+});
+
 add_task(function* basicRegistration() {
-  resetFxA();
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
     profile_uri: BASE_URL + "/profile",
     state: "state",
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
+  resetFxA();
+  // Create a fake FxA hawk session token
+  const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
+  Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
 
   let tokenData = yield MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state");
   is(tokenData.access_token, "code1_access_token", "Check access_token");
   is(tokenData.scope, "profile", "Check scope");
   is(tokenData.token_type, "bearer", "Check token_type");
 });
 
 add_task(function* registrationWithInvalidState() {
@@ -122,16 +166,20 @@ add_task(function* registrationWithInval
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
     profile_uri: BASE_URL + "/profile",
     state: "invalid_state",
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
 
+  // Create a fake FxA hawk session token
+  const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
+  Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
+
   let tokenPromise = MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state");
   yield tokenPromise.then(body => {
     ok(false, "Promise should have rejected");
   },
   error => {
     is(error.code, 400, "Check error code");
   });
 });
@@ -166,34 +214,28 @@ add_task(function* basicAuthorizationAnd
     profile_uri: BASE_URL + "/profile",
     state: "state",
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
 
   info("registering");
   mockPushHandler.pushUrl = "https://localhost/pushUrl/guest";
   yield MozLoopService.register(mockPushHandler);
-  let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST);
-  let padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join("");
-  ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check guest hawk token");
 
   // Normally the same pushUrl would be registered but we change it in the test
   // to be able to check for success on the second registration.
   mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa";
 
   let tokenData = yield MozLoopService.logInToFxA();
   ise(tokenData.access_token, "code1_access_token", "Check access_token");
   ise(tokenData.scope, "profile", "Check scope");
   ise(tokenData.token_type, "bearer", "Check token_type");
 
   let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
   ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL");
-  prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
-  padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join("");
-  ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check FxA hawk token");
 });
 
 add_task(function* loginWithParams401() {
   resetFxA();
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
--- a/browser/components/loop/test/mochitest/browser_loop_fxa_server.js
+++ b/browser/components/loop/test/mochitest/browser_loop_fxa_server.js
@@ -61,16 +61,17 @@ add_task(function* token_request() {
   let params = {
     client_id: "my_client_id",
     content_uri: "https://example.com/content/",
     oauth_uri: "https://example.com/oauth/",
     profile_uri: "https://example.com/profile/",
     state: "my_state",
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
+
   let request = yield promiseToken("my_code", params.state);
   ise(request.status, 200, "Check token response status");
   ise(request.response.access_token, "my_code_access_token", "Check access_token");
   ise(request.response.scope, "profile", "Check scope");
   ise(request.response.token_type, "bearer", "Check token_type");
 });
 
 add_task(function* token_request_invalid_state() {
@@ -106,16 +107,17 @@ function promiseParams() {
   return deferred.promise;
 }
 
 function promiseToken(code, state) {
   let deferred = Promise.defer();
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
               createInstance(Ci.nsIXMLHttpRequest);
   xhr.open("POST", BASE_URL + "/fxa-oauth/token", true);
+  xhr.setRequestHeader("Authorization", "Hawk ...");
   xhr.responseType = "json";
   xhr.addEventListener("load", () => {
     info("/fxa-oauth/token response:\n" + JSON.stringify(xhr.response, null, 4));
     deferred.resolve(xhr);
   });
   xhr.addEventListener("error", deferred.reject);
   let payload = {
     code: code,
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -1,13 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).
-                               MozLoopServiceInternal;
+const HAWK_TOKEN_LENGTH = 64;
+const {
+  LOOP_SESSION_TYPE,
+  MozLoopServiceInternal,
+} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
 
 var gMozLoopAPI;
 
 function promiseGetMozLoopAPI() {
   let deferred = Promise.defer();
   let loopPanel = document.getElementById("loop-notification-panel");
   let btn = document.getElementById("loop-call-button");
 
@@ -91,19 +94,22 @@ function promiseOAuthParamsSetup(baseURL
   xhr.addEventListener("error", error => deferred.reject(error));
   xhr.send();
 
   return deferred.promise;
 }
 
 function resetFxA() {
   let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
+  global.gHawkClient = null;
   global.gFxAOAuthClientPromise = null;
   global.gFxAOAuthClient = null;
   global.gFxAOAuthTokenData = null;
+  const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
+  Services.prefs.clearUserPref(fxASessionPref);
 }
 
 function promiseDeletedOAuthParams(baseURL) {
   let deferred = Promise.defer();
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
               createInstance(Ci.nsIXMLHttpRequest);
   xhr.open("DELETE", baseURL + "/setup_params", true);
   xhr.addEventListener("load", () => deferred.resolve(xhr));
--- a/browser/components/loop/test/mochitest/loop_fxa.sjs
+++ b/browser/components/loop/test/mochitest/loop_fxa.sjs
@@ -12,16 +12,17 @@ const HAWK_TOKEN_LENGTH = 64;
 
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 
 /**
  * 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.*/,"")) {
     case "/setup_params": // Test-only
       setup_params(request, response);
       return;
     case "/fxa-oauth/params":
       params(request, response);
       return;
     case encodeURIComponent("/oauth/authorization"):
@@ -97,16 +98,24 @@ function params(request, response) {
     if (!(paramName in params)) {
       dump("Warning: " + paramName + " is a required parameter\n");
     }
   }
 
   // Save the result so we have the effective `state` value.
   setSharedState("/fxa-oauth/params", JSON.stringify(params));
   response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+
+  let client_id = params.client_id || "";
+  // Pad the client_id with "X" until the token length to simulate a token
+  let padding = "X".repeat(HAWK_TOKEN_LENGTH - client_id.length);
+  if (params.test_error !== "params_no_hawk") {
+    response.setHeader("Hawk-Session-Token", client_id + padding, false);
+  }
+
   response.write(JSON.stringify(params, null, 2));
 }
 
 /**
  * GET /oauth/authorization endpoint for the test params.
  *
  * Redirect to a test page that uses WebChannel to complete the web flow.
  */
@@ -127,16 +136,23 @@ function token(request, response) {
   let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
 
   if (params.test_error && params.test_error == "token_401") {
     response.setStatusLine(request.httpVersion, 401, "Unauthorized");
     response.write("401 Unauthorized");
     return;
   }
 
+  if (!request.hasHeader("Authorization") ||
+        !request.getHeader("Authorization").startsWith("Hawk")) {
+    response.setStatusLine(request.httpVersion, 401, "Missing Hawk");
+    response.write("401 Missing Hawk Authorization header");
+    return;
+  }
+
   let body = NetUtil.readInputStreamToString(request.bodyInputStream,
                                              request.bodyInputStream.available());
   let payload = JSON.parse(body);
   if (!params.state || params.state !== payload.state) {
     response.setStatusLine(request.httpVersion, 400, "State mismatch");
     response.write("State mismatch");
     return;
   }
@@ -148,28 +164,30 @@ function token(request, response) {
   };
   response.setHeader("Content-Type", "application/json; charset=utf-8", false);
   response.write(JSON.stringify(tokenData, null, 2));
 }
 
 /**
  * POST /registration
  *
- * Mock Loop registration endpoint which simply returns the simplePushURL with
- * padding as the hawk session token.
+ * Mock Loop registration endpoint. Hawk Authorization headers are expected only for FxA sessions.
  */
 function registration(request, response) {
   let body = NetUtil.readInputStreamToString(request.bodyInputStream,
                                              request.bodyInputStream.available());
   let payload = JSON.parse(body);
+  if (payload.simplePushURL == "https://localhost/pushUrl/fxa" &&
+       (!request.hasHeader("Authorization") ||
+        !request.getHeader("Authorization").startsWith("Hawk"))) {
+    response.setStatusLine(request.httpVersion, 401, "Missing Hawk");
+    response.write("401 Missing Hawk Authorization header");
+    return;
+  }
   setSharedState("/registration", body);
-  let pushURL = payload.simplePushURL;
-  // Pad the pushURL with "X" to the token length to simulate a token
-  let padding = new Array(HAWK_TOKEN_LENGTH - pushURL.length).fill("X").join("");
-  response.setHeader("hawk-session-token", pushURL + padding, false);
 }
 
 /**
  * GET /get_registration
  *
  * Used for testing purposes to check if registration succeeded by returning the POST body.
  */
 function get_registration(request, response) {