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.
*