Bug 1047617 - Register logged in user with the Loop server. r=ckarlof,vladikoff,markh
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Mon, 25 Aug 2014 22:11:35 -0400
changeset 201568 356d27a0d25d82b39956aaed9d2da34e871e945d
parent 201567 5af48430563eeb4f7a5de564afdd32c9b3f13816
child 201569 fcafd91816f79dc347885b8f6e0de4fb77720ba4
push id27374
push userryanvm@gmail.com
push dateTue, 26 Aug 2014 18:16:27 +0000
treeherdermozilla-central@c850f3af9f2c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersckarlof, vladikoff, markh
bugs1047617
milestone34.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 1047617 - Register logged in user with the Loop server. r=ckarlof,vladikoff,markh
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/loop_fxa.sjs
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -55,16 +55,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
 let gRegisteredDeferred = null;
 let gPushHandler = null;
 let gHawkClient = null;
 let gRegisteredLoopServer = false;
 let gLocalizedStrings =  null;
 let gInitializeTimer = null;
 let gFxAOAuthClientPromise = null;
 let gFxAOAuthClient = null;
+let gFxAOAuthTokenData = null;
 
 /**
  * Internal helper methods and state
  *
  * The registration is a two-part process. First we need to connect to
  * and register with the push server. Then we need to take the result of that
  * and register with the Loop server.
  */
