Bug 1132293 - Let reliers access derived encryption keys through FxAccountsOAuthClient. r=mhammond
authorRyan Kelly <rfkelly@mozilla.com>
Mon, 09 Mar 2015 21:46:00 -0400
changeset 233591 342879febebadf78aae3409f3ef112e51ce4ce94
parent 233590 7c899c6d817e12d7ae091520dca3bb72ec9ad2ad
child 233592 e5e2240f4d6496942e2d94d06a89df6600a5762a
push id28419
push userryanvm@gmail.com
push dateFri, 13 Mar 2015 20:10:13 +0000
treeherdermozilla-central@38154607d807 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmhammond
bugs1132293
milestone39.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 1132293 - Let reliers access derived encryption keys through FxAccountsOAuthClient. r=mhammond
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_fxa_oauth.js
browser/base/content/test/general/browser_fxa_oauth_with_keys.html
browser/components/loop/MozLoopService.jsm
services/fxaccounts/FxAccountsOAuthClient.jsm
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -7,16 +7,17 @@ support-files =
   app_subframe_bug575561.html
   authenticate.sjs
   aboutHome_content_script.js
   browser_bug479408_sample.html
   browser_bug678392-1.html
   browser_bug678392-2.html
   browser_bug970746.xhtml
   browser_fxa_oauth.html
+  browser_fxa_oauth_with_keys.html
   browser_fxa_profile_channel.html
   browser_registerProtocolHandler_notification.html
   browser_ssl_error_reports_content.js
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   browser_web_channel.html
   bug592338.html
   bug792517-2.html
--- a/browser/base/content/test/general/browser_fxa_oauth.js
+++ b/browser/base/content/test/general/browser_fxa_oauth.js
@@ -12,21 +12,22 @@ thisTestLeaksUncaughtRejectionsAndShould
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthClient",
   "resource://gre/modules/FxAccountsOAuthClient.jsm");
 
 const HTTP_PATH = "http://example.com";
 const HTTP_ENDPOINT = "/browser/browser/base/content/test/general/browser_fxa_oauth.html";
