From ec0e5f70b89397aefb8c0c5b5646f3a6320aa189 Mon Sep 17 00:00:00 2001 From: Edouard Oger Date: Mon, 13 Mar 2017 14:35:41 -0400 Subject: [PATCH] Bug 1254643 - Delete FxA device when Fennec Firefox Account is removed. r=Grisha MozReview-Commit-ID: H4lJlXGYIBg --HG-- extra : rebase_source : f5862b9591def7ee300c6f02ff7750ead043241a --- .../gecko/background/fxa/FxAccountClient.java | 1 + .../background/fxa/FxAccountClient20.java | 36 ++++++++++++++++ .../mozilla/gecko/fxa/FxAccountConstants.java | 3 ++ .../fxa/authenticator/AndroidFxAccount.java | 9 ++++ .../receivers/FxAccountDeletedService.java | 43 ++++++++++++++++++- .../gecko/fxa/login/MockFxAccountClient.java | 20 +++++++++ 6 files changed, 111 insertions(+), 1 deletion(-) diff --git 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 index ed959ff0eef4..5ef203abd4cb 100644 --- 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 @@ -19,6 +19,7 @@ public interface FxAccountClient { public void keys(byte[] keyFetchToken, RequestDelegate requestDelegate); public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate requestDelegate); public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate requestDelegate); + public void destroyDevice(byte[] sessionToken, String deviceId, RequestDelegate requestDelegate); public void deviceList(byte[] sessionToken, RequestDelegate requestDelegate); public void notifyDevices(byte[] sessionToken, List deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate requestDelegate); } diff --git 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 index 596f4525e752..8c3dd3950c13 100644 --- 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 @@ -822,6 +822,42 @@ public class FxAccountClient20 implements FxAccountClient { post(resource, body); } + @Override + public void destroyDevice(byte[] sessionToken, String deviceId, RequestDelegate 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(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 delegate) { final byte[] tokenId = new byte[32]; diff --git 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 index c6147b32358f..6fa1ebac33c1 100644 --- 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 @@ -52,6 +52,9 @@ public class FxAccountConstants { 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 diff --git 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 index f440e93ac9dd..d110a51f2173 100644 --- 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 @@ -693,6 +693,15 @@ public class AndroidFxAccount { 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; } diff --git 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 index 60a63a5e11d5..e2a58eb1413f 100644 --- 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 @@ -7,8 +7,11 @@ 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; @@ -16,10 +19,13 @@ 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. @@ -69,6 +75,8 @@ public class FxAccountDeletedService extends IntentService { return; } + // Delete current device the from FxA devices list. + deleteFxADevice(intent); // Fire up gecko and unsubscribe push final Intent geckoIntent = new Intent(); @@ -76,7 +84,6 @@ public class FxAccountDeletedService extends IntentService { 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); @@ -151,4 +158,38 @@ public class FxAccountDeletedService extends IntentService { 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() { + @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."); + } + }); + } } diff --git 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 index 868e90cd2fd3..e697a3d196b3 100644 --- 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 @@ -202,6 +202,26 @@ public class MockFxAccountClient implements FxAccountClient { } } + @Override + public void destroyDevice(byte[] sessionToken, String deviceId, RequestDelegate 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 requestDelegate) { String email = sessionTokens.get(Utils.byte2Hex(sessionToken));