@@ -190,17 +191,20 @@ let MozLoopServiceInternal = {
 
     let credentials;
     if (sessionToken) {
       // true = use a hex key, as required by the server (see bug 1032738).
       credentials = deriveHawkCredentials(sessionToken, "sessionToken",
                                           2 * 32, true);
     }
 
-    return gHawkClient.request(path, method, credentials, payloadObj);
+    return gHawkClient.request(path, method, credentials, payloadObj).catch(error => {
+      console.error("Loop hawkRequest error:", error);
+      throw error;
+    });
   },
 
   /**
    * Used to store a session token from a request if it exists in the headers.
    *
    * @param {Object} headers The request headers, which may include a
    *                         "hawk-session-token" to be saved.
    * @return true on success or no token, false on failure.
@@ -487,42 +491,69 @@ let MozLoopServiceInternal = {
         } catch (ex) {
           gFxAOAuthClientPromise = null;
           throw ex;
         }
         return gFxAOAuthClient;
       },
       error => {
         gFxAOAuthClientPromise = null;
-        return error;
+        throw error;
       }
     );
 
     return gFxAOAuthClientPromise;
   }),
 
   /**
-   * Params => web flow => code
+   * Get the OAuth client and do the authorization web flow to get an OAuth code.
+   *
+   * @return {Promise}
    */
   promiseFxAOAuthAuthorization: function() {
     let deferred = Promise.defer();
     this.promiseFxAOAuthClient().then(
       client => {
         client.onComplete = this._fxAOAuthComplete.bind(this, deferred);
         client.launchWebFlow();
       },
       error => {
-        Cu.reportError(error);
+        console.error(error);
         deferred.reject(error);
       }
     );
     return deferred.promise;
   },
 
   /**
+   * Get the OAuth token using the OAuth code and state.
+   *
+   * The caller should approperiately handle 4xx errors (which should lead to a logout)
+   * and 5xx or connectivity issues with messaging to try again later.
+   *
+   * @param {String} code
+   * @param {String} state
+   *
+   * @return {Promise} resolving with OAuth token data.
+   */
+  promiseFxAOAuthToken: function(code, state) {
+    if (!code || !state) {
+      throw new Error("promiseFxAOAuthToken: code and state are required.");
+    }
+
+    let payload = {
+      code: code,
+      state: state,
+    };
+    return this.hawkRequest("/fxa-oauth/token", "POST", payload).then(response => {
+      return JSON.parse(response.body);
+    });
+  },
+
+  /**
    * Called once gFxAOAuthClient fires onComplete.
    *
    * @param {Deferred} deferred used to resolve or reject the gFxAOAuthClientPromise
    * @param {Object} result (with code and state)
    */
   _fxAOAuthComplete: function(deferred, result) {
     gFxAOAuthClientPromise = null;
 
@@ -553,18 +584,24 @@ let gInitializeTimerFunc = () => {
  */
 this.MozLoopService = {
 #ifdef DEBUG
   // Test-only helpers
   get internal() {
     return MozLoopServiceInternal;
   },
 
+  get gFxAOAuthTokenData() {
+    return gFxAOAuthTokenData;
+  },
+
   resetFxA: function() {
     gFxAOAuthClientPromise = null;
+    gFxAOAuthClient = null;
+    gFxAOAuthTokenData = null;
   },
 #endif
 
   set initializeTimerFunc(value) {
     gInitializeTimerFunc = value;
   },
 
   /**
@@ -735,16 +772,23 @@ this.MozLoopService = {
    *
    * The caller should be prepared to handle rejections related to network, server or login errors.
    *
    * @return {Promise} that resolves when the FxA login flow is complete.
    */
   logInToFxA: function() {
     return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
+    }).then(tokenData => {
+      gFxAOAuthTokenData = tokenData;
+      return tokenData;
+    },
+    error => {
+      gFxAOAuthTokenData = null;
+      throw error;
     });
   },
 
   /**
    * Performs a hawk based request to the loop server.
    *
    * @param {String} path The path to make the request to.
    * @param {String} method The request method, e.g. 'POST', 'GET'.
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -85,8 +85,127 @@ add_task(function* invalidState() {
     state: "invalid_state",
   };
   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* basicRegistration() {
+  MozLoopService.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);
+
+  let tokenData = yield MozLoopService.internal.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() {
+  MozLoopService.resetFxA();
+  let params = {
+    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);
+
+  let tokenPromise = MozLoopService.internal.promiseFxAOAuthToken("code1", "state");
+  yield tokenPromise.then(body => {
+    ok(false, "Promise should have rejected");
+  },
+  error => {
+    is(error.code, 400, "Check error code");
+  });
+});
+
+add_task(function* registrationWith401() {
+  MozLoopService.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: "token_401",
+  };
+  yield promiseOAuthParamsSetup(BASE_URL, params);
+
+  let tokenPromise = MozLoopService.internal.promiseFxAOAuthToken("code1", "state");
+  yield tokenPromise.then(body => {
+    ok(false, "Promise should have rejected");
+  },
+  error => {
+    is(error.code, 401, "Check error code");
+  });
+});
+
+add_task(function* basicAuthorizationAndRegistration() {
+  MozLoopService.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);
+
+  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");
+});
+
+add_task(function* loginWithParams401() {
+  MozLoopService.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_401",
+  };
+  yield promiseOAuthParamsSetup(BASE_URL, params);
+
+  let loginPromise = MozLoopService.logInToFxA();
+  yield loginPromise.then(tokenData => {
+    ok(false, "Promise should have rejected");
+  },
+  error => {
+    ise(error.code, 401, "Check error code");
+    ise(MozLoopService.gFxAOAuthTokenData, null, "Check there is no saved token data");
+  });
+});
+
+add_task(function* loginWithRegistration401() {
+  MozLoopService.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: "token_401",
+  };
+  yield promiseOAuthParamsSetup(BASE_URL, params);
+
+  let loginPromise = MozLoopService.logInToFxA();
+  yield loginPromise.then(tokenData => {
+    ok(false, "Promise should have rejected");
+  },
+  error => {
+    ise(error.code, 401, "Check error code");
+    ise(MozLoopService.gFxAOAuthTokenData, null, "Check there is no saved token data");
+  });
+});
--- a/browser/components/loop/test/mochitest/browser_loop_fxa_server.js
+++ b/browser/components/loop/test/mochitest/browser_loop_fxa_server.js
@@ -64,17 +64,17 @@ add_task(function* token_request() {
     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.scopes, "", "Check scopes");
+  ise(request.response.scope, "profile", "Check scope");
   ise(request.response.token_type, "bearer", "Check token_type");
 });
 
 add_task(function* token_request_invalid_state() {
   let params = {
     client_id: "my_client_id",
     content_uri: "https://example.com/content/",
     oauth_uri: "https://example.com/oauth/",
--- a/browser/components/loop/test/mochitest/loop_fxa.sjs
+++ b/browser/components/loop/test/mochitest/loop_fxa.sjs
@@ -71,19 +71,23 @@ function params(request, response) {
     response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
     response.setHeader("Allow", "POST", false);
 
     // Add a button to make a POST request to make this endpoint easier to debug in the browser.
     response.write("<form method=POST><button type=submit>POST</button></form>");
     return;
   }
 
-  let origin = request.scheme + "://" + request.host + ":" + request.port;
+  let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
 
-  let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
+  if (params.test_error && params.test_error == "params_401") {
+    response.setStatusLine(request.httpVersion, 401, "Unauthorized");
+    response.write("401 Unauthorized");
+    return;
+  }
 
   // Warn if required parameters are missing.
   for (let paramName of REQUIRED_PARAMS) {
     if (!(paramName in params)) {
       dump("Warning: " + paramName + " is a required parameter\n");
     }
   }
 
@@ -108,25 +112,32 @@ function oauth_authorization(request, re
  *
  * Validate the state parameter with the server session state and if it matches, exchange the code
  * for an OAuth Token.
  * Parameters: code & state as JSON in the POST body.
  * Response: JSON containing an object of OAuth token information.
  */
 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;
+  }
+
   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;
   }
 
   let tokenData = {
     access_token: payload.code + "_access_token",
-    scopes: "",
+    scope: "profile",
     token_type: "bearer",
   };
   response.setHeader("Content-Type", "application/json; charset=utf-8", false);
   response.write(JSON.stringify(tokenData, null, 2));
 }