Bug 1267760 - Send push public key and auth secret when registering/updating a device. r=kitcambridge draft
authorEdouard Oger <eoger@fastmail.com>
Tue, 24 May 2016 16:19:47 -0700
changeset 374024 cb346255a723fea2e1da91d497c6265c1e9b8a7f
parent 373935 111970c738234569c8c180319155327316335deb
child 522524 311177892496a3d0fcd34dd5957c587d1dad7224
push id19900
push userbmo:edouard.oger@gmail.com
push dateWed, 01 Jun 2016 17:33:51 +0000
reviewerskitcambridge
bugs1267760
milestone49.0a1
Bug 1267760 - Send push public key and auth secret when registering/updating a device. r=kitcambridge MozReview-Commit-ID: BVwaijvSsLL
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsClient.jsm
services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -291,16 +291,20 @@ function copyObjectProperties(from, to, 
     if (desc.set) {
       desc.set = desc.set.bind(thisArg);
     }
 
     Object.defineProperty(to, prop, desc);
   }
 }
 
+function urlsafeBase64Encode(key) {
+  return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
+}
+
 /**
  * The public API's constructor.
  */
 this.FxAccounts = function (mockInternal) {
   let internal = new FxAccountsInternal();
   let external = {};
 
   // Copy all public properties to the 'external' object.
@@ -350,17 +354,17 @@ function FxAccountsInternal() {
  */
 FxAccountsInternal.prototype = {
   // The timeout (in ms) we use to poll for a verified mail for the first 2 mins.
   VERIFICATION_POLL_TIMEOUT_INITIAL: 15000, // 15 seconds
   // And how often we poll after the first 2 mins.
   VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 30000, // 30 seconds.
   // The current version of the device registration, we use this to re-register
   // devices after we update what we send on device registration.
-  DEVICE_REGISTRATION_VERSION: 1,
+  DEVICE_REGISTRATION_VERSION: 2,
 
   _fxAccountsClient: null,
 
   // All significant initialization should be done in this initialize() method,
   // as it's called after this object has been mocked for tests.
   initialize() {
     this.currentTimer = null;
     this.currentAccountState = this.newAccountState();
@@ -1508,16 +1512,22 @@ FxAccountsInternal.prototype = {
 
     return this.fxaPushService.registerPushEndpoint().then(subscription => {
       const deviceName = this._getDeviceName();
       let deviceOptions = {};
 
       // if we were able to obtain a subscription
       if (subscription && subscription.endpoint) {
         deviceOptions.pushCallback = subscription.endpoint;
+        let publicKey = subscription.getKey('p256dh');
+        let authKey = subscription.getKey('auth');
+        if (publicKey && authKey) {
+          deviceOptions.pushPublicKey = urlsafeBase64Encode(publicKey);
+          deviceOptions.pushAuthKey = urlsafeBase64Encode(authKey);
+        }
       }
 
       if (signedInUser.deviceId) {
         log.debug("updating existing device details");
         return this.fxAccountsClient.updateDevice(
           signedInUser.sessionToken, signedInUser.deviceId, deviceName, deviceOptions);
       }
 
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -362,16 +362,20 @@ this.FxAccountsClient.prototype = {
    * @param  name
    *         Device name
    * @param  type
    *         Device type (mobile|desktop)
    * @param  [options]
    *         Extra device options
    * @param  [options.pushCallback]
    *         `pushCallback` push endpoint callback
+   * @param  [options.pushPublicKey]
+   *         `pushPublicKey` push public key (URLSafe Base64 string)
+   * @param  [options.pushAuthKey]
+   *         `pushAuthKey` push auth secret (URLSafe Base64 string)
    * @return Promise
    *         Resolves to an object:
    *         {
    *           id: Device identifier
    *           createdAt: Creation time (milliseconds since epoch)
    *           name: Name of device
    *           type: Type of device (mobile|desktop)
    *         }
@@ -380,16 +384,20 @@ this.FxAccountsClient.prototype = {
     let path = "/account/device";
 
     let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
     let body = { name, type };
 
     if (options.pushCallback) {
       body.pushCallback = options.pushCallback;
     }
+    if (options.pushPublicKey && options.pushAuthKey) {
+      body.pushPublicKey = options.pushPublicKey;
+      body.pushAuthKey = options.pushAuthKey;
+    }
 
     return this._request(path, "POST", creds, body);
   },
 
   /**
    * Update the session or name for an existing device
    *
    * @method updateDevice
@@ -398,31 +406,39 @@ this.FxAccountsClient.prototype = {
    * @param  id
    *         Device identifier
    * @param  name
    *         Device name
    * @param  [options]
    *         Extra device options
    * @param  [options.pushCallback]
    *         `pushCallback` push endpoint callback
+   * @param  [options.pushPublicKey]
+   *         `pushPublicKey` push public key (URLSafe Base64 string)
+   * @param  [options.pushAuthKey]
+   *         `pushAuthKey` push auth secret (URLSafe Base64 string)
    * @return Promise
    *         Resolves to an object:
    *         {
    *           id: Device identifier
    *           name: Device name
    *         }
    */
   updateDevice(sessionTokenHex, id, name, options = {}) {
     let path = "/account/device";
 
     let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
     let body = { id, name };
     if (options.pushCallback) {
       body.pushCallback = options.pushCallback;
     }
+    if (options.pushPublicKey && options.pushAuthKey) {
+      body.pushPublicKey = options.pushPublicKey;
+      body.pushAuthKey = options.pushAuthKey;
+    }
 
     return this._request(path, "POST", creds, body);
   },
 
   /**
    * Delete a device and its associated session token, signing the user
    * out of the server.
    *
--- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
@@ -11,16 +11,19 @@ Cu.import("resource://gre/modules/FxAcco
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 
 initTestLogging("Trace");
 
 var log = Log.repository.getLogger("Services.FxAccounts.test");
 log.level = Log.Level.Debug;
 
+const BOGUS_PUBLICKEY = "BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc";
+const BOGUS_AUTHKEY = "GSsIiaD2Mr83iPqwFNK4rw";
+
 Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace");
 Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace;
 
 Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1");
 Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123");
 Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", "http://example.com/v1");
 Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "http://accounts.example.com/");
 
@@ -101,17 +104,22 @@ function MockFxAccounts(device = {}) {
     _getDeviceName() {
       return device.name || "mock device name";
     },
     fxAccountsClient: new MockFxAccountsClient(device),
     fxaPushService: {
       registerPushEndpoint() {
         return new Promise((resolve) => {
           resolve({
-            endpoint: "http://mochi.test:8888"
+            endpoint: "http://mochi.test:8888",
+            getKey: function(type) {
+              return ChromeUtils.base64URLDecode(
+                type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY,
+                { padding: "ignore" });
+            }
           });
         });
       },
     },
     DEVICE_REGISTRATION_VERSION
   });
 }
 
@@ -157,16 +165,18 @@ add_task(function* test_updateDeviceRegi
   do_check_eq(spy.updateDevice.count, 0);
   do_check_eq(spy.getDeviceList.count, 0);
   do_check_eq(spy.registerDevice.count, 1);
   do_check_eq(spy.registerDevice.args[0].length, 4);
   do_check_eq(spy.registerDevice.args[0][0], credentials.sessionToken);
   do_check_eq(spy.registerDevice.args[0][1], deviceName);
   do_check_eq(spy.registerDevice.args[0][2], "desktop");
   do_check_eq(spy.registerDevice.args[0][3].pushCallback, "http://mochi.test:8888");
+  do_check_eq(spy.registerDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
+  do_check_eq(spy.registerDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
 
   const state = fxa.internal.currentAccountState;
   const data = yield state.getUserAccountData();
 
   do_check_eq(data.deviceId, "newly-generated device id");
   do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
 });
 
@@ -208,16 +218,18 @@ add_task(function* test_updateDeviceRegi
   do_check_eq(spy.registerDevice.count, 0);
   do_check_eq(spy.getDeviceList.count, 0);
   do_check_eq(spy.updateDevice.count, 1);
   do_check_eq(spy.updateDevice.args[0].length, 4);
   do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
   do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
   do_check_eq(spy.updateDevice.args[0][2], deviceName);
   do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
+  do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
+  do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
 
   const state = fxa.internal.currentAccountState;
   const data = yield state.getUserAccountData();
 
   do_check_eq(data.deviceId, credentials.deviceId);
   do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
 });
 
@@ -265,16 +277,18 @@ add_task(function* test_updateDeviceRegi
   do_check_eq(spy.getDeviceList.count, 0);
   do_check_eq(spy.registerDevice.count, 0);
   do_check_eq(spy.updateDevice.count, 1);
   do_check_eq(spy.updateDevice.args[0].length, 4);
   do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
   do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
   do_check_eq(spy.updateDevice.args[0][2], deviceName);
   do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
+  do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
+  do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
 
 
   const state = fxa.internal.currentAccountState;
   const data = yield state.getUserAccountData();
 
   do_check_null(data.deviceId);
   do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
 });
@@ -328,16 +342,18 @@ add_task(function* test_updateDeviceRegi
   do_check_eq(result, credentials.deviceId);
   do_check_eq(spy.registerDevice.count, 0);
   do_check_eq(spy.updateDevice.count, 1);
   do_check_eq(spy.updateDevice.args[0].length, 4);
   do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
   do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
   do_check_eq(spy.updateDevice.args[0][2], deviceName);
   do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
+  do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
+  do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
   do_check_eq(spy.getDeviceList.count, 1);
   do_check_eq(spy.getDeviceList.args[0].length, 1);
   do_check_eq(spy.getDeviceList.args[0][0], credentials.sessionToken);
   do_check_true(spy.getDeviceList.time >= spy.updateDevice.time);
 
   const state = fxa.internal.currentAccountState;
   const data = yield state.getUserAccountData();