Bug 1250782 - FxAccountClient: add registerOrUpdateDevice method. r=mcomella
authorEdouard Oger <eoger@fastmail.com>
Wed, 18 May 2016 12:35:50 -0700
changeset 304071 17541521c2ece62312b1e907a7675c7c7ba0711c
parent 304070 985cadcc9ed5a98f520e6988f556dadbb26266a9
child 304072 c8020e8d970ed9a437750a8ac54ef444c0844833
push id30411
push userkwierso@gmail.com
push dateFri, 08 Jul 2016 00:26:45 +0000
treeherdermozilla-central@23dc78b7b57e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella
bugs1250782
milestone50.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 1250782 - FxAccountClient: add registerOrUpdateDevice method. r=mcomella MozReview-Commit-ID: 6cGQTZq5G1W
mobile/android/base/android-services.mozbuild
mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java
mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -829,16 +829,17 @@ sync_java_files = [TOPSRCDIR + '/mobile/
     'fxa/authenticator/AndroidFxAccount.java',
     'fxa/authenticator/FxAccountAuthenticator.java',
     'fxa/authenticator/FxAccountAuthenticatorService.java',
     'fxa/authenticator/FxAccountLoginDelegate.java',
     'fxa/authenticator/FxAccountLoginException.java',
     'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java',
     'fxa/FirefoxAccounts.java',
     'fxa/FxAccountConstants.java',
+    'fxa/FxAccountDevice.java',
     'fxa/login/BaseRequestDelegate.java',
     'fxa/login/Cohabiting.java',
     'fxa/login/Doghouse.java',
     'fxa/login/Engaged.java',
     'fxa/login/FxAccountLoginStateMachine.java',
     'fxa/login/FxAccountLoginTransition.java',
     'fxa/login/Married.java',
     'fxa/login/MigratedFromSync11.java',
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.background.fxa;
 
 import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
+import org.mozilla.gecko.fxa.FxAccountDevice;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 
 public interface FxAccountClient {
   public void accountStatus(String uid, RequestDelegate<AccountStatusResponse> requestDelegate);
   public void recoveryEmailStatus(byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate);
   public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
   public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
+  public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> requestDelegate);
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
@@ -5,16 +5,17 @@
 package org.mozilla.gecko.background.fxa;
 
 import org.json.simple.JSONArray;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.fxa.FxAccountDevice;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.HKDF;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.Resource;
@@ -769,9 +770,51 @@ public class FxAccountClient20 implement
           login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, queryParameters, delegate);
         } catch (Exception innerException) {
           delegate.handleError(innerException);
           return;
         }
       }
     });
   }
