diff --git a/mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java b/mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java index c8480e73b52d..4f233695bc8a 100644 --- a/mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java +++ b/mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java @@ -32,6 +32,7 @@ public class FxAccountOAuthClient10 extends FxAccountAbstractClient { protected static final String JSON_KEY_RESPONSE_TYPE = "response_type"; protected static final String JSON_KEY_SCOPE = "scope"; protected static final String JSON_KEY_STATE = "state"; + protected static final String JSON_KEY_TOKEN = "token"; protected static final String JSON_KEY_TOKEN_TYPE = "token_type"; // access_token: A string that can be used for authorized requests to service providers. @@ -98,4 +99,31 @@ public class FxAccountOAuthClient10 extends FxAccountAbstractClient { post(resource, requestBody, delegate); } + + public void deleteToken(final String token, final RequestDelegate delegate) { + final BaseResource resource; + try { + resource = new BaseResource(new URI(serverURI + "destroy")); + } catch (URISyntaxException e) { + invokeHandleError(delegate, e); + return; + } + + resource.delegate = new ResourceDelegate(resource, delegate) { + @Override + public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { + try { + delegate.handleSuccess(null); + return; + } catch (Exception e) { + delegate.handleError(e); + return; + } + } + }; + + final ExtendedJSONObject requestBody = new ExtendedJSONObject(); + requestBody.put(JSON_KEY_TOKEN, token); + post(resource, requestBody, delegate); + } } diff --git a/mobile/android/base/fxa/FxAccountConstants.java b/mobile/android/base/fxa/FxAccountConstants.java index e181cf3de37e..0d63c12f05cf 100644 --- a/mobile/android/base/fxa/FxAccountConstants.java +++ b/mobile/android/base/fxa/FxAccountConstants.java @@ -52,7 +52,7 @@ public class FxAccountConstants { * can be received only by Firefox channels sharing the same Android Firefox * Account type. *

- * See {@link org.mozilla.gecko.fxa.AndroidFxAccount#makeDeletedAccountIntent(android.content.Context, android.accounts.Account)} + * See {@link org.mozilla.gecko.fxa.AndroidFxAccount#makeDeletedAccountIntent()} * for contents of the intent. * * See bug 790931 for additional information in the context of Sync. @@ -66,6 +66,8 @@ public class FxAccountConstants { 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_OAUTH_SERVICE_ENDPOINT_KEY = "account_oauth_service_endpoint"; + public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens"; /** * This signing-level permission protects broadcast intents that should be diff --git a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java index 4f8c16420699..d4e60e40000b 100644 --- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java +++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java @@ -11,11 +11,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.background.ReadingListConstants; import org.mozilla.gecko.background.common.GlobalConstants; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.fxa.FxAccountUtils; @@ -73,6 +75,19 @@ public class AndroidFxAccount { public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel"; public static final String BUNDLE_KEY_STATE = "state"; + // Services may request OAuth tokens from the Firefox Account dynamically. + // Each such token is prefixed with "oauth::" and a service-dependent scope. + // Such tokens should be destroyed when the account is removed from the device. + // This list collects all the known "oauth::" token types in order to delete them when necessary. + private static final List KNOWN_OAUTH_TOKEN_TYPES; + static { + final List list = new ArrayList<>(); + if (AppConstants.MOZ_ANDROID_READING_LIST_SERVICE) { + list.add(ReadingListConstants.AUTH_TOKEN_TYPE); + } + KNOWN_OAUTH_TOKEN_TYPES = Collections.unmodifiableList(list); + } + public static final Map DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP; static { final HashMap m = new HashMap(); @@ -281,6 +296,15 @@ public class AndroidFxAccount { return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER); } + public String getOAuthServerURI() { + // Allow testing against stage. + if (FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT.equals(getAccountServerURI())) { + return FxAccountConstants.STAGE_OAUTH_SERVER_ENDPOINT; + } else { + return FxAccountConstants.DEFAULT_OAUTH_SERVER_ENDPOINT; + } + } + private String constructPrefsPath(String product, long version, String extra) throws GeneralSecurityException, UnsupportedEncodingException { String profile = getProfile(); String username = account.name; @@ -599,18 +623,29 @@ public class AndroidFxAccount { /** * Create an intent announcing that a Firefox account will be deleted. * - * @param context - * Android context. - * @param account - * Android account being removed. * @return Intent to broadcast. */ - public static Intent makeDeletedAccountIntent(final Context context, final Account account) { + public Intent makeDeletedAccountIntent() { final Intent intent = new Intent(FxAccountConstants.ACCOUNT_DELETED_ACTION); + final List tokens = new ArrayList<>(); intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION)); intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name); + + // Get the tokens from AccountManager. Note: currently, only reading list service supports OAuth. The following logic will + // be extended in future to support OAuth for other services. + for (String tokenKey : KNOWN_OAUTH_TOKEN_TYPES) { + final String authToken = accountManager.peekAuthToken(account, tokenKey); + if (authToken != null) { + 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()])); return intent; } diff --git a/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java b/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java index c043ed102e55..3a0c219212b3 100644 --- a/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java +++ b/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java @@ -182,15 +182,7 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator { Logger.info(LOG_TAG, "Fetching oauth token with scope: " + scope); final Responder responder = new Responder(response, fxAccount); - - // Allow testing against stage. - final boolean usingStageAuthServer = FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT.equals(fxAccount.getAccountServerURI()); - final String oauthServerUri; - if (usingStageAuthServer) { - oauthServerUri = FxAccountConstants.STAGE_OAUTH_SERVER_ENDPOINT; - } else { - oauthServerUri = FxAccountConstants.DEFAULT_OAUTH_SERVER_ENDPOINT; - } + final String oauthServerUri = fxAccount.getOAuthServerURI(); final String audience; try { @@ -360,7 +352,8 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator { // // Broadcast intents protected with permissions are secure, so it's okay // to include private information such as a password. - final Intent intent = AndroidFxAccount.makeDeletedAccountIntent(context, account); + final AndroidFxAccount androidFxAccount = new AndroidFxAccount(context, account); + final Intent intent = androidFxAccount.makeDeletedAccountIntent(); Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " + "broadcasting secure intent " + intent.getAction() + "."); context.sendBroadcast(intent, FxAccountConstants.PER_ACCOUNT_TYPE_PERMISSION); diff --git a/mobile/android/base/fxa/receivers/FxAccountDeletedService.java b/mobile/android/base/fxa/receivers/FxAccountDeletedService.java index b208e3057a72..68ba18854d73 100644 --- a/mobile/android/base/fxa/receivers/FxAccountDeletedService.java +++ b/mobile/android/base/fxa/receivers/FxAccountDeletedService.java @@ -4,7 +4,12 @@ package org.mozilla.gecko.fxa.receivers; +import java.util.concurrent.Executor; + import org.mozilla.gecko.background.common.log.Logger; +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.sync.FxAccountNotificationManager; import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; @@ -108,6 +113,47 @@ public class FxAccountDeletedService extends IntentService { // Bug 1147275: Delete cached oauth tokens. There's no way to query all // oauth tokens from Android, so this is tricky to do comprehensively. We // can query, individually, for specific oauth tokens to delete, however. + final String oauthServerURI = intent.getStringExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY); + final String[] tokens = intent.getStringArrayExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS); + if (oauthServerURI != null && tokens != null) { + final Executor directExecutor = new Executor() { + @Override + public void execute(Runnable runnable) { + runnable.run(); + } + }; + + final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerURI, directExecutor); + + for (String token : tokens) { + if (token == null) { + Logger.error(LOG_TAG, "Cached OAuth token is null; should never happen. Ignoring."); + continue; + } + try { + oauthClient.deleteToken(token, new FxAccountAbstractClient.RequestDelegate() { + @Override + public void handleSuccess(Void result) { + Logger.info(LOG_TAG, "Successfully deleted cached OAuth token."); + } + + @Override + public void handleError(Exception e) { + Logger.error(LOG_TAG, "Failed to delete cached OAuth token; ignoring.", e); + } + + @Override + public void handleFailure(FxAccountAbstractClientRemoteException e) { + Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e); + } + }); + } 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."); + } } public static void deletePickle(final Context context) { diff --git a/mobile/android/base/resources/drawable-hdpi/fxaccount_checkbox.png b/mobile/android/base/resources/drawable-hdpi/fxaccount_checkbox.png old mode 100644 new mode 100755 diff --git a/mobile/android/base/resources/drawable-hdpi/fxaccount_mail.png b/mobile/android/base/resources/drawable-hdpi/fxaccount_mail.png old mode 100644 new mode 100755