Bug 1346061 part 1 - Expose getSessionToken() on State. r?nalexander draft
authorEdouard Oger <eoger@fastmail.com>
Wed, 22 Mar 2017 13:07:00 -0400
changeset 552538 314023e343c0be3aba2d30bff204665965484638
parent 552445 d4af7ec6cfcd9b81cd1f433a00b412de61e95b62
child 552539 d49ddc1568ccd4f5d9079ecdd532c80cf6d190a6
push id51379
push userbmo:eoger@fastmail.com
push dateTue, 28 Mar 2017 16:39:50 +0000
reviewersnalexander
bugs1346061
milestone55.0a1
Bug 1346061 part 1 - Expose getSessionToken() on State. r?nalexander We need to access sessionToken in the Engaged state in order to perform device registration. We expose getSessionToken() on the base State class, to allow customers to get the sessionToken easily instead of having to downcast the TokensAndKeysState/Engaged states. MozReview-Commit-ID: 8s2C350noUG
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
@@ -13,17 +13,16 @@ import android.util.Log;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount.InvalidFxAState;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 
 import java.io.UnsupportedEncodingException;
 import java.lang.ref.WeakReference;
@@ -118,45 +117,47 @@ public class FxAccountDeviceRegistrator 
     final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
     intent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile());
     return intent;
   }
 
   @Override
   public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
     if ("FxAccountsPush:Subscribe:Response".equals(event)) {
-      try {
-        doFxaRegistration(message.getBundle("subscription"));
-      } catch (InvalidFxAState e) {
-        Log.d(LOG_TAG, "Invalid state when trying to register with FxA ", e);
-      }
+      doFxaRegistration(message.getBundle("subscription"));
     } else {
       Log.e(LOG_TAG, "No action defined for " + event);
     }
   }
 
-  private void doFxaRegistration(GeckoBundle subscription) throws InvalidFxAState {
+  private void doFxaRegistration(GeckoBundle subscription) {
     final Context context = this.context.get();
     if (this.context == null) {
       throw new IllegalStateException("Application context has been gc'ed");
     }
     doFxaRegistration(context, subscription, true);
   }
 
-  private static void doFxaRegistration(final Context context, final GeckoBundle subscription, final boolean allowRecursion) throws InvalidFxAState {
+  private static void doFxaRegistration(final Context context, final GeckoBundle subscription, final boolean allowRecursion) {
     String pushCallback = subscription.getString("pushCallback");
     String pushPublicKey = subscription.getString("pushPublicKey");
     String pushAuthKey = subscription.getString("pushAuthKey");
 
     final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
     if (fxAccount == null) {
       Log.e(LOG_TAG, "AndroidFxAccount is null");
       return;
     }
-    final byte[] sessionToken = fxAccount.getSessionToken();
+    final byte[] sessionToken;
+    try {
+      sessionToken = fxAccount.getState().getSessionToken();
+    } catch (State.NotASessionTokenState e) {
+      Log.e(LOG_TAG, "Could not get a session token", e);
+      return;
+    }
     final FxAccountDevice device;
     String deviceId = fxAccount.getDeviceId();
     String clientName = getClientName(fxAccount, context);
     if (TextUtils.isEmpty(deviceId)) {
       Log.i(LOG_TAG, "Attempting registration for a new device");
       device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey);
     } else {
       Log.i(LOG_TAG, "Attempting registration for an existing device");
@@ -291,23 +292,18 @@ public class FxAccountDeviceRegistrator 
       public void handleSuccess(FxAccountDevice[] devices) {
         for (FxAccountDevice device : devices) {
           if (device.isCurrentDevice) {
             fxAccount.setFxAUserData(device.id, 0, 0L); // Reset device registration version/timestamp
             if (!allowRecursion) {
               Log.d(LOG_TAG, "Failure to register a device on the second try");
               break;
             }
-            try {
-              doFxaRegistration(context, subscription, false);
-              return;
-            } catch (InvalidFxAState e) {
-              Log.d(LOG_TAG, "Invalid state when trying to recover from a session conflict ", e);
-              break;
-            }
+            doFxaRegistration(context, subscription, false);
+            return;
           }
         }
         onError();
       }
     });
   }
 
   private void setupListeners() throws ClassNotFoundException, NoSuchMethodException,
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
@@ -25,17 +25,16 @@ import org.mozilla.gecko.background.comm
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.fxa.login.StateFactory;
-import org.mozilla.gecko.fxa.login.TokensAndKeysState;
 import org.mozilla.gecko.fxa.sync.FxAccountProfileService;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.setup.Constants;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
@@ -605,34 +604,16 @@ public class AndroidFxAccount {
       StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
       Logger.debug(LOG_TAG, "Account is in state " + stateLabel);
       return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
     } catch (Exception e) {
       throw new IllegalStateException("could not get state", e);
     }
   }
 
