Bug 1117829 - Add Firefox Account-backed oauth client. r=rnewman
authorNick Alexander <nalexander@mozilla.com>
Mon, 18 Aug 2014 14:07:57 -0700
changeset 229592 4d10b6a2cd5fb4f643975769b4bfefb16ff716f8
parent 229591 8fa40864937c47a3718c9aea162d98540c35017a
child 229593 b6f1c25cf603905b438d197b0af8819e9c7e578e
push id11374
push usernalexander@mozilla.com
push dateWed, 18 Feb 2015 19:04:45 +0000
treeherderfx-team@b6f1c25cf603 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs1117829
milestone38.0a1
Bug 1117829 - Add Firefox Account-backed oauth client. r=rnewman The oauth client exchanges Firefox Account assertions for oauth token grants. The client_id is assumed to have the "canGrant" capability on the oauth endpoint. ======== https://github.com/mozilla-services/android-sync/commit/d1a25c8233240a5a1c03de5394c2be92819779c2 Author: Nick Alexander <nalexander@mozilla.com> Bug 1117829 - Part 3: Add FxA oauth and profile clients. ======== https://github.com/mozilla-services/android-sync/commit/6c52ce9b53c3ef2ec34ddf7410fdfa8501925948 Author: Nick Alexander <nalexander@mozilla.com> Date: Mon Aug 18 13:53:56 2014 -0700 Bug 1117829 - Part 2: Support remote verifier v1 and v2. ======== https://github.com/mozilla-services/android-sync/commit/679e972d2cf98be26778be4e88972ebd285ec511 Author: Nick Alexander <nalexander@mozilla.com> Date: Mon Aug 18 11:52:45 2014 -0700 Bug 1117829 - Part 1: Generalize bearer token auth header providers. ======== https://github.com/mozilla-services/android-sync/commit/b55a14fe889b6d13d7603d9d202dd9f67e9add0e Author: Nick Alexander <nalexander@mozilla.com> Date: Mon Aug 18 13:54:46 2014 -0700 Bug 1117829 - Pre: Add static methods for cross-class testing. ======== https://github.com/mozilla-services/android-sync/commit/5576662dd3528a53c5a9549619c7d31ed7a4860a Author: Nick Alexander <nalexander@mozilla.com> Date: Mon Aug 18 11:42:45 2014 -0700 Bug 1117829 - Pre: Fix debug printing of JWT structures.
mobile/android/base/android-services.mozbuild
mobile/android/base/background/fxa/oauth/FxAccountAbstractClient.java
mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java
mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java
mobile/android/base/background/fxa/oauth/FxAccountOAuthRemoteError.java
mobile/android/base/background/fxa/profile/FxAccountProfileClient10.java
mobile/android/base/browserid/JSONWebTokenUtils.java
mobile/android/base/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java
mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java
mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient10.java
mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient20.java
mobile/android/base/fxa/activities/FxAccountStatusFragment.java
mobile/android/base/sync/net/AbstractBearerTokenAuthHeaderProvider.java
mobile/android/base/sync/net/BearerAuthHeaderProvider.java
mobile/android/base/sync/net/BrowserIDAuthHeaderProvider.java
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -793,17 +793,22 @@ sync_java_files = [
     'background/fxa/FxAccount20LoginDelegate.java',
     'background/fxa/FxAccountAgeLockoutHelper.java',
     'background/fxa/FxAccountClient.java',
     'background/fxa/FxAccountClient10.java',
     'background/fxa/FxAccountClient20.java',
     'background/fxa/FxAccountClientException.java',
     'background/fxa/FxAccountRemoteError.java',
     'background/fxa/FxAccountUtils.java',
+    'background/fxa/oauth/FxAccountAbstractClient.java',
+    'background/fxa/oauth/FxAccountAbstractClientException.java',
+    'background/fxa/oauth/FxAccountOAuthClient10.java',
+    'background/fxa/oauth/FxAccountOAuthRemoteError.java',
     'background/fxa/PasswordStretcher.java',
+    'background/fxa/profile/FxAccountProfileClient10.java',
     'background/fxa/QuickPasswordStretcher.java',
     'background/fxa/SkewHandler.java',
     'background/healthreport/AndroidConfigurationProvider.java',
     'background/healthreport/Environment.java',
     'background/healthreport/EnvironmentBuilder.java',
     'background/healthreport/EnvironmentV1.java',
     'background/healthreport/EnvironmentV2.java',
     'background/healthreport/HealthReportBroadcastReceiver.java',
@@ -830,17 +835,19 @@ sync_java_files = [
     'background/preferences/PreferenceManagerCompat.java',
     'browserid/ASNUtils.java',
     'browserid/BrowserIDKeyPair.java',
     'browserid/DSACryptoImplementation.java',
     'browserid/JSONWebTokenUtils.java',
     'browserid/MockMyIDTokenFactory.java',
     'browserid/RSACryptoImplementation.java',
     'browserid/SigningPrivateKey.java',
-    'browserid/verifier/BrowserIDRemoteVerifierClient.java',
+    'browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java',
+    'browserid/verifier/BrowserIDRemoteVerifierClient10.java',
+    'browserid/verifier/BrowserIDRemoteVerifierClient20.java',
     'browserid/verifier/BrowserIDVerifierClient.java',
     'browserid/verifier/BrowserIDVerifierDelegate.java',
     'browserid/verifier/BrowserIDVerifierException.java',
     'browserid/VerifyingPublicKey.java',
     'fxa/AccountLoader.java',
     'fxa/activities/FxAccountAbstractActivity.java',
     'fxa/activities/FxAccountAbstractSetupActivity.java',
     'fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java',
@@ -957,20 +964,22 @@ sync_java_files = [
     'sync/MetaGlobalException.java',
     'sync/MetaGlobalMissingEnginesException.java',
     'sync/MetaGlobalNotSetException.java',
     'sync/middleware/Crypto5MiddlewareRepository.java',
     'sync/middleware/Crypto5MiddlewareRepositorySession.java',
     'sync/middleware/MiddlewareRepository.java',
     'sync/middleware/MiddlewareRepositorySession.java',
     'sync/MigrationSentinelSyncStage.java',
+    'sync/net/AbstractBearerTokenAuthHeaderProvider.java',
     'sync/net/AuthHeaderProvider.java',
     'sync/net/BaseResource.java',
     'sync/net/BaseResourceDelegate.java',
     'sync/net/BasicAuthHeaderProvider.java',
+    'sync/net/BearerAuthHeaderProvider.java',
     'sync/net/BrowserIDAuthHeaderProvider.java',
     'sync/net/ConnectionMonitorThread.java',
     'sync/net/HandleProgressException.java',
     'sync/net/HawkAuthHeaderProvider.java',
     'sync/net/HMACAuthHeaderProvider.java',
     'sync/net/HttpResponseObserver.java',
     'sync/net/Resource.java',
     'sync/net/ResourceDelegate.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/oauth/FxAccountAbstractClient.java
@@ -0,0 +1,225 @@
+/* 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.background.fxa.oauth;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+
+import org.mozilla.gecko.background.fxa.FxAccountClientException;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientMalformedResponseException;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpHeaders;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+
+public abstract class FxAccountAbstractClient {
+  protected static final String LOG_TAG = FxAccountAbstractClient.class.getSimpleName();
+
+  protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
+  protected static final String AUTHORIZATION_RESPONSE_TYPE = "token";
+
+  public static final String JSON_KEY_ERROR = "error";
+  public static final String JSON_KEY_MESSAGE = "message";
+  public static final String JSON_KEY_CODE = "code";
+  public static final String JSON_KEY_ERRNO = "errno";
+
+  protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE };
+  protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
+
+  /**
+   * The server's URI.
+   * <p>
+   * We assume throughout that this ends with a trailing slash (and guarantee as
+   * much in the constructor).
+   */
+  protected final String serverURI;
+
+  protected final Executor executor;
+
+  public FxAccountAbstractClient(String serverURI, Executor executor) {
+    if (serverURI == null) {
+      throw new IllegalArgumentException("Must provide a server URI.");
+    }
+    if (executor == null) {
+      throw new IllegalArgumentException("Must provide a non-null executor.");
+    }
+    this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
+    if (!this.serverURI.endsWith("/")) {
+      throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
+    }
+    this.executor = executor;
+  }
+
+  /**
+   * Process a typed value extracted from a successful response (in an
+   * endpoint-dependent way).
+   */
+  public interface RequestDelegate<T> {
+    public void handleError(Exception e);
+    public void handleFailure(FxAccountAbstractClientRemoteException e);
+    public void handleSuccess(T result);
+  }
+
+  /**
+   * Intepret a response from the auth server.
+   * <p>
+   * Throw an appropriate exception on errors; otherwise, return the response's
+   * status code.
+   *
+   * @return response's HTTP status code.
+   * @throws FxAccountClientException
+   */
+  public static int validateResponse(HttpResponse response) throws FxAccountAbstractClientRemoteException {
+    final int status = response.getStatusLine().getStatusCode();
+    if (status == 200) {
+      return status;
+    }
+    int code;
+    int errno;
+    String error;
+    String message;
+    ExtendedJSONObject body;
+    try {
+      body = new SyncStorageResponse(response).jsonObjectBody();
+      body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
+      body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
+      code = body.getLong(JSON_KEY_CODE).intValue();
+      errno = body.getLong(JSON_KEY_ERRNO).intValue();
+      error = body.getString(JSON_KEY_ERROR);
+      message = body.getString(JSON_KEY_MESSAGE);
+    } catch (Exception e) {
+      throw new FxAccountAbstractClientMalformedResponseException(response);
+    }
+    throw new FxAccountAbstractClientRemoteException(response, code, errno, error, message, body);
+  }
+
+  protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        delegate.handleError(e);
+      }
+    });
+  }
+
+  protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) {
+    try {
+      if (requestBody == null) {
+        resource.post((HttpEntity) null);
+      } else {
+        resource.post(requestBody);
+      }
+    } catch (UnsupportedEncodingException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+  }
+
+  /**
+   * Translate resource callbacks into request callbacks invoked on the provided
+   * executor.
+   * <p>
+   * Override <code>handleSuccess</code> to parse the body of the resource
+   * request and call the request callback. <code>handleSuccess</code> is
+   * invoked via the executor, so you don't need to delegate further.
+   */
+  protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
+    protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
+
+    protected final RequestDelegate<T> delegate;
+
+    /**
+     * Create a delegate for an un-authenticated resource.
+     */
+    public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) {
+      super(resource);
+      this.delegate = delegate;
+    }
+
+    @Override
+    public AuthHeaderProvider getAuthHeaderProvider() {
+      return super.getAuthHeaderProvider();
+    }
+
+    @Override
+    public String getUserAgent() {
+      return FxAccountConstants.USER_AGENT;
+    }
+
+    @Override
+    public void handleHttpResponse(HttpResponse response) {
+      try {
+        final int status = validateResponse(response);
+        invokeHandleSuccess(status, response);
+      } catch (FxAccountAbstractClientRemoteException e) {
+        invokeHandleFailure(e);
+      }
+    }
+
+    protected void invokeHandleFailure(final FxAccountAbstractClientRemoteException e) {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          delegate.handleFailure(e);
+        }
+      });
+    }
+
+    protected void invokeHandleSuccess(final int status, final HttpResponse response) {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
+            ResourceDelegate.this.handleSuccess(status, response, body);
+          } catch (Exception e) {
+            delegate.handleError(e);
+          }
+        }
+      });
+    }
+
+    @Override
+    public void handleHttpProtocolException(final ClientProtocolException e) {
+      invokeHandleError(delegate, e);
+    }
+
+    @Override
+    public void handleHttpIOException(IOException e) {
+      invokeHandleError(delegate, e);
+    }
+
+    @Override
+    public void handleTransportException(GeneralSecurityException e) {
+      invokeHandleError(delegate, e);
+    }
+
+    @Override
+    public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+      super.addHeaders(request, client);
+
+      // The basics.
+      final Locale locale = Locale.getDefault();
+      request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Utils.getLanguageTag(locale));
+      request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java
@@ -0,0 +1,63 @@
+/* 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.background.fxa.oauth;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
+ */
+public class FxAccountAbstractClientException extends Exception {
+  private static final long serialVersionUID = 1953459541558266597L;
+
+  public FxAccountAbstractClientException(String detailMessage) {
+    super(detailMessage);
+  }
+
+  public FxAccountAbstractClientException(Exception e) {
+    super(e);
+  }
+
+  public static class FxAccountAbstractClientRemoteException extends FxAccountAbstractClientException {
+    private static final long serialVersionUID = 1209313149952001097L;
+
+    public final HttpResponse response;
+    public final long httpStatusCode;
+    public final long apiErrorNumber;
+    public final String error;
+    public final String message;
+    public final ExtendedJSONObject body;
+
+    public FxAccountAbstractClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) {
+      super(new HTTPFailureException(new SyncStorageResponse(response)));
+      if (body == null) {
+        throw new IllegalArgumentException("body must not be null");
+      }
+      this.response = response;
+      this.httpStatusCode = httpStatusCode;
+      this.apiErrorNumber = apiErrorNumber;
+      this.error = error;
+      this.message = message;
+      this.body = body;
+    }
+
+    @Override
+    public String toString() {
+      return "<FxAccountAbstractClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
+    }
+  }
+
+  public static class FxAccountAbstractClientMalformedResponseException extends FxAccountAbstractClientRemoteException {
+    private static final long serialVersionUID = 1209313149952001098L;
+
+    public FxAccountAbstractClientMalformedResponseException(HttpResponse response) {
+      super(response, 0, FxAccountOAuthRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", new ExtendedJSONObject());
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java
@@ -0,0 +1,101 @@
+/* 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.background.fxa.oauth;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.Executor;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * Talk to an fxa-oauth-server to get "implicitly granted" OAuth tokens.
+ * <p>
+ * To use this client, you will need a pre-allocated fxa-oauth-server
+ * "client_id" with special "implicit grant" permissions.
+ * <p>
+ * This client was written against the API documented at <a href="https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md">https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md</a>.
+ */
+public class FxAccountOAuthClient10 extends FxAccountAbstractClient {
+  protected static final String LOG_TAG = FxAccountOAuthClient10.class.getSimpleName();
+
+  protected static final String AUTHORIZATION_RESPONSE_TYPE = "token";
+
+  protected static final String JSON_KEY_ACCESS_TOKEN = "access_token";
+  protected static final String JSON_KEY_ASSERTION = "assertion";
+  protected static final String JSON_KEY_CLIENT_ID = "client_id";
+  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_TYPE = "token_type";
+
+  // access_token: A string that can be used for authorized requests to service providers.
+  // scope: A string of space-separated permissions that this token has. May differ from requested scopes, since user can deny permissions.
+  // token_type: A string representing the token type. Currently will always be "bearer".
+  protected static final String[] AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_ACCESS_TOKEN, JSON_KEY_SCOPE, JSON_KEY_TOKEN_TYPE };
+
+  public FxAccountOAuthClient10(String serverURI, Executor executor) {
+    super(serverURI, executor);
+  }
+
+  /**
+   * Thin container for an authorization response.
+   */
+  public static class AuthorizationResponse {
+    public final String access_token;
+    public final String token_type;
+    public final String scope;
+
+    public AuthorizationResponse(String access_token, String token_type, String scope) {
+      this.access_token = access_token;
+      this.token_type = token_type;
+      this.scope = scope;
+    }
+  }
+
+  public void authorization(String client_id, String assertion, String state, String scope,
+                            RequestDelegate<AuthorizationResponse> delegate) {
+    final BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "authorization"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<AuthorizationResponse>(resource, delegate) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          body.throwIfFieldsMissingOrMisTyped(AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
+          String access_token = body.getString(JSON_KEY_ACCESS_TOKEN);
+          String token_type = body.getString(JSON_KEY_TOKEN_TYPE);
+          String scope = body.getString(JSON_KEY_SCOPE);
+          delegate.handleSuccess(new AuthorizationResponse(access_token, token_type, scope));
+          return;
+        } catch (Exception e) {
+          delegate.handleError(e);
+          return;
+        }
+      }
+    };
+
+    final ExtendedJSONObject requestBody = new ExtendedJSONObject();
+    requestBody.put(JSON_KEY_RESPONSE_TYPE, AUTHORIZATION_RESPONSE_TYPE);
+    requestBody.put(JSON_KEY_CLIENT_ID, client_id);
+    requestBody.put(JSON_KEY_ASSERTION, assertion);
+    if (scope != null) {
+      requestBody.put(JSON_KEY_SCOPE, scope);
+    }
+    if (state != null) {
+      requestBody.put(JSON_KEY_STATE, state);
+    }
+
+    post(resource, requestBody, delegate);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/oauth/FxAccountOAuthRemoteError.java
@@ -0,0 +1,19 @@
+/* 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.background.fxa.oauth;
+
+public interface FxAccountOAuthRemoteError {
+  public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
+  public static final int UNKNOWN_CLIENT_ID = 101;
+  public static final int INCORRECT_CLIENT_SECRET = 102;
+  public static final int REDIRECT_URI_DOES_NOT_MATCH_REGISTERED_VALUE = 103;
+  public static final int INVALID_FXA_ASSERTION = 104;
+  public static final int UNKNOWN_CODE = 105;
+  public static final int INCORRECT_CODE = 106;
+  public static final int EXPIRED_CODE = 107;
+  public static final int INVALID_TOKEN = 108;
+  public static final int INVALID_REQUEST_PARAMETER = 109;
+  public static final int UNKNOWN_ERROR = 999;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/profile/FxAccountProfileClient10.java
@@ -0,0 +1,59 @@
+/* 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.background.fxa.profile;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.Executor;
+
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+
+/**
+ * Talk to an fxa-profile-server to get profile information like name, age, gender, and avatar image.
+ * <p>
+ * This client was written against the API documented at <a href="https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md">https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md</a>.
+ */
+public class FxAccountProfileClient10 extends FxAccountAbstractClient {
+  public FxAccountProfileClient10(String serverURI, Executor executor) {
+    super(serverURI, executor);
+  }
+
+  public void profile(final String token, RequestDelegate<ExtendedJSONObject> delegate) {
+    BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "profile"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate) {
+      @Override
+      public AuthHeaderProvider getAuthHeaderProvider() {
+        return new BearerAuthHeaderProvider(token);
+      }
+
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          delegate.handleSuccess(body);
+          return;
+        } catch (Exception e) {
+          delegate.handleError(e);
+          return;
+        }
+      }
+    };
+
+    resource.get();
+  }
+}
--- a/mobile/android/base/browserid/JSONWebTokenUtils.java
+++ b/mobile/android/base/browserid/JSONWebTokenUtils.java
@@ -181,18 +181,18 @@ public class JSONWebTokenUtils {
    */
   public static boolean dumpCertificate(String input) {
     ExtendedJSONObject c = parseCertificate(input);
     try {
       if (c == null) {
         System.out.println("Malformed certificate -- got exception trying to dump contents.");
         return false;
       }
-      System.out.println("certificate header:    " + c.getString("header"));
-      System.out.println("certificate payload:   " + c.getString("payload"));
+      System.out.println("certificate header:    " + c.getObject("header").toJSONString());
+      System.out.println("certificate payload:   " + c.getObject("payload").toJSONString());
       System.out.println("certificate signature: " + c.getString("signature"));
       return true;
     } catch (Exception e) {
       System.out.println("Malformed certificate -- got exception trying to dump contents.");
       return false;
     }
   }
 
@@ -239,18 +239,18 @@ public class JSONWebTokenUtils {
   public static boolean dumpAssertion(String input) {
     ExtendedJSONObject a = parseAssertion(input);
     try {
       if (a == null) {
         System.out.println("Malformed assertion -- got exception trying to dump contents.");
         return false;
       }
       dumpCertificate(a.getString("certificate"));
-      System.out.println("assertion   header:    " + a.getString("header"));
-      System.out.println("assertion   payload:   " + a.getString("payload"));
+      System.out.println("assertion   header:    " + a.getObject("header").toJSONString());
+      System.out.println("assertion   payload:   " + a.getObject("payload").toJSONString());
       System.out.println("assertion   signature: " + a.getString("signature"));
       return true;
     } catch (Exception e) {
       System.out.println("Malformed assertion -- got exception trying to dump contents.");
       return false;
     }
   }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java
@@ -0,0 +1,95 @@
+/* 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.browserid.verifier;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierErrorResponseException;
+import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierMalformedResponseException;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+
+public abstract class AbstractBrowserIDRemoteVerifierClient implements BrowserIDVerifierClient {
+  public static final String LOG_TAG = AbstractBrowserIDRemoteVerifierClient.class.getSimpleName();
+
+  protected static class RemoteVerifierResourceDelegate extends BaseResourceDelegate {
+    private final BrowserIDVerifierDelegate delegate;
+
+    protected RemoteVerifierResourceDelegate(Resource resource, BrowserIDVerifierDelegate delegate) {
+      super(resource);
+      this.delegate = delegate;
+    }
+
+    @Override
+    public String getUserAgent() {
+      return null;
+    }
+
+    @Override
+    public void handleHttpResponse(HttpResponse response) {
+      SyncResponse res = new SyncResponse(response);
+      int statusCode = res.getStatusCode();
+      Logger.debug(LOG_TAG, "Got response with status code " + statusCode + ".");
+
+      if (statusCode != 200) {
+        delegate.handleError(new BrowserIDVerifierErrorResponseException("Expected status code 200."));
+        return;
+      }
+
+      ExtendedJSONObject o = null;
+      try {
+        o = res.jsonObjectBody();
+      } catch (Exception e) {
+        delegate.handleError(new BrowserIDVerifierMalformedResponseException(e));
+        return;
+      }
+
+      String status = o.getString("status");
+      if ("failure".equals(status)) {
+        delegate.handleFailure(o);
+        return;
+      }
+
+      if (!("okay".equals(status))) {
+        delegate.handleError(new BrowserIDVerifierMalformedResponseException("Expected status okay, got '" + status + "'."));
+        return;
+      }
+
+      delegate.handleSuccess(o);
+    }
+
+    @Override
+    public void handleTransportException(GeneralSecurityException e) {
+      Logger.warn(LOG_TAG, "Got transport exception.", e);
+      delegate.handleError(e);
+    }
+
+    @Override
+    public void handleHttpProtocolException(ClientProtocolException e) {
+      Logger.warn(LOG_TAG, "Got protocol exception.", e);
+      delegate.handleError(e);
+    }
+
+    @Override
+    public void handleHttpIOException(IOException e) {
+      Logger.warn(LOG_TAG, "Got IO exception.", e);
+      delegate.handleError(e);
+    }
+  }
+
+  protected final URI verifierUri;
+
+  public AbstractBrowserIDRemoteVerifierClient(URI verifierUri) {
+    this.verifierUri = verifierUri;
+  }
+}
deleted file mode 100644
--- a/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/* 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.browserid.verifier;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import java.util.List;
-
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierErrorResponseException;
-import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierMalformedResponseException;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.sync.net.BaseResource;
-import org.mozilla.gecko.sync.net.BaseResourceDelegate;
-import org.mozilla.gecko.sync.net.Resource;
-import org.mozilla.gecko.sync.net.SyncResponse;
-
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.NameValuePair;
-import ch.boye.httpclientandroidlib.client.ClientProtocolException;
-import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
-import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
-
-public class BrowserIDRemoteVerifierClient implements BrowserIDVerifierClient {
-  protected static class RemoteVerifierResourceDelegate extends BaseResourceDelegate {
-    private final BrowserIDVerifierDelegate delegate;
-
-    protected RemoteVerifierResourceDelegate(Resource resource, BrowserIDVerifierDelegate delegate) {
-      super(resource);
-      this.delegate = delegate;
-    }
-
-    @Override
-    public String getUserAgent() {
-      return null;
-    }
-
-    @Override
-    public void handleHttpResponse(HttpResponse response) {
-      SyncResponse res = new SyncResponse(response);
-      int statusCode = res.getStatusCode();
-      Logger.debug(LOG_TAG, "Got response with status code " + statusCode + ".");
-
-      if (statusCode != 200) {
-        delegate.handleError(new BrowserIDVerifierErrorResponseException("Expected status code 200."));
-        return;
-      }
-
-      ExtendedJSONObject o = null;
-      try {
-        o = res.jsonObjectBody();
-      } catch (Exception e) {
-        delegate.handleError(new BrowserIDVerifierMalformedResponseException(e));
-        return;
-      }
-
-      String status = o.getString("status");
-      if ("failure".equals(status)) {
-        delegate.handleFailure(o);
-        return;
-      }
-
-      if (!("okay".equals(status))) {
-        delegate.handleError(new BrowserIDVerifierMalformedResponseException("Expected status okay, got '" + status + "'."));
-        return;
-      }
-
-      delegate.handleSuccess(o);
-    }
-
-    @Override
-    public void handleTransportException(GeneralSecurityException e) {
-      Logger.warn(LOG_TAG, "Got transport exception.", e);
-      delegate.handleError(e);
-    }
-
-    @Override
-    public void handleHttpProtocolException(ClientProtocolException e) {
-      Logger.warn(LOG_TAG, "Got protocol exception.", e);
-      delegate.handleError(e);
-    }
-
-    @Override
-    public void handleHttpIOException(IOException e) {
-      Logger.warn(LOG_TAG, "Got IO exception.", e);
-      delegate.handleError(e);
-    }
-  }
-
-  public static final String LOG_TAG = "BrowserIDRemoteVerifierClient";
-
-  public static final String DEFAULT_VERIFIER_URL = "https://verifier.login.persona.org/verify";
-
-  protected final URI verifierUri;
-
-  public BrowserIDRemoteVerifierClient(URI verifierUri) {
-    this.verifierUri = verifierUri;
-  }
-
-  public BrowserIDRemoteVerifierClient() throws URISyntaxException {
-    this.verifierUri = new URI(DEFAULT_VERIFIER_URL);
-  }
-
-  @Override
-  public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
-    if (audience == null) {
-      throw new IllegalArgumentException("audience cannot be null.");
-    }
-    if (assertion == null) {
-      throw new IllegalArgumentException("assertion cannot be null.");
-    }
-    if (delegate == null) {
-      throw new IllegalArgumentException("delegate cannot be null.");
-    }
-
-    BaseResource r = new BaseResource(verifierUri);
-
-    r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
-
-    List<NameValuePair> nvps = Arrays.asList(new NameValuePair[] {
-        new BasicNameValuePair("audience", audience),
-        new BasicNameValuePair("assertion", assertion) });
-
-    try {
-      r.post(new UrlEncodedFormEntity(nvps, "UTF-8"));
-    } catch (UnsupportedEncodingException e) {
-      delegate.handleError(e);
-    }
-  }
-}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient10.java
@@ -0,0 +1,62 @@
+/* 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.browserid.verifier;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.mozilla.gecko.sync.net.BaseResource;
+
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+
+/**
+ * The verifier protocol changed: version 1 posts form-encoded data; version 2
+ * posts JSON data.
+ */
+public class BrowserIDRemoteVerifierClient10 extends AbstractBrowserIDRemoteVerifierClient {
+  public static final String LOG_TAG = BrowserIDRemoteVerifierClient10.class.getSimpleName();
+
+  public static final String DEFAULT_VERIFIER_URL = "https://verifier.login.persona.org/verify";
+
+  public BrowserIDRemoteVerifierClient10() throws URISyntaxException {
+    super(new URI(DEFAULT_VERIFIER_URL));
+  }
+
+  public BrowserIDRemoteVerifierClient10(URI verifierUri) {
+    super(verifierUri);
+  }
+
+  @Override
+  public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
+    if (audience == null) {
+      throw new IllegalArgumentException("audience cannot be null.");
+    }
+    if (assertion == null) {
+      throw new IllegalArgumentException("assertion cannot be null.");
+    }
+    if (delegate == null) {
+      throw new IllegalArgumentException("delegate cannot be null.");
+    }
+
+    BaseResource r = new BaseResource(verifierUri);
+
+    r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
+
+    List<NameValuePair> nvps = Arrays.asList(new NameValuePair[] {
+        new BasicNameValuePair("audience", audience),
+        new BasicNameValuePair("assertion", assertion) });
+
+    try {
+      r.post(new UrlEncodedFormEntity(nvps, "UTF-8"));
+    } catch (UnsupportedEncodingException e) {
+      delegate.handleError(e);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient20.java
@@ -0,0 +1,59 @@
+/* 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.browserid.verifier;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+/**
+ * The verifier protocol changed: version 1 posts form-encoded data; version 2
+ * posts JSON data.
+ */
+public class BrowserIDRemoteVerifierClient20 extends AbstractBrowserIDRemoteVerifierClient {
+  public static final String LOG_TAG = BrowserIDRemoteVerifierClient20.class.getSimpleName();
+
+  public static final String DEFAULT_VERIFIER_URL = "https://verifier.accounts.firefox.com/v2";
+
+  protected static final String JSON_KEY_ASSERTION = "assertion";
+  protected static final String JSON_KEY_AUDIENCE = "audience";
+
+  public BrowserIDRemoteVerifierClient20() throws URISyntaxException {
+    super(new URI(DEFAULT_VERIFIER_URL));
+  }
+
+  public BrowserIDRemoteVerifierClient20(URI verifierUri) {
+    super(verifierUri);
+  }
+
+  @Override
+  public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
+    if (audience == null) {
+      throw new IllegalArgumentException("audience cannot be null.");
+    }
+    if (assertion == null) {
+      throw new IllegalArgumentException("assertion cannot be null.");
+    }
+    if (delegate == null) {
+      throw new IllegalArgumentException("delegate cannot be null.");
+    }
+
+    BaseResource r = new BaseResource(verifierUri);
+    r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
+
+    final ExtendedJSONObject requestBody = new ExtendedJSONObject();
+    requestBody.put(JSON_KEY_AUDIENCE, audience);
+    requestBody.put(JSON_KEY_ASSERTION, assertion);
+
+    try {
+      r.post(requestBody);
+    } catch (UnsupportedEncodingException e) {
+      delegate.handleError(e);
+    }
+  }
+}
--- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
+++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
@@ -1,14 +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.fxa.activities;
 
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.preferences.PreferenceFragment;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
@@ -34,44 +38,27 @@ import android.preference.EditTextPrefer
 import android.preference.Preference;
 import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
 import android.preference.PreferenceCategory;
 import android.preference.PreferenceScreen;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-
 /**
  * A fragment that displays the status of an AndroidFxAccount.
  * <p>
  * The owning activity is responsible for providing an AndroidFxAccount at
  * appropriate times.
  */
 public class FxAccountStatusFragment
     extends PreferenceFragment
     implements OnPreferenceClickListener, OnPreferenceChangeListener {
   private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
 
-    /**
-     * If a device claims to have synced before this date, we will assume it has never synced.
-     */
-    private static final Date EARLIEST_VALID_SYNCED_DATE;
-    static {
-        final Calendar c = GregorianCalendar.getInstance();
-        c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
-        EARLIEST_VALID_SYNCED_DATE = c.getTime();
-    }
   // When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
   // before trying to sync. Should we kill off the fragment before the sync
   // request happens, that's okay: the runnable will run if the UI thread is
   // still around to service it, and since we're not updating any UI, we'll just
   // schedule the sync as usual. See also comment below about garbage
   // collection.
   private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
   private static final long LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS = 60 * 1000;
@@ -537,19 +524,16 @@ public class FxAccountStatusFragment
     deviceNamePreference.setSummary(clientName);
     deviceNamePreference.setText(clientName);
 
     updateSyncNowPreference();
   }
 
   // This is a helper function similar to TabsAccessor.getLastSyncedString() to calculate relative "Last synced" time span.
   private String getLastSyncedString(final long startTime) {
-    if (new Date(startTime).before(EARLIEST_VALID_SYNCED_DATE)) {
-      return getActivity().getString(R.string.remote_tabs_never_synced);
-    }
     final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(startTime);
     return getActivity().getResources().getString(R.string.fxaccount_status_last_synced, relativeTimeSpanString);
   }
 
   protected void updateSyncNowPreference() {
     final boolean currentlySyncing = fxAccount.isCurrentlySyncing();
     syncNowPreference.setEnabled(!currentlySyncing);
     if (currentlySyncing) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/net/AbstractBearerTokenAuthHeaderProvider.java
@@ -0,0 +1,34 @@
+/* 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.sync.net;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+
+/**
+ * An <code>AuthHeaderProvider</code> that returns an Authorization header for
+ * bearer tokens, adding a simple prefix.
+ */
+public abstract class AbstractBearerTokenAuthHeaderProvider implements AuthHeaderProvider {
+  protected final String header;
+
+  public AbstractBearerTokenAuthHeaderProvider(String token) {
+    if (token == null) {
+      throw new IllegalArgumentException("token must not be null.");
+    }
+
+    this.header = getPrefix() + " " + token;
+  }
+
+  protected abstract String getPrefix();
+
+  @Override
+  public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) {
+    return new BasicHeader("Authorization", header);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/net/BearerAuthHeaderProvider.java
@@ -0,0 +1,22 @@
+/* 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.sync.net;
+
+/**
+ * An <code>AuthHeaderProvider</code> that returns an Authorization header for
+ * Bearer tokens in the format expected by a Mozilla Firefox Accounts Profile Server.
+ * <p>
+ * See <a href="https://github.com/mozilla/fxa-profile-server/blob/master/docs/API.md">https://github.com/mozilla/fxa-profile-server/blob/master/docs/API.md</a>.
+ */
+public class BearerAuthHeaderProvider extends AbstractBearerTokenAuthHeaderProvider {
+  public BearerAuthHeaderProvider(String token) {
+    super(token);
+  }
+
+  @Override
+  protected String getPrefix() {
+    return "Bearer";
+  }
+}
--- a/mobile/android/base/sync/net/BrowserIDAuthHeaderProvider.java
+++ b/mobile/android/base/sync/net/BrowserIDAuthHeaderProvider.java
@@ -1,37 +1,23 @@
 /* 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.sync.net;
 
-import ch.boye.httpclientandroidlib.Header;
-import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
-import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
-import ch.boye.httpclientandroidlib.message.BasicHeader;
-import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
-
 /**
  * An <code>AuthHeaderProvider</code> that returns an Authorization header for
  * BrowserID assertions in the format expected by a Mozilla Services Token
  * Server.
  * <p>
  * See <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>.
  */
-public class BrowserIDAuthHeaderProvider implements AuthHeaderProvider {
-  protected final String assertion;
-
+public class BrowserIDAuthHeaderProvider extends AbstractBearerTokenAuthHeaderProvider {
   public BrowserIDAuthHeaderProvider(String assertion) {
-    if (assertion == null) {
-      throw new IllegalArgumentException("assertion must not be null.");
-    }
-
-    this.assertion = assertion;
+    super(assertion);
   }
 
   @Override
-  public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) {
-    Header header = new BasicHeader("Authorization", "BrowserID " + assertion);
-
-    return header;
+  protected String getPrefix() {
+    return "BrowserID";
   }
 }