From fe56bccd25dd5f6b7d258d4859e81602aa813f2d Mon Sep 17 00:00:00 2001 From: simonpoole Date: Sat, 6 Jan 2024 19:11:47 +0100 Subject: [PATCH] Support OAuth 2 This adds a minimal implementation of OAuth 2 as that works (likely only) with the current OpenStreetMap API. Fixes https://github.com/MarcusWolschon/osmeditor4android/issues/2401 --- src/main/java/de/blau/android/Authorize.java | 36 ++- .../de/blau/android/net/OAuth1aHelper.java | 207 ++++++++++++++ .../de/blau/android/net/OAuth2Helper.java | 218 +++++++++++++++ .../blau/android/net/OAuth2Interceptor.java | 29 ++ .../java/de/blau/android/net/OAuthHelper.java | 178 ++---------- src/main/java/de/blau/android/osm/Server.java | 48 ++-- .../android/prefs/AdvancedPrefDatabase.java | 3 +- .../android/prefs/VespucciURLActivity.java | 261 ++++++++---------- .../android/resources/KeyDatabaseHelper.java | 26 +- .../android/resources/KeyDatabaseTest.java | 2 +- 10 files changed, 657 insertions(+), 351 deletions(-) create mode 100644 src/main/java/de/blau/android/net/OAuth1aHelper.java create mode 100644 src/main/java/de/blau/android/net/OAuth2Helper.java create mode 100644 src/main/java/de/blau/android/net/OAuth2Interceptor.java diff --git a/src/main/java/de/blau/android/Authorize.java b/src/main/java/de/blau/android/Authorize.java index c4da7426bf..d671a62c75 100644 --- a/src/main/java/de/blau/android/Authorize.java +++ b/src/main/java/de/blau/android/Authorize.java @@ -18,8 +18,10 @@ import de.blau.android.contract.Schemes; import de.blau.android.dialogs.Progress; import de.blau.android.exception.OsmException; -import de.blau.android.net.OAuthHelper; +import de.blau.android.net.OAuth2Helper; +import de.blau.android.net.OAuth1aHelper; import de.blau.android.osm.Server; +import de.blau.android.prefs.API.Auth; import de.blau.android.prefs.Preferences; import de.blau.android.util.ActivityResultHandler; import de.blau.android.util.ScreenMessage; @@ -28,7 +30,7 @@ import oauth.signpost.exception.OAuthException; /** - * Perform OAuth authorisation of this app + * Perform OAuth 1/2 authorisation of this app * * @author simon * @@ -131,31 +133,35 @@ protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); Server server = prefs.getServer(); - String apiName = server.getApiName(); - OAuthHelper oa; - try { - oa = new OAuthHelper(this, apiName); - } catch (OsmException oe) { - server.setOAuth(false); // ups something went wrong turn oauth off - ScreenMessage.barError(this, getString(R.string.toast_no_oauth, apiName)); - return; - } - Log.d(DEBUG_TAG, "oauth auth for " + apiName); + Auth auth = server.getAuthentication(); + Log.d(DEBUG_TAG, "oauth auth for " + apiName + " " + auth); String authUrl = null; String errorMessage = null; try { - authUrl = oa.getRequestToken(); + if (auth == Auth.OAUTH1A) { + OAuth1aHelper oa = new OAuth1aHelper(this, apiName); + authUrl = oa.getRequestToken(); + } else if (auth == Auth.OAUTH2) { + OAuth2Helper oa = new OAuth2Helper(this, apiName); + authUrl = oa.getAuthorisationUrl(this); + } + } catch (OsmException oe) { + server.setOAuth(false); // ups something went wrong turn oauth off + errorMessage = getString(R.string.toast_no_oauth, apiName); } catch (OAuthException e) { - errorMessage = OAuthHelper.getErrorMessage(this, e); + errorMessage = OAuth1aHelper.getErrorMessage(this, e); } catch (ExecutionException e) { errorMessage = getString(R.string.toast_oauth_communication); } catch (TimeoutException e) { errorMessage = getString(R.string.toast_oauth_timeout); } if (authUrl == null) { - ScreenMessage.barError(this, errorMessage); + Log.e(DEBUG_TAG, "onCreate error " + errorMessage); + if (errorMessage != null) { + ScreenMessage.barError(this, errorMessage); + } return; } Log.d(DEBUG_TAG, "authURl " + authUrl); diff --git a/src/main/java/de/blau/android/net/OAuth1aHelper.java b/src/main/java/de/blau/android/net/OAuth1aHelper.java new file mode 100644 index 0000000000..003f6956ba --- /dev/null +++ b/src/main/java/de/blau/android/net/OAuth1aHelper.java @@ -0,0 +1,207 @@ +package de.blau.android.net; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import de.blau.android.App; +import de.blau.android.PostAsyncActionHandler; +import de.blau.android.exception.OsmException; +import de.blau.android.prefs.API.Auth; +import de.blau.android.resources.KeyDatabaseHelper; +import de.blau.android.util.ExecutorTask; +import oauth.signpost.OAuthConsumer; +import oauth.signpost.OAuthProvider; +import oauth.signpost.exception.OAuthCommunicationException; +import oauth.signpost.exception.OAuthException; +import oauth.signpost.exception.OAuthExpectationFailedException; +import oauth.signpost.exception.OAuthMessageSignerException; +import oauth.signpost.exception.OAuthNotAuthorizedException; +import se.akerfeldt.okhttp.signpost.OkHttpOAuthConsumer; +import se.akerfeldt.okhttp.signpost.OkHttpOAuthProvider; + +/** + * Helper class for signpost oAuth more or less based on text below + * + */ +public class OAuth1aHelper extends OAuthHelper { + private static final String DEBUG_TAG = OAuth1aHelper.class.getSimpleName(); + + private static final String CALLBACK_URL = "vespucci:/oauth/"; + private static final String AUTHORIZE_PATH = "oauth/authorize"; + private static final String ACCESS_TOKEN_PATH = "oauth/access_token"; + private static final String REQUEST_TOKEN_PATH = "oauth/request_token"; + + private static final String OAUTH_VERIFIER_PARAMTER = "oauth_verifier"; + private static final String OAUTH_TOKEN_PARAMETER = "oauth_token"; + + private static OAuthConsumer mConsumer; + private static OAuthProvider mProvider; + private static String mCallbackUrl; + + /** + * Construct a new helper instance + * + * @param context an Android Context + * @param apiName the base URL for the API instance + * + * @throws OsmException if no configuration could be found for the API instance + */ + public OAuth1aHelper(@NonNull Context context, @NonNull String apiName) throws OsmException { + try (KeyDatabaseHelper keyDatabase = new KeyDatabaseHelper(context)) { + OAuthConfiguration configuration = KeyDatabaseHelper.getOAuthConfiguration(keyDatabase.getReadableDatabase(), apiName, Auth.OAUTH1A); + if (configuration != null) { + init(configuration.getKey(), configuration.getSecret(), configuration.getOauthUrl()); + return; + } + logMissingApi(apiName); + throw new OsmException("No matching OAuth configuration found for API " + apiName); + } + } + + /** + * Initialize the fields + * + * @param key OAuth 1a key + * @param secret OAuth 1a secret + * @param oauthUrl URL to use for authorization + */ + private static void init(String key, String secret, String oauthUrl) { + mConsumer = new OkHttpOAuthConsumer(key, secret); + mProvider = new OkHttpOAuthProvider(oauthUrl + REQUEST_TOKEN_PATH, oauthUrl + ACCESS_TOKEN_PATH, oauthUrl + AUTHORIZE_PATH, App.getHttpClient()); + mProvider.setOAuth10a(true); + mCallbackUrl = CALLBACK_URL; + } + + /** + * this constructor is for access to the singletons + */ + public OAuth1aHelper() { + } + + /** + * Returns an OAuthConsumer initialized with the consumer keys for the API in question + * + * @param context an Android Context + * @param apiName the name of the API configuration + * + * @return an initialized OAuthConsumer or null if something blows up + */ + @Nullable + public OkHttpOAuthConsumer getOkHttpConsumer(Context context, @NonNull String apiName) { + try (KeyDatabaseHelper keyDatabase = new KeyDatabaseHelper(context)) { + OAuthConfiguration configuration = KeyDatabaseHelper.getOAuthConfiguration(keyDatabase.getReadableDatabase(), apiName, Auth.OAUTH1A); + if (configuration != null) { + return new OkHttpOAuthConsumer(configuration.getKey(), configuration.getSecret()); + } + logMissingApi(apiName); + return null; + } + } + + /** + * Get the request token + * + * @return the token or null + * @throws OAuthException if an error happened during the OAuth handshake + * @throws TimeoutException if we waited too long for a response + * @throws ExecutionException + */ + public String getRequestToken() throws OAuthException, TimeoutException, ExecutionException { + Log.d(DEBUG_TAG, "getRequestToken"); + class RequestTokenTask extends ExecutorTask { + private OAuthException ex = null; + + /** + * Create a new RequestTokenTask + * + * @param executorService ExecutorService to run this on + * @param handler an Handler + */ + RequestTokenTask() { + super(); + } + + @Override + protected String doInBackground(Void param) { + try { + return mProvider.retrieveRequestToken(mConsumer, mCallbackUrl); + } catch (OAuthException e) { + Log.d(DEBUG_TAG, "getRequestToken " + e); + ex = e; + } + return null; + } + + /** + * Get the any OAuthException that was thrown + * + * @return the exception + */ + OAuthException getException() { + return ex; + } + } + + RequestTokenTask requester = new RequestTokenTask(); + requester.execute(); + String result = null; + try { + result = requester.get(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { // NOSONAR cancel does interrupt the thread in question + requester.cancel(); + throw new TimeoutException(e.getMessage()); + } + if (result == null) { + OAuthException ex = requester.getException(); + if (ex != null) { + throw ex; + } + } + return result; + } + + @Override + protected ExecutorTask getAccessTokenTask(Context context, Uri data, PostAsyncActionHandler handler) { + String oauthToken = data.getQueryParameter(OAUTH_TOKEN_PARAMETER); + final String oauthVerifier = data.getQueryParameter(OAUTH_VERIFIER_PARAMTER); + + if ((oauthToken == null) && (oauthVerifier == null)) { + Log.i(DEBUG_TAG, "got oauth verifier " + oauthToken + " " + oauthVerifier); + throw new IllegalArgumentException("No token or verifier"); + } + return new ExecutorTask() { + + @Override + protected Boolean doInBackground(Void arg) + throws OAuthMessageSignerException, OAuthNotAuthorizedException, OAuthExpectationFailedException, OAuthCommunicationException { + if (mProvider == null || mConsumer == null) { + throw new OAuthExpectationFailedException("OAuthHelper not initialized!"); + } + mProvider.retrieveAccessToken(mConsumer, oauthVerifier); + setAccessToken(context, mConsumer.getToken(), mConsumer.getTokenSecret()); + return true; + } + + @Override + protected void onBackgroundError(Exception e) { + handler.onError(null); + } + + @Override + protected void onPostExecute(Boolean success) { + Log.d(DEBUG_TAG, "oAuthHandshake onPostExecute"); + if (success != null && success) { + handler.onSuccess(); + } else { + handler.onError(null); + } + } + }; + } +} diff --git a/src/main/java/de/blau/android/net/OAuth2Helper.java b/src/main/java/de/blau/android/net/OAuth2Helper.java new file mode 100644 index 0000000000..76bfbcc0ad --- /dev/null +++ b/src/main/java/de/blau/android/net/OAuth2Helper.java @@ -0,0 +1,218 @@ +package de.blau.android.net; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +import android.content.Context; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; +import androidx.annotation.NonNull; +import de.blau.android.App; +import de.blau.android.AsyncResult; +import de.blau.android.PostAsyncActionHandler; +import de.blau.android.exception.OsmException; +import de.blau.android.osm.OsmXml; +import de.blau.android.prefs.API; +import de.blau.android.prefs.API.Auth; +import de.blau.android.prefs.AdvancedPrefDatabase; +import de.blau.android.resources.KeyDatabaseHelper; +import de.blau.android.util.ExecutorTask; +import okhttp3.FormBody; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * Helper class for OAuth2 + * + * This implements the authentication as of section 4.1 of RFC 6749 and a PKCE challenge as per RFC 7636 as currently + * implemented by the OSM API and nothing more. + * + */ +public class OAuth2Helper extends OAuthHelper { + + private static final String DEBUG_TAG = OAuth2Helper.class.getSimpleName(); + + public static final String REDIRECT_URI = "vespucci:/oauth2/"; + private static final String AUTHORIZE_PATH = "oauth2/authorize"; + public static final String ACCESS_TOKEN_PATH = "oauth2/token"; + + public static final String STATE_PARAM = "state"; + private static final String REDIRECT_URI_PARAM = "redirect_uri"; + private static final String SCOPE_PARAM = "scope"; + private static final String CLIENT_ID_PARAM = "client_id"; + private static final String RESPONSE_TYPE_PARAM = "response_type"; + private static final String GRANT_TYPE_PARAM = "grant_type"; + private static final String AUTHORIZATION_CODE_VALUE = "authorization_code"; + private static final String CODE_PARAM = "code"; + private static final String CODE_CHALLENGE_PARAM = "code_challenge"; + private static final String CODE_CHALLENGE_METHOD_PARAM = "code_challenge_method"; + private static final String METHOD_SHA_256_VALUE = "S256"; + private static final String CODE_VERIFIER_PARAM = "code_verifier"; + private static final String ERROR_DESCRIPTION_PARAM = "error_description"; + private static final String ERROR_PARAM = "error"; + + private static final List SCOPES = Arrays.asList("read_prefs", "write_prefs", "write_api", "read_gpx", "write_gpx", "write_notes"); + + private static final char[] PKCE_CHARS = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', + 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.', '_', '~' }; + + private static final String ACCESS_TOKEN_FIELD = "access_token"; + + private String apiName; + + private OAuthConfiguration configuration; + + /** + * Construct a new helper instance + * + * @param context an Android Context + * @param apiName the API name + * + * @throws OsmException if no configuration could be found for the API instance + */ + public OAuth2Helper(@NonNull Context context, @NonNull String apiName) throws OsmException { + try (KeyDatabaseHelper keyDatabase = new KeyDatabaseHelper(context)) { + configuration = KeyDatabaseHelper.getOAuthConfiguration(keyDatabase.getReadableDatabase(), apiName, Auth.OAUTH2); + if (configuration != null) { + this.apiName = apiName; + return; + } + logMissingApi(apiName); + } catch (IllegalArgumentException e) { + Log.e(DEBUG_TAG, e.getMessage()); + } + throw new OsmException("No matching OAuth 2 configuration found for API " + apiName); + } + + /** + * Generate a SHA-256 hash of the challenge String and Base64 encode it + * + * See https://datatracker.ietf.org/doc/html/rfc8414 + * + * @param challenge the challenge string + * @return a Base64 encoded String of the hash + * @throws NoSuchAlgorithmException + */ + @NonNull + private String hashAndEncodeChallenge(@NonNull String challenge) throws NoSuchAlgorithmException { + return Base64.encodeToString(MessageDigest.getInstance("SHA-256").digest(challenge.getBytes(Charset.forName("US-ASCII"))), + Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); + } + + /** + * Get a String of random chars of length length + * + * @param length the number of chars in the String + * @return a String of random chars of length length + */ + @NonNull + private String createCodeVerifier(int length) { + Random r = App.getRandom(); + StringBuilder result = new StringBuilder(); + for (int i = 0; i < length; i++) { + result.append(PKCE_CHARS[r.nextInt(PKCE_CHARS.length)]); + } + return result.toString(); + } + + /** + * Get the a authorisation url + * + * @param context an Android Context + * @return the authorisation Url as a String + * @throws OsmException for configuration errors + */ + @NonNull + public String getAuthorisationUrl(Context context) throws OsmException { + String codeVerifier = createCodeVerifier(128); + setAccessToken(context, null, codeVerifier); // the PKCE challenge requires state, so we store it here + try { + URL base = new URL(configuration.getOauthUrl()); + return new HttpUrl.Builder().scheme(base.getProtocol()).host(base.getHost()).addPathSegments(AUTHORIZE_PATH) + .addQueryParameter(RESPONSE_TYPE_PARAM, CODE_PARAM).addQueryParameter(CLIENT_ID_PARAM, configuration.getKey()) + .addQueryParameter(SCOPE_PARAM, TextUtils.join(" ", SCOPES)).addQueryParameter(REDIRECT_URI_PARAM, REDIRECT_URI) + .addQueryParameter(STATE_PARAM, apiName).addQueryParameter(CODE_CHALLENGE_METHOD_PARAM, METHOD_SHA_256_VALUE) + .addQueryParameter(CODE_CHALLENGE_PARAM, hashAndEncodeChallenge(codeVerifier)).build().url().toString(); + } catch (MalformedURLException | NoSuchAlgorithmException e) { + throw new OsmException("Configuration error " + e.getMessage()); + } + } + + @Override + ExecutorTask getAccessTokenTask(Context context, Uri data, PostAsyncActionHandler handler) { + String error = data.getQueryParameter(ERROR_PARAM); + if (error != null) { + String description = data.getQueryParameter(ERROR_DESCRIPTION_PARAM); + throw new IllegalArgumentException(description == null ? error : description); + } + return new ExecutorTask() { + @Override + protected Response doInBackground(Void param) throws IOException { + try (AdvancedPrefDatabase prefDb = new AdvancedPrefDatabase(context)) { + API api = prefDb.getCurrentAPI(); + String code = data.getQueryParameter(CODE_PARAM); + RequestBody requestBody = new FormBody.Builder().add(CODE_PARAM, code).add(GRANT_TYPE_PARAM, AUTHORIZATION_CODE_VALUE) + .add(REDIRECT_URI_PARAM, OAuth2Helper.REDIRECT_URI).add(CLIENT_ID_PARAM, configuration.getKey()) + .add(CODE_VERIFIER_PARAM, api.accesstokensecret).build(); + URL base = new URL(configuration.getOauthUrl()); + URL accessTokenUrl = new HttpUrl.Builder().scheme(base.getProtocol()).host(base.getHost()).addPathSegments(OAuth2Helper.ACCESS_TOKEN_PATH) + .build().url(); + Request request = new Request.Builder().url(accessTokenUrl).post(requestBody).build(); + + OkHttpClient.Builder builder = App.getHttpClient().newBuilder().connectTimeout(TIMEOUT, TimeUnit.SECONDS).readTimeout(TIMEOUT, + TimeUnit.SECONDS); + return builder.build().newCall(request).execute(); + } + } + + @Override + protected void onBackgroundError(Exception e) { + handler.onError(new AsyncResult(0, e.getMessage())); + } + + @Override + protected void onPostExecute(Response result) { + Log.d(DEBUG_TAG, "oAuthHandshake onPostExecute"); + if (result.isSuccessful()) { + try (BufferedReader rd = new BufferedReader(new InputStreamReader(result.body().byteStream(), Charset.forName(OsmXml.UTF_8)))) { + JsonElement root = JsonParser.parseReader(rd); + if (root.isJsonObject()) { + JsonElement accessToken = ((JsonObject) root).get(ACCESS_TOKEN_FIELD); + if (accessToken instanceof JsonElement) { + setAccessToken(context, accessToken.getAsString(), null); + } + } + handler.onSuccess(); + } catch (IOException | JsonSyntaxException e) { + Log.e(DEBUG_TAG, "Opening " + e.getMessage()); + handler.onError(new AsyncResult(0, e.getMessage())); + } + return; + } + Log.e(DEBUG_TAG, "Handshake fail " + result.code() + " " + result.message()); + handler.onError(new AsyncResult(result.code(), result.message())); + } + }; + } +} diff --git a/src/main/java/de/blau/android/net/OAuth2Interceptor.java b/src/main/java/de/blau/android/net/OAuth2Interceptor.java new file mode 100644 index 0000000000..6f9ba89633 --- /dev/null +++ b/src/main/java/de/blau/android/net/OAuth2Interceptor.java @@ -0,0 +1,29 @@ +package de.blau.android.net; + +import java.io.IOException; + +import androidx.annotation.NonNull; +import okhttp3.Interceptor; +import okhttp3.Response; + +public class OAuth2Interceptor implements Interceptor { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER = "Bearer "; + + private final String accessToken; + + /** + * Create an new interceptor that adds an authorization header + * + * @param accessToken the value to use for the header + */ + public OAuth2Interceptor(@NonNull String accessToken) { + this.accessToken = accessToken; + } + + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request().newBuilder().header(AUTHORIZATION_HEADER, BEARER + accessToken).build()); + } +} diff --git a/src/main/java/de/blau/android/net/OAuthHelper.java b/src/main/java/de/blau/android/net/OAuthHelper.java index 64cd859181..e25998df2f 100644 --- a/src/main/java/de/blau/android/net/OAuthHelper.java +++ b/src/main/java/de/blau/android/net/OAuthHelper.java @@ -5,39 +5,24 @@ import java.util.concurrent.TimeoutException; import android.content.Context; +import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import de.blau.android.App; +import de.blau.android.PostAsyncActionHandler; import de.blau.android.R; -import de.blau.android.exception.OsmException; -import de.blau.android.resources.KeyDatabaseHelper; +import de.blau.android.prefs.AdvancedPrefDatabase; import de.blau.android.util.ExecutorTask; -import oauth.signpost.OAuthConsumer; -import oauth.signpost.OAuthProvider; -import oauth.signpost.exception.OAuthCommunicationException; import oauth.signpost.exception.OAuthException; -import oauth.signpost.exception.OAuthExpectationFailedException; -import oauth.signpost.exception.OAuthMessageSignerException; -import oauth.signpost.exception.OAuthNotAuthorizedException; -import se.akerfeldt.okhttp.signpost.OkHttpOAuthConsumer; -import se.akerfeldt.okhttp.signpost.OkHttpOAuthProvider; /** - * Helper class for signpost oAuth more or less based on text below + * Helper class for oAuth implementations * */ -public class OAuthHelper { - private static final String DEBUG_TAG = "OAuthHelper"; +public abstract class OAuthHelper { + private static final String DEBUG_TAG = OAuthHelper.class.getSimpleName(); - private static final String CALLBACK_URL = "vespucci:/oauth/"; - private static final String AUTHORIZE_PATH = "oauth/authorize"; - private static final String ACCESS_TOKEN_PATH = "oauth/access_token"; - private static final String REQUEST_TOKEN_PATH = "oauth/request_token"; - - private static OAuthConsumer mConsumer; - private static OAuthProvider mProvider; - private static String mCallbackUrl; + protected static final int TIMEOUT = 10; public static class OAuthConfiguration { private String key; @@ -91,154 +76,45 @@ public String getOauthUrl() { } /** - * Construct a new helper instance - * - * @param context an Android Context - * @param apiName the base URL for the API instance - * - * @throws OsmException if no configuration could be found for the API instance - */ - public OAuthHelper(@NonNull Context context, @NonNull String apiName) throws OsmException { - try (KeyDatabaseHelper keyDatabase = new KeyDatabaseHelper(context)) { - OAuthConfiguration configuration = KeyDatabaseHelper.getOAuthConfiguration(keyDatabase.getReadableDatabase(), apiName); - if (configuration != null) { - init(configuration.getKey(), configuration.getSecret(), configuration.getOauthUrl()); - return; - } - logMissingApi(apiName); - throw new OsmException("No matching OAuth configuration found for API " + apiName); - } - } - - /** - * Initialize the fields + * Create a log message for an unmatched api * - * @param key OAuth 1a key - * @param secret OAuth 1a secret - * @param oauthUrl URL to use for authorization + * @param apiName the api url */ - private static void init(String key, String secret, String oauthUrl) { - mConsumer = new OkHttpOAuthConsumer(key, secret); - mProvider = new OkHttpOAuthProvider(oauthUrl + REQUEST_TOKEN_PATH, oauthUrl + ACCESS_TOKEN_PATH, oauthUrl + AUTHORIZE_PATH, App.getHttpClient()); - mProvider.setOAuth10a(true); - mCallbackUrl = CALLBACK_URL; + protected void logMissingApi(@Nullable String apiName) { + Log.d(DEBUG_TAG, "No matching API for " + apiName + "found"); } - /** - * this constructor is for access to the singletons - */ - public OAuthHelper() { - } + abstract ExecutorTask getAccessTokenTask(@NonNull Context context, @NonNull Uri data, @NonNull PostAsyncActionHandler handler); /** - * Returns an OAuthConsumer initialized with the consumer keys for the API in question - * - * @param context an Android Context - * @param apiName the name of the API configuration - * - * @return an initialized OAuthConsumer or null if something blows up + * @param context + * @param accessToken */ - @Nullable - public OkHttpOAuthConsumer getOkHttpConsumer(Context context, @NonNull String apiName) { - try (KeyDatabaseHelper keyDatabase = new KeyDatabaseHelper(context)) { - OAuthConfiguration configuration = KeyDatabaseHelper.getOAuthConfiguration(keyDatabase.getReadableDatabase(), apiName); - if (configuration != null) { - return new OkHttpOAuthConsumer(configuration.getKey(), configuration.getSecret()); - } - logMissingApi(apiName); - return null; + protected void setAccessToken(final Context context, String accessToken, String secret) { + try (AdvancedPrefDatabase prefDb = new AdvancedPrefDatabase(context)) { + prefDb.setAPIAccessToken(accessToken, secret); } } /** - * Create a log message for an unmatched api - * - * @param apiName the api url - */ - private void logMissingApi(@Nullable String apiName) { - Log.d(DEBUG_TAG, "No matching API for " + apiName + "found"); - } - - /** - * Get the request token + * Run a task to retrieve and set in the configuration the access token for the authentication method * - * @return the token or null - * @throws OAuthException if an error happened during the OAuth handshake - * @throws TimeoutException if we waited too long for a response - * @throws ExecutionException + * @param context + * @param data + * @param handler + * @throws TimeoutException if the task timeouts + * @throws ExecutionException if the Task couldn't be exceuted */ - public String getRequestToken() throws OAuthException, TimeoutException, ExecutionException { - Log.d(DEBUG_TAG, "getRequestToken"); - class RequestTokenTask extends ExecutorTask { - private OAuthException ex = null; - - /** - * Create a new RequestTokenTask - * - * @param executorService ExecutorService to run this on - * @param handler an Handler - */ - RequestTokenTask() { - super(); - } - - @Override - protected String doInBackground(Void param) { - try { - return mProvider.retrieveRequestToken(mConsumer, mCallbackUrl); - } catch (OAuthException e) { - Log.d(DEBUG_TAG, "getRequestToken " + e); - ex = e; - } - return null; - } - - /** - * Get the any OAuthException that was thrown - * - * @return the exception - */ - OAuthException getException() { - return ex; - } - } - - RequestTokenTask requester = new RequestTokenTask(); + public void getAccessToken(@NonNull final Context context, @NonNull Uri data, @NonNull PostAsyncActionHandler handler) + throws TimeoutException, ExecutionException { + ExecutorTask requester = getAccessTokenTask(context, data, handler); requester.execute(); - String result = null; try { - result = requester.get(10, TimeUnit.SECONDS); + requester.get(TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { // NOSONAR cancel does interrupt the thread in question requester.cancel(); throw new TimeoutException(e.getMessage()); } - if (result == null) { - OAuthException ex = requester.getException(); - if (ex != null) { - throw ex; - } - } - return result; - } - - /** - * Queries the service provider for an access token. - * - * @param verifier OAuth 1.0a verification code - * @return the access token - * @throws OAuthMessageSignerException - * @throws OAuthNotAuthorizedException - * @throws OAuthExpectationFailedException - * @throws OAuthCommunicationException - */ - public String[] getAccessToken(String verifier) - throws OAuthMessageSignerException, OAuthNotAuthorizedException, OAuthExpectationFailedException, OAuthCommunicationException { - Log.d(DEBUG_TAG, "verifier: " + verifier); - if (mProvider == null || mConsumer == null) { - throw new OAuthExpectationFailedException("OAuthHelper not initialized!"); - } - mProvider.retrieveAccessToken(mConsumer, verifier); - return new String[] { mConsumer.getToken(), mConsumer.getTokenSecret() }; } /** diff --git a/src/main/java/de/blau/android/osm/Server.java b/src/main/java/de/blau/android/osm/Server.java index 7c349dc1b1..1b67062d8f 100644 --- a/src/main/java/de/blau/android/osm/Server.java +++ b/src/main/java/de/blau/android/osm/Server.java @@ -47,7 +47,8 @@ import de.blau.android.exception.OsmException; import de.blau.android.exception.OsmIOException; import de.blau.android.exception.OsmServerException; -import de.blau.android.net.OAuthHelper; +import de.blau.android.net.OAuth2Interceptor; +import de.blau.android.net.OAuth1aHelper; import de.blau.android.prefs.API; import de.blau.android.prefs.API.Auth; import de.blau.android.services.util.MBTileProviderDataBase; @@ -132,10 +133,10 @@ public class Server { /** * use oauth */ - private Auth auth; + private Auth authentication; /** - * oauth access token + * oauth 1 and 2 access token */ private final String accesstoken; @@ -203,13 +204,13 @@ public Server(@NonNull Context context, @NonNull final API api, @NonNull final S this.notesURL = api.notesurl; this.password = api.pass; this.username = api.user; - this.auth = api.auth; + this.authentication = api.auth; this.generator = generator; this.accesstoken = api.accesstoken; this.accesstokensecret = api.accesstokensecret; - if (auth == Auth.OAUTH1A) { - oAuthConsumer = new OAuthHelper().getOkHttpConsumer(context, name); + if (authentication == Auth.OAUTH1A) { + oAuthConsumer = new OAuth1aHelper().getOkHttpConsumer(context, name); if (oAuthConsumer != null) { oAuthConsumer.setTokenWithSecret(accesstoken, accesstokensecret); } @@ -671,14 +672,6 @@ public static InputStream openConnection(@Nullable final Context context, @NonNu Request request = new Request.Builder().url(url).build(); OkHttpClient.Builder builder = App.getHttpClient().newBuilder().connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).readTimeout(readTimeout, TimeUnit.MILLISECONDS); - // if (oauth) { - // OAuthHelper oa = new OAuthHelper(); - // OkHttpOAuthConsumer consumer = oa.getOkHttpConsumer(getBaseUrl(getReadOnlyUrl())); - // if (consumer != null) { - // consumer.setTokenWithSecret(accesstoken, accesstokensecret); - // builder.addInterceptor(new SigningInterceptor(consumer)); - // } - // } OkHttpClient client = builder.build(); Call readCall = client.newCall(request); Response readCallResponse = readCall.execute(); @@ -752,7 +745,7 @@ public void toXml(XmlSerializer serializer, Long changeSetId) throws IllegalArgu * @return true if either oauth is set or we have login information */ public boolean isLoginSet() { - return (username != null && (password != null && !"".equals(username) && !"".equals(password))) || auth != Auth.BASIC; + return (username != null && (password != null && !"".equals(username) && !"".equals(password))) || authentication != Auth.BASIC; } /** @@ -789,7 +782,7 @@ private void sendPayload(@NonNull final OutputStream outputStream, @NonNull fina @NonNull Response openConnectionForAuthenticatedAccess(@NonNull final URL url, @NonNull final String requestMethod, @Nullable final RequestBody body) throws IOException { - Log.d(DEBUG_TAG, "openConnectionForWriteAccess url " + url); + Log.d(DEBUG_TAG, "openConnectionForWriteAccess url " + url + " authentication " + authentication); Request.Builder requestBuilder = new Request.Builder().url(url); if (body != null) { @@ -815,20 +808,18 @@ Response openConnectionForAuthenticatedAccess(@NonNull final URL url, @NonNull f OkHttpClient.Builder builder = App.getHttpClient().newBuilder().connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS).readTimeout(TIMEOUT, TimeUnit.MILLISECONDS); - switch (auth) { + switch (authentication) { case OAUTH1A: builder.addInterceptor(new SigningInterceptor(oAuthConsumer)); break; case OAUTH2: + builder.addInterceptor(new OAuth2Interceptor(accesstoken)); + break; case BASIC: builder.addInterceptor(new BasicAuthInterceptor(username, password)); } - OkHttpClient client = builder.build(); - - Call call = client.newCall(request); - - return call.execute(); + return builder.build().newCall(request).execute(); } /** @@ -1844,7 +1835,7 @@ private void parseBug(@NonNull Note bug, @NonNull InputStream inputStream) throw * @return true if we are using OAuth but have not retrieved the accesstoken yet */ public boolean needOAuthHandshake() { - return auth == Auth.OAUTH1A && ((accesstoken == null) || (accesstokensecret == null)); + return authentication == Auth.OAUTH1A && ((accesstoken == null) || (accesstokensecret == null)); } /** @@ -1853,7 +1844,7 @@ public boolean needOAuthHandshake() { * @param t the value to set the flag to */ public void setOAuth(boolean t) { - auth = t ? Auth.OAUTH1A : Auth.BASIC; + authentication = t ? Auth.OAUTH1A : Auth.BASIC; } /** @@ -1861,7 +1852,7 @@ public void setOAuth(boolean t) { * @return true if oauth is enabled */ public boolean getOAuth() { - return auth == Auth.OAUTH1A; + return authentication == Auth.OAUTH1A || authentication == Auth.OAUTH2; } /** @@ -1980,4 +1971,11 @@ public void closeMapSplitSource() { public String getApiName() { return name; } + + /** + * @return the auth + */ + public Auth getAuthentication() { + return authentication; + } } diff --git a/src/main/java/de/blau/android/prefs/AdvancedPrefDatabase.java b/src/main/java/de/blau/android/prefs/AdvancedPrefDatabase.java index ecc14ea557..4a6b306772 100644 --- a/src/main/java/de/blau/android/prefs/AdvancedPrefDatabase.java +++ b/src/main/java/de/blau/android/prefs/AdvancedPrefDatabase.java @@ -40,7 +40,7 @@ * @author Jan * @author Simon Poole */ -public class AdvancedPrefDatabase extends SQLiteOpenHelper { +public class AdvancedPrefDatabase extends SQLiteOpenHelper implements AutoCloseable { private static final String DEBUG_TAG = "AdvancedPrefDB"; @@ -381,7 +381,6 @@ public synchronized void setAPIAccessToken(@Nullable String token, @Nullable Str values.put(ACCESSTOKEN_COL, token); values.put(ACCESSTOKENSECRET_COL, secret); db.update(APIS_TABLE, values, WHERE_ID, new String[] { currentAPI }); - Log.d(DEBUG_TAG, "setAPIAccessToken " + token + " secret " + secret); db.close(); resetCurrentServer(); } diff --git a/src/main/java/de/blau/android/prefs/VespucciURLActivity.java b/src/main/java/de/blau/android/prefs/VespucciURLActivity.java index 8d59dc49e4..d83dc09603 100644 --- a/src/main/java/de/blau/android/prefs/VespucciURLActivity.java +++ b/src/main/java/de/blau/android/prefs/VespucciURLActivity.java @@ -1,7 +1,6 @@ package de.blau.android.prefs; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import android.content.Intent; @@ -18,23 +17,21 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import de.blau.android.AsyncResult; import de.blau.android.Authorize; +import de.blau.android.PostAsyncActionHandler; import de.blau.android.R; +import de.blau.android.exception.OsmException; import de.blau.android.net.OAuthHelper; +import de.blau.android.net.OAuth2Helper; +import de.blau.android.net.OAuth1aHelper; import de.blau.android.prefs.AdvancedPrefDatabase.PresetInfo; -import de.blau.android.util.ACRAHelper; -import de.blau.android.util.ExecutorTask; import de.blau.android.util.ScreenMessage; -import oauth.signpost.exception.OAuthException; /** - * Will process vespucci:// URLs. Accepts the following URL parameters:
- * - * preseturl - preset URL to add to the preset list
- * presetname - name for the preset (if it gets added)
- * - * oauth_token = oauth token, used during retrieving oauth access tokens
- * oauth_verifier - oauth verifier, used during retrieving oauth access tokens
+ * Will process vespucci:// URLs.
+ * + * Handles "preset", "oauth" and "oauth2" paths. * * @author Jan * @author Simon @@ -43,22 +40,38 @@ public class VespucciURLActivity extends AppCompatActivity implements OnClickListener { private static final String DEBUG_TAG = VespucciURLActivity.class.getSimpleName(); - private static final int REQUEST_PRESETEDIT = 0; - private static final String OAUTH_VERIFIER_PARAMTER = "oauth_verifier"; - private static final String OAUTH_TOKEN_PARAMETER = "oauth_token"; - public static final String PRESET_PATH = "preset"; // we don't actuall check for this current - public static final String PRESETNAME_PARAMETER = "presetname"; - public static final String PRESETURL_PARAMETER = "preseturl"; - - private String preseturl; - private String presetname; - private String oauthToken; - private String oauthVerifier; + private static final int REQUEST_PRESETEDIT = 0; + private static final String OAUTH1A_PATH = "oauth"; + private static final String OAUTH2_PATH = "oauth2"; + public static final String PRESET_PATH = "preset"; + public static final String PRESETNAME_PARAMETER = "presetname"; + public static final String PRESETURL_PARAMETER = "preseturl"; + + private String preseturl; + private String presetname; + private AdvancedPrefDatabase prefdb; private boolean downloadSucessful = false; private View mainView; + private PostAsyncActionHandler oauthResultHandler = new PostAsyncActionHandler() { + + @Override + public void onSuccess() { + Intent intent = new Intent(VespucciURLActivity.this, Authorize.class); + intent.setAction(Authorize.ACTION_FINISH_OAUTH); + startActivity(intent); + } + + @Override + public void onError(AsyncResult result) { + ScreenMessage.toastTopError(VespucciURLActivity.this, getString(R.string.toast_oauth_handshake_failed, result.getMessage())); + onSuccess(); + } + + }; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { Preferences prefs = new Preferences(this); @@ -74,79 +87,96 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } @Override - protected void onStart() { + protected void onResume() { Uri data = getIntent().getData(); - if (data != null) { - try { - preseturl = data.getQueryParameter(PRESETURL_PARAMETER); - presetname = data.getQueryParameter(PRESETNAME_PARAMETER); - oauthToken = data.getQueryParameter(OAUTH_TOKEN_PARAMETER); - oauthVerifier = data.getQueryParameter(OAUTH_VERIFIER_PARAMTER); - } catch (Exception ex) { - Log.e(DEBUG_TAG, "Uri " + data + " caused " + ex); - ACRAHelper.nocrashReport(ex, ex.getMessage()); - finish(); - } - } else { - Log.e(DEBUG_TAG, "Received null Uri, ignoring"); + if (data == null) { + Log.i(DEBUG_TAG, "onResume intent without URI"); + super.onResume(); + finish(); + return; } - super.onStart(); - } - - @Override - protected void onResume() { - Log.i(DEBUG_TAG, "onResume"); - // determining what activity to do based purely on the parameters is rather hackish - if ((oauthToken != null) && (oauthVerifier != null)) { + String path = stripPathSeperators(data.getPath()); + Log.i(DEBUG_TAG, "onResume " + path); + switch (path) { + case OAUTH1A_PATH: + case OAUTH2_PATH: // NOSONAR mainView.setVisibility(View.GONE); - Log.i(DEBUG_TAG, "got oauth verifier " + oauthToken + " " + oauthVerifier); - String errorMessage = null; + final String apiName = data.getQueryParameter(OAuth2Helper.STATE_PARAM); try { - oAuthHandshake(oauthVerifier); - } catch (OAuthException e) { - errorMessage = OAuthHelper.getErrorMessage(this, e); + OAuthHelper oa = OAUTH1A_PATH.equals(path) ? new OAuth1aHelper() : new OAuth2Helper(getBaseContext(), apiName); + oa.getAccessToken(getBaseContext(), data, oauthResultHandler); } catch (ExecutionException e) { - errorMessage = getString(R.string.toast_oauth_communication); + ScreenMessage.toastTopError(this, getString(R.string.toast_oauth_communication)); } catch (TimeoutException e) { - errorMessage = getString(R.string.toast_oauth_timeout); - } - if (errorMessage != null) { - ScreenMessage.toastTopError(this, errorMessage); + ScreenMessage.toastTopError(this, getString(R.string.toast_oauth_timeout)); + } catch (OsmException e) { + ScreenMessage.toastTopError(this, getString(R.string.toast_no_oauth, apiName)); + } catch (IllegalArgumentException e) { + ScreenMessage.toastTopError(this, getString(R.string.toast_oauth_handshake_failed, e.getMessage())); } + default: // NOSONAR fall through is intentional setResult(RESULT_OK); finish(); - } else { - mainView.findViewById(R.id.urldialog_nodata).setVisibility(preseturl == null ? View.VISIBLE : View.GONE); - - if (preseturl != null) { - ActionBar actionbar = getSupportActionBar(); - if (actionbar != null) { - actionbar.setDisplayShowHomeEnabled(true); - actionbar.setDisplayHomeAsUpEnabled(true); - actionbar.setTitle(R.string.preset_download_title); - actionbar.setDisplayShowTitleEnabled(true); - actionbar.show(); - } - mainView.findViewById(R.id.urldialog_layoutPreset).setVisibility(View.VISIBLE); - - ((TextView) mainView.findViewById(R.id.urldialog_textPresetName)).setText(presetname); - ((TextView) mainView.findViewById(R.id.urldialog_textPresetURL)).setText(preseturl); - PresetInfo existingPreset = prefdb.getPresetByURL(preseturl); - if (downloadSucessful) { - mainView.findViewById(R.id.urldialog_textPresetSuccessful).setVisibility(View.VISIBLE); - mainView.findViewById(R.id.urldialog_textPresetExists).setVisibility(View.GONE); - } else { - mainView.findViewById(R.id.urldialog_textPresetExists).setVisibility(existingPreset != null ? View.VISIBLE : View.GONE); - mainView.findViewById(R.id.urldialog_textPresetSuccessful).setVisibility(View.GONE); - } - mainView.findViewById(R.id.urldialog_checkboxEnable).setVisibility(existingPreset == null ? View.VISIBLE : View.GONE); - mainView.findViewById(R.id.urldialog_buttonAddPreset).setVisibility(existingPreset == null ? View.VISIBLE : View.GONE); - ((Button) mainView.findViewById(R.id.urldialog_buttonAddPreset)).setOnClickListener(this); - } + break; + case PRESET_PATH: + setupPresetUi(data); + break; } super.onResume(); } + /** + * Show the preset download UI + * + * @param data the Uri to use + */ + private void setupPresetUi(@NonNull Uri data) { + mainView.findViewById(R.id.urldialog_nodata).setVisibility(preseturl == null ? View.VISIBLE : View.GONE); + preseturl = data.getQueryParameter(PRESETURL_PARAMETER); + presetname = data.getQueryParameter(PRESETNAME_PARAMETER); + if (preseturl != null) { + ActionBar actionbar = getSupportActionBar(); + if (actionbar != null) { + actionbar.setDisplayShowHomeEnabled(true); + actionbar.setDisplayHomeAsUpEnabled(true); + actionbar.setTitle(R.string.preset_download_title); + actionbar.setDisplayShowTitleEnabled(true); + actionbar.show(); + } + mainView.findViewById(R.id.urldialog_layoutPreset).setVisibility(View.VISIBLE); + + ((TextView) mainView.findViewById(R.id.urldialog_textPresetName)).setText(presetname); + ((TextView) mainView.findViewById(R.id.urldialog_textPresetURL)).setText(preseturl); + PresetInfo existingPreset = prefdb.getPresetByURL(preseturl); + if (downloadSucessful) { + mainView.findViewById(R.id.urldialog_textPresetSuccessful).setVisibility(View.VISIBLE); + mainView.findViewById(R.id.urldialog_textPresetExists).setVisibility(View.GONE); + } else { + mainView.findViewById(R.id.urldialog_textPresetExists).setVisibility(existingPreset != null ? View.VISIBLE : View.GONE); + mainView.findViewById(R.id.urldialog_textPresetSuccessful).setVisibility(View.GONE); + } + mainView.findViewById(R.id.urldialog_checkboxEnable).setVisibility(existingPreset == null ? View.VISIBLE : View.GONE); + mainView.findViewById(R.id.urldialog_buttonAddPreset).setVisibility(existingPreset == null ? View.VISIBLE : View.GONE); + ((Button) mainView.findViewById(R.id.urldialog_buttonAddPreset)).setOnClickListener(this); + } + } + + /** + * Remove leading and trailing slashes from a String + * + * @param path the String to remove the slashes from + * @return the String + */ + private String stripPathSeperators(@NonNull String path) { + if (path.startsWith("/")) { + path = path.substring(1); + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return path; + } + @Override public void onClick(View v) { if (v.getId() == R.id.urldialog_buttonAddPreset) { @@ -173,71 +203,4 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { downloadSucessful = true; } } - - /** - * Process the OAuth callback - * - * @param verifier the verifier - * @throws OAuthException - * @throws TimeoutException - * @throws ExecutionException - */ - private void oAuthHandshake(@NonNull String verifier) throws OAuthException, TimeoutException, ExecutionException { - - class OAuthAccessTokenTask extends ExecutorTask { - private OAuthException ex = null; - - /** - * Create a new instance - */ - OAuthAccessTokenTask() { - super(); - } - - @Override - protected Boolean doInBackground(String verifier) { - OAuthHelper oa = new OAuthHelper(); // if we got here it has already been initialized once - try { - String[] access = oa.getAccessToken(verifier); - prefdb.setAPIAccessToken(access[0], access[1]); - } catch (OAuthException e) { - Log.d(DEBUG_TAG, "oAuthHandshake: " + e); - ex = e; - return false; - } - return true; - } - - @Override - protected void onPostExecute(Boolean success) { - Log.d(DEBUG_TAG, "oAuthHandshake onPostExecute"); - Intent intent = new Intent(VespucciURLActivity.this, Authorize.class); - intent.setAction(Authorize.ACTION_FINISH_OAUTH); - startActivity(intent); - } - - /** - * Get the any OAuthException that was thrown - * - * @return the exception - */ - OAuthException getException() { - return ex; - } - } - - OAuthAccessTokenTask requester = new OAuthAccessTokenTask(); - requester.execute(verifier); - try { - if (Boolean.FALSE.equals(requester.get(60, TimeUnit.SECONDS))) { - OAuthException ex = requester.getException(); - if (ex != null) { - throw ex; - } - } - } catch (InterruptedException e) { // NOSONAR cancel does interrupt the thread in question - requester.cancel(); - throw new TimeoutException(e.getMessage()); - } - } } diff --git a/src/main/java/de/blau/android/resources/KeyDatabaseHelper.java b/src/main/java/de/blau/android/resources/KeyDatabaseHelper.java index ad5fb3c17c..7b22a53a0d 100644 --- a/src/main/java/de/blau/android/resources/KeyDatabaseHelper.java +++ b/src/main/java/de/blau/android/resources/KeyDatabaseHelper.java @@ -18,6 +18,7 @@ import androidx.annotation.Nullable; import de.blau.android.contract.Files; import de.blau.android.net.OAuthHelper.OAuthConfiguration; +import de.blau.android.prefs.API.Auth; import de.blau.android.util.ScreenMessage; /** @@ -31,7 +32,7 @@ public class KeyDatabaseHelper extends SQLiteOpenHelper { private static final String DEBUG_TAG = "KeyDatabase"; private static final String DATABASE_NAME = "keys"; - private static final int DATABASE_VERSION = 3; + private static final int DATABASE_VERSION = 4; private static final int FIELD_COUNT = 4; private static final String AND = " AND "; @@ -45,7 +46,7 @@ public class KeyDatabaseHelper extends SQLiteOpenHelper { private static final String TRUE = "true"; public enum EntryType { - IMAGERY, API_KEY, API_OAUTH1_KEY + IMAGERY, API_KEY, API_OAUTH1_KEY, API_OAUTH2_KEY } /** @@ -63,7 +64,7 @@ public void onCreate(SQLiteDatabase db) { try { db.execSQL( "CREATE TABLE keys (name TEXT, type TEXT, key TEXT DEFAULT NULL, add1 TEXT DEFAULT NULL, add2 TEXT DEFAULT NULL, custom INTEGER DEFAULT 0)"); - db.execSQL("CREATE UNIQUE INDEX idx_keys ON keys (name)"); + db.execSQL("CREATE UNIQUE INDEX idx_keys ON keys (name, type)"); } catch (SQLException e) { Log.w(DEBUG_TAG, "Problem creating database", e); } @@ -76,6 +77,10 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE " + KEYS_TABLE); onCreate(db); } + if (oldVersion <= 3 && newVersion >= 4) { + db.execSQL("DROP INDEX idx_keys"); + db.execSQL("CREATE UNIQUE INDEX idx_keys ON keys (name, type)"); + } } /** @@ -150,23 +155,28 @@ public static String getKey(@NonNull SQLiteDatabase db, @NonNull String name, @N } /** - * Retrieve the OAuth configuration for an API + * Retrieve the OAuth2 configuration for an API * * @param db readable SQLiteDatabase * @param name the API name + * @param auth current Authentication type * @return a configuration or null if none found */ @Nullable - public static OAuthConfiguration getOAuthConfiguration(@NonNull SQLiteDatabase db, @NonNull String name) { + public static OAuthConfiguration getOAuthConfiguration(@NonNull SQLiteDatabase db, @NonNull String name, @NonNull Auth auth) { + final boolean oAuth1a = auth == Auth.OAUTH1A; try (Cursor dbresult = db.query(KEYS_TABLE, new String[] { KEY_FIELD, ADD1_FIELD, ADD2_FIELD }, - NAME_FIELD + "='" + name + "'" + AND + TYPE_FIELD + "='" + EntryType.API_OAUTH1_KEY + "'", null, null, null, null)) { + NAME_FIELD + "='" + name + "'" + AND + TYPE_FIELD + "='" + (oAuth1a ? EntryType.API_OAUTH1_KEY : EntryType.API_OAUTH2_KEY) + "'", null, null, + null, null)) { if (dbresult.getCount() == 1) { - OAuthConfiguration result = new OAuthConfiguration(); boolean haveEntry = dbresult.moveToFirst(); if (haveEntry) { try { + OAuthConfiguration result = new OAuthConfiguration(); result.setKey(dbresult.getString(dbresult.getColumnIndexOrThrow(KEY_FIELD))); - result.setSecret(dbresult.getString(dbresult.getColumnIndexOrThrow(ADD1_FIELD))); + if (oAuth1a) { + result.setSecret(dbresult.getString(dbresult.getColumnIndexOrThrow(ADD1_FIELD))); + } result.setOauthUrl(dbresult.getString(dbresult.getColumnIndexOrThrow(ADD2_FIELD))); return result; } catch (IllegalArgumentException iaex) { diff --git a/src/test/java/de/blau/android/resources/KeyDatabaseTest.java b/src/test/java/de/blau/android/resources/KeyDatabaseTest.java index ed85c69f24..ca76707bbe 100644 --- a/src/test/java/de/blau/android/resources/KeyDatabaseTest.java +++ b/src/test/java/de/blau/android/resources/KeyDatabaseTest.java @@ -15,7 +15,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.LargeTest; import de.blau.android.contract.Files; -import de.blau.android.net.OAuthHelper.OAuthConfiguration; +import de.blau.android.net.OAuth1aHelper.OAuthConfiguration; import de.blau.android.resources.KeyDatabaseHelper.EntryType; @RunWith(RobolectricTestRunner.class)