+
+  /**
+   * Registers a device given a valid session token.
+   *
+   * @param sessionToken to query.
+   * @param delegate to invoke callbacks.
+   */
+  @Override
+  public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> delegate) {
+    final byte[] tokenId = new byte[32];
+    final byte[] reqHMACKey = new byte[32];
+    final byte[] requestKey = new byte[32];
+    try {
+      HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
+    } catch (Exception e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    final BaseResource resource;
+    final ExtendedJSONObject body;
+    try {
+      resource = getBaseResource("account/device");
+      body = device.toJson();
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<FxAccountDevice>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          delegate.handleSuccess(FxAccountDevice.fromJson(body));
+        } catch (Exception e) {
+          delegate.handleError(e);
+        }
+      }
+    };
+
+    post(resource, body, delegate);
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java
@@ -21,11 +21,12 @@ public interface FxAccountRemoteError {
   public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114;
   public static final int INVALID_NONCE_IN_REQUEST_SIGNATURE = 115;
   public static final int ENDPOINT_IS_NO_LONGER_SUPPORTED = 116;
   public static final int INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT = 117;
   public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118;
   public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119;
   public static final int INCORRECT_EMAIL_CASE = 120;
   public static final int ACCOUNT_LOCKED = 121;
+  public static final int UNKNOWN_DEVICE = 123;
   public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201;
   public static final int UNKNOWN_ERROR = 999;
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class FxAccountDevice {
+
+  public static final String JSON_KEY_NAME = "name";
+  public static final String JSON_KEY_ID = "id";
+  public static final String JSON_KEY_TYPE = "type";
+  public static final String JSON_KEY_ISCURRENTDEVICE = "isCurrentDevice";
+
+  public String id;
+  public String name;
+  public String type;
+  public Boolean isCurrentDevice;
+
+  public FxAccountDevice(String name, String id, String type, Boolean isCurrentDevice) {
+    this.name = name;
+    this.id = id;
+    this.type = type;
+    this.isCurrentDevice = isCurrentDevice;
+  }
+
+  public static FxAccountDevice forRegister(String name, String type) {
+    return new FxAccountDevice(name, null, type, null);
+  }
+
+  public static FxAccountDevice forUpdate(String id, String name) {
+    return new FxAccountDevice(name, id, null, null);
+  }
+
+  public static FxAccountDevice fromJson(ExtendedJSONObject json) {
+    String name = json.getString(JSON_KEY_NAME);
+    String id = json.getString(JSON_KEY_ID);
+    String type = json.getString(JSON_KEY_TYPE);
+    Boolean isCurrentDevice = json.getBoolean(JSON_KEY_ISCURRENTDEVICE);
+    return new FxAccountDevice(name, id, type, isCurrentDevice);
+  }
+
+  public ExtendedJSONObject toJson() {
+    final ExtendedJSONObject body = new ExtendedJSONObject();
+    if (this.name != null) {
+      body.put(JSON_KEY_NAME, this.name);
+    }
+    if (this.id != null) {
+      body.put(JSON_KEY_ID, this.id);
+    }
+    if (this.type != null) {
+      body.put(JSON_KEY_TYPE, this.type);
+    }
+    return body;
+  }
+}
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
@@ -1,30 +1,34 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.fxa.login;
 
+import android.text.TextUtils;
+
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.FxAccountDevice;
 import org.mozilla.gecko.browserid.MockMyIDTokenFactory;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
 import java.io.UnsupportedEncodingException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.UUID;
 
 import ch.boye.httpclientandroidlib.HttpStatus;
 import ch.boye.httpclientandroidlib.ProtocolVersion;
 import ch.boye.httpclientandroidlib.entity.StringEntity;
 import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 
 public class MockFxAccountClient implements FxAccountClient {
   protected static MockMyIDTokenFactory mockMyIdTokenFactory = new MockMyIDTokenFactory();
@@ -37,24 +41,26 @@ public class MockFxAccountClient impleme
 
   public static class User {
     public final String email;
     public final byte[] quickStretchedPW;
     public final String uid;
     public boolean verified;
     public final byte[] kA;
     public final byte[] wrapkB;
+    public final Map<String, FxAccountDevice> devices;
 
     public User(String email, byte[] quickStretchedPW) {
       this.email = email;
       this.quickStretchedPW = quickStretchedPW;
       this.uid = "uid/" + this.email;
       this.verified = false;
       this.kA = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES);
       this.wrapkB = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES);
+      this.devices = new HashMap<String, FxAccountDevice>();
     }
   }
 
   protected LoginResponse addLogin(User user, byte[] sessionToken, byte[] keyFetchToken) {
     // byte[] sessionToken = Utils.generateRandomBytes(8);
     if (sessionToken != null) {
       sessionTokens.put(Utils.byte2Hex(sessionToken), user.email);
     }
@@ -149,9 +155,44 @@ public class MockFxAccountClient impleme
       final long dur = certificateDurationInMilliseconds;
       final long exp = iat + dur;
       String certificate = mockMyIdTokenFactory.createMockMyIDCertificate(RSACryptoImplementation.createPublicKey(publicKey), "test", iat, exp);
       requestDelegate.handleSuccess(certificate);
     } catch (Exception e) {
       requestDelegate.handleError(e);
     }
   }
+
+  @Override
+  public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice deviceToRegister, RequestDelegate<FxAccountDevice> requestDelegate) {
+    String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
+    User user = users.get(email);
+    if (email == null || user == null) {
+      handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken");
+      return;
+    }
+    if (!user.verified) {
+      handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
+      return;
+    }
+    try {
+      String deviceId = deviceToRegister.id;
+      if (TextUtils.isEmpty(deviceId)) { // Create
+        deviceId = UUID.randomUUID().toString();
+        FxAccountDevice device = new FxAccountDevice(deviceToRegister.name, deviceId, deviceToRegister.type, null);
+        deviceToRegister.id = deviceId;
+      } else { // Update
+        FxAccountDevice existingDevice = user.devices.get(deviceId);
+        if (existingDevice != null) {
+          if (!TextUtils.isEmpty(deviceToRegister.name)) {
+            existingDevice.name = deviceToRegister.name;
+          } // We could also update the other fields..
+        } else { // Device unknown
+          handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.UNKNOWN_DEVICE, "device is unknown");
+          return;
+        }
+      }
+      requestDelegate.handleSuccess(deviceToRegister);
+    } catch (Exception e) {
+      requestDelegate.handleError(e);
+    }
+  }
 }