From c6f843087658a0776227af95b6ea9c7da9f08157 Mon Sep 17 00:00:00 2001 From: davigonz Date: Tue, 25 Jul 2017 12:14:22 +0200 Subject: [PATCH] Move OAuth2GetRefreshedAccessTokenOperation to the library and prepare RemoteOperation to retry the last failed operation when access token expires --- ...ava => OAuth2GetAccessTokenOperation.java} | 4 +- ...Auth2GetRefreshedAccessTokenOperation.java | 243 ++++++++++++++++++ .../oauth/OwnCloudOAuth2RequestBuilder.java | 2 +- .../common/operations/RemoteOperation.java | 32 ++- 4 files changed, 269 insertions(+), 12 deletions(-) rename src/com/owncloud/android/lib/common/network/authentication/oauth/{OAuth2GetAccessToken.java => OAuth2GetAccessTokenOperation.java} (98%) create mode 100644 src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetRefreshedAccessTokenOperation.java diff --git a/src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetAccessToken.java b/src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetAccessTokenOperation.java similarity index 98% rename from src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetAccessToken.java rename to src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetAccessTokenOperation.java index f22b3ceb..23ef9876 100644 --- a/src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetAccessToken.java +++ b/src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetAccessTokenOperation.java @@ -45,7 +45,7 @@ import java.util.HashMap; import java.util.Map; -public class OAuth2GetAccessToken extends RemoteOperation { +public class OAuth2GetAccessTokenOperation extends RemoteOperation { private String mGrantType; private String mCode; @@ -57,7 +57,7 @@ public class OAuth2GetAccessToken extends RemoteOperation { private Map mResultTokenMap; - public OAuth2GetAccessToken( + public OAuth2GetAccessTokenOperation( String grantType, String code, String clientId, diff --git a/src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetRefreshedAccessTokenOperation.java b/src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetRefreshedAccessTokenOperation.java new file mode 100644 index 00000000..c2c36fd5 --- /dev/null +++ b/src/com/owncloud/android/lib/common/network/authentication/oauth/OAuth2GetRefreshedAccessTokenOperation.java @@ -0,0 +1,243 @@ +/** + * ownCloud Android client application + * + * @author David González Verdugo + * + * Copyright (C) 2017 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.lib.common.network.authentication.oauth; + +import android.net.Uri; + +import com.owncloud.android.lib.common.OwnCloudBasicCredentials; +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.OwnCloudCredentials; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; +import com.owncloud.android.lib.common.utils.Log_OC; + +import org.apache.commons.httpclient.NameValuePair; +import org.apache.commons.httpclient.methods.PostMethod; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class OAuth2GetRefreshedAccessToken extends RemoteOperation { + + private static final String TAG = OAuth2GetRefreshedAccessToken.class.getSimpleName(); + + private String mClientId; + private String mClientSecret; + private String mGrantType; + + private String mOAuth2RefreshAccessTokenQueryParams; + + private Map mOAuth2ParsedRefreshAccessTokenQueryParams; + + private Map mResultTokenMap; + + public OAuth2GetRefreshedAccessToken( + String clientId, + String secretId, + String grantType, + String oAuth2RefreshAccessTokenQueryParams + ) { + + mClientId = clientId; + mClientSecret = secretId; + mGrantType = grantType; + mOAuth2RefreshAccessTokenQueryParams = oAuth2RefreshAccessTokenQueryParams; + mOAuth2ParsedRefreshAccessTokenQueryParams = new HashMap<>(); + mResultTokenMap = null; + } + + @Override + protected RemoteOperationResult run(OwnCloudClient client) { + + RemoteOperationResult result = null; + PostMethod postMethod = null; + + try { + parseAuthorizationResponse(); + + if (mOAuth2ParsedRefreshAccessTokenQueryParams.keySet().contains(OAuth2Constants. + KEY_ERROR)) { + if (OAuth2Constants.VALUE_ERROR_ACCESS_DENIED.equals( + mOAuth2ParsedRefreshAccessTokenQueryParams.get(OAuth2Constants.KEY_ERROR))) { + result = new RemoteOperationResult(ResultCode.OAUTH2_ERROR_ACCESS_DENIED); + } else { + result = new RemoteOperationResult(ResultCode.OAUTH2_ERROR); + } + } + + if (result == null) { + NameValuePair[] nameValuePairs = new NameValuePair[3]; + nameValuePairs[0] = new NameValuePair(OAuth2Constants.KEY_GRANT_TYPE, mGrantType); + nameValuePairs[1] = new NameValuePair(OAuth2Constants.KEY_CLIENT_ID, mClientId); + nameValuePairs[2] = new NameValuePair(OAuth2Constants.KEY_REFRESH_TOKEN, + mOAuth2ParsedRefreshAccessTokenQueryParams.get(OAuth2Constants.KEY_REFRESH_TOKEN)); + + + Uri.Builder uriBuilder = client.getBaseUri().buildUpon(); + + postMethod = new PostMethod(uriBuilder.build().toString()); + postMethod.setRequestBody(nameValuePairs); + + OwnCloudCredentials oauthCredentials = new OwnCloudBasicCredentials( + mClientId, + mClientSecret + ); + OwnCloudCredentials oldCredentials = switchClientCredentials(oauthCredentials); + + client.executeMethod(postMethod); + switchClientCredentials(oldCredentials); + + String response = postMethod.getResponseBodyAsString(); + Log_OC.d(TAG, "OAUTH2: raw response from POST TOKEN: " + response); + if (response != null && response.length() > 0) { + JSONObject tokenJson = new JSONObject(response); + parseNewAccessTokenResult(tokenJson); + if (mResultTokenMap.get(OAuth2Constants.KEY_ERROR) != null || + mResultTokenMap.get(OAuth2Constants.KEY_ACCESS_TOKEN) == null) { + result = new RemoteOperationResult(ResultCode.OAUTH2_ERROR); + + } else { + result = new RemoteOperationResult(true, postMethod); + ArrayList data = new ArrayList<>(); + data.add(mResultTokenMap); + result.setData(data); + } + + } else { + result = new RemoteOperationResult(false, postMethod); + client.exhaustResponse(postMethod.getResponseBodyAsStream()); + } + } + + } catch (Exception e) { + result = new RemoteOperationResult(e); + + } finally { + if (postMethod != null) + postMethod.releaseConnection(); // let the connection available for other methods + + if (result.isSuccess()) { + Log_OC.i(TAG, "OAuth2 TOKEN REQUEST with auth code " + + mOAuth2ParsedRefreshAccessTokenQueryParams.get("code") + " to " + + client.getWebdavUri() + ": " + result.getLogMessage()); + + } else if (result.getException() != null) { + Log_OC.e(TAG, "OAuth2 TOKEN REQUEST with auth code " + + mOAuth2ParsedRefreshAccessTokenQueryParams.get("code") + " to " + client. + getWebdavUri() + ": " + result.getLogMessage(), result.getException()); + + } else if (result.getCode() == ResultCode.OAUTH2_ERROR) { + Log_OC.e(TAG, "OAuth2 TOKEN REQUEST with auth code " + + mOAuth2ParsedRefreshAccessTokenQueryParams.get("code") + " to " + client. + getWebdavUri() + ": " + ((mResultTokenMap != null) ? mResultTokenMap. + get(OAuth2Constants.KEY_ERROR) : "NULL")); + + } else { + Log_OC.e(TAG, "OAuth2 TOKEN REQUEST with auth code " + + mOAuth2ParsedRefreshAccessTokenQueryParams.get("code") + " to " + client. + getWebdavUri() + ": " + result.getLogMessage()); + } + } + + return result; + } + + private OwnCloudCredentials switchClientCredentials(OwnCloudCredentials newCredentials) { + // work-around for POC with owncloud/oauth2 app, that doesn't allow client + OwnCloudCredentials previousCredentials = getClient().getCredentials(); + getClient().setCredentials(newCredentials); + return previousCredentials; + } + + private void parseAuthorizationResponse() { + String[] pairs = mOAuth2RefreshAccessTokenQueryParams.split("&"); + int i = 0; + String key = ""; + String value = ""; + StringBuilder sb = new StringBuilder(); + while (pairs.length > i) { + int j = 0; + String[] part = pairs[i].split("="); + while (part.length > j) { + String p = part[j]; + if (j == 0) { + key = p; + sb.append(key + " = "); + } else if (j == 1) { + value = p; + mOAuth2ParsedRefreshAccessTokenQueryParams.put(key, value); + sb.append(value + "\n"); + } + + Log_OC.v(TAG, "[" + i + "," + j + "] = " + p); + j++; + } + i++; + } + } + + private void parseNewAccessTokenResult(JSONObject tokenJson) throws JSONException { + mResultTokenMap = new HashMap<>(); + + if (tokenJson.has(OAuth2Constants.KEY_ACCESS_TOKEN)) { + mResultTokenMap.put(OAuth2Constants.KEY_ACCESS_TOKEN, tokenJson. + getString(OAuth2Constants.KEY_ACCESS_TOKEN)); + } + if (tokenJson.has(OAuth2Constants.KEY_TOKEN_TYPE)) { + mResultTokenMap.put(OAuth2Constants.KEY_TOKEN_TYPE, tokenJson. + getString(OAuth2Constants.KEY_TOKEN_TYPE)); + } + if (tokenJson.has(OAuth2Constants.KEY_EXPIRES_IN)) { + mResultTokenMap.put(OAuth2Constants.KEY_EXPIRES_IN, tokenJson. + getString(OAuth2Constants.KEY_EXPIRES_IN)); + } + if (tokenJson.has(OAuth2Constants.KEY_REFRESH_TOKEN)) { + mResultTokenMap.put(OAuth2Constants.KEY_REFRESH_TOKEN, tokenJson. + getString(OAuth2Constants.KEY_REFRESH_TOKEN)); + } + if (tokenJson.has(OAuth2Constants.KEY_SCOPE)) { + mResultTokenMap.put(OAuth2Constants.KEY_SCOPE, tokenJson. + getString(OAuth2Constants.KEY_SCOPE)); + } + if (tokenJson.has(OAuth2Constants.KEY_ERROR)) { + mResultTokenMap.put(OAuth2Constants.KEY_ERROR, tokenJson. + getString(OAuth2Constants.KEY_ERROR)); + } + if (tokenJson.has(OAuth2Constants.KEY_ERROR_DESCRIPTION)) { + mResultTokenMap.put(OAuth2Constants.KEY_ERROR_DESCRIPTION, tokenJson. + getString(OAuth2Constants.KEY_ERROR_DESCRIPTION)); + } + if (tokenJson.has(OAuth2Constants.KEY_ERROR_URI)) { + mResultTokenMap.put(OAuth2Constants.KEY_ERROR_URI, tokenJson. + getString(OAuth2Constants.KEY_ERROR_URI)); + } + + if (tokenJson.has(OAuth2Constants.KEY_USER_ID)) { // not standard + mResultTokenMap.put(OAuth2Constants.KEY_USER_ID, tokenJson. + getString(OAuth2Constants.KEY_USER_ID)); + } + } +} \ No newline at end of file diff --git a/src/com/owncloud/android/lib/common/network/authentication/oauth/OwnCloudOAuth2RequestBuilder.java b/src/com/owncloud/android/lib/common/network/authentication/oauth/OwnCloudOAuth2RequestBuilder.java index 83549879..91266b6d 100644 --- a/src/com/owncloud/android/lib/common/network/authentication/oauth/OwnCloudOAuth2RequestBuilder.java +++ b/src/com/owncloud/android/lib/common/network/authentication/oauth/OwnCloudOAuth2RequestBuilder.java @@ -68,7 +68,7 @@ public class OwnCloudOAuth2RequestBuilder implements OAuth2RequestBuilder { switch(mRequest) { case CREATE_ACCESS_TOKEN: OAuth2ClientConfiguration clientConfiguration = mOAuth2Provider.getClientConfiguration(); - return new OAuth2GetAccessToken( + return new OAuth2GetAccessTokenOperation( mGrantType.getValue(), mCode, clientConfiguration.getClientId(), diff --git a/src/com/owncloud/android/lib/common/operations/RemoteOperation.java b/src/com/owncloud/android/lib/common/operations/RemoteOperation.java index 2a5a206c..5af81bfe 100644 --- a/src/com/owncloud/android/lib/common/operations/RemoteOperation.java +++ b/src/com/owncloud/android/lib/common/operations/RemoteOperation.java @@ -94,6 +94,13 @@ public abstract class RemoteOperation implements Runnable { private Handler mListenerHandler = null; + /** + * Counter to establish the number of times a failed operation will be repeated due to + * an authorization error + */ + private int MAX_REPEAT_COUNTER = 1; + + /** * Abstract method to implement the operation in derived classes. */ @@ -235,8 +242,11 @@ public abstract class RemoteOperation implements Runnable { @Override public final void run() { RemoteOperationResult result = null; - boolean repeat = false; + boolean repeat; + int repeatCounter = 0; do { + repeat = false; + try { grantOwnCloudClient(); result = run(mClient); @@ -249,12 +259,16 @@ public abstract class RemoteOperation implements Runnable { if (shouldInvalidateAccountCredentials(result)) { boolean invalidated = invalidateAccountCredentials(); if (invalidated && - mClient.getCredentials().authTokenCanBeRefreshed()) { - mClient = null; - repeat = true; - // this will result in a new loop, and grantOwnCloudClient() will - // create a new instance for mClient, refreshing the token via the account - // manager + mClient.getCredentials().authTokenCanBeRefreshed() && + repeatCounter < MAX_REPEAT_COUNTER) { + + mClient = null; + repeat = true; + repeatCounter++; + + // this will result in a new loop, and grantOwnCloudClient() will + // create a new instance for mClient, refreshing the token via the account + // manager } // else: operation will finish with ResultCode.UNAUTHORIZED } @@ -297,8 +311,8 @@ public abstract class RemoteOperation implements Runnable { boolean should = ResultCode.UNAUTHORIZED.equals(result.getCode()); // invalid credentials - should &= (mClient.getCredentials() != null && ! // real credentials - (mClient.getCredentials() instanceof OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials)); + should &= (mClient.getCredentials() != null && // real credentials + !(mClient.getCredentials() instanceof OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials)); should &= (mAccount != null && mContext != null); // have all the needed to effectively invalidate