+const HTTP_ENDPOINT_WITH_KEYS = "/browser/browser/base/content/test/general/browser_fxa_oauth_with_keys.html";
 
 let gTests = [
   {
     desc: "FxA OAuth - should open a new tab, complete OAuth flow",
-    run: function* () {
+    run: function () {
       return new Promise(function(resolve, reject) {
         let tabOpened = false;
         let properUrl = "http://example.com/browser/browser/base/content/test/general/browser_fxa_oauth.html";
         let queryStrings = [
           "action=signin",
           "client_id=client_id",
           "scope=",
           "state=state",
@@ -66,16 +67,179 @@ let gTests = [
 
         client.onComplete = function(tokenData) {
           Assert.ok(tabOpened);
           Assert.equal(tokenData.code, "code1");
           Assert.equal(tokenData.state, "state");
           resolve();
         };
 
+        client.onError = reject;
+
+        client.launchWebFlow();
+      });
+    }
+  },
+  {
+    desc: "FxA OAuth - should receive an error when there's a state mismatch",
+    run: function () {
+      return new Promise(function(resolve, reject) {
+        let tabOpened = false;
+
+        waitForTab(function (tab) {
+          Assert.ok("Tab successfully opened");
+
+          // It should have passed in the expected non-matching state value.
+          let queryString = gBrowser.currentURI.spec.split('?')[1];
+          Assert.ok(queryString.indexOf('state=different-state') >= 0);
+
+          tabOpened = true;
+        });
+
+        let client = new FxAccountsOAuthClient({
+          parameters: {
+            state: "different-state",
+            client_id: "client_id",
+            oauth_uri: HTTP_PATH,
+            content_uri: HTTP_PATH,
+          },
+          authorizationEndpoint: HTTP_ENDPOINT
+        });
+
+        client.onComplete = reject;
+
+        client.onError = function(err) {
+          Assert.ok(tabOpened);
+          Assert.equal(err.message, "OAuth flow failed. State doesn't match");
+          resolve();
+        };
+
+        client.launchWebFlow();
+      });
+    }
+  },
+  {
+    desc: "FxA OAuth - should be able to request keys during OAuth flow",
+    run: function () {
+      return new Promise(function(resolve, reject) {
+        let tabOpened = false;
+
+        waitForTab(function (tab) {
+          Assert.ok("Tab successfully opened");
+
+          // It should have asked for keys.
+          let queryString = gBrowser.currentURI.spec.split('?')[1];
+          Assert.ok(queryString.indexOf('keys=true') >= 0);
+
+          tabOpened = true;
+        });
+
+        let client = new FxAccountsOAuthClient({
+          parameters: {
+            state: "state",
+            client_id: "client_id",
+            oauth_uri: HTTP_PATH,
+            content_uri: HTTP_PATH,
+            keys: true,
+          },
+          authorizationEndpoint: HTTP_ENDPOINT_WITH_KEYS
+        });
+
+        client.onComplete = function(tokenData, keys) {
+          Assert.ok(tabOpened);
+          Assert.equal(tokenData.code, "code1");
+          Assert.equal(tokenData.state, "state");
+          Assert.equal(keys.kAr, "kAr");
+          Assert.equal(keys.kBr, "kBr");
+          resolve();
+        };
+
+        client.onError = reject;
+
+        client.launchWebFlow();
+      });
+    }
+  },
+  {
+    desc: "FxA OAuth - should not receive keys if not explicitly requested",
+    run: function () {
+      return new Promise(function(resolve, reject) {
+        let tabOpened = false;
+
+        waitForTab(function (tab) {
+          Assert.ok("Tab successfully opened");
+
+          // It should not have asked for keys.
+          let queryString = gBrowser.currentURI.spec.split('?')[1];
+          Assert.ok(queryString.indexOf('keys=true') == -1);
+
+          tabOpened = true;
+        });
+
+        let client = new FxAccountsOAuthClient({
+          parameters: {
+            state: "state",
+            client_id: "client_id",
+            oauth_uri: HTTP_PATH,
+            content_uri: HTTP_PATH
+          },
+          // This endpoint will cause the completion message to contain keys.
+          authorizationEndpoint: HTTP_ENDPOINT_WITH_KEYS
+        });
+
+        client.onComplete = function(tokenData, keys) {
+          Assert.ok(tabOpened);
+          Assert.equal(tokenData.code, "code1");
+          Assert.equal(tokenData.state, "state");
+          Assert.strictEqual(keys, undefined);
+          resolve();
+        };
+
+        client.onError = reject;
+
+        client.launchWebFlow();
+      });
+    }
+  },
+  {
+    desc: "FxA OAuth - should receive an error if keys could not be obtained",
+    run: function () {
+      return new Promise(function(resolve, reject) {
+        let tabOpened = false;
+
+        waitForTab(function (tab) {
+          Assert.ok("Tab successfully opened");
+
+          // It should have asked for keys.
+          let queryString = gBrowser.currentURI.spec.split('?')[1];
+          Assert.ok(queryString.indexOf('keys=true') >= 0);
+
+          tabOpened = true;
+        });
+
+        let client = new FxAccountsOAuthClient({
+          parameters: {
+            state: "state",
+            client_id: "client_id",
+            oauth_uri: HTTP_PATH,
+            content_uri: HTTP_PATH,
+            keys: true,
+          },
+          // This endpoint will cause the completion message not to contain keys.
+          authorizationEndpoint: HTTP_ENDPOINT
+        });
+
+        client.onComplete = reject;
+
+        client.onError = function(err) {
+          Assert.ok(tabOpened);
+          Assert.equal(err.message, "OAuth flow failed. Keys were not returned");
+          resolve();
+        };
+
         client.launchWebFlow();
       });
     }
   }
 ]; // gTests
 
 function waitForTab(aCallback) {
   let container = gBrowser.tabContainer;
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_fxa_oauth_with_keys.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>fxa_oauth_test</title>
+</head>
+<body>
+<script>
+  window.onload = function(){
+    var event = new window.CustomEvent("WebChannelMessageToChrome", {
+      detail: {
+        id: "oauth_client_id",
+        message: {
+          command: "oauth_complete",
+          data: {
+            state: "state",
+            code: "code1",
+            closeWindow: "signin",
+            keys: { kAr: 'kAr', kBr: 'kBr' },
+          },
+        },
+      },
+    });
+
+    window.dispatchEvent(event);
+  };
+</script>
+</body>
+</html>
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -959,16 +959,17 @@ let MozLoopServiceInternal = {
    *
    * @return {Promise}
    */
   promiseFxAOAuthAuthorization: function() {
     let deferred = Promise.defer();
     this.promiseFxAOAuthClient().then(
       client => {
         client.onComplete = this._fxAOAuthComplete.bind(this, deferred);
+        client.onError = this._fxAOAuthError.bind(this, deferred);
         client.launchWebFlow();
       },
       error => {
         log.error(error);
         deferred.reject(error);
       }
     );
     return deferred.promise;
@@ -998,28 +999,34 @@ let MozLoopServiceInternal = {
       return JSON.parse(response.body);
     },
     error => {this._hawkRequestError(error);});
   },
 
   /**
    * Called once gFxAOAuthClient fires onComplete.
    *
-   * @param {Deferred} deferred used to resolve or reject the gFxAOAuthClientPromise
+   * @param {Deferred} deferred used to resolve the gFxAOAuthClientPromise
    * @param {Object} result (with code and state)
    */
   _fxAOAuthComplete: function(deferred, result) {
     gFxAOAuthClientPromise = null;
+    // Note: The state was already verified in FxAccountsOAuthClient.
+    deferred.resolve(result);
+  },
 
-    // Note: The state was already verified in FxAccountsOAuthClient.
-    if (result) {
-      deferred.resolve(result);
-    } else {
-      deferred.reject("Invalid token data");
-    }
+  /**
+   * Called if gFxAOAuthClient fires onError.
+   *
+   * @param {Deferred} deferred used to reject the gFxAOAuthClientPromise
+   * @param {Object} error object returned by FxAOAuthClient
+   */
+  _fxAOAuthError: function(deferred, err) {
+    gFxAOAuthClientPromise = null;
+    deferred.reject(err);
   },
 };
 Object.freeze(MozLoopServiceInternal);
 
 
 let gInitializeTimerFunc = (deferredInitialization) => {
   // Kick off the push notification service into registering after a timeout.
   // This ensures we're not doing too much straight after the browser's finished
--- a/services/fxaccounts/FxAccountsOAuthClient.jsm
+++ b/services/fxaccounts/FxAccountsOAuthClient.jsm
@@ -32,16 +32,19 @@ Cu.importGlobalProperties(["URL"]);
  *     @param {String} options.parameters.oauth_uri
  *     The FxA OAuth server uri
  *     @param {String} options.parameters.content_uri
  *     The FxA Content server uri
  *     @param {String} [options.parameters.scope]
  *     Optional. A colon-separated list of scopes that the user has authorized
  *     @param {String} [options.parameters.action]
  *     Optional. If provided, should be either signup or signin.
+ *     @param {Boolean} [options.parameters.keys]
+ *     Optional. If true then relier-specific encryption keys will be
+ *     available in the second argument to onComplete.
  *   @param [authorizationEndpoint] {String}
  *   Optional authorization endpoint for the OAuth server
  * @constructor
  */
 this.FxAccountsOAuthClient = function(options) {
   this._validateOptions(options);
   this.parameters = options.parameters;
   this._configureChannel();
@@ -55,27 +58,37 @@ this.FxAccountsOAuthClient = function(op
   }
 
   let params = this._fxaOAuthStartUrl.searchParams;
   params.append("client_id", this.parameters.client_id);
   params.append("state", this.parameters.state);
   params.append("scope", this.parameters.scope || "");
   params.append("action", this.parameters.action || "signin");
   params.append("webChannelId", this._webChannelId);
+  if (this.parameters.keys) {
+    params.append("keys", "true");
+  }
 
 };
 
 this.FxAccountsOAuthClient.prototype = {
   /**
    * Function that gets called once the OAuth flow is complete.
-   * The callback will receive null as it's argument if there is a state mismatch or an object with
-   * code and state properties otherwise.
+   * The callback will receive an object with code and state properties.
+   * If the keys parameter was specified and true, the callback will receive
+   * a second argument with kAr and kBr properties.
    */
   onComplete: null,
   /**
+   * Function that gets called if there is an error during the OAuth flow,
+   * for example due to a state mismatch.
+   * The callback will receive an Error object as its argument.
+   */
+  onError: null,
+  /**
    * Configuration object that stores all OAuth parameters.
    */
   parameters: null,
   /**
    * WebChannel that is used to communicate with content page.
    */
   _channel: null,
   /**
@@ -111,16 +124,17 @@ this.FxAccountsOAuthClient.prototype = {
     }
   },
 
   /**
    * Release all resources that are in use.
    */
   tearDown: function() {
     this.onComplete = null;
+    this.onError = null;
     this._complete = true;
     this._channel.stopListening();
     this._channel = null;
   },
 
   /**
    * Configures WebChannel id and origin
    *
@@ -155,31 +169,47 @@ this.FxAccountsOAuthClient.prototype = {
      */
     let listener = function (webChannelId, message, target) {
       if (message) {
         let command = message.command;
         let data = message.data;
 
         switch (command) {
           case "oauth_complete":
-            // validate the state parameter and call onComplete
+            // validate the returned state and call onComplete or onError
             let result = null;
-            if (this.parameters.state === data.state) {
+            let err = null;
+
+            if (this.parameters.state !== data.state) {
+              err = new Error("OAuth flow failed. State doesn't match");
+            } else if (this.parameters.keys && !data.keys) {
+              err = new Error("OAuth flow failed. Keys were not returned");
+            } else {
               result = {
                 code: data.code,
                 state: data.state
               };
-              log.debug("OAuth flow completed.");
-            } else {
-              log.debug("OAuth flow failed. State doesn't match");
             }
 
-            if (this.onComplete) {
-              this.onComplete(result);
+            if (err) {
+              log.debug(err.message);
+              if (this.onError) {
+                this.onError(err);
+              }
+            } else {
+              log.debug("OAuth flow completed.");
+              if (this.onComplete) {
+                if (this.parameters.keys) {
+                  this.onComplete(result, data.keys);
+                } else {
+                  this.onComplete(result);
+                }
+              }
             }
+
             // onComplete will be called for this client only once
             // calling onComplete again will result in a failure of the OAuth flow
             this.tearDown();
 
             // if the message asked to close the tab
             if (data.closeWindow && target) {
               // for e10s reasons the best way is to use the TabBrowser to close the tab.
               let tabbrowser = target.getTabBrowser();