From ac56d2a2ca1fabc0b8094a2864b2891bb7d55853 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Mon, 18 Aug 2014 14:07:57 -0700 Subject: [PATCH] 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 Bug 1117829 - Part 3: Add FxA oauth and profile clients. ======== https://github.com/mozilla-services/android-sync/commit/6c52ce9b53c3ef2ec34ddf7410fdfa8501925948 Author: Nick Alexander 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 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 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 Date: Mon Aug 18 11:42:45 2014 -0700 Bug 1117829 - Pre: Fix debug printing of JWT structures. --- mobile/android/base/android-services.mozbuild | 11 +- .../fxa/oauth/FxAccountAbstractClient.java | 225 ++++++++++++++++++ .../FxAccountAbstractClientException.java | 63 +++++ .../fxa/oauth/FxAccountOAuthClient10.java | 101 ++++++++ .../fxa/oauth/FxAccountOAuthRemoteError.java | 19 ++ .../fxa/profile/FxAccountProfileClient10.java | 59 +++++ .../base/browserid/JSONWebTokenUtils.java | 8 +- ...bstractBrowserIDRemoteVerifierClient.java} | 49 +--- .../BrowserIDRemoteVerifierClient10.java | 62 +++++ .../BrowserIDRemoteVerifierClient20.java | 59 +++++ .../activities/FxAccountStatusFragment.java | 24 +- ...AbstractBearerTokenAuthHeaderProvider.java | 34 +++ .../sync/net/BearerAuthHeaderProvider.java | 22 ++ .../sync/net/BrowserIDAuthHeaderProvider.java | 22 +- 14 files changed, 670 insertions(+), 88 deletions(-) create mode 100644 mobile/android/base/background/fxa/oauth/FxAccountAbstractClient.java create mode 100644 mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java create mode 100644 mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java create mode 100644 mobile/android/base/background/fxa/oauth/FxAccountOAuthRemoteError.java create mode 100644 mobile/android/base/background/fxa/profile/FxAccountProfileClient10.java rename mobile/android/base/browserid/verifier/{BrowserIDRemoteVerifierClient.java => AbstractBrowserIDRemoteVerifierClient.java} (63%) create mode 100644 mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient10.java create mode 100644 mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient20.java create mode 100644 mobile/android/base/sync/net/AbstractBearerTokenAuthHeaderProvider.java create mode 100644 mobile/android/base/sync/net/BearerAuthHeaderProvider.java diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild index 379e3171a5e0..c491d30de823 100644 --- a/mobile/android/base/android-services.mozbuild +++ b/mobile/android/base/android-services.mozbuild @@ -798,7 +798,12 @@ sync_java_files = [ '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', @@ -835,7 +840,9 @@ sync_java_files = [ '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', @@ -962,10 +969,12 @@ sync_java_files = [ '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', diff --git a/mobile/android/base/background/fxa/oauth/FxAccountAbstractClient.java b/mobile/android/base/background/fxa/oauth/FxAccountAbstractClient.java new file mode 100644 index 000000000000..57bc60686712 --- /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. + *

+ * 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 { + public void handleError(Exception e); + public void handleFailure(FxAccountAbstractClientRemoteException e); + public void handleSuccess(T result); + } + + /** + * Intepret a response from the auth server. + *

+ * 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 void invokeHandleError(final RequestDelegate delegate, final Exception e) { + executor.execute(new Runnable() { + @Override + public void run() { + delegate.handleError(e); + } + }); + } + + protected void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate 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. + *

+ * Override handleSuccess to parse the body of the resource + * request and call the request callback. handleSuccess is + * invoked via the executor, so you don't need to delegate further. + */ + protected abstract class ResourceDelegate extends BaseResourceDelegate { + protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body); + + protected final RequestDelegate delegate; + + /** + * Create a delegate for an un-authenticated resource. + */ + public ResourceDelegate(final Resource resource, final RequestDelegate 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); + } + } +} diff --git a/mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java b/mobile/android/base/background/fxa/oauth/FxAccountAbstractClientException.java new file mode 100644 index 000000000000..17f2ba888496 --- /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 https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md. + */ +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 ""; + } + } + + 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()); + } + } +} diff --git a/mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java b/mobile/android/base/background/fxa/oauth/FxAccountOAuthClient10.java new file mode 100644 index 000000000000..c8480e73b52d --- /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. + *

+ * To use this client, you will need a pre-allocated fxa-oauth-server + * "client_id" with special "implicit grant" permissions. + *

