diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild index 33b3c8cc2c70..be0db17fa60c 100644 --- a/mobile/android/base/android-services.mozbuild +++ b/mobile/android/base/android-services.mozbuild @@ -868,6 +868,7 @@ sync_java_files = [ 'fxa/authenticator/FxAccountAuthenticatorService.java', 'fxa/authenticator/FxAccountLoginDelegate.java', 'fxa/authenticator/FxAccountLoginException.java', + 'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java', 'fxa/FirefoxAccounts.java', 'fxa/FxAccountConstants.java', 'fxa/login/BaseRequestDelegate.java', @@ -1171,6 +1172,7 @@ reading_list_service_java_files = [ 'reading/ReadingListClientRecordFactory.java', 'reading/ReadingListConstants.java', 'reading/ReadingListDeleteDelegate.java', + 'reading/ReadingListInvalidAuthenticationException.java', 'reading/ReadingListRecord.java', 'reading/ReadingListRecordDelegate.java', 'reading/ReadingListRecordResponse.java', diff --git a/mobile/android/base/background/fxa/FxAccountUtils.java b/mobile/android/base/background/fxa/FxAccountUtils.java index 490ab6d7f376..2d29725a072d 100644 --- a/mobile/android/base/background/fxa/FxAccountUtils.java +++ b/mobile/android/base/background/fxa/FxAccountUtils.java @@ -14,7 +14,6 @@ import java.security.NoSuchAlgorithmException; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.R; -import org.mozilla.gecko.R.string; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.nativecode.NativeCrypto; import org.mozilla.gecko.sync.Utils; @@ -23,7 +22,6 @@ import org.mozilla.gecko.sync.crypto.KeyBundle; import org.mozilla.gecko.sync.crypto.PBKDF2; import android.content.Context; -import android.os.Build; public class FxAccountUtils { private static final String LOG_TAG = FxAccountUtils.class.getSimpleName(); diff --git a/mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java b/mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java index 17f2ba888496..21025af0a7ba 100644 --- a/mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java +++ b/mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java @@ -9,6 +9,7 @@ import org.mozilla.gecko.sync.HTTPFailureException; import org.mozilla.gecko.sync.net.SyncStorageResponse; import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.HttpStatus; /** * From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md. @@ -51,6 +52,10 @@ public class FxAccountAbstractClientException extends Exception { public String toString() { return ""; } + + public boolean isInvalidAuthentication() { + return this.httpStatusCode == HttpStatus.SC_UNAUTHORIZED; + } } public static class FxAccountAbstractClientMalformedResponseException extends FxAccountAbstractClientRemoteException { diff --git a/mobile/android/base/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java b/mobile/android/base/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java new file mode 100644 index 000000000000..ff31223224bd --- /dev/null +++ b/mobile/android/base/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java @@ -0,0 +1,84 @@ +/* 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.authenticator; + +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +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.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; +import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; +import org.mozilla.gecko.fxa.login.Married; +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.sync.FxAccountNotificationManager; +import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; + +import android.content.Context; + +public abstract class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate { + protected final static String LOG_TAG = LoginStateMachineDelegate.class.getSimpleName(); + + protected final Context context; + protected final AndroidFxAccount fxAccount; + protected final Executor executor; + protected final FxAccountClient client; + + public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) { + this.context = context; + this.fxAccount = fxAccount; + this.executor = Executors.newSingleThreadExecutor(); + this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor); + } + + abstract public void handleNotMarried(State notMarried); + abstract public void handleMarried(Married married); + + @Override + public FxAccountClient getClient() { + return client; + } + + @Override + public long getCertificateDurationInMilliseconds() { + return 12 * 60 * 60 * 1000; + } + + @Override + public long getAssertionDurationInMilliseconds() { + return 15 * 60 * 1000; + } + + @Override + public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { + return StateFactory.generateKeyPair(); + } + + @Override + public void handleTransition(Transition transition, State state) { + Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); + } + + @Override + public void handleFinal(State state) { + Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); + fxAccount.setState(state); + // Update any notifications displayed. + final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID); + notificationManager.update(context, fxAccount); + + if (state.getStateLabel() != StateLabel.Married) { + handleNotMarried(state); + return; + } else { + handleMarried((Married) state); + } + } +} diff --git a/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java b/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java index 08ed9ef65514..45a747444bff 100644 --- a/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java +++ b/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java @@ -4,9 +4,31 @@ package org.mozilla.gecko.fxa.authenticator; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + 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.FxAccountUtils; +import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate; +import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException; +import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10; +import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.JSONWebTokenUtils; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity; +import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; +import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; +import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; +import org.mozilla.gecko.fxa.login.Married; +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.sync.FxAccountNotificationManager; +import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; import android.accounts.AbstractAccountAuthenticator; import android.accounts.Account; @@ -19,6 +41,7 @@ import android.os.Bundle; public class FxAccountAuthenticator extends AbstractAccountAuthenticator { public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName(); + public static final int UNKNOWN_ERROR_CODE = 999; protected final Context context; protected final AccountManager accountManager; @@ -68,12 +91,190 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator { return null; } + protected static class Responder { + final AccountAuthenticatorResponse response; + final Account account; + + public Responder(AccountAuthenticatorResponse response, Account account) { + this.response = response; + this.account = account; + } + + public void fail(Exception e) { + Logger.warn(LOG_TAG, "Responding with error!", e); + final Bundle result = new Bundle(); + result.putInt(AccountManager.KEY_ERROR_CODE, UNKNOWN_ERROR_CODE); + result.putString(AccountManager.KEY_ERROR_MESSAGE, e.toString()); + response.onResult(result); + } + + public void succeed(String authToken) { + Logger.info(LOG_TAG, "Responding with success!"); + final Bundle result = new Bundle(); + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + result.putString(AccountManager.KEY_AUTHTOKEN, authToken); + response.onResult(result); + } + } + + public abstract static class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate { + protected final Context context; + protected final AndroidFxAccount fxAccount; + protected final Executor executor; + protected final FxAccountClient client; + + public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) { + this.context = context; + this.fxAccount = fxAccount; + this.executor = Executors.newSingleThreadExecutor(); + this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor); + } + + @Override + public FxAccountClient getClient() { + return client; + } + + @Override + public long getCertificateDurationInMilliseconds() { + return 12 * 60 * 60 * 1000; + } + + @Override + public long getAssertionDurationInMilliseconds() { + return 15 * 60 * 1000; + } + + @Override + public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { + return StateFactory.generateKeyPair(); + } + + @Override + public void handleTransition(Transition transition, State state) { + Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); + } + + abstract public void handleNotMarried(State notMarried); + abstract public void handleMarried(Married married); + + @Override + public void handleFinal(State state) { + Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); + fxAccount.setState(state); + // Update any notifications displayed. + final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID); + notificationManager.update(context, fxAccount); + + if (state.getStateLabel() != StateLabel.Married) { + handleNotMarried(state); + return; + } else { + handleMarried((Married) state); + } + } + } + + protected void getOAuthToken(final AccountAuthenticatorResponse response, final AndroidFxAccount fxAccount, final String scope) throws NetworkErrorException { + Logger.info(LOG_TAG, "Fetching oauth token with scope: " + scope); + + final Responder responder = new Responder(response, fxAccount.getAndroidAccount()); + + final String oauthServerUri = FxAccountConstants.DEFAULT_OAUTH_SERVER_ENDPOINT; + final String audience; + try { + audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token. + } catch (Exception e) { + Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e); + responder.fail(e); + return; + } + + final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); + + stateMachine.advance(fxAccount.getState(), StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) { + @Override + public void handleNotMarried(State state) { + final String message = "Cannot fetch oauth token from state: " + state.getStateLabel(); + Logger.warn(LOG_TAG, message); + responder.fail(new RuntimeException(message)); + } + + @Override + public void handleMarried(final Married married) { + final String assertion; + try { + assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); + if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { + JSONWebTokenUtils.dumpAssertion(assertion); + } + } catch (Exception e) { + Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e); + responder.fail(e); + return; + } + + final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor); + Logger.debug(LOG_TAG, "OAuth fetch for scope: " + scope); + oauthClient.authorization(FxAccountConstants.OAUTH_CLIENT_ID_FENNEC, assertion, null, scope, new RequestDelegate() { + @Override + public void handleSuccess(AuthorizationResponse result) { + Logger.debug(LOG_TAG, "OAuth success."); + FxAccountUtils.pii(LOG_TAG, "Fetched oauth token: " + result.access_token); + responder.succeed(result.access_token); + } + + @Override + public void handleFailure(FxAccountAbstractClientRemoteException e) { + Logger.error(LOG_TAG, "OAuth failure.", e); + if (e.isInvalidAuthentication()) { + // We were married, generated an assertion, and our assertion was rejected by the + // oauth client. If it's a 401, we probably have a stale certificate. If instead of + // a stale certificate we have bad credentials, the state machine will fail to sign + // our public key and drive us back to Separated. + fxAccount.setState(married.makeCohabitingState()); + } + responder.fail(e); + } + + @Override + public void handleError(Exception e) { + Logger.error(LOG_TAG, "OAuth error.", e); + responder.fail(e); + } + }); + } + }); + } + @Override public Bundle getAuthToken(final AccountAuthenticatorResponse response, final Account account, final String authTokenType, final Bundle options) throws NetworkErrorException { - Logger.debug(LOG_TAG, "getAuthToken"); + Logger.debug(LOG_TAG, "getAuthToken: " + authTokenType); + // If we have a cached authToken, hand it over. + final String cachedAuthToken = AccountManager.get(context).peekAuthToken(account, authTokenType); + if (cachedAuthToken != null && !cachedAuthToken.isEmpty()) { + Logger.info(LOG_TAG, "Return cached token."); + final Bundle result = new Bundle(); + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + result.putString(AccountManager.KEY_AUTHTOKEN, cachedAuthToken); + return result; + } + + // If we're asked for an oauth::scope token, try to generate one. + final String oauthPrefix = "oauth::"; + if (authTokenType != null && authTokenType.startsWith(oauthPrefix)) { + final String scope = authTokenType.substring(oauthPrefix.length()); + final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); + getOAuthToken(response, fxAccount, scope); + return null; + } + + // Otherwise, fail. Logger.warn(LOG_TAG, "Returning null bundle for getAuthToken."); return null; diff --git a/mobile/android/base/fxa/receivers/FxAccountDeletedService.java b/mobile/android/base/fxa/receivers/FxAccountDeletedService.java index 14768cfb1e71..50094c0c706a 100644 --- a/mobile/android/base/fxa/receivers/FxAccountDeletedService.java +++ b/mobile/android/base/fxa/receivers/FxAccountDeletedService.java @@ -68,6 +68,10 @@ public class FxAccountDeletedService extends IntentService { // Remove any displayed notifications. new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID).clear(context); + + // 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. } public static void deletePickle(final Context context) { diff --git a/mobile/android/base/fxa/sync/FxAccountNotificationManager.java b/mobile/android/base/fxa/sync/FxAccountNotificationManager.java index f467df8f9ed5..b3230d8feb53 100644 --- a/mobile/android/base/fxa/sync/FxAccountNotificationManager.java +++ b/mobile/android/base/fxa/sync/FxAccountNotificationManager.java @@ -65,7 +65,7 @@ public class FxAccountNotificationManager { * @param fxAccount * Firefox Account to reflect to the notification manager. */ - protected void update(Context context, AndroidFxAccount fxAccount) { + public void update(Context context, AndroidFxAccount fxAccount) { final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); final State state = fxAccount.getState(); diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java index 3f0411e06ac5..7e925340f198 100644 --- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -6,7 +6,6 @@ package org.mozilla.gecko.fxa.sync; import java.net.URI; import java.net.URISyntaxException; -import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -15,24 +14,19 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; 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.FxAccountUtils; import org.mozilla.gecko.background.fxa.SkewHandler; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; import org.mozilla.gecko.browserid.JSONWebTokenUtils; import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.authenticator.AccountPickler; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; +import org.mozilla.gecko.fxa.authenticator.FxADefaultLoginStateMachineDelegate; import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator; import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; import org.mozilla.gecko.fxa.login.Married; 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.sync.BackoffHandler; import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.PrefsBackoffHandler; @@ -456,38 +450,17 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { // and extend the background delay even further into the future. schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler); - final String authServerEndpoint = fxAccount.getAccountServerURI(); final String tokenServerEndpoint = fxAccount.getTokenServerURI(); final URI tokenServerEndpointURI = new URI(tokenServerEndpoint); final String audience = FxAccountUtils.getAudienceForURL(tokenServerEndpoint); - // TODO: why doesn't the loginPolicy extract the audience from the account? - final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor); final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); - stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() { + stateMachine.advance(state, StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) { @Override - public FxAccountClient getClient() { - return client; - } - - @Override - public long getCertificateDurationInMilliseconds() { - return 12 * 60 * 60 * 1000; - } - - @Override - public long getAssertionDurationInMilliseconds() { - return 15 * 60 * 1000; - } - - @Override - public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { - return StateFactory.generateKeyPair(); - } - - @Override - public void handleTransition(Transition transition, State state) { - Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); + public void handleNotMarried(State notMarried) { + Logger.info(LOG_TAG, "handleNotMarried: in " + notMarried.getStateLabel()); + schedulePolicy.onHandleFinal(notMarried.getNeededAction()); + syncDelegate.handleCannotSync(notMarried); } private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) { @@ -495,18 +468,11 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { } @Override - public void handleFinal(State state) { - Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); - fxAccount.setState(state); - schedulePolicy.onHandleFinal(state.getNeededAction()); - notificationManager.update(context, fxAccount); - try { - if (state.getStateLabel() != StateLabel.Married) { - syncDelegate.handleCannotSync(state); - return; - } + public void handleMarried(Married married) { + schedulePolicy.onHandleFinal(married.getNeededAction()); + Logger.info(LOG_TAG, "handleMarried: in " + married.getStateLabel()); - final Married married = (Married) state; + try { final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); /* diff --git a/mobile/android/base/reading/ReadingListInvalidAuthenticationException.java b/mobile/android/base/reading/ReadingListInvalidAuthenticationException.java new file mode 100644 index 000000000000..57189e903d04 --- /dev/null +++ b/mobile/android/base/reading/ReadingListInvalidAuthenticationException.java @@ -0,0 +1,18 @@ +/* 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.reading; + +import org.mozilla.gecko.sync.net.MozResponse; + +public class ReadingListInvalidAuthenticationException extends Exception { + private static final long serialVersionUID = 7112459541558266597L; + + public final MozResponse response; + + public ReadingListInvalidAuthenticationException(MozResponse response) { + super(); + this.response = response; + } +} diff --git a/mobile/android/base/reading/ReadingListSyncAdapter.java b/mobile/android/base/reading/ReadingListSyncAdapter.java index 87be010335f5..7c52b178ba04 100644 --- a/mobile/android/base/reading/ReadingListSyncAdapter.java +++ b/mobile/android/base/reading/ReadingListSyncAdapter.java @@ -6,7 +6,6 @@ package org.mozilla.gecko.reading; import java.net.URI; import java.net.URISyntaxException; -import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -15,30 +14,15 @@ import java.util.concurrent.TimeUnit; import org.mozilla.gecko.background.common.PrefsBranch; 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.FxAccountUtils; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException; -import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10; -import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.browserid.JSONWebTokenUtils; import org.mozilla.gecko.db.BrowserContract.ReadingListItems; -import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; -import org.mozilla.gecko.fxa.login.Married; -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.sync.FxAccountSyncDelegate; import org.mozilla.gecko.sync.net.AuthHeaderProvider; import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider; import android.accounts.Account; +import android.accounts.AccountManager; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.ContentResolver; @@ -59,8 +43,7 @@ public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter { this.executor = Executors.newSingleThreadExecutor(); } - - static final class SyncAdapterSynchronizerDelegate implements ReadingListSynchronizerDelegate { + protected static abstract class SyncAdapterSynchronizerDelegate implements ReadingListSynchronizerDelegate { private final FxAccountSyncDelegate syncDelegate; private final ContentProviderClient cpc; private final SyncResult result; @@ -73,9 +56,14 @@ public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter { this.result = result; } + abstract public void onInvalidAuthentication(); + @Override public void onUnableToSync(Exception e) { Logger.warn(LOG_TAG, "Unable to sync.", e); + if (e instanceof ReadingListInvalidAuthenticationException) { + onInvalidAuthentication(); + } cpc.release(); syncDelegate.handleError(e); } @@ -120,6 +108,55 @@ public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter { } } + private void syncWithAuthorization(final Context context, + final Account account, + final SyncResult syncResult, + final FxAccountSyncDelegate syncDelegate, + final String authToken, + final SharedPreferences sharedPrefs, + final Bundle extras) { + final AuthHeaderProvider auth = new BearerAuthHeaderProvider(authToken); + + final String endpointString = ReadingListConstants.DEFAULT_PROD_ENDPOINT; + final URI endpoint; + Logger.info(LOG_TAG, "Syncing reading list against " + endpointString); + try { + endpoint = new URI(endpointString); + } catch (URISyntaxException e) { + // Should never happen. + Logger.error(LOG_TAG, "Unexpected malformed URI for reading list service: " + endpointString); + syncDelegate.handleError(e); + return; + } + + final PrefsBranch branch = new PrefsBranch(sharedPrefs, "readinglist."); + final ReadingListClient remote = new ReadingListClient(endpoint, auth); + final ContentProviderClient cpc = getContentProviderClient(context); // Released by the inner SyncAdapterSynchronizerDelegate. + + final LocalReadingListStorage local = new LocalReadingListStorage(cpc); + String localName = branch.getString(PREF_LOCAL_NAME, null); + if (localName == null) { + localName = FxAccountUtils.defaultClientName(context); + } + + // Make sure DB rows don't refer to placeholder values. + local.updateLocalNames(localName); + + final ReadingListSynchronizer synchronizer = new ReadingListSynchronizer(branch, remote, local); + + synchronizer.syncAll(new SyncAdapterSynchronizerDelegate(syncDelegate, cpc, syncResult) { + @Override + public void onInvalidAuthentication() { + // The reading list server rejected our oauth token! Invalidate it. Next + // time through, we'll request a new one, which will drive the login + // state machine, produce a new assertion, and eventually a fresh token. + Logger.info(LOG_TAG, "Invalidating oauth token after 401!"); + AccountManager.get(context).invalidateAuthToken(account.type, authToken); + } + }); + // TODO: backoffs, and everything else handled by a SessionCallback. + } + @Override public void onPerformSync(final Account account, final Bundle extras, final String authority, final ContentProviderClient provider, final SyncResult syncResult) { Logger.setThreadLogTag(ReadingListConstants.GLOBAL_LOG_TAG); @@ -128,150 +165,31 @@ public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter { final Context context = getContext(); final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - // If this sync was triggered by user action, this will be true. - final boolean isImmediate = (extras != null) && - (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) || - extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)); - final CountDownLatch latch = new CountDownLatch(1); final FxAccountSyncDelegate syncDelegate = new FxAccountSyncDelegate(latch, syncResult, fxAccount); + + final AccountManager accountManager = AccountManager.get(context); + // If we have an auth failure that requires user intervention, FxA will show system + // notifications prompting the user to re-connect as it advances the internal account state. + // true causes the auth token fetch to return null on failure immediately, rather than doing + // Mysterious Internal Work to try to get the token. + final boolean notifyAuthFailure = true; try { - final State state; - try { - state = fxAccount.getState(); - } catch (Exception e) { - Logger.error(LOG_TAG, "Unable to sync.", e); - return; + final String authToken = accountManager.blockingGetAuthToken(account, ReadingListConstants.AUTH_TOKEN_TYPE, notifyAuthFailure); + if (authToken == null) { + throw new RuntimeException("Couldn't get oauth token! Aborting sync."); } - - final String oauthServerUri = FxAccountConstants.STAGE_OAUTH_SERVER_ENDPOINT; - final String authServerEndpoint = fxAccount.getAccountServerURI(); - final String audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token. - final SharedPreferences sharedPrefs = fxAccount.getReadingListPrefs(); - final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor); - final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); - - stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() { - @Override - public FxAccountClient getClient() { - return client; - } - - @Override - public long getCertificateDurationInMilliseconds() { - return 12 * 60 * 60 * 1000; - } - - @Override - public long getAssertionDurationInMilliseconds() { - return 15 * 60 * 1000; - } - - @Override - public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { - return StateFactory.generateKeyPair(); - } - - @Override - public void handleTransition(Transition transition, State state) { - Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); - } - - @Override - public void handleFinal(State state) { - Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); - fxAccount.setState(state); - - // TODO: scheduling, notifications. - try { - if (state.getStateLabel() != StateLabel.Married) { - syncDelegate.handleCannotSync(state); - return; - } - - final Married married = (Married) state; - final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); - JSONWebTokenUtils.dumpAssertion(assertion); - - final String clientID = FxAccountConstants.OAUTH_CLIENT_ID_FENNEC; - final String scope = ReadingListConstants.OAUTH_SCOPE_READINGLIST; - syncWithAssertion(clientID, scope, assertion, sharedPrefs, extras); - } catch (Exception e) { - syncDelegate.handleError(e); - return; - } - } - - private void syncWithAssertion(final String client_id, final String scope, final String assertion, - final SharedPreferences sharedPrefs, final Bundle extras) { - final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor); - Logger.debug(LOG_TAG, "OAuth fetch."); - oauthClient.authorization(client_id, assertion, null, scope, new RequestDelegate() { - @Override - public void handleSuccess(AuthorizationResponse result) { - Logger.debug(LOG_TAG, "OAuth success."); - syncWithAuthorization(result, sharedPrefs, extras); - } - - @Override - public void handleFailure(FxAccountAbstractClientRemoteException e) { - Logger.error(LOG_TAG, "OAuth failure.", e); - syncDelegate.handleError(e); - } - - @Override - public void handleError(Exception e) { - Logger.error(LOG_TAG, "OAuth error.", e); - syncDelegate.handleError(e); - } - }); - } - - private void syncWithAuthorization(AuthorizationResponse authResponse, - SharedPreferences sharedPrefs, - Bundle extras) { - final AuthHeaderProvider auth = new BearerAuthHeaderProvider(authResponse.access_token); - - final String endpointString = ReadingListConstants.DEFAULT_DEV_ENDPOINT; - final URI endpoint; - Logger.info(LOG_TAG, "XXX Syncing to " + endpointString); - try { - endpoint = new URI(endpointString); - } catch (URISyntaxException e) { - // Should never happen. - Logger.error(LOG_TAG, "Unexpected malformed URI for reading list service: " + endpointString); - syncDelegate.handleError(e); - return; - } - - final PrefsBranch branch = new PrefsBranch(sharedPrefs, "readinglist."); - final ReadingListClient remote = new ReadingListClient(endpoint, auth); - final ContentProviderClient cpc = getContentProviderClient(context); // TODO: make sure I'm always released! - - final LocalReadingListStorage local = new LocalReadingListStorage(cpc); - String localName = branch.getString(PREF_LOCAL_NAME, null); - if (localName == null) { - localName = FxAccountUtils.defaultClientName(context); - } - - // Make sure DB rows don't refer to placeholder values. - local.updateLocalNames(localName); - - final ReadingListSynchronizer synchronizer = new ReadingListSynchronizer(branch, remote, local); - - synchronizer.syncAll(new SyncAdapterSynchronizerDelegate(syncDelegate, cpc, syncResult)); - // TODO: backoffs, and everything else handled by a SessionCallback. - } - }); + syncWithAuthorization(context, account, syncResult, syncDelegate, authToken, sharedPrefs, extras); latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); Logger.info(LOG_TAG, "Reading list sync done."); - } catch (Exception e) { + // We can get lots of exceptions here; handle them uniformly. Logger.error(LOG_TAG, "Got error syncing.", e); syncDelegate.handleError(e); } + /* * TODO: * * Account error notifications. How do we avoid these overlapping with Sync? @@ -290,7 +208,6 @@ public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter { */ } - private ContentProviderClient getContentProviderClient(Context context) { final ContentResolver contentResolver = context.getContentResolver(); final ContentProviderClient client = contentResolver.acquireContentProviderClient(ReadingListItems.CONTENT_URI); diff --git a/mobile/android/base/reading/ReadingListSynchronizer.java b/mobile/android/base/reading/ReadingListSynchronizer.java index 0b147fb0ed79..e1e5032b0579 100644 --- a/mobile/android/base/reading/ReadingListSynchronizer.java +++ b/mobile/android/base/reading/ReadingListSynchronizer.java @@ -774,7 +774,11 @@ public class ReadingListSynchronizer { final int statusCode = response.getStatusCode(); Logger.error(LOG_TAG, "Download failed. since = " + since + ". Response: " + statusCode); response.logResponseBody(LOG_TAG); - delegate.fail(); + if (response.isInvalidAuthentication()) { + delegate.fail(new ReadingListInvalidAuthenticationException(response)); + } else { + delegate.fail(); + } } @Override diff --git a/mobile/android/base/sync/net/MozResponse.java b/mobile/android/base/sync/net/MozResponse.java index 14dc565c5ca2..25d61d095a7d 100644 --- a/mobile/android/base/sync/net/MozResponse.java +++ b/mobile/android/base/sync/net/MozResponse.java @@ -19,6 +19,7 @@ import org.mozilla.gecko.sync.NonObjectJSONException; import ch.boye.httpclientandroidlib.Header; import ch.boye.httpclientandroidlib.HttpEntity; import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.HttpStatus; import ch.boye.httpclientandroidlib.impl.cookie.DateParseException; import ch.boye.httpclientandroidlib.impl.cookie.DateUtils; @@ -42,6 +43,10 @@ public class MozResponse { return this.getStatusCode() == 200; } + public boolean isInvalidAuthentication() { + return this.getStatusCode() == HttpStatus.SC_UNAUTHORIZED; + } + /** * Fetch the content type of the HTTP response body. *