-  public byte[] getSessionToken() throws InvalidFxAState {
-    State state = getState();
-    StateLabel stateLabel = state.getStateLabel();
-    if (stateLabel == StateLabel.Cohabiting || stateLabel == StateLabel.Married) {
-      TokensAndKeysState tokensAndKeysState = (TokensAndKeysState) state;
-      return tokensAndKeysState.getSessionToken();
-    }
-    throw new InvalidFxAState("Cannot get sessionToken: not in a TokensAndKeysState state");
-  }
-
-  public static class InvalidFxAState extends Exception {
-    private static final long serialVersionUID = -8537626959811195978L;
-
-    public InvalidFxAState(String message) {
-      super(message);
-    }
-  }
-
   /**
    * <b>For debugging only!</b>
    */
   public void dump() {
     if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
       return;
     }
     ExtendedJSONObject o = toJSONObject();
@@ -690,19 +671,19 @@ public class AndroidFxAccount {
     }
 
     // Update intent with tokens and service URI.
     intent.putExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY, getOAuthServerURI());
     // Deleted broadcasts are package-private, so there's no security risk include the tokens in the extras
     intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS, tokens.toArray(new String[tokens.size()]));
 
     try {
-      intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_SESSION_TOKEN, getSessionToken());
-    } catch (InvalidFxAState e) {
-      Logger.warn(LOG_TAG, "Could not get a session token, ignoring.", e);
+      intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_SESSION_TOKEN, getState().getSessionToken());
+    } catch (State.NotASessionTokenState e) {
+      // Ignore, if sessionToken is null we won't try to do anything anyway.
     }
     intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_SERVER_URI, getAccountServerURI());
     intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_DEVICE_ID, getDeviceId());
 
     return intent;
   }
 
   /**
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java
@@ -1,28 +1,28 @@
 /* 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.login;
 
-import java.security.NoSuchAlgorithmException;
-
 import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountVerified;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
+import java.security.NoSuchAlgorithmException;
+
 public class Engaged extends State {
   private static final String LOG_TAG = Engaged.class.getSimpleName();
 
   protected final byte[] sessionToken;
   protected final byte[] keyFetchToken;
   protected final byte[] unwrapkB;
 
   public Engaged(String email, String uid, boolean verified, byte[] unwrapkB, byte[] sessionToken, byte[] keyFetchToken) {
@@ -80,12 +80,13 @@ public class Engaged extends State {
   @Override
   public Action getNeededAction() {
     if (!verified) {
       return Action.NeedsVerification;
     }
     return Action.None;
   }
 
+  @Override
   public byte[] getSessionToken() {
     return sessionToken;
   }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java
@@ -6,16 +6,25 @@ package org.mozilla.gecko.fxa.login;
 
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
 public abstract class State {
   public static final long CURRENT_VERSION = 3L;
 
+  public class NotASessionTokenState extends Exception {
+
+    private static final long serialVersionUID = 8628129091996684799L;
+
+    public NotASessionTokenState(String message) {
+      super(message);
+    }
+  }
+
   public enum StateLabel {
     Engaged,
     Cohabiting,
     Married,
     Separated,
     Doghouse,
     MigratedFromSync11,
   }
@@ -64,9 +73,14 @@ public abstract class State {
 
   public State makeMigratedFromSync11State(String password) {
     return new MigratedFromSync11(email, uid, verified, password);
   }
 
   public abstract void execute(ExecuteDelegate delegate);
 
   public abstract Action getNeededAction();
+
+  // Should be overridden in states that have a sessionToken
+  public byte[] getSessionToken() throws NotASessionTokenState {
+    throw new NotASessionTokenState("Cannot get a session token in " + stateLabel);
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java
@@ -29,16 +29,17 @@ public abstract class TokensAndKeysState
     // Fields are non-null by constructor.
     o.put("sessionToken", Utils.byte2Hex(sessionToken));
     o.put("kA", Utils.byte2Hex(kA));
     o.put("kB", Utils.byte2Hex(kB));
     o.put("keyPair", keyPair.toJSONObject());
     return o;
   }
 
+  @Override
   public byte[] getSessionToken() {
     return sessionToken;
   }
 
   @Override
   public Action getNeededAction() {
     return Action.None;
   }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
@@ -24,16 +24,17 @@ import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClientException;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.sync.CommandProcessor;
 import org.mozilla.gecko.sync.CommandProcessor.Command;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.HTTPFailureException;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.CryptoException;
@@ -189,19 +190,21 @@ public class SyncClientsEngineStage exte
         Log.e(LOG_TAG, "Can't notify other clients: no account");
         return;
       }
       final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
       final ExtendedJSONObject payload = createNotifyDevicesPayload();
 
       final byte[] sessionToken;
       try {
-        sessionToken = fxAccount.getSessionToken();
-      } catch (AndroidFxAccount.InvalidFxAState invalidFxAState) {
-        Log.e(LOG_TAG, "Could not get session token", invalidFxAState);
+        sessionToken = fxAccount.getState().getSessionToken();
+      } catch (State.NotASessionTokenState e) {
+        // Most of the time we should never reach this, but there can be races with the account
+        // state, so better safe than sorry.
+        Log.e(LOG_TAG, "Could not get a session token during Sync (?)", e);
         return;
       }
 
       // API doc : https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountdevicesnotify
       final FxAccountClient fxAccountClient = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
       fxAccountClient.notifyDevices(sessionToken, devicesToNotify, payload, NOTIFY_TAB_SENT_TTL_SECS, new FxAccountClient20.RequestDelegate<ExtendedJSONObject>() {
         @Override
         public void handleError(Exception e) {