+ * This client was written against the API documented at https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md. + */ +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 delegate) { + final BaseResource resource; + try { + resource = new BaseResource(new URI(serverURI + "authorization")); + } catch (URISyntaxException e) { + invokeHandleError(delegate, e); + return; + } + + resource.delegate = new ResourceDelegate(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); + } +} diff --git a/mobile/android/base/background/fxa/oauth/FxAccountOAuthRemoteError.java b/mobile/android/base/background/fxa/oauth/FxAccountOAuthRemoteError.java new file mode 100644 index 000000000000..d949d316be86 --- /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; +} diff --git a/mobile/android/base/background/fxa/profile/FxAccountProfileClient10.java b/mobile/android/base/background/fxa/profile/FxAccountProfileClient10.java new file mode 100644 index 000000000000..cb851a8db3a1 --- /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. + *

+ * This client was written against the API documented at https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md. + */ +public class FxAccountProfileClient10 extends FxAccountAbstractClient { + public FxAccountProfileClient10(String serverURI, Executor executor) { + super(serverURI, executor); + } + + public void profile(final String token, RequestDelegate delegate) { + BaseResource resource; + try { + resource = new BaseResource(new URI(serverURI + "profile")); + } catch (URISyntaxException e) { + invokeHandleError(delegate, e); + return; + } + + resource.delegate = new ResourceDelegate(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(); + } +} diff --git a/mobile/android/base/browserid/JSONWebTokenUtils.java b/mobile/android/base/browserid/JSONWebTokenUtils.java index b9093bba8e4d..a236ecdf1202 100644 --- a/mobile/android/base/browserid/JSONWebTokenUtils.java +++ b/mobile/android/base/browserid/JSONWebTokenUtils.java @@ -186,8 +186,8 @@ public class JSONWebTokenUtils { 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) { @@ -244,8 +244,8 @@ public class JSONWebTokenUtils { 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) { diff --git a/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java b/mobile/android/base/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java similarity index 63% rename from mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java rename to mobile/android/base/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java index 14109498bb35..aa8db2d481d5 100644 --- a/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java +++ b/mobile/android/base/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java @@ -5,29 +5,23 @@ 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 { +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; @@ -93,44 +87,9 @@ public class BrowserIDRemoteVerifierClient implements BrowserIDVerifierClient { } } - 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) { + public AbstractBrowserIDRemoteVerifierClient(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 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); - } - } } diff --git a/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient10.java b/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient10.java new file mode 100644 index 000000000000..f61a82323fe9 --- /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 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); + } + } +} diff --git a/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient20.java b/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient20.java new file mode 100644 index 000000000000..9a1f786e0bc9 --- /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); + } + } +} diff --git a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java index bbb6fab8565f..ad7009611595 100644 --- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java +++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java @@ -4,6 +4,10 @@ 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; @@ -39,14 +43,6 @@ 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. *

@@ -58,15 +54,6 @@ public class FxAccountStatusFragment 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 @@ -542,9 +529,6 @@ public class FxAccountStatusFragment // 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); } diff --git a/mobile/android/base/sync/net/AbstractBearerTokenAuthHeaderProvider.java b/mobile/android/base/sync/net/AbstractBearerTokenAuthHeaderProvider.java new file mode 100644 index 000000000000..e3b4f25b1eca --- /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 AuthHeaderProvider 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); + } +} diff --git a/mobile/android/base/sync/net/BearerAuthHeaderProvider.java b/mobile/android/base/sync/net/BearerAuthHeaderProvider.java new file mode 100644 index 000000000000..d142d50d957d --- /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 AuthHeaderProvider that returns an Authorization header for + * Bearer tokens in the format expected by a Mozilla Firefox Accounts Profile Server. + *

+ * See https://github.com/mozilla/fxa-profile-server/blob/master/docs/API.md. + */ +public class BearerAuthHeaderProvider extends AbstractBearerTokenAuthHeaderProvider { + public BearerAuthHeaderProvider(String token) { + super(token); + } + + @Override + protected String getPrefix() { + return "Bearer"; + } +} diff --git a/mobile/android/base/sync/net/BrowserIDAuthHeaderProvider.java b/mobile/android/base/sync/net/BrowserIDAuthHeaderProvider.java index c5446e29bd16..5004673b3528 100644 --- a/mobile/android/base/sync/net/BrowserIDAuthHeaderProvider.java +++ b/mobile/android/base/sync/net/BrowserIDAuthHeaderProvider.java @@ -4,12 +4,6 @@ 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 AuthHeaderProvider that returns an Authorization header for * BrowserID assertions in the format expected by a Mozilla Services Token @@ -17,21 +11,13 @@ import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; *

* See http://docs.services.mozilla.com/token/apis.html. */ -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"; } }