Bug 1254643 - Delete FxA device when Fennec Firefox Account is removed. r?grisha
MozReview-Commit-ID: H4lJlXGYIBg
--- 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
@@ -14,11 +14,12 @@ import org.mozilla.gecko.sync.ExtendedJS
import java.util.List;
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);
+ public void destroyDevice(byte[] sessionToken, String deviceId, RequestDelegate<ExtendedJSONObject> requestDelegate);
public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate);
public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> 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
@@ -818,16 +818,52 @@ public class FxAccountClient20 implement
}
}
};
post(resource, body);
}
@Override
+ public void destroyDevice(byte[] sessionToken, String deviceId, RequestDelegate<ExtendedJSONObject> 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 = new ExtendedJSONObject();
+ body.put("id", deviceId);
+ try {
+ resource = getBaseResource("account/device/destroy");
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ delegate.handleSuccess(body);
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+ };
+
+ post(resource, body);
+ }
+
+ @Override
public void deviceList(byte[] sessionToken, 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);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java
@@ -47,16 +47,19 @@ public class FxAccountConstants {
*/
public static final long ACCOUNT_DELETED_INTENT_VERSION = 1;
public static final String ACCOUNT_DELETED_INTENT_VERSION_KEY = "account_deleted_intent_version";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_KEY = "account_deleted_intent_account";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE = "account_deleted_intent_profile";
public static final String ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY = "account_oauth_service_endpoint";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens";
+ public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_SESSION_TOKEN = "account_deleted_intent_session_token";
+ public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_SERVER_URI = "account_deleted_intent_account_server_uri";
+ public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_DEVICE_ID = "account_deleted_intent_account_device_id";
/**
* This action is broadcast when an Android Firefox Account's internal state
* is changed.
* <p>
* It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and
* can be received only by Firefox versions sharing the same Android Firefox
* Account type.
--- 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
@@ -688,16 +688,25 @@ public class AndroidFxAccount {
tokens.add(authToken);
}
}
// 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_SERVER_URI, getAccountServerURI());
+ intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_DEVICE_ID, getDeviceId());
+
return intent;
}
/**
* Create an intent announcing that the profile JSON attached to this Firefox Account has been updated.
* <p>
* It is not guaranteed that the profile JSON has changed.
*
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
@@ -2,29 +2,35 @@
* 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.receivers;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
+import android.text.TextUtils;
import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClientException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/**
* A background service to clean up after a Firefox Account is deleted.
* <p>
* Note that we specifically handle deleting the pickle file using a Service and a
* BroadcastReceiver, rather than a background thread, to allow channels sharing a Firefox account
* to delete their respective pickle files (since, if one remains, the account will be restored
* when that channel is used).
@@ -64,24 +70,25 @@ public class FxAccountDeletedService ext
final String accountName = intent.getStringExtra(
FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY);
if (accountName == null) {
Logger.warn(LOG_TAG, "Intent malformed: no account name given. Not cleaning up after " +
"deleted Account.");
return;
}
+ // Delete current device the from FxA devices list.
+ deleteFxADevice(intent);
// Fire up gecko and unsubscribe push
final Intent geckoIntent = new Intent();
geckoIntent.setAction("create-services");
geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
geckoIntent.putExtra("category", "android-push-service");
geckoIntent.putExtra("data", "android-fxa-unsubscribe");
- final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME",
intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE));
context.startService(geckoIntent);
// Delete client database and non-local tabs.
Logger.info(LOG_TAG, "Deleting the entire Fennec clients database and non-local tabs");
FennecTabsRepository.deleteNonLocalClientsAndTabs(context);
@@ -146,9 +153,43 @@ public class FxAccountDeletedService ext
} catch (Exception e) {
Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e);
}
}
} else {
Logger.error(LOG_TAG, "Cached OAuth server URI is null or cached OAuth tokens are null; ignoring.");
}
}
+
+ // Remove our current device from the FxA device list.
+ private void deleteFxADevice(Intent intent) {
+ final byte[] sessionToken = intent.getByteArrayExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_SESSION_TOKEN);
+ if (sessionToken == null) {
+ Logger.warn(LOG_TAG, "Empty session token, skipping FxA device destruction.");
+ return;
+ }
+ final String deviceId = intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_DEVICE_ID);
+ if (TextUtils.isEmpty(deviceId)) {
+ Logger.warn(LOG_TAG, "Empty FxA device ID, skipping FxA device destruction.");
+ return;
+ }
+
+ ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
+ final String accountServerURI = intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_SERVER_URI);
+ final FxAccountClient20 fxAccountClient = new FxAccountClient20(accountServerURI, executor);
+ fxAccountClient.destroyDevice(sessionToken, deviceId, new FxAccountClient20.RequestDelegate<ExtendedJSONObject>() {
+ @Override
+ public void handleError(Exception e) {
+ Logger.error(LOG_TAG, "Error while trying to delete the FxA device; ignoring.", e);
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientException.FxAccountClientRemoteException e) {
+ Logger.error(LOG_TAG, "Exception while trying to delete the FxA device; ignoring.", e);
+ }
+
+ @Override
+ public void handleSuccess(ExtendedJSONObject result) {
+ Logger.info(LOG_TAG, "Successfully deleted the FxA device.");
+ }
+ });
+ }
}
--- 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
@@ -198,16 +198,36 @@ public class MockFxAccountClient impleme
}
}
} catch (Exception e) {
requestDelegate.handleError(e);
}
}
@Override
+ public void destroyDevice(byte[] sessionToken, String deviceId, RequestDelegate<ExtendedJSONObject> 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;
+ }
+ if(user.devices.containsKey(deviceId)) {
+ user.devices.remove(deviceId);
+ requestDelegate.handleSuccess(new ExtendedJSONObject());
+ } else {
+ handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.UNKNOWN_DEVICE, "device is unknown");
+ }
+ }
+
+ @Override
public void deviceList(byte[] sessionToken